From faffe186d409d3df96817ac730072fb0569a529f Mon Sep 17 00:00:00 2001 From: fdie <5943122+fdie@users.noreply.github.com> Date: Thu, 19 Mar 2026 01:31:49 +0100 Subject: [PATCH 1/3] use the crypto library native call `pbkdf2_hmac` if available --- rebar.config | 1 + src/scram.erl | 90 ++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/rebar.config b/rebar.config index bf021e5..82f5425 100644 --- a/rebar.config +++ b/rebar.config @@ -22,6 +22,7 @@ {erl_opts, [debug_info, {src_dirs, ["asn1", "src"]}, nowarn_export_all, {platform_define, "^(R|1|2[012])", 'USE_OLD_CRYPTO_HMAC'}, + {platform_define, "^(R|1|2[0123])", 'NO_PBKDF2_HMAC'}, {platform_define, "^(R|1|2[01])", 'USE_GETHOSTBYNAME'}, {platform_define, "^(R|1|2[0123]|24\.[012])", 'USE_ADDRPORTCONNECT'}, {i, "include"}]}. diff --git a/src/scram.erl b/src/scram.erl index 63d66db..900006f 100644 --- a/src/scram.erl +++ b/src/scram.erl @@ -25,6 +25,8 @@ server_signature/3, client_signature/3, client_key/2, client_key_xor/2]). +-compile({nowarn_unused_function, [{hi_legacy,4}, {hi_legacy_round,4}]}). + -type algo() :: sha | sha256 | sha512. -spec salted_password(algo(), binary(), binary(), non_neg_integer()) -> binary(). @@ -60,15 +62,29 @@ client_key_xor(ClientProof, ClientSignature) -> server_signature(Algo, ServerKey, AuthMessage) -> sha_mac(Algo, ServerKey, AuthMessage). +-ifdef(NO_PBKDF2_HMAC). hi(Algo, Password, Salt, IterationCount) -> + hi_legacy(Algo, Password, Salt, IterationCount). +-else. +hi(_Algo, _Password, _Salt, 0) -> + <<>>; +hi(sha, Password, Salt, IterationCount) -> + crypto:pbkdf2_hmac(sha, Password, Salt, IterationCount, 20); +hi(sha256, Password, Salt, IterationCount) -> + crypto:pbkdf2_hmac(sha256, Password, Salt, IterationCount, 32); +hi(sha512, Password, Salt, IterationCount) -> + crypto:pbkdf2_hmac(sha512, Password, Salt, IterationCount, 64). +-endif. + +hi_legacy(Algo, Password, Salt, IterationCount) -> U1 = sha_mac(Algo, Password, <>), - crypto:exor(U1, hi_round(Algo, Password, U1, IterationCount - 1)). + crypto:exor(U1, hi_legacy_round(Algo, Password, U1, IterationCount - 1)). -hi_round(Algo, Password, UPrev, 1) -> +hi_legacy_round(Algo, Password, UPrev, 1) -> sha_mac(Algo, Password, UPrev); -hi_round(Algo, Password, UPrev, IterationCount) -> +hi_legacy_round(Algo, Password, UPrev, IterationCount) -> U = sha_mac(Algo, Password, UPrev), - crypto:exor(U, hi_round(Algo, Password, U, IterationCount - 1)). + crypto:exor(U, hi_legacy_round(Algo, Password, U, IterationCount - 1)). -ifdef(USE_OLD_CRYPTO_HMAC). sha_mac(Algo, Key, Data) -> @@ -77,3 +93,69 @@ sha_mac(Algo, Key, Data) -> sha_mac(Algo, Key, Data) -> crypto:mac(hmac, Algo, Key, Data). -endif. + +-ifdef(EUNIT). +-include_lib("eunit/include/eunit.hrl"). + +scram_algo_test_() -> [ + {<<"scram_algo_", (atom_to_binary(Algo))/binary, "_test">>, + fun() -> + Password = <<"pencil">>, + Salt = base64:decode(<<"QSXCR+Q6sek8bf92">>), + Iterations = Iterations, + SaltedPassword = salted_password(Algo, Password, Salt, Iterations), + ?assertEqual(DkLen, byte_size(SaltedPassword)), + ClientKey = client_key(Algo, SaltedPassword), + ?assertEqual(DkLen, byte_size(ClientKey)), + StoredKey = stored_key(Algo, ClientKey), + ?assertEqual(DkLen, byte_size(StoredKey)), + ServerKey = server_key(Algo, SaltedPassword), + ?assertEqual(DkLen, byte_size(ServerKey)), + AuthMessage = <<"p,y,t,h,o,n,r,,">>, + ServerSig = server_signature(Algo, ServerKey, AuthMessage), + ?assertEqual(DkLen, byte_size(ServerSig)), + ClientSig = client_signature(Algo, StoredKey, AuthMessage), + ?assertEqual(DkLen, byte_size(ClientSig)), + ?assertEqual(ServerKey, crypto:mac(hmac, Algo, SaltedPassword, <<"Server Key">>)), + ?assertEqual(StoredKey, crypto:hash(Algo, client_key(Algo, SaltedPassword))) + end + } || {Algo, DkLen, Iterations} <- [{sha, 20, 4096}, {sha256, 32, 4096}, {sha512, 64, 10000}]]. + +client_key_xor_test() -> + Key1 = <<1,2,3,4,5,6,7,8,9,10>>, + Key2 = <<10,9,8,7,6,5,4,3,2,1>>, + Result = client_key_xor(Key1, Key2), + ?assertEqual(<<11,11,11,3,3,3,3,11,11,11>>, Result), + ?assertEqual(Key1, client_key_xor(Result, Key2)). + +scram_zero_iterations_test() -> + Password = <<"test">>, + Salt = <<"salt">>, + ?assertEqual(<<>>, salted_password(sha, Password, Salt, 0)), + ?assertEqual(<<>>, salted_password(sha256, Password, Salt, 0)), + ?assertEqual(<<>>, salted_password(sha512, Password, Salt, 0)). + +-ifndef(NO_PBKDF2_HMAC). +pbkdf2_hmac_vs_hi_legacy_test_() -> [ + {timeout, 30, + {<<"scram_hi_", (atom_to_binary(Algo))/binary, "_test">>, + fun() -> + lists:foreach( + fun(_) -> + Password = << <<(rand:uniform(255))>> || _ <- lists:seq(1, rand:uniform(128)) >>, + Salt = << <<(rand:uniform(255))>> || _ <- lists:seq(1, rand:uniform(64)) >>, + Iterations = 1 + rand:uniform(10000), + SaltedPassword1 = hi(Algo, Password, Salt, Iterations), + SaltedPassword2 = hi_legacy(Algo, Password, Salt, Iterations), + ?assertEqual(byte_size(SaltedPassword1), DkLen), + ?assertEqual(SaltedPassword1, SaltedPassword2) + end, + lists:seq(1, 500) + ) + end + } + } || {Algo, DkLen} <- [{sha, 20}, {sha256, 32}, {sha512, 64}] +]. +-endif. + +-endif. From 429da5e7970212218996551f3471efed01545885 Mon Sep 17 00:00:00 2001 From: fdie <5943122+fdie@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:29:55 +0100 Subject: [PATCH 2/3] fix build on OTP-20 --- src/scram.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scram.erl b/src/scram.erl index 900006f..bc3e423 100644 --- a/src/scram.erl +++ b/src/scram.erl @@ -98,7 +98,7 @@ sha_mac(Algo, Key, Data) -> -include_lib("eunit/include/eunit.hrl"). scram_algo_test_() -> [ - {<<"scram_algo_", (atom_to_binary(Algo))/binary, "_test">>, + {<<"scram_algo_", (atom_to_binary(Algo, latin1))/binary, "_test">>, fun() -> Password = <<"pencil">>, Salt = base64:decode(<<"QSXCR+Q6sek8bf92">>), @@ -138,7 +138,7 @@ scram_zero_iterations_test() -> -ifndef(NO_PBKDF2_HMAC). pbkdf2_hmac_vs_hi_legacy_test_() -> [ {timeout, 30, - {<<"scram_hi_", (atom_to_binary(Algo))/binary, "_test">>, + {<<"scram_hi_", (atom_to_binary(Algo, latin1))/binary, "_test">>, fun() -> lists:foreach( fun(_) -> From ee0eb35c222ea3bf05981d7beff1964289480f1a Mon Sep 17 00:00:00 2001 From: fdie <5943122+fdie@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:32:51 +0100 Subject: [PATCH 3/3] introduced in OTP 24.2 thus discard 24.0 & 24.1 --- rebar.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebar.config b/rebar.config index 82f5425..852cdfe 100644 --- a/rebar.config +++ b/rebar.config @@ -22,7 +22,7 @@ {erl_opts, [debug_info, {src_dirs, ["asn1", "src"]}, nowarn_export_all, {platform_define, "^(R|1|2[012])", 'USE_OLD_CRYPTO_HMAC'}, - {platform_define, "^(R|1|2[0123])", 'NO_PBKDF2_HMAC'}, + {platform_define, "^(R|1|2[0123]|24.[01])", 'NO_PBKDF2_HMAC'}, {platform_define, "^(R|1|2[01])", 'USE_GETHOSTBYNAME'}, {platform_define, "^(R|1|2[0123]|24\.[012])", 'USE_ADDRPORTCONNECT'}, {i, "include"}]}.