defmodule Pbkdf2.Base do @moduledoc """ Base module for the Pbkdf2 password hashing library. """ use Bitwise alias Pbkdf2.{Base64, Tools} @max_length bsl(1, 32) - 1 @doc """ Generate a salt for use with Django's version of pbkdf2. ## Examples To create a valid Django hash, using pbkdf2_sha256: salt = django_salt(12) opts = [digest: :sha256, format: :django] Pbkdf2.Base.hash_password(password, salt, opts) This example uses 160_000 rounds. Add `rounds: number` to the opts if you want to change the number of rounds. """ def django_salt(len) do :crypto.strong_rand_bytes(len * 2) |> Pbkdf2.Base64.encode |> String.replace(~r{[.|/]}, "") |> :binary.part(0, len) end @doc """ Hash a password using Pbkdf2. ## Configurable parameters 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 There are four options (rounds can be used to override the value in the config): * rounds - the number of rounds * the amount of computation, given in number of iterations * the default is 160_000 * this can also be set in the config file * output_fmt - the output format of the hash * the default is modular crypt format * digest - the sha algorithm that pbkdf2 will use * the default is sha512 * length - the length, in bytes, of the hash * the default is 64 for sha512 and 32 for sha256 """ def hash_password(password, salt, opts \\ []) def hash_password(password, salt, opts) when is_binary(password) and is_binary(salt) and byte_size(salt) > 7 do {rounds, output_fmt, {digest, length}} = get_opts(opts) if length > @max_length do raise ArgumentError, "length must be equal to or less than #{@max_length}" end pbkdf2(password, salt, rounds, digest, length, 1, [], 0) |> format(salt, digest, rounds, output_fmt) end def hash_password(_, _, _) do raise ArgumentError, "The password and salt should be strings and " <> "the salt must be at least 8 bytes long" end @doc """ Verify a password by comparing it with the stored Pbkdf2 hash. """ def verify_pass(password, hash, salt, rounds, digest, length, output_fmt) do salt = output_fmt == :modular and Base64.decode(salt) pbkdf2(password, salt, String.to_integer(rounds), digest, length, 1, [], 0) |> verify_format(output_fmt) |> Tools.secure_check(hash) end defp get_opts(opts) do {Keyword.get(opts, :rounds, Application.get_env(:pbkdf2_elixir, :rounds, 160_000)), Keyword.get(opts, :format, :modular), case opts[:digest] do :sha256 -> {:sha256, opts[:length] || 32} _ -> {:sha512, opts[:length] || 64} end} end defp pbkdf2(_password, _salt, _rounds, _digest, dklen, _block_index, acc, length) when length >= dklen do key = acc |> Enum.reverse |> IO.iodata_to_binary <> = key bin end defp pbkdf2(password, salt, rounds, digest, dklen, block_index, acc, length) do initial = :crypto.hmac(digest, password, <>) block = iterate(password, rounds - 1, digest, initial, initial) pbkdf2(password, salt, rounds, digest, dklen, block_index + 1, [block | acc], byte_size(block) + length) end defp iterate(_password, 0, _digest, _prev, acc), do: acc defp iterate(password, round, digest, prev, acc) do next = :crypto.hmac(digest, password, prev) iterate(password, round - 1, digest, next, :crypto.exor(next, acc)) end defp format(hash, salt, digest, rounds, :modular) do "$pbkdf2-#{digest}$#{rounds}$#{Base64.encode(salt)}$#{Base64.encode(hash)}" end defp format(hash, salt, digest, rounds, :django) do "pbkdf2_#{digest}$#{rounds}$#{salt}$#{Base.encode64(hash)}" end defp format(hash, _salt, _digest, _rounds, :hex), do: Base.encode16(hash, case: :lower) defp verify_format(hash, :modular), do: Base64.encode(hash) defp verify_format(hash, :django), do: Base.encode64(hash) defp verify_format(hash, _), do: hash end