defmodule Kryptex.EncryptedField do
@moduledoc """
Parameterized Ecto type that encrypts values on `dump/3` and decrypts on `load/3`.
Use it directly:
field :email, Kryptex.EncryptedField, source_type: :string
Or through `Kryptex.Schema.encrypted_field/3`.
"""
@behaviour Ecto.ParameterizedType
alias Kryptex.Cipher
@impl true
def type(_params), do: :binary
@impl true
def init(opts) do
source_type = Keyword.get(opts, :source_type, :string)
unless valid_source_type?(source_type) do
raise ArgumentError,
"expected :source_type to be an Ecto.Type primitive/module, got: #{inspect(source_type)}"
end
%{source_type: source_type}
end
@impl true
def cast(value, %{source_type: source_type}) do
Ecto.Type.cast(source_type, value)
end
@impl true
def load(ciphertext, _loader, %{source_type: source_type}) when is_binary(ciphertext) do
with {:ok, serialized} <- Cipher.decrypt(ciphertext),
{:ok, raw_term} <- safe_binary_to_term(serialized),
{:ok, loaded} <- Ecto.Type.load(source_type, raw_term) do
{:ok, loaded}
else
_ -> :error
end
end
def load(_, _, _), do: :error
@impl true
def dump(value, dumper, %{source_type: source_type}) do
with {:ok, dumped} <- Ecto.Type.dump(source_type, value, dumper),
serialized <- :erlang.term_to_binary(dumped),
{:ok, ciphertext} <- Cipher.encrypt(serialized) do
{:ok, ciphertext}
else
_ -> :error
end
end
@impl true
def embed_as(_format, _params), do: :self
@impl true
def equal?(left, right, _params), do: left == right
defp safe_binary_to_term(serialized) do
{:ok, :erlang.binary_to_term(serialized, [:safe])}
rescue
_ -> :error
end
defp valid_source_type?(type) when is_atom(type), do: true
defp valid_source_type?(_), do: false
end