Current section

Files

Jump to
cloak lib cloak fields hmac.ex
Raw

lib/cloak/fields/hmac.ex

defmodule Cloak.Fields.HMAC do
@moduledoc """
A custom `Ecto.Type` for hashing fields using `:crypto.hmac/3`.
HMAC is **more secure** than `Cloak.Fields.SHA256`, because it uses a secret
to obfuscate the hash. This makes it harder to guess the value of the field.
## Why
If you store a hash of a field's value, you can then query on it as a proxy
for an encrypted field. This works because HMAC is deterministic and
always results in the same value, while secure encryption does not. Be
warned, however, that hashing will expose which fields have the same value,
because they will contain the same hash.
## Configuration
Create an `HMAC` field in your project:
defmodule MyApp.Hashed.HMAC do
use Cloak.Fields.HMAC, otp_app: :my_app
end
Then, configure it with a `:secret` and `:algorithm`, either using
mix configuration:
config :my_app, MyApp.Hashed.HMAC,
algorithm: :sha512,
secret: "secret"
Or using the `init/1` callback to fetch configuration at runtime:
defmodule MyApp.Hashed.HMAC do
use Cloak.Fields.HMAC, otp_app: :my_app
@impl Cloak.Fields.HMAC
def init(config) do
config = Keyword.merge(config, [
algorithm: :sha512,
secret: System.get_env("HMAC_SECRET")
])
{:ok, config}
end
end
## Usage
Create the hash field with the type `:binary`. Add it to your schema
definition like this:
schema "table" do
field :field_name, MyApp.Encrypted.Binary
field :field_name_hash, MyApp.Hashed.HMAC
end
Ensure that the hash is updated whenever the target field changes with the
`put_change/3` function:
def changeset(struct, attrs \\\\ %{}) do
struct
|> cast(attrs, [:field_name, :field_name_hash])
|> put_hashed_fields()
end
defp put_hashed_fields(changeset) do
changeset
|> put_change(:field_name_hash, get_field(changeset, :field_name))
end
Query the Repo using the `:field_name_hash` in any place you would typically
query by `:field_name`.
user = Repo.get_by(User, email_hash: "user@email.com")
"""
@typedoc "HMAC algorithms supported by Cloak.Field.HMAC"
@type algorithms :: :md5 | :ripemd160 | :sha | :sha224 | :sha256 | :sha384 | :sha512
@doc """
Configures the `HMAC` field using runtime information.
## Example
@impl Cloak.Fields.HMAC
def init(config) do
config = Keyword.merge(config, [
algorithm: :sha512,
secret: System.get_env("HMAC_SECRET")
])
{:ok, config}
end
"""
@callback init(config :: Keyword.t()) :: {:ok, Keyword.t()} | {:error, any}
@doc false
defmacro __using__(opts) do
otp_app = Keyword.fetch!(opts, :otp_app)
quote do
@behaviour Cloak.Fields.HMAC
@behaviour Ecto.Type
@algorithms ~w[
md5
ripemd160
sha
sha224
sha256
sha384
sha512
]a
@impl Cloak.Fields.HMAC
def init(config) do
{:ok, config}
end
@impl Ecto.Type
def type, do: :binary
@impl Ecto.Type
def cast(nil) do
{:ok, nil}
end
def cast(value) when is_binary(value) do
{:ok, value}
end
def cast(_value) do
:error
end
@impl Ecto.Type
def dump(nil) do
{:ok, nil}
end
def dump(value) when is_binary(value) do
config = build_config()
{:ok, :crypto.hmac(config[:algorithm], config[:secret], value)}
end
def dump(_value) do
:error
end
@impl Ecto.Type
def load(value) do
{:ok, value}
end
defoverridable init: 1, type: 0, cast: 1, dump: 1, load: 1
defp build_config do
{:ok, config} =
unquote(otp_app)
|> Application.get_env(__MODULE__, [])
|> init()
validate_config(config)
end
defp validate_config(config) do
unless is_binary(config[:secret]) do
secret = inspect(config[:secret])
raise Cloak.InvalidConfig, "#{secret} is an invalid secret for #{inspect(__MODULE__)}"
end
unless config[:algorithm] in @algorithms do
algo = inspect(config[:algorithm])
raise Cloak.InvalidConfig,
"#{algo} is an invalid hash algorithm for #{inspect(__MODULE__)}"
end
config
end
end
end
end