%%%------------------------------------------------------------------- %%% @author Juan Jose Comellas %%% @copyright (C) 2015 Cielo24, Inc. %%% @doc Implementation of password-based key derivation function 2 that %%% is compatible with the Python pbkdf2 library. For more information %%% see https://github.com/dlitz/python-pbkdf2. %%% @end %%%------------------------------------------------------------------- -module(cet_pbkdf2). -author('Juan Jose Comellas '). -export([encode/3, encode/5]). -export([decode/1]). -export([verify/2, verify/3]). -export([hash/3, hash/5]). -export([secure_compare/2]). -export([make_salt/1]). -export_type([hash_option/0]). -define(LENGTH, 20). -define(MAX_LENGTH, 16#ffffffff). -define(ROUNDS, 400). -define(PREFIX, "p5k2"). -type hash_option() :: {hmac, crypto:hash_algorithm()} | {rounds, non_neg_integer()} | {length, non_neg_integer()}. -spec encode(Secret :: binary(), Salt :: binary(), [hash_option()]) -> Token :: binary(). encode(Secret, Salt, Options) -> MacType = proplists:get_value(hmac, Options, sha), Rounds = proplists:get_value(rounds, Options, ?ROUNDS), Length = proplists:get_value(length, Options, ?LENGTH), encode(MacType, Secret, Salt, Rounds, Length). -spec encode(crypto:hash_algorithm(), Secret :: binary(), Salt :: binary(), Rounds :: non_neg_integer(), Length :: non_neg_integer()) -> Token :: binary(). encode(MacType, Secret, Salt, Rounds, Length) -> Hash = base64:encode(hash(MacType, Secret, Salt, Rounds, Length)), iolist_to_binary([<<"$" ?PREFIX "$">>, integer_to_binary(Rounds, 16), $$, Salt, $$, Hash]). -spec decode(Token :: binary()) -> {ok, {Hash :: binary(), Salt :: binary(), Rounds :: non_neg_integer()}} | {error, Reason :: term()}. decode(Token) -> case binary:split(Token, <<"$">>, [global]) of [<<>>, <>, HexRounds, Salt, Base64Hash] -> Rounds = binary_to_integer(HexRounds, 16), Hash = base64:decode(Base64Hash), {ok, {Hash, Salt, Rounds}}; _ -> {error, invalid_pbkdf2_token} end. %% @doc Verify that a Token was created with the given Secret. -spec verify(Token :: binary(), Secret :: binary()) -> ok | {error, Reason :: term()}. verify(Token, Secret) -> verify(Token, sha, Secret). -spec verify(Token :: binary(), crypto:hash_algorithm(), Secret :: binary()) -> ok | {error, Reason :: term()}. verify(Token, MacType, Secret) -> case decode(Token) of {ok, {Hash1, Salt, Rounds}} -> Hash2 = hash(MacType, Secret, Salt, Rounds, byte_size(Hash1)), case secure_compare(Hash1, Hash2) of true -> ok; false -> {error, pbkdf2_key_mismatch} end; Error -> Error end. %% @doc Generate a key derived from an existing secret. The PBKDF2 key %% derivation function has five input parameters: %% %% DK = PBKDF2(PRF, Password, Salt, c, dkLen) %% %% where: %% %% - PRF is a pseudorandom function of two parameters with output length hLen %% (e.g. a keyed HMAC). %% - Password is the master password from which a derived key is generated. %% - Salt is a sequence of bits, known as a cryptographic salt. %% - c is the number of iterations desired. %% - dkLen is the desired length of the derived key. %% - DK is the generated derived key. %% %% Each hLen-bit block Ti of derived key DK, is computed as follows: %% %% DK = T1 || T2 || ... || Tdklen/hlen %% Ti = F(Password, Salt, c, i) %% %% The function F is the xor (^) of c iterations of chained PRFs. %% The first iteration of PRF uses Password as the PRF key and Salt concatenated %% with i encoded as a big-endian 32-bit integer. (Note that i is a 1-based index.) %% Subsequent iterations of PRF use Password as the PRF key and the output of the %% previous PRF computation as the salt: %% %% F(Password, Salt, c, i) = U1 ^ U2 ^ ... ^ Uc %% %% where: %% %% U1 = PRF(Password, Salt || INT_32_BE(i)) %% U2 = PRF(Password, U1) %% ... %% Uc = PRF(Password, Uc-1) %% -spec hash(Secret :: binary(), Salt :: binary(), [hash_option()]) -> Hash :: binary(). hash(Secret, Salt, Options) -> MacType = proplists:get_value(hmac, Options, sha), Rounds = proplists:get_value(rounds, Options, ?ROUNDS), Length = proplists:get_value(length, Options, ?LENGTH), hash(MacType, Secret, Salt, Rounds, Length). hash(MacType, Secret, Salt, Rounds, MaxLength) when is_atom(MacType), is_binary(Secret), is_binary(Salt), is_integer(Rounds), MaxLength < ?MAX_LENGTH -> hash(MacType, Secret, Salt, Rounds, MaxLength, 1, []). hash(MacType, Secret, Salt, Rounds, MaxLength, BlockIndex, Acc) -> case iolist_size(Acc) < MaxLength of true -> Block = hash(MacType, Secret, Salt, Rounds, BlockIndex, 1, <<>>, <<>>), hash(MacType, Secret, Salt, Rounds, MaxLength, BlockIndex + 1, [Block | Acc]); false -> <> = iolist_to_binary(lists:reverse(Acc)), Key end. hash(MacType, Secret, Salt, Rounds, BlockIndex, Round, _Prev, _Acc0) when Round =:= 1 -> Initial = crypto:hmac(MacType, Secret, <>), hash(MacType, Secret, Salt, Rounds, BlockIndex, Round + 1, Initial, Initial); hash(MacType, Secret, Salt, Rounds, BlockIndex, Round, Prev, Acc0) when Round =< Rounds -> Next = crypto:hmac(MacType, Secret, Prev), Acc = crypto:exor(Next, Acc0), hash(MacType, Secret, Salt, Rounds, BlockIndex, Round + 1, Next, Acc); hash(_MacType, _Secret, _Salt, _Rounds, _BlockIndex, _Round, _Prev, Acc) -> Acc. %% @doc Compare two binaries without breaking at the first different character %% to avoid timing attacks. -spec secure_compare(binary(), binary()) -> boolean(). secure_compare(Bin1, Bin2) -> secure_compare(Bin1, Bin2, 0). secure_compare(<>, <>, Acc) -> secure_compare(Rest1, Rest2, Acc bor (Char1 bxor Char2)); secure_compare(_Bin1, _Bin2, Acc) -> Acc =:= 0. -spec make_salt(Length :: non_neg_integer()) -> Salt :: binary(). make_salt(Length) -> base64:encode(rand_bytes(Length)). -spec rand_bytes(Length :: non_neg_integer()) -> binary(). rand_bytes(Length) when is_integer(Length), Length > 0 -> try crypto:strong_rand_bytes(Length) of Bytes -> Bytes catch _:low_entropy -> crypto:rand_bytes(Length) end. -ifdef(TEST). %%-------------------------------------------------------------------- %% Tests %%-------------------------------------------------------------------- -include_lib("eunit/include/eunit.hrl"). -define(RFC6070_TEST_VECTORS, [ {[sha, <<"password">>, <<"salt">>, 1, 20], <<"0c60c80f961f0e71f3a9b524af6012062fe037a6">>}, {[sha, <<"password">>, <<"salt">>, 2, 20], <<"ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957">>}, {[sha, <<"password">>, <<"salt">>, 4096, 20], <<"4b007901b765489abead49d926f721d065a429c1">>}, {[sha, <<"passwordPASSWORDpassword">>, <<"saltSALTsaltSALTsaltSALTsaltSALTsalt">>, 4096, 25], <<"3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038">>}, {[sha, <<"pass\0word">>, <<"sa\0lt">>, 4096, 16], <<"56fa6aa75548099dcc37d7f03425e0c3">>}, {[sha, <<"password">>, <<"salt">>, 16777216, 20], <<"eefe3d61cd4da4e4e9945b3d6ba2158c2634e984">>} ]). pbkdf2_hex(Args) -> io:format("Running test with Args = ~p~n", [Args]), Key = apply(mdps_pbkdf2, hash, Args), bstr:hexencode(Key). rfc6070_correctness_test_() -> [ {timeout, 60, ?_assertEqual(Expected, pbkdf2_hex(Args))} || {Args, Expected} <- ?RFC6070_TEST_VECTORS ]. -define(RFC3962_TEST_VECTORS, [ {[sha, <<"password">>, <<"ATHENA.MIT.EDUraeburn">>, 1, 16], <<"cdedb5281bb2f801565a1122b2563515">>}, {[sha, <<"password">>, <<"ATHENA.MIT.EDUraeburn">>, 1, 32], <<"cdedb5281bb2f801565a1122b2563515" "0ad1f7a04bb9f3a333ecc0e2e1f70837">>}, {[sha, <<"password">>, <<"ATHENA.MIT.EDUraeburn">>, 2, 16], <<"01dbee7f4a9e243e988b62c73cda935d">>}, {[sha, <<"password">>, <<"ATHENA.MIT.EDUraeburn">>, 2, 32], <<"01dbee7f4a9e243e988b62c73cda935d" "a05378b93244ec8f48a99e61ad799d86">>}, {[sha, <<"password">>, <<"ATHENA.MIT.EDUraeburn">>, 1200, 16], <<"5c08eb61fdf71e4e4ec3cf6ba1f5512b">>}, {[sha, <<"password">>, <<"ATHENA.MIT.EDUraeburn">>, 1200, 32], <<"5c08eb61fdf71e4e4ec3cf6ba1f5512b" "a7e52ddbc5e5142f708a31e2e62b1e13">>}, {[sha, <<"password">>, binary:encode_unsigned(16#1234567878563412), 5, 16], <<"d1daa78615f287e6a1c8b120d7062a49">>}, {[sha, <<"password">>, binary:encode_unsigned(16#1234567878563412), 5, 32], <<"d1daa78615f287e6a1c8b120d7062a49" "3f98d203e6be49a6adf4fa574b6e64ee">>}, {[sha, <<"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX">>, <<"pass phrase equals block size">>, 1200, 16], <<"139c30c0966bc32ba55fdbf212530ac9">>}, {[sha, <<"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX">>, <<"pass phrase equals block size">>, 1200, 32], <<"139c30c0966bc32ba55fdbf212530ac9" "c5ec59f1a452f5cc9ad940fea0598ed1">>}, {[sha, <<"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX">>, <<"pass phrase exceeds block size">>, 1200, 16], <<"9ccad6d468770cd51b10e6a68721be61">>}, {[sha, <<"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX">>, <<"pass phrase exceeds block size">>, 1200, 32], <<"9ccad6d468770cd51b10e6a68721be61" "1a8b4d282601db3b36be9246915ec82a">>} ]). rfc3962_correctness_test_() -> [ {timeout, 60, ?_assertEqual(Expected, pbkdf2_hex(Args))} || {Args, Expected} <- ?RFC3962_TEST_VECTORS ]. -endif.