Store encrypted HTML
This commit is contained in:
parent
56d2df5140
commit
4ba50f9fb5
|
@ -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 =
|
||||||
:crypto.crypto_one_time_aead(
|
do_decrypt(
|
||||||
:aes_256_gcm,
|
|
||||||
key,
|
|
||||||
iv,
|
|
||||||
page.encrypted_content,
|
page.encrypted_content,
|
||||||
<<>>,
|
page.encrypted_content_iv,
|
||||||
tag,
|
page.encrypted_content_tag,
|
||||||
false
|
key
|
||||||
)
|
)
|
||||||
|
|
||||||
%__MODULE__{page | content: content}
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 %>
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue