Current section

Files

Jump to
ash_cloak lib ash_cloak.ex
Raw

lib/ash_cloak.ex

defmodule AshCloak do
@moduledoc """
An extension for encrypting attributes of a resource.
See the getting started guide for more information.
"""
@transformers [
AshCloak.Transformers.SetupEncryption
]
@cloak %Spark.Dsl.Section{
name: :cloak,
describe: "Encrypt attributes of a resource",
schema: [
vault: [
type: {:behaviour, Cloak.Vault},
doc: "The vault to use to encrypt & decrypt the value",
required: true
],
attributes: [
type: {:wrap_list, :atom},
default: [],
doc:
"The attribute or attributes to encrypt. The attribute will be renamed to `encrypted_{attribute}`, and a calculation with the same name will be added."
],
decrypt_by_default: [
type: {:wrap_list, :atom},
default: [],
doc:
"A list of attributes that should be decrypted (their calculation should be loaded) by default."
],
on_decrypt: [
type: {:or, [{:fun, 4}, :mfa]},
doc:
"A function to call when decrypting any value. Takes the resource, field, records, and calculation context. Must return `:ok` or `{:error, error}`"
]
]
}
use Spark.Dsl.Extension, sections: [@cloak], transformers: @transformers
@doc """
Encrypts and writes to an encrypted attribute.
If the changeset is pending (i.e not currently running the action), then it is added as a before_action hook.
Otherwise, it is run immediately
"""
@spec encrypt_and_set(Ash.Changeset.t(), attr :: atom, term :: term) :: Ash.Changeset.t()
def encrypt_and_set(changeset, key, value) do
if key in AshCloak.Info.cloak_attributes!(changeset.resource) do
if changeset.phase == :pending do
Ash.Changeset.before_action(changeset, &do_encrypt_and_set(&1, key, value))
else
do_encrypt_and_set(changeset, key, value)
end
else
raise "Attempted to encrypt and set attribute #{inspect(key)} for resource #{inspect(changeset.resource)}, but it is not configured for encryption."
end
end
@doc false
def do_encrypt(resource, value) do
vault = AshCloak.Info.cloak_vault!(resource)
value
|> :erlang.term_to_binary()
|> vault.encrypt!()
|> Base.encode64()
end
defp do_encrypt_and_set(changeset, key, value) do
encrypted_value = do_encrypt(changeset.resource, value)
encryption_target = String.to_existing_atom("encrypted_#{key}")
changeset
|> Ash.Changeset.force_change_attribute(encryption_target, encrypted_value)
|> Ash.Changeset.delete_argument(key)
|> Map.update!(:params, fn params ->
Map.drop(params, [key, to_string(key)])
end)
end
end