Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
157 lines (112 sloc) 5.05 KB
defmodule Pbkdf2 do
@moduledoc """
Elixir wrapper for the Pbkdf2 password hashing function.
This module implements the Comeonin and Comeonin.PasswordHash behaviours,
providing the following functions:
* `add_hash/2` - takes a password as input and returns a map containing the password hash
* `check_pass/3` - takes a user struct and password as input and verifies the password
* `no_user_verify/1` - runs the hash function, but always returns false
* `hash_pwd_salt/2` - hashes the password with a randomly-generated salt
* `verify_pass/2` - verifies a password
For a lower-level API, see Pbkdf2.Base.
## Configuration
The following parameter can be set in the config file:
* rounds - computational cost
* the number of rounds
* 160_000 is the default
If you are hashing passwords in your tests, it can be useful to add
the following to the `config/test.exs` file:
config :pbkdf2_elixir,
rounds: 1
NB. do not use this value in production.
## Options
In addition to the options listed below, the `add_hash`, `no_user_verify`
and `hash_pwd_salt` functions all take options that are then passed on
to the `hash_password` function in the Pbkdf2.Base module.
See the documentation for `Pbkdf2.Base.hash_password` for details.
### add_hash
* `:hash_key` - the key used in the map for the password hash
* the default is `password_hash`
* `:salt_len` - the length of the random salt
* the default is 16 (the minimum is 8) bytes
### check_pass
* `:hash_key` - the key used in the user struct for the password hash
* if this is not set, `check_pass` will look for `password_hash`, and then `encrypted_password`
* `:hide_user` - run `no_user_verify` to prevent user enumeration
* the default is true
* set this to false if you do not want to hide usernames
### hash_pwd_salt
* `:salt_len` - the length of the random salt
* the default is 16 (the minimum is 8) bytes
## Examples
The following examples show how to hash a password with a randomly-generated
salt and then verify a password:
iex> hash = Pbkdf2.hash_pwd_salt("password")
...> Pbkdf2.verify_pass("password", hash)
true
iex> hash = Pbkdf2.hash_pwd_salt("password")
...> Pbkdf2.verify_pass("incorrect", hash)
false
### add_hash
The `put_pass_hash` function below is an example of how you can use
`add_hash` to add the password hash to the Ecto changeset.
defp put_pass_hash(%Ecto.Changeset{valid?: true, changes:
%{password: password}} = changeset) do
change(changeset, Pbkdf2.add_hash(password))
end
defp put_pass_hash(changeset), do: changeset
This function will return a changeset with `%{password_hash: password_hash, password: nil}`
added to the `changes` map.
### check_pass
The following is an example of calling this function with no options:
def verify_user(%{"password" => password} = params) do
params
|> Accounts.get_by()
|> Pbkdf2.check_pass(password)
end
The `Accounts.get_by` function in this example takes the user parameters
(for example, email and password) as input and returns a user struct or nil.
## Pbkdf2
Pbkdf2 is a password-based key derivation function
that uses a password, a variable-length salt and an iteration
count and applies a pseudorandom function to these to
produce a key.
The original implementation used SHA-1 as the pseudorandom function,
but this version uses HMAC-SHA-512, the default, or HMAC-SHA-256.
## Warning
It is recommended that you set a maximum length for the password
when using Pbkdf2. This maximum length should not prevent valid users from setting
long passwords. It is instead needed to combat denial-of-service attacks.
As an example, Django sets the maximum length to 4096 bytes.
For more information, see [this link](https://www.djangoproject.com/weblog/2013/sep/15/security/).
"""
use Comeonin
alias Pbkdf2.Base
@doc """
Generate a random salt.
The minimum length of the salt is 8 bytes and the maximum length is
1024. The default length for the salt is 16 bytes. We do not recommend
using a salt shorter than the default.
"""
def gen_salt(salt_length \\ 16)
def gen_salt(salt_length) when salt_length in 8..1024 do
:crypto.strong_rand_bytes(salt_length)
end
def gen_salt(_) do
raise ArgumentError, """
The salt is the wrong length. It should be between 8 and 1024 bytes long.
"""
end
@impl true
def hash_pwd_salt(password, opts \\ []) do
Base.hash_password(password, Keyword.get(opts, :salt_len, 16) |> gen_salt, opts)
end
@impl true
def verify_pass(password, stored_hash) do
[alg, rounds, salt, hash] = String.split(stored_hash, "$", trim: true)
digest = if alg =~ "sha512", do: :sha512, else: :sha256
Base.verify_pass(password, hash, salt, digest, rounds, output(stored_hash))
end
defp output("$pbkdf2" <> _), do: :modular
defp output("pbkdf2" <> _), do: :django
end
You can’t perform that action at this time.