Skip to content
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

➖ Remove dependency on PBKDF2 #42

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
310 changes: 151 additions & 159 deletions lib/cloak_ecto/types/pbkdf2.ex
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: "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
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]
Copy link

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 SHA256 and 210,000 iterations for SHA512. Would you be amenable to bumping the default number of iterations and also updating the docs?

Copy link
Owner Author

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.


{: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
Expand Down
6 changes: 0 additions & 6 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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]},
Expand Down