wiki/lib/wiki/content/upload.ex

106 lines
2.8 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()
defp 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 delete_file(upload :: t()) :: :ok
def delete_file(%__MODULE__{relative_path: filename}) do
File.rm(Path.join(upload_dir(), filename))
end
end