defmodule Wiki.Content.Page do use Ecto.Schema import Ecto.Changeset schema "pages" do field :encrypted_content, :binary field :encrypted_content_iv, :binary field :encrypted_content_tag, :binary field :content, :string, virtual: true field :content_encryption_key, :string, virtual: true field :title, :string belongs_to :user, Wiki.Accounts.User timestamps() end @doc false def changeset(page, attrs) do page |> cast(attrs, [ :title, :content, :content_encryption_key, :user_id ]) |> encrypt_changeset() |> validate_required([ :title, :encrypted_content, :user_id ]) end defp encrypt_changeset(%Ecto.Changeset{changes: %{content: _}} = changeset) do content = get_change(changeset, :content) key = get_field(changeset, :content_encryption_key) {encrypted_content, tag, iv} = do_encrypt(content, key) changeset |> put_change(:encrypted_content, encrypted_content) |> put_change(:encrypted_content_tag, tag) |> put_change(:encrypted_content_iv, iv) |> delete_change(:content) |> delete_change(:content_encryption_key) end defp encrypt_changeset(changeset), do: changeset @iv_size 16 defp do_encrypt(text, key) do # key is a base16 encoded string (comes from Argon2.Base.hash_password w/ the format: :raw_hash option) key = Base.decode16!(key, case: :lower) iv = :crypto.strong_rand_bytes(@iv_size) {encrypted_text, tag} = :crypto.crypto_one_time_aead(:aes_256_gcm, key, iv, text, <<>>, true) |> IO.inspect() {encrypted_text, tag, iv} end def decrypt_content(page) do key = Base.decode16!(page.content_encryption_key, case: :lower) iv = page.encrypted_content_iv tag = page.encrypted_content_tag content = :crypto.crypto_one_time_aead( :aes_256_gcm, key, iv, page.encrypted_content, <<>>, tag, false ) %__MODULE__{page | content: content} end end