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. """ import Cryppo.Base64 alias Cryppo.{EncryptionArtefacts, Serialization} @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_bson | :invalid_encryption_artefacts, String.t()} def load(s) when is_binary(s) do with {:ok, encryption_artefacts_base64} <- decode_base64(s) do load_artefacts(encryption_artefacts_base64) 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 load_artefacts(_), do: {:error, :invalid_encryption_artefacts} defp unwrap_bin(nil), do: nil defp unwrap_bin(%Cyanide.Binary{subtype: :generic, data: ""}), do: nil defp unwrap_bin(%Cyanide.Binary{subtype: :generic, data: bin}), do: bin defimpl Serialization do @spec serialize(EncryptionArtefacts.t()) :: String.t() | {:error, :cannot_bson_encode} def serialize(%EncryptionArtefacts{ initialization_vector: iv, authentication_tag: at, additional_authenticated_data: ad }) do with {:ok, bytes} <- serialize_for_version(iv, at, ad) do Base.url_encode64(bytes, padding: true) end end @spec serialize_for_version(any, any, any) :: {:error, :cannot_bson_encode} | {:ok, binary} defp serialize_for_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 = <> {:ok, with_version_prefix} end end 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: %Cyanide.Binary{subtype: :generic, data: ""} defp wrap_bin(bin), do: %Cyanide.Binary{subtype: :generic, data: bin} end end