In AshHq
we use cloak to encrypt data. We do it with a combination of change
s and calculation
s.
For instance:
attribute :encrypted_name, :string
attribute :encrypted_address, :string
Then, in actions we write to those attributes using arguments:
update :update_merch_settings do
argument :address, :string
argument :name, :string
accept [:shirt_size]
change set_attribute(:encrypted_address, arg(:address))
change set_attribute(:encrypted_name, arg(:name))
end
end
We don’t write to the encrypted attributes directly to prevent double encryption.
This is what our encryption change looks like:
defmodule AshHq.Changes.Encrypt do
@moduledoc "Encrypt attributes that are being changed before submitting the action"
use Ash.Resource.Change
def change(changeset, opts, _) do
Ash.Changeset.before_action(changeset, fn changeset ->
Enum.reduce(opts[:fields], changeset, fn field, changeset ->
case Ash.Changeset.fetch_change(changeset, field) do
{:ok, value} when is_binary(value) ->
new_value =
value
|> AshHq.Vault.encrypt!()
|> Base.encode64()
Ash.Changeset.force_change_attribute(changeset, field, new_value)
_ ->
changeset
end
end)
end)
end
end
And we apply it on every action like so:
changes do
...
change {AshHq.Changes.Encrypt, fields: [:encrypted_address, :encrypted_name]}
end
Then, we can decrypt it on demand using calculations:
calculations do
calculate :address, :string, {Decrypt, field: :encrypted_address}
calculate :name, :string, {Decrypt, field: :encrypted_name}
end
And that calculation looks like this:
defmodule AshHq.Calculations.Decrypt do
@moduledoc "Decrypts a given value on demand"
use Ash.Calculation
@impl Ash.Calculation
def calculate(records, opts, _) do
{:ok,
Enum.map(records, fn record ->
record
|> Map.get(opts[:field])
|> case do
nil ->
nil
value ->
value
|> Base.decode64!()
|> AshHq.Vault.decrypt!()
end
end)}
end
@impl Ash.Calculation
def load(_, opts, _) do
[opts[:field]]
end
end
This was a very early addition to AshHq
. I’d love to see an encryption extension at some point 