2020-07-30 00:49:51 +00:00
|
|
|
defmodule Wiki.Content.Page do
|
|
|
|
use Ecto.Schema
|
|
|
|
import Ecto.Changeset
|
2020-08-02 20:12:38 +00:00
|
|
|
import Ecto.Query
|
|
|
|
alias Wiki.Repo
|
|
|
|
alias Wiki.Content.PageLink
|
2020-07-30 00:49:51 +00:00
|
|
|
|
2020-07-30 02:21:25 +00:00
|
|
|
@type t() :: %__MODULE__{
|
|
|
|
encrypted_content: binary(),
|
|
|
|
encrypted_content_iv: binary(),
|
|
|
|
encrypted_content_tag: binary(),
|
|
|
|
content: String.t() | nil,
|
|
|
|
content_encryption_key: String.t() | nil,
|
2020-08-01 23:37:16 +00:00
|
|
|
encrypted_html: binary(),
|
|
|
|
encrypted_html_iv: binary(),
|
|
|
|
encrypted_html_tag: binary(),
|
2020-07-30 02:21:25 +00:00
|
|
|
html: String.t() | nil,
|
|
|
|
title: String.t(),
|
2020-08-01 20:43:55 +00:00
|
|
|
user: Wiki.Accounts.User.t(),
|
|
|
|
uploads: [Wiki.Content.Upload.t()]
|
2020-07-30 02:21:25 +00:00
|
|
|
}
|
|
|
|
|
2020-07-30 00:49:51 +00:00
|
|
|
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
|
2020-08-01 23:37:16 +00:00
|
|
|
field :encrypted_html, :binary
|
|
|
|
field :encrypted_html_iv, :binary
|
|
|
|
field :encrypted_html_tag, :binary
|
2020-07-30 02:21:25 +00:00
|
|
|
field :html, :string, virtual: true
|
2020-07-30 00:49:51 +00:00
|
|
|
field :title, :string
|
|
|
|
|
|
|
|
belongs_to :user, Wiki.Accounts.User
|
2020-08-01 20:43:55 +00:00
|
|
|
has_many :uploads, Wiki.Content.Upload
|
2020-07-30 00:49:51 +00:00
|
|
|
|
2020-08-02 20:12:38 +00:00
|
|
|
# Pages that this page links to
|
|
|
|
has_many :_pages_linked_to, PageLink, foreign_key: :from_id
|
|
|
|
has_many :pages_linked_to, through: [:_pages_linked_to, :to]
|
|
|
|
|
|
|
|
# Pages that link to this page
|
|
|
|
has_many :_pages_linked_from, PageLink, foreign_key: :to_id
|
|
|
|
has_many :pages_linked_from, through: [:_pages_linked_from, :from]
|
|
|
|
|
2020-07-30 00:49:51 +00:00
|
|
|
timestamps()
|
|
|
|
end
|
|
|
|
|
|
|
|
@doc false
|
2020-07-30 02:21:25 +00:00
|
|
|
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
|
2020-07-30 00:49:51 +00:00
|
|
|
|> 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)
|
2020-08-01 23:37:16 +00:00
|
|
|
|
2020-07-30 00:49:51 +00:00
|
|
|
{encrypted_content, tag, iv} = do_encrypt(content, key)
|
|
|
|
|
2020-08-01 23:37:16 +00:00
|
|
|
user_id = get_field(changeset, :user_id)
|
2020-08-02 20:12:38 +00:00
|
|
|
html = Wiki.Content.Renderer.render(content, user_id)
|
2020-08-01 23:37:16 +00:00
|
|
|
{encrypted_html, html_tag, html_iv} = do_encrypt(html, key)
|
|
|
|
|
2020-07-30 00:49:51 +00:00
|
|
|
changeset
|
|
|
|
|> put_change(:encrypted_content, encrypted_content)
|
|
|
|
|> put_change(:encrypted_content_tag, tag)
|
|
|
|
|> put_change(:encrypted_content_iv, iv)
|
|
|
|
|> delete_change(:content)
|
2020-08-01 23:37:16 +00:00
|
|
|
|> put_change(:encrypted_html, encrypted_html)
|
|
|
|
|> put_change(:encrypted_html_tag, html_tag)
|
|
|
|
|> put_change(:encrypted_html_iv, html_iv)
|
2020-07-30 00:49:51 +00:00
|
|
|
|> 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)
|
|
|
|
|
2020-08-01 23:37:16 +00:00
|
|
|
{encrypted_text, tag} = :crypto.crypto_one_time_aead(:aes_256_gcm, key, iv, text, <<>>, true)
|
2020-07-30 00:49:51 +00:00
|
|
|
|
|
|
|
{encrypted_text, tag, iv}
|
|
|
|
end
|
|
|
|
|
|
|
|
def decrypt_content(page) do
|
2020-08-01 23:37:16 +00:00
|
|
|
key = page.content_encryption_key
|
2020-07-30 00:49:51 +00:00
|
|
|
|
|
|
|
content =
|
2020-08-01 23:37:16 +00:00
|
|
|
do_decrypt(
|
2020-07-30 00:49:51 +00:00
|
|
|
page.encrypted_content,
|
2020-08-01 23:37:16 +00:00
|
|
|
page.encrypted_content_iv,
|
|
|
|
page.encrypted_content_tag,
|
|
|
|
key
|
2020-07-30 00:49:51 +00:00
|
|
|
)
|
|
|
|
|
2020-08-01 23:37:16 +00:00
|
|
|
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
|
|
|
|
)
|
2020-07-30 00:49:51 +00:00
|
|
|
end
|
2020-08-02 20:12:38 +00:00
|
|
|
|
|
|
|
@spec update_page_links(page :: __MODULE__.t()) :: :ok
|
|
|
|
def update_page_links(%__MODULE__{content: content, id: id} = page) when is_binary(content) do
|
|
|
|
linked_page_ids = Wiki.Content.Renderer.get_linked_pages(page)
|
|
|
|
|
|
|
|
if length(linked_page_ids) == 0 do
|
|
|
|
Repo.delete_all(from link in PageLink, where: link.from_id == ^id)
|
|
|
|
else
|
|
|
|
Repo.delete_all(
|
|
|
|
from link in PageLink, where: link.from_id == ^id and not (link.to_id in ^linked_page_ids)
|
|
|
|
)
|
|
|
|
|
|
|
|
values = Enum.map(linked_page_ids, fn to_id -> [from_id: id, to_id: to_id] end)
|
|
|
|
Repo.insert_all(PageLink, values, on_conflict: :nothing)
|
|
|
|
end
|
|
|
|
|
|
|
|
:ok
|
|
|
|
end
|
2020-07-30 00:49:51 +00:00
|
|
|
end
|