Store encrypted HTML

This commit is contained in:
Shadowfacts 2020-08-01 19:37:16 -04:00
parent 56d2df5140
commit 4ba50f9fb5
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
5 changed files with 81 additions and 32 deletions

View File

@ -8,6 +8,9 @@ defmodule Wiki.Content.Page do
encrypted_content_tag: binary(), encrypted_content_tag: binary(),
content: String.t() | nil, content: String.t() | nil,
content_encryption_key: String.t() | nil, content_encryption_key: String.t() | nil,
encrypted_html: binary(),
encrypted_html_iv: binary(),
encrypted_html_tag: binary(),
html: String.t() | nil, html: String.t() | nil,
title: String.t(), title: String.t(),
user: Wiki.Accounts.User.t(), user: Wiki.Accounts.User.t(),
@ -20,6 +23,9 @@ defmodule Wiki.Content.Page do
field :encrypted_content_tag, :binary field :encrypted_content_tag, :binary
field :content, :string, virtual: true field :content, :string, virtual: true
field :content_encryption_key, :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 :html, :string, virtual: true
field :title, :string field :title, :string
@ -55,13 +61,21 @@ defmodule Wiki.Content.Page do
defp encrypt_changeset(%Ecto.Changeset{changes: %{content: _}} = changeset) do defp encrypt_changeset(%Ecto.Changeset{changes: %{content: _}} = changeset) do
content = get_change(changeset, :content) content = get_change(changeset, :content)
key = get_field(changeset, :content_encryption_key) key = get_field(changeset, :content_encryption_key)
{encrypted_content, tag, iv} = do_encrypt(content, 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 changeset
|> put_change(:encrypted_content, encrypted_content) |> put_change(:encrypted_content, encrypted_content)
|> put_change(:encrypted_content_tag, tag) |> put_change(:encrypted_content_tag, tag)
|> put_change(:encrypted_content_iv, iv) |> put_change(:encrypted_content_iv, iv)
|> delete_change(:content) |> 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) |> delete_change(:content_encryption_key)
end end
@ -74,29 +88,38 @@ defmodule Wiki.Content.Page do
key = Base.decode16!(key, case: :lower) key = Base.decode16!(key, case: :lower)
iv = :crypto.strong_rand_bytes(@iv_size) iv = :crypto.strong_rand_bytes(@iv_size)
{encrypted_text, tag} = {encrypted_text, tag} = :crypto.crypto_one_time_aead(:aes_256_gcm, key, iv, text, <<>>, true)
:crypto.crypto_one_time_aead(:aes_256_gcm, key, iv, text, <<>>, true)
|> IO.inspect()
{encrypted_text, tag, iv} {encrypted_text, tag, iv}
end end
def decrypt_content(page) do def decrypt_content(page) do
key = Base.decode16!(page.content_encryption_key, case: :lower) key = page.content_encryption_key
iv = page.encrypted_content_iv
tag = page.encrypted_content_tag
content = 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( :crypto.crypto_one_time_aead(
:aes_256_gcm, :aes_256_gcm,
key, key,
iv, iv,
page.encrypted_content, encrypted_text,
<<>>, <<>>,
tag, tag,
false false
) )
%__MODULE__{page | content: content}
end end
end end

View File

@ -5,37 +5,61 @@ defmodule Wiki.Content.Renderer do
alias WikiWeb.Endpoint alias WikiWeb.Endpoint
import Ecto.Query import Ecto.Query
@spec render(Page.t()) :: String.t() @spec render(String.t(), integer()) :: {String.t(), [integer()]}
def render(page) do def render(content, user_id) do
page.content {content, linked_pages} = replace_page_links(content, user_id)
|> replace_page_links(page)
|> render_markdown() {
render_markdown(content),
Enum.uniq(linked_pages)
}
end end
@page_link_regex ~r/\[\[.*?\]\]/ @page_link_regex ~r/\[\[.*?\]\]/
@spec replace_page_links(content :: String.t(), root_page :: Page.t()) :: String.t() @spec replace_page_links(content :: String.t(), user_id :: integer()) ::
defp replace_page_links(content, root_page) do {String.t(), [integer()]}
String.replace(content, @page_link_regex, fn match -> defp replace_page_links(content, user_id) do
results = Regex.scan(@page_link_regex, content, return: :index)
results
|> Enum.reverse()
|> Enum.reduce({content, []}, fn [{start, length}], {str, acc} ->
# can't use String.slice here, because the indicies produced by Regex.scan are character indices, and slice operates on graphemes
match = :erlang.binary_part(str, start, length)
title = String.slice(match, 2..-3) title = String.slice(match, 2..-3)
lower_title = String.downcase(title) lower_title = String.downcase(title)
path = str_before = :erlang.binary_part(str, 0, start)
str_after = :erlang.binary_part(str, start + length, byte_size(str) - start - length)
{path, acc} =
from(p in Page) from(p in Page)
|> where([p], p.user_id == ^root_page.user_id) |> where([p], p.user_id == ^user_id)
|> where([p], fragment("lower(?)", p.title) == ^lower_title) |> where([p], fragment("lower(?)", p.title) == ^lower_title)
|> select([p], p.id) |> select([p], p.id)
|> Repo.one() |> Repo.one()
|> case do |> case do
nil -> nil ->
Routes.page_path(Endpoint, :new, title: title) {
Routes.page_path(Endpoint, :new, title: title),
acc
}
linked_page_id -> linked_page_id ->
Routes.page_path(Endpoint, :show, linked_page_id) {
Routes.page_path(Endpoint, :show, linked_page_id),
[linked_page_id | acc]
}
end end
# convert link to markdown, because [[title]] -> link transformation happens before markdown parsing page_link = "[#{title}](#{path})"
"[#{title}](#{path})"
{
str_before <> page_link <> str_after,
acc
}
end) end)
end end

View File

@ -86,8 +86,7 @@ defmodule WikiWeb.PageController do
def show(conn, _params) do def show(conn, _params) do
page = Repo.preload(conn.assigns.page, :uploads) page = Repo.preload(conn.assigns.page, :uploads)
rendered_content = Content.Renderer.render(page) render(conn, "show.html", page: page)
render(conn, "show.html", page: page, content: rendered_content)
end end
def edit(conn, _params) do def edit(conn, _params) do

View File

@ -3,7 +3,7 @@
<span><%= link "Back", to: Routes.page_path(@conn, :index) %></span> <span><%= link "Back", to: Routes.page_path(@conn, :index) %></span>
<div> <div>
<%= raw(@content) %> <%= raw(@page.html) %>
</div> </div>
<%= for upload <- @page.uploads do %> <%= for upload <- @page.uploads do %>

View File

@ -7,12 +7,15 @@ defmodule Wiki.Repo.Migrations.CreatePages do
add :encrypted_content, :binary add :encrypted_content, :binary
add :encrypted_content_iv, :binary add :encrypted_content_iv, :binary
add :encrypted_content_tag, :binary add :encrypted_content_tag, :binary
add :encrypted_html, :binary
add :encrypted_html_iv, :binary
add :encrypted_html_tag, :binary
add :user_id, references(:users, on_delete: :delete_all) add :user_id, references(:users, on_delete: :delete_all)
timestamps() timestamps()
end end
create index(:pages, [:user_id]) create index(:pages, [:user_id])
create index(:pages, ["(lower(title))"], name: :pages_lowercase_title_index) create unique_index(:pages, ["(lower(title))"], name: :pages_lowercase_title_index)
end end
end end