defmodule Wiki.Content.Page do use Ecto.Schema import Ecto.Changeset @type t() :: %__MODULE__{ encrypted_content: binary(), encrypted_content_iv: binary(), encrypted_content_tag: binary(), content: String.t() | nil, content_encryption_key: String.t() | nil, encrypted_html: binary(), encrypted_html_iv: binary(), encrypted_html_tag: binary(), html: String.t() | nil, title: String.t(), user: Wiki.Accounts.User.t(), uploads: [Wiki.Content.Upload.t()] } 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 :encrypted_html, :binary field :encrypted_html_iv, :binary field :encrypted_html_tag, :binary field :html, :string, virtual: true field :title, :string belongs_to :user, Wiki.Accounts.User has_many :uploads, Wiki.Content.Upload timestamps() end @doc false def changeset(page, attrs, options \\ []) do changeset = page |> cast(attrs, [ :title, :content, :content_encryption_key, :user_id ]) if Keyword.get(options, :encrypt, true) do encrypt_changeset(changeset) else changeset end |> 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) user_id = get_field(changeset, :user_id) {html, _linked_pages} = Wiki.Content.Renderer.render(content, user_id) {encrypted_html, html_tag, html_iv} = do_encrypt(html, key) changeset |> put_change(:encrypted_content, encrypted_content) |> put_change(:encrypted_content_tag, tag) |> put_change(:encrypted_content_iv, iv) |> delete_change(:content) |> put_change(:encrypted_html, encrypted_html) |> put_change(:encrypted_html_tag, html_tag) |> put_change(:encrypted_html_iv, html_iv) |> 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) {encrypted_text, tag, iv} end def decrypt_content(page) do key = page.content_encryption_key content = do_decrypt( page.encrypted_content, page.encrypted_content_iv, page.encrypted_content_tag, key ) html = do_decrypt(page.encrypted_html, page.encrypted_html_iv, page.encrypted_html_tag, key) %__MODULE__{page | content: content, html: html} end defp do_decrypt(encrypted_text, iv, tag, key) do key = Base.decode16!(key, case: :lower) :crypto.crypto_one_time_aead( :aes_256_gcm, key, iv, encrypted_text, <<>>, tag, false ) end end