Skip to content

Commit

Permalink
Merge 33d529a into c2fe2fd
Browse files Browse the repository at this point in the history
  • Loading branch information
danielberkompas committed Nov 12, 2022
2 parents c2fe2fd + 33d529a commit 9209a46
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 165 deletions.
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]

{: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

0 comments on commit 9209a46

Please sign in to comment.