1defmodule Comeonin.Bcrypt do
2  @moduledoc """
3  Module to handle bcrypt authentication.
4
5  To generate a password hash, use the `hashpwsalt` function:
6
7      Comeonin.Bcrypt.hashpwsalt("hard to guess")
8
9  To check the password against a password hash, use the `checkpw` function:
10
11      Comeonin.Bcrypt.checkpw("hard to guess", stored_hash)
12
13  There is also a `dummy_checkpw`, which can be used to stop an attacker guessing
14  a username by timing the responses.
15
16  See the documentation for each function for more details.
17
18  Most users will not need to use any of the other functions in this module.
19
20  ## Bcrypt
21
22  Bcrypt is a key derivation function for passwords designed by Niels Provos
23  and David Mazières. Bcrypt is an adaptive function, which means that it can
24  be configured to remain slow and resistant to brute-force attacks even as
25  computational power increases.
26
27  The computationally intensive code is run in C, using Erlang NIFs. One concern
28  about NIFs is that they block the Erlang VM, and so it is better to make
29  sure these functions do not run for too long. This bcrypt implementation
30  has been adapted so that each NIF runs for as short a time as possible.
31
32  ## Bcrypt versions
33
34  This bcrypt implementation is based on the latest OpenBSD version, which
35  fixed a small issue that affected some passwords longer than 72 characters.
36  By default, it produces hashes with the prefix `$2b$`, and it can check
37  hashes with either the `$2b$` prefix or the older `$2a$` prefix.
38
39  It is also possible to generate hashes with the `$2a$` prefix by running
40  the following command:
41
42      Comeonin.Bcrypt.hashpass("hard to guess", Comeonin.Bcrypt.gen_salt(12, true))
43
44  This option should only be used if you need to generate hashes that are
45  then checked by older libraries.
46  """
47
48  use Bitwise
49  alias Comeonin.{Bcrypt.Base64, Config, Tools}
50
51  @salt_len 16
52
53  @compile {:autoload, false}
54  @on_load {:init, 0}
55
56  def init do
57    path = :filename.join(:code.priv_dir(:comeonin), 'bcrypt_nif')
58    :erlang.load_nif(path, 0)
59  end
60
61  @doc """
62  Generate a salt for use with the `hashpass` function.
63
64  The log_rounds parameter determines the computational complexity
65  of the generation of the password hash. Its default is 12, the minimum is 4,
66  and the maximum is 31.
67
68  The `legacy` option is for generating salts with the old `$2a$` prefix.
69  Only use this option if you need to generate hashes that are then checked
70  by older libraries.
71  """
72  def gen_salt(log_rounds, legacy \\ false)
73  def gen_salt(log_rounds, _) when not is_integer(log_rounds) do
74    raise ArgumentError, "Wrong type. log_rounds should be an integer between 4 and 31."
75  end
76  def gen_salt(log_rounds, legacy) when log_rounds in 4..31 do
77    :crypto.strong_rand_bytes(16)
78    |> :binary.bin_to_list
79    |> fmt_salt(zero_str(log_rounds), legacy)
80  end
81  def gen_salt(log_rounds, legacy) when log_rounds < 4, do: gen_salt(4, legacy)
82  def gen_salt(log_rounds, legacy) when log_rounds > 31, do: gen_salt(31, legacy)
83  def gen_salt, do: gen_salt(Config.bcrypt_log_rounds)
84
85  @doc """
86  Hash the password using bcrypt.
87
88  In most cases, you will want to use the `hashpwsalt` function instead.
89  Use this function if you want more control over the generation of the
90  salt.
91  """
92  def hashpass(password, salt) when is_binary(salt) and is_binary(password) do
93    if byte_size(salt) == 29 do
94      hashpw(:binary.bin_to_list(password), :binary.bin_to_list(salt))
95    else
96      raise ArgumentError, "The salt is the wrong length."
97    end
98  end
99  def hashpass(_password, _salt) do
100    raise ArgumentError, "Wrong type. The password and salt need to be strings."
101  end
102
103  @doc """
104  Hash the password with a salt which is randomly generated.
105
106  To change the complexity (and the time taken) of the  password hash
107  calculation, you need to change the value for `bcrypt_log_rounds`
108  in the config file.
109  """
110  def hashpwsalt(password) do
111    hashpass(password, gen_salt(Config.bcrypt_log_rounds))
112  end
113
114  @doc """
115  Check the password.
116
117  The check is performed in constant time to avoid timing attacks.
118  """
119  def checkpw(password, hash) when is_binary(password) and is_binary(hash) do
120    hashpw(:binary.bin_to_list(password), :binary.bin_to_list(hash))
121    |> Tools.secure_check(hash)
122  end
123  def checkpw(_password, _hash) do
124    raise ArgumentError, "Wrong type. The password and hash need to be strings."
125  end
126
127  @doc """
128  Perform a dummy check for a user that does not exist.
129
130  This always returns false. The reason for implementing this check is
131  in order to make user enumeration by timing responses more difficult.
132  """
133  @dialyzer({:nowarn_function, dummy_checkpw: 0})
134  def dummy_checkpw do
135    hashpwsalt("password")
136    false
137  end
138
139  @doc """
140  Initialize the P-box and S-box tables with the digits of Pi,
141  and then start the key expansion process.
142  """
143  def bf_init(key, key_len, salt)
144  def bf_init(_, _, _), do: :erlang.nif_error(:not_loaded)
145
146  @doc """
147  The main key expansion function.
148  """
149  def bf_expand0(state, input, input_len)
150  def bf_expand0(_, _, _), do: :erlang.nif_error(:not_loaded)
151
152  @doc """
153  Encrypt and return the hash.
154  """
155  def bf_encrypt(state)
156  def bf_encrypt(_), do: :erlang.nif_error(:not_loaded)
157
158  defp hashpw(password, salt) do
159    [prefix, log_rounds, salt] = Enum.take(salt, 29) |> :string.tokens('$')
160    bcrypt(password, salt, prefix, log_rounds)
161    |> fmt_hash(salt, prefix, zero_str(log_rounds))
162  end
163
164  defp bcrypt(key, salt, prefix, log_rounds) when prefix in ['2b', '2a'] do
165    key_len = if prefix == '2b' and length(key) > 72, do: 73, else: length(key) + 1
166    {salt, rounds} = prepare_keys(salt, List.to_integer(log_rounds))
167    bf_init(key, key_len, salt)
168    |> expand_keys(key, key_len, salt, rounds)
169    |> bf_encrypt
170  end
171  defp bcrypt(_, _, prefix, _) do
172    raise ArgumentError, "Comeonin Bcrypt does not support the #{prefix} prefix."
173  end
174
175  defp prepare_keys(salt, log_rounds) when log_rounds in 4..31 do
176    {Base64.decode(salt), bsl(1, log_rounds)}
177  end
178  defp prepare_keys(_, _) do
179    raise ArgumentError, "Wrong number of rounds."
180  end
181
182  defp expand_keys(state, _key, _key_len, _salt, 0), do: state
183  defp expand_keys(state, key, key_len, salt, rounds) do
184    bf_expand0(state, key, key_len)
185    |> bf_expand0(salt, @salt_len)
186    |> expand_keys(key, key_len, salt, rounds - 1)
187  end
188
189  defp zero_str(log_rounds) do
190    if log_rounds < 10, do: "0#{log_rounds}", else: "#{log_rounds}"
191  end
192
193  defp fmt_salt(salt, log_rounds, false), do: "$2b$#{log_rounds}$#{Base64.encode(salt)}"
194  defp fmt_salt(salt, log_rounds, true), do: "$2a$#{log_rounds}$#{Base64.encode(salt)}"
195
196  defp fmt_hash(hash, salt, prefix, log_rounds) do
197    "$#{prefix}$#{log_rounds}$#{Base64.normalize(salt)}#{Base64.encode(hash)}"
198  end
199end
200