diff --git a/lib/cloak_ecto/types/pbkdf2.ex b/lib/cloak_ecto/types/pbkdf2.ex index b9b297b..540b6d4 100644 --- a/lib/cloak_ecto/types/pbkdf2.ex +++ b/lib/cloak_ecto/types/pbkdf2.ex @@ -1,131 +1,114 @@ -if Code.ensure_loaded?(:pbkdf2) do - defmodule Cloak.Ecto.PBKDF2 do - @moduledoc """ - A custom `Ecto.Type` for deriving a key for fields using - [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2). +defmodule Cloak.Ecto.PBKDF2 do + @moduledoc """ + A custom `Ecto.Type` for deriving a key for fields using + [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2). - PBKDF2 is **more secure** than `Cloak.Ecto.HMAC` and - `Cloak.Fields.SHA256` because it uses [key - stretching](https://en.wikipedia.org/wiki/Key_stretching) to increase the - amount of time to compute hashes. This slows down brute-force attacks. + PBKDF2 is **more secure** than `Cloak.Ecto.HMAC` and + `Cloak.Fields.SHA256` because it uses [key + stretching](https://en.wikipedia.org/wiki/Key_stretching) to increase the + amount of time to compute hashes. This slows down brute-force attacks. - ## Why + ## 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 PBKDF2 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. + 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 PBKDF2 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. - ## Dependency + ## Configuration - To use this field type, you must install the `:pbkdf2` library in your - `mix.exs` file. + Create a `PBKDF2` field in your project: - {:pbkdf2, "~> 2.0"} - - If you are using Erlang >= 24, you will need to use a forked version, - because `pbkdf2` version `2.0.0` uses `:crypto.hmac` functions that were - removed in Erlang 24. - - {:pbkdf2, "~> 2.0", github: "miniclip/erlang-pbkdf2"} + defmodule MyApp.Hashed.PBKDF2 do + use Cloak.Ecto.PBKDF2, otp_app: :my_app + end - ## Configuration + Then, configure it with a `:secret`, an `:algorithm`, the maximum `:size` + of the stored key (in bytes), and a number of `:iterations`, either using + mix configuration: - Create a `PBKDF2` field in your project: + config :my_app, MyApp.Hashed.PBKDF2, + algorithm: :sha256, + iterations: 10_000, + secret: "secret", + size: 64 - defmodule MyApp.Hashed.PBKDF2 do - use Cloak.Ecto.PBKDF2, otp_app: :my_app - end + Or using the `init/1` callback to fetch configuration at runtime: - Then, configure it with a `:secret`, an `:algorithm`, the maximum `:size` - of the stored key (in bytes), and a number of `:iterations`, either using - mix configuration: + defmodule MyApp.Hashed.PBKDF2 do + use Cloak.Ecto.PBKDF2, otp_app: :my_app - config :my_app, MyApp.Hashed.PBKDF2, - algorithm: :sha256, - iterations: 10_000, - secret: "secret", - size: 64 + @impl Cloak.Ecto.PBKDF2 + def init(config) do + config = Keyword.merge(config, [ + algorithm: :sha256, + iterations: 10_000, + secret: System.get_env("PBKDF2_SECRET") + ]) - Or using the `init/1` callback to fetch configuration at runtime: + {:ok, config} + end + end - defmodule MyApp.Hashed.PBKDF2 do - use Cloak.Ecto.PBKDF2, otp_app: :my_app + ## Usage - @impl Cloak.Ecto.PBKDF2 - def init(config) do - config = Keyword.merge(config, [ - algorithm: :sha256, - iterations: 10_000, - secret: System.get_env("PBKDF2_SECRET") - ]) + Create the hash field with the type `:binary`. Add it to your schema + definition like this: - {:ok, config} - end - end + schema "table" do + field :field_name, MyApp.Encrypted.Binary + field :field_name_hash, MyApp.Hashed.PBKDF2 + end - ## Usage + Ensure that the hash is updated whenever the target field changes with the + `put_change/3` function: - Create the hash field with the type `:binary`. Add it to your schema - definition like this: + def changeset(struct, attrs \\\\ %{}) do + struct + |> cast(attrs, [:field_name, :field_name_hash]) + |> put_hashed_fields() + end - schema "table" do - field :field_name, MyApp.Encrypted.Binary - field :field_name_hash, MyApp.Hashed.PBKDF2 - end + defp put_hashed_fields(changeset) do + changeset + |> put_change(:field_name_hash, get_field(changeset, :field_name)) + end - Ensure that the hash is updated whenever the target field changes with the - `put_change/3` function: + Query the Repo using the `:field_name_hash` in any place you would typically + query by `:field_name`. - def changeset(struct, attrs \\\\ %{}) do - struct - |> cast(attrs, [:field_name, :field_name_hash]) - |> put_hashed_fields() - end + user = Repo.get_by(User, email_hash: "user@email.com") + """ - defp put_hashed_fields(changeset) do - changeset - |> put_change(:field_name_hash, get_field(changeset, :field_name)) - end + @typedoc "Digest algorithms supported by Cloak.Field.PBKDF2" + @type algorithms :: :md4 | :md5 | :ripemd160 | :sha | :sha224 | :sha256 | :sha384 | :sha512 - Query the Repo using the `:field_name_hash` in any place you would typically - query by `:field_name`. + @doc """ + Configures the `PBKDF2` field using runtime information. - user = Repo.get_by(User, email_hash: "user@email.com") - """ + ## Example - @typedoc "Digest algorithms supported by Cloak.Field.PBKDF2" - @type algorithms :: :md4 | :md5 | :ripemd160 | :sha | :sha224 | :sha256 | :sha384 | :sha512 + @impl Cloak.Ecto.PBKDF2 + def init(config) do + config = Keyword.merge(config, [ + algorithm: :sha256, + secret: System.get_env("PBKDF2_SECRET") + ]) - @doc """ - Configures the `PBKDF2` field using runtime information. + {:ok, config} + end + """ + @callback init(config :: Keyword.t()) :: {:ok, Keyword.t()} | {:error, any} - ## Example + @doc false + defmacro __using__(opts) do + otp_app = Keyword.fetch!(opts, :otp_app) - @impl Cloak.Ecto.PBKDF2 - def init(config) do - config = Keyword.merge(config, [ - algorithm: :sha256, - secret: System.get_env("PBKDF2_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.Ecto.PBKDF2 - @behaviour Ecto.Type - @algorithms ~w[ - md4 - md5 - ripemd160 + quote do + @behaviour Cloak.Ecto.PBKDF2 + @behaviour Ecto.Type + @algorithms ~w[ sha sha224 sha256 @@ -133,87 +116,96 @@ if Code.ensure_loaded?(:pbkdf2) do sha512 ]a - @impl Cloak.Ecto.PBKDF2 - def init(config) do - defaults = [algorithm: :sha256, iterations: 10_000, size: 32] + @impl Cloak.Ecto.PBKDF2 + def init(config) do + defaults = [algorithm: :sha256, iterations: 10_000, size: 32] - {:ok, defaults |> Keyword.merge(config)} - end + {:ok, defaults |> Keyword.merge(config)} + end - @impl Ecto.Type - def type, do: :binary + @impl Ecto.Type + def type, do: :binary - @impl Ecto.Type - def cast(nil), do: {:ok, nil} - def cast(value) when is_binary(value), do: {:ok, value} - def cast(_value), do: :error + @impl Ecto.Type + def cast(nil), do: {:ok, nil} + def cast(value) when is_binary(value), do: {:ok, value} + def cast(_value), do: :error - @impl Ecto.Type - def dump(nil), do: {:ok, nil} + @impl Ecto.Type + def dump(nil), do: {:ok, nil} - def dump(value) when is_binary(value) do - config = build_config() - :pbkdf2.pbkdf2({:hmac, config[:algorithm]}, value, config[:secret], config[:size]) - end + def dump(value) when is_binary(value) do + config = build_config() - def dump(_value), do: :error + hash = + :crypto.pbkdf2_hmac( + config[:algorithm], + value, + config[:secret], + config[:iterations], + config[:size] + ) - @impl Ecto.Type - def embed_as(_format) do - :self - end + {:ok, hash} + end - @impl Ecto.Type - def equal?(term1, term2) do - term1 == term2 - end + def dump(_value), do: :error - @impl Ecto.Type - def load(value), do: {:ok, value} + @impl Ecto.Type + def embed_as(_format) do + :self + end - defoverridable init: 1, type: 0, cast: 1, dump: 1, load: 1 + @impl Ecto.Type + def equal?(term1, term2) do + term1 == term2 + end - defp build_config do - {:ok, config} = - unquote(otp_app) - |> Application.get_env(__MODULE__, []) - |> init() + @impl Ecto.Type + def load(value), do: {:ok, value} - validate_config(config) - end + defoverridable init: 1, type: 0, cast: 1, dump: 1, load: 1 - defp validate_config(config) do - m = inspect(__MODULE__) + defp build_config do + {:ok, config} = + unquote(otp_app) + |> Application.get_env(__MODULE__, []) + |> init() - unless is_binary(config[:secret]) do - secret = inspect(config[:secret]) + validate_config(config) + end + + defp validate_config(config) do + m = inspect(__MODULE__) - raise Cloak.InvalidConfig, "#{secret} is an invalid secret for #{m}" - end + unless is_binary(config[:secret]) do + secret = inspect(config[:secret]) - unless config[:algorithm] in @algorithms do - algo = inspect(config[:algorithm]) + raise Cloak.InvalidConfig, "#{secret} is an invalid secret for #{m}" + end - raise Cloak.InvalidConfig, - "#{algo} is an invalid hash algorithm for #{m}, must be in #{inspect(@algorithms)}" - end + unless config[:algorithm] in @algorithms do + algo = inspect(config[:algorithm]) - unless is_integer(config[:iterations]) && config[:iterations] > 0 do - iterations = inspect(config[:iterations]) + raise Cloak.InvalidConfig, + "#{algo} is an invalid hash algorithm for #{m}, must be in #{inspect(@algorithms)}" + end - raise Cloak.InvalidConfig, - "Iterations must be a positive integer for #{m}, got: #{iterations}" - end + unless is_integer(config[:iterations]) && config[:iterations] > 0 do + iterations = inspect(config[:iterations]) - unless is_integer(config[:size]) && config[:size] > 0 do - size = inspect(config[:size]) + raise Cloak.InvalidConfig, + "Iterations must be a positive integer for #{m}, got: #{iterations}" + end - raise Cloak.InvalidConfig, - "Size should be a positive integer for #{m}, got: #{size}" - end + unless is_integer(config[:size]) && config[:size] > 0 do + size = inspect(config[:size]) - config + raise Cloak.InvalidConfig, + "Size should be a positive integer for #{m}, got: #{size}" end + + config end end end diff --git a/mix.exs b/mix.exs index d613139..65f1231 100644 --- a/mix.exs +++ b/mix.exs @@ -37,12 +37,6 @@ defmodule Cloak.Ecto.MixProject do [ {:cloak, "~> 1.1.1"}, {:ecto, "~> 3.0"}, - # Must use a forked version of pbkdf2 to support Erlang 24. Because Hex only - # allows hex packages to be dependencies, this dep cannot be listed as an - # optional dependency anymore. - # - # See https://github.com/basho/erlang-pbkdf2/pull/12 - {:pbkdf2, "~> 2.0", github: "miniclip/erlang-pbkdf2", only: [:dev, :test]}, {:ex_doc, ">= 0.0.0", only: :dev}, {:excoveralls, ">= 0.0.0", only: :test}, {:ecto_sql, ">= 0.0.0", only: [:dev, :test]},