key_generator.ex (3432B)
1 defmodule Plug.Crypto.KeyGenerator do 2 @moduledoc """ 3 `KeyGenerator` implements PBKDF2 (Password-Based Key Derivation Function 2), 4 part of PKCS #5 v2.0 (Password-Based Cryptography Specification). 5 6 It can be used to derive a number of keys for various purposes from a given 7 secret. This lets applications have a single secure secret, but avoid reusing 8 that key in multiple incompatible contexts. 9 10 The returned key is a binary. You may invoke functions in the `Base` module, 11 such as `Base.url_encode64/2`, to convert this binary into a textual 12 representation. 13 14 See http://tools.ietf.org/html/rfc2898#section-5.2 15 """ 16 17 import Bitwise 18 @max_length bsl(1, 32) - 1 19 20 @doc """ 21 Returns a derived key suitable for use. 22 23 ## Options 24 25 * `:iterations` - defaults to 1000 (increase to at least 2^16 if used for passwords); 26 * `:length` - a length in octets for the derived key. Defaults to 32; 27 * `:digest` - an hmac function to use as the pseudo-random function. Defaults to `:sha256`; 28 * `:cache` - an ETS table name to be used as cache. 29 Only use an ETS table as cache if the secret and salt is a bound set of values. 30 For example: `:ets.new(:your_name, [:named_table, :public, read_concurrency: true])` 31 32 """ 33 def generate(secret, salt, opts \\ []) do 34 iterations = Keyword.get(opts, :iterations, 1000) 35 length = Keyword.get(opts, :length, 32) 36 digest = Keyword.get(opts, :digest, :sha256) 37 cache = Keyword.get(opts, :cache) 38 generate(secret, salt, iterations, length, digest, cache) 39 end 40 41 @doc false 42 def generate(secret, salt, iterations, length, digest, cache) do 43 cond do 44 not is_integer(iterations) or iterations < 1 -> 45 raise ArgumentError, "iterations must be an integer >= 1" 46 47 length > @max_length -> 48 raise ArgumentError, "length must be less than or equal to #{@max_length}" 49 50 true -> 51 with_cache(cache, {secret, salt, iterations, length, digest}, fn -> 52 generate(hmac_fun(digest, secret), salt, iterations, length, 1, [], 0) 53 end) 54 end 55 rescue 56 e -> reraise e, Plug.Crypto.prune_args_from_stacktrace(__STACKTRACE__) 57 end 58 59 defp with_cache(nil, _key, fun), do: fun.() 60 61 defp with_cache(ets, key, fun) do 62 case :ets.lookup(ets, key) do 63 [{_key, value}] -> 64 value 65 66 [] -> 67 value = fun.() 68 :ets.insert(ets, [{key, value}]) 69 value 70 end 71 end 72 73 defp generate(_fun, _salt, _iterations, max_length, _block_index, acc, length) 74 when length >= max_length do 75 acc 76 |> IO.iodata_to_binary() 77 |> binary_part(0, max_length) 78 end 79 80 defp generate(fun, salt, iterations, max_length, block_index, acc, length) do 81 initial = fun.(<<salt::binary, block_index::integer-size(32)>>) 82 block = iterate(fun, iterations - 1, initial, initial) 83 length = byte_size(block) + length 84 85 generate(fun, salt, iterations, max_length, block_index + 1, [acc | block], length) 86 end 87 88 defp iterate(_fun, 0, _prev, acc), do: acc 89 90 defp iterate(fun, iteration, prev, acc) do 91 next = fun.(prev) 92 iterate(fun, iteration - 1, next, :crypto.exor(next, acc)) 93 end 94 95 # TODO: remove when we require OTP 22.1 96 if Code.ensure_loaded?(:crypto) and function_exported?(:crypto, :mac, 4) do 97 defp hmac_fun(digest, key), do: &:crypto.mac(:hmac, digest, key, &1) 98 else 99 defp hmac_fun(digest, key), do: &:crypto.hmac(digest, key, &1) 100 end 101 end