Skip to content

Commit 2e4559c

Browse files
authored
Use builtin :crypto functions (when available) for PBKDF2 and secure constant-time comparison (#37)
1 parent ae684cd commit 2e4559c

3 files changed

Lines changed: 87 additions & 22 deletions

File tree

lib/plug/crypto.ex

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ defmodule Plug.Crypto do
66
`Plug.Crypto.MessageEncryptor`, and `Plug.Crypto.MessageVerifier`.
77
"""
88

9-
import Bitwise
109
alias Plug.Crypto.{KeyGenerator, MessageVerifier, MessageEncryptor}
1110

1211
@doc """
@@ -113,16 +112,12 @@ defmodule Plug.Crypto do
113112
@spec masked_compare(binary(), binary(), binary()) :: boolean()
114113
def masked_compare(left, right, mask)
115114
when is_binary(left) and is_binary(right) and is_binary(mask) do
116-
byte_size(left) == byte_size(right) and masked_compare(left, right, mask, 0)
115+
byte_size(left) == byte_size(right) and byte_size(right) == byte_size(mask) and
116+
crypto_exor_hash_equals(left, right, mask)
117117
end
118118

119-
defp masked_compare(<<x, left::binary>>, <<y, right::binary>>, <<z, mask::binary>>, acc) do
120-
xorred = bxor(x, bxor(y, z))
121-
masked_compare(left, right, mask, acc ||| xorred)
122-
end
123-
124-
defp masked_compare(<<>>, <<>>, <<>>, acc) do
125-
acc === 0
119+
defp crypto_exor_hash_equals(x, y, z) do
120+
crypto_hash_equals(mask(x, y), z)
126121
end
127122

128123
@doc """
@@ -132,16 +127,28 @@ defmodule Plug.Crypto do
132127
"""
133128
@spec secure_compare(binary(), binary()) :: boolean()
134129
def secure_compare(left, right) when is_binary(left) and is_binary(right) do
135-
byte_size(left) == byte_size(right) and secure_compare(left, right, 0)
130+
byte_size(left) == byte_size(right) and crypto_hash_equals(left, right)
136131
end
137132

138-
defp secure_compare(<<x, left::binary>>, <<y, right::binary>>, acc) do
139-
xorred = bxor(x, y)
140-
secure_compare(left, right, acc ||| xorred)
141-
end
133+
# TODO: remove when we require OTP 25.0
134+
if Code.ensure_loaded?(:crypto) and function_exported?(:crypto, :hash_equals, 2) do
135+
defp crypto_hash_equals(x, y) do
136+
:crypto.hash_equals(x, y)
137+
end
138+
else
139+
defp crypto_hash_equals(x, y) do
140+
legacy_secure_compare(x, y, 0)
141+
end
142+
143+
defp legacy_secure_compare(<<x, left::binary>>, <<y, right::binary>>, acc) do
144+
import Bitwise
145+
xorred = bxor(x, y)
146+
legacy_secure_compare(left, right, acc ||| xorred)
147+
end
142148

143-
defp secure_compare(<<>>, <<>>, acc) do
144-
acc === 0
149+
defp legacy_secure_compare(<<>>, <<>>, acc) do
150+
acc === 0
151+
end
145152
end
146153

147154
@doc """

lib/plug/crypto/key_generator.ex

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,20 @@ defmodule Plug.Crypto.KeyGenerator do
3131
3232
"""
3333
def generate(secret, salt, opts \\ []) do
34+
force_legacy_mode = Keyword.get(opts, :force_legacy_mode, false)
3435
iterations = Keyword.get(opts, :iterations, 1000)
3536
length = Keyword.get(opts, :length, 32)
3637
digest = Keyword.get(opts, :digest, :sha256)
3738
cache = Keyword.get(opts, :cache)
38-
generate(secret, salt, iterations, length, digest, cache)
39+
generate(secret, salt, iterations, length, digest, force_legacy_mode, cache)
3940
end
4041

4142
@doc false
4243
def generate(secret, salt, iterations, length, digest, cache) do
44+
generate(secret, salt, iterations, length, digest, false, cache)
45+
end
46+
47+
defp generate(secret, salt, iterations, length, digest, force_legacy_mode, cache) do
4348
cond do
4449
not is_integer(iterations) or iterations < 1 ->
4550
raise ArgumentError, "iterations must be an integer >= 1"
@@ -49,13 +54,23 @@ defmodule Plug.Crypto.KeyGenerator do
4954

5055
true ->
5156
with_cache(cache, {secret, salt, iterations, length, digest}, fn ->
52-
generate(hmac_fun(digest, secret), salt, iterations, length, 1, [], 0)
57+
pbkdf2_hmac(digest, secret, salt, iterations, length, force_legacy_mode)
5358
end)
5459
end
5560
rescue
5661
e -> reraise e, Plug.Crypto.prune_args_from_stacktrace(__STACKTRACE__)
5762
end
5863

64+
defp pbkdf2_hmac(digest, secret, salt, iterations, length, force_legacy_mode) do
65+
# TODO: remove when we require OTP 24.2
66+
if not force_legacy_mode and Code.ensure_loaded?(:crypto) and
67+
function_exported?(:crypto, :pbkdf2_hmac, 5) do
68+
:crypto.pbkdf2_hmac(digest, secret, salt, iterations, length)
69+
else
70+
legacy_pbkdf2_hmac(hmac_fun(digest, secret), salt, iterations, length, 1, [], 0)
71+
end
72+
end
73+
5974
defp with_cache(nil, _key, fun), do: fun.()
6075

6176
defp with_cache(ets, key, fun) do
@@ -70,19 +85,19 @@ defmodule Plug.Crypto.KeyGenerator do
7085
end
7186
end
7287

73-
defp generate(_fun, _salt, _iterations, max_length, _block_index, acc, length)
88+
defp legacy_pbkdf2_hmac(_fun, _salt, _iterations, max_length, _block_index, acc, length)
7489
when length >= max_length do
7590
acc
7691
|> IO.iodata_to_binary()
7792
|> binary_part(0, max_length)
7893
end
7994

80-
defp generate(fun, salt, iterations, max_length, block_index, acc, length) do
95+
defp legacy_pbkdf2_hmac(fun, salt, iterations, max_length, block_index, acc, length) do
8196
initial = fun.(<<salt::binary, block_index::integer-size(32)>>)
8297
block = iterate(fun, iterations - 1, initial, initial)
8398
length = byte_size(block) + length
8499

85-
generate(fun, salt, iterations, max_length, block_index + 1, [acc | block], length)
100+
legacy_pbkdf2_hmac(fun, salt, iterations, max_length, block_index + 1, [acc | block], length)
86101
end
87102

88103
defp iterate(_fun, 0, _prev, acc), do: acc

test/plug/crypto/key_generator_test.exs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
defmodule Plug.Crypto.KeyGeneratorTest do
22
use ExUnit.Case, async: true
33

4-
import Plug.Crypto.KeyGenerator
54
import Bitwise
65

76
@max_length bsl(1, 32) - 1
@@ -64,5 +63,49 @@ defmodule Plug.Crypto.KeyGeneratorTest do
6463
"6e88be8bad7eae9d9e10aa061224034fed48d03fcbad968b56006784539d5214ce970d912ec2049b04231d47c2eb88506945b26b2325e6adfeeba08895ff9587"
6564
end
6665

66+
test "digest :sha works" do
67+
key = generate("password", "salt", iterations: 1, length: 16, digest: :sha)
68+
assert byte_size(key) == 16
69+
assert to_hex(key) == "0c60c80f961f0e71f3a9b524af601206"
70+
end
71+
72+
test "digest :sha224 works" do
73+
key = generate("password", "salt", iterations: 1, length: 16, digest: :sha224)
74+
assert byte_size(key) == 16
75+
assert to_hex(key) == "3c198cbdb9464b7857966bd05b7bc92b"
76+
end
77+
78+
test "digest :sha256 works" do
79+
key = generate("password", "salt", iterations: 1, length: 16, digest: :sha256)
80+
assert byte_size(key) == 16
81+
assert to_hex(key) == "120fb6cffcf8b32c43e7225256c4f837"
82+
end
83+
84+
test "digest :sha384 works" do
85+
key = generate("password", "salt", iterations: 1, length: 16, digest: :sha384)
86+
assert byte_size(key) == 16
87+
assert to_hex(key) == "c0e14f06e49e32d73f9f52ddf1d0c5c7"
88+
end
89+
90+
test "digest :sha512 works" do
91+
key = generate("password", "salt", iterations: 1, length: 16, digest: :sha512)
92+
assert byte_size(key) == 16
93+
assert to_hex(key) == "867f70cf1ade02cff3752599a3a53dc4"
94+
end
95+
96+
def generate(secret, salt, opts \\ []) do
97+
key = Plug.Crypto.KeyGenerator.generate(secret, salt, opts)
98+
99+
legacy_key =
100+
Plug.Crypto.KeyGenerator.generate(
101+
secret,
102+
salt,
103+
Keyword.merge(opts, force_legacy_mode: true)
104+
)
105+
106+
assert key == legacy_key
107+
key
108+
end
109+
67110
def to_hex(value), do: Base.encode16(value, case: :lower)
68111
end

0 commit comments

Comments
 (0)