-
Notifications
You must be signed in to change notification settings - Fork 35
➖ Remove dependency on PBKDF2 #42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
danielberkompas
wants to merge
1
commit into
master
Choose a base branch
from
replace-pbkdf2
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,219 +1,211 @@ | ||
| 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: "[email protected]") | ||
| """ | ||
|
|
||
| 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: "[email protected]") | ||
| """ | ||
| ## 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 | ||
| sha384 | ||
| 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 | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OWASP suggests 600,000 iterations for
SHA256and 210,000 iterations forSHA512. Would you be amenable to bumping the default number of iterations and also updating the docs?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll do that in a separate PR, so that it will show up in the automatic changelog more easily.