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