159 lines
4.2 KiB
Elixir
159 lines
4.2 KiB
Elixir
defmodule Wiki.Content.Upload do
|
|
use Ecto.Schema
|
|
import Ecto.Changeset
|
|
|
|
@type t() :: %__MODULE__{
|
|
relative_path: String.t(),
|
|
content_type: String.t(),
|
|
encryption_iv: binary(),
|
|
page: Wiki.Content.Page.t()
|
|
}
|
|
|
|
schema "uploads" do
|
|
field :relative_path, :string
|
|
field :content_type, :string
|
|
field :encryption_iv, :binary
|
|
|
|
belongs_to :page, Wiki.Content.Page
|
|
|
|
timestamps()
|
|
end
|
|
|
|
@doc false
|
|
def changeset(upload, attrs) do
|
|
upload
|
|
|> cast(attrs, [:relative_path, :content_type, :encryption_iv, :page_id])
|
|
|> validate_required([:relative_path, :content_type, :encryption_iv])
|
|
end
|
|
|
|
@spec create_from_plug(upload :: Plug.Upload.t(), key :: binary()) :: t()
|
|
def create_from_plug(upload, key) do
|
|
{encrypted_file_path, iv} = encrypt_contents(upload, key)
|
|
|
|
changeset(%__MODULE__{}, %{
|
|
content_type: upload.content_type,
|
|
relative_path: encrypted_file_path,
|
|
encryption_iv: iv
|
|
})
|
|
end
|
|
|
|
@spec encrypt_contents(upload :: Plug.Upload.t(), key :: binary()) :: {String.t(), binary()}
|
|
defp encrypt_contents(upload, key) do
|
|
filename = "#{Ecto.UUID.generate()}.enc"
|
|
output_path = Path.join(upload_dir(), filename)
|
|
|
|
File.mkdir_p(upload_dir())
|
|
:ok = File.touch(output_path)
|
|
|
|
{:ok, input} = File.open(upload.path, [:read, :binary])
|
|
{:ok, output} = File.open(output_path, [:write, :binary])
|
|
|
|
iv = :crypto.strong_rand_bytes(16)
|
|
state = :crypto.crypto_init(:aes_256_ctr, key, iv, true)
|
|
|
|
:ok = encrypt_loop(state, input, output)
|
|
|
|
final_data = :crypto.crypto_final(state)
|
|
:ok = IO.binwrite(output, final_data)
|
|
|
|
:ok = File.close(input)
|
|
:ok = File.close(output)
|
|
|
|
{filename, iv}
|
|
end
|
|
|
|
@spec encrypt_loop(state :: :crypto.crypto_state(), input :: IO.device(), output :: IO.device()) ::
|
|
:ok
|
|
|
|
defp encrypt_loop(state, input, output) do
|
|
# why 4k?
|
|
case IO.binread(input, 4096) do
|
|
:eof ->
|
|
:ok
|
|
|
|
data ->
|
|
encrypted_data = :crypto.crypto_update(state, data)
|
|
:ok = IO.binwrite(output, encrypted_data)
|
|
|
|
encrypt_loop(state, input, output)
|
|
end
|
|
end
|
|
|
|
@spec upload_dir() :: String.t()
|
|
def upload_dir() do
|
|
dir = Application.get_env(:wiki, :upload_dir)
|
|
|
|
if is_nil(dir) do
|
|
raise "expected `:wiki, :upload_dir` to be configured"
|
|
end
|
|
|
|
dir
|
|
end
|
|
|
|
@spec decrypt_content(upload :: t(), key :: binary()) :: binary()
|
|
def decrypt_content(%__MODULE__{relative_path: filename, encryption_iv: iv}, key) do
|
|
path = Path.join(upload_dir(), filename)
|
|
encrypted_data = File.read!(path)
|
|
|
|
:crypto.crypto_one_time(:aes_256_ctr, key, iv, encrypted_data, false)
|
|
end
|
|
|
|
@spec decrypt_to_file(t(), binary(), String.t()) :: File.t()
|
|
def decrypt_to_file(upload, key, path) when is_binary(path) do
|
|
{:ok, output} = File.open(path, [:write, :binary])
|
|
|
|
:ok = decrypt_to_file(upload, key, output)
|
|
|
|
File.close(output)
|
|
end
|
|
|
|
@spec decrypt_to_file(t(), binary(), File.io_device()) :: File.t()
|
|
def decrypt_to_file(upload, key, output) do
|
|
decrypt_stream(upload, key)
|
|
|> Enum.each(fn data ->
|
|
IO.binwrite(output, data)
|
|
end)
|
|
|
|
:ok
|
|
end
|
|
|
|
@spec decrypt_stream(t(), binary()) :: Enumerable.t()
|
|
def decrypt_stream(%__MODULE__{relative_path: filename, encryption_iv: iv}, key) do
|
|
start_fun = fn ->
|
|
state = :crypto.crypto_init(:aes_256_ctr, key, iv, false)
|
|
path = Path.join(upload_dir(), filename)
|
|
{:ok, input} = File.open(path, [:read, :binary])
|
|
{state, input, false}
|
|
end
|
|
|
|
next_fun = fn {state, input_dev, halt} ->
|
|
if halt do
|
|
{:halt, {state, input_dev}}
|
|
else
|
|
# why 4k?
|
|
{halt, data} =
|
|
case IO.binread(input_dev, 4096) do
|
|
:eof ->
|
|
{true, :crypto.crypto_final(state)}
|
|
|
|
data ->
|
|
{false, :crypto.crypto_update(state, data)}
|
|
end
|
|
|
|
{[data], {state, input_dev, halt}}
|
|
end
|
|
end
|
|
|
|
after_fun = fn {_state, input_dev} ->
|
|
File.close(input_dev)
|
|
end
|
|
|
|
Stream.resource(start_fun, next_fun, after_fun)
|
|
end
|
|
|
|
@spec delete_file(upload :: t()) :: :ok
|
|
def delete_file(%__MODULE__{relative_path: filename}) do
|
|
File.rm(Path.join(upload_dir(), filename))
|
|
end
|
|
end
|