Current section
Files
Jump to
Current section
Files
lib/cryppo/encryption_artefacts.ex
defmodule Cryppo.EncryptionArtefacts do
@moduledoc """
A struct for encryption artefacts
Each encryption strategy can use `Cryppo.EncryptionArtefacts` if it
makes sense for the underlying cipher.
"""
alias Cryppo.{EncryptionArtefacts, Serialization, Yaml}
import Cryppo.Base64
@typedoc "Struct `Cryppo.EncryptionArtefacts`"
@type t :: %__MODULE__{
initialization_vector: binary | nil,
authentication_tag: binary | nil,
additional_authenticated_data: binary | nil
}
defstruct [:initialization_vector, :authentication_tag, :additional_authenticated_data]
# 65 is the version byte for encryption artefacts encoded with BSON
@current_version "A"
@spec current_version :: <<_::8>>
def current_version, do: @current_version
@doc false
@spec load(String.t()) :: {:ok, t()} | {:error, :invalid_base64 | :invalid_yaml | :invalid_bson}
def load(s) when is_binary(s) do
with {:ok, encryption_artefacts_base64} <- decode_base64(s) do
load_artefacts(encryption_artefacts_base64)
end
end
@spec load_artefacts(binary) ::
{:error, :invalid_bson | :invalid_yaml}
| {:ok, EncryptionArtefacts.t()}
defp load_artefacts(<<"---", _::binary>> = bin) do
with {:ok, %{} = artefacts_map} <- Yaml.decode(bin) do
{:ok,
%__MODULE__{
initialization_vector: to_nil(artefacts_map["iv"]),
authentication_tag: to_nil(artefacts_map["at"]),
additional_authenticated_data: to_nil(artefacts_map["ad"])
}}
end
end
defp load_artefacts(<<@current_version::binary, bin::binary>>) do
with {:ok, map} <- Cyanide.decode(bin) do
{:ok,
%__MODULE__{
initialization_vector: unwrap_bin(map["iv"]),
authentication_tag: unwrap_bin(map["at"]),
additional_authenticated_data: map["ad"]
}}
end
end
defp unwrap_bin(nil), do: nil
defp unwrap_bin({0x0, ""}), do: nil
defp unwrap_bin({0x0, bin}), do: bin
defp to_nil(""), do: nil
defp to_nil(v), do: v
defimpl Serialization do
@spec serialize(EncryptionArtefacts.t(), Keyword.t()) ::
String.t() | {:error, :cannot_bson_encode | :unrecognized_format}
def serialize(
%EncryptionArtefacts{
initialization_vector: iv,
authentication_tag: at,
additional_authenticated_data: ad
},
opts \\ []
) do
version = Keyword.get(opts, :version, :latest_version)
with {:ok, bytes} <- serialize_for_version({version, {iv, at, ad}}) do
Base.url_encode64(bytes, padding: true)
end
end
@spec serialize_for_version({atom, {any, any, any}}) ::
{:error, :cannot_bson_encode | :unrecognized_format} | {:ok, binary}
defp serialize_for_version({:legacy, {iv, at, ad}}) do
{:ok, Yaml.encode(%{"iv" => iv, "at" => at, "ad" => ad})}
end
defp serialize_for_version({:latest_version, {iv, at, ad}}) do
with_wrapped_binaries =
if non_empty_string?(ad),
do: %{"ad" => ad},
else: %{}
with_wrapped_binaries =
[{"iv", iv}, {"at", at}]
|> Enum.filter(fn {_k, v} -> non_empty_string?(v) end)
|> Enum.map(fn {k, v} -> {k, wrap_bin(v)} end)
|> Enum.into(with_wrapped_binaries)
with {:ok, bin} <- Cyanide.encode(with_wrapped_binaries) do
with_version_prefix = <<EncryptionArtefacts.current_version()::binary, bin::binary>>
{:ok, with_version_prefix}
end
end
defp serialize_for_version({_, {_, _, _}}), do: {:error, :unrecognized_format}
defp non_empty_string?(s), do: is_binary(s) && s != ""
# 0x0 is a marker for generic binary subtype in BSON
# see http://bsonspec.org/spec.html
defp wrap_bin(nil), do: {0x0, ""}
defp wrap_bin(bin), do: {0x0, bin}
end
end