Packages

Gleam bindings to fast_pbkdf2 NIF of PBKDF2 (Password-Based Key Derivation Function 2) for Erlang.

Current section

Files

Jump to
pinkdf2 src pinkdf2.erl
Raw

src/pinkdf2.erl

-module(pinkdf2).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
-export([get_salt/0, with_config/5, with_defaults/2]).
-export_type([pbkdf2_keys/0, pbkdf2_error/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Password-Based Key Derivation Function 2 in Gleam for Erlang as defined in RFC 2898\n"
" https://datatracker.ietf.org/doc/html/rfc2898\n"
).
-type pbkdf2_keys() :: {pbkdf2_keys, bitstring(), binary()}.
-type pbkdf2_error() :: {unsupported_algorithm, binary()} |
key_derived_length_too_long.
-file("src/pinkdf2.gleam", 81).
?DOC(
" Generates a base64-encoded salt with a minimum size of 64 bytes.\n"
" It is provided here for convenience, but it is based on the same underlying Erlang function as `crypto.strong_rand_bytes`.\n"
).
-spec get_salt() -> binary().
get_salt() ->
extern:get_salt().
-file("src/pinkdf2.gleam", 152).
-spec new_prf(gleam@crypto:hash_algorithm()) -> fun((bitstring(), bitstring()) -> bitstring()).
new_prf(Alg) ->
fun(Key, Data) -> gleam_crypto_ffi:hmac(Data, Alg, Key) end.
-file("src/pinkdf2.gleam", 156).
-spec max_key_length() -> integer().
max_key_length() ->
_pipe = erlang:'bsl'(1, 32),
gleam@int:subtract(_pipe, 1).
-file("src/pinkdf2.gleam", 160).
-spec d_len_too_long(integer(), integer()) -> boolean().
d_len_too_long(H_len, D_len) ->
Max_len = begin
_pipe = max_key_length(),
gleam@int:multiply(_pipe, H_len)
end,
D_len > Max_len.
-file("src/pinkdf2.gleam", 167).
-spec allowed_algorithm(gleam@crypto:hash_algorithm()) -> {ok,
gleam@crypto:hash_algorithm()} |
{error, pbkdf2_error()}.
allowed_algorithm(Alg) ->
case Alg of
md5 ->
{error,
{unsupported_algorithm,
<<"Insecure algorithm. Please select Sha224, Sha256, Sha384, or Sha512."/utf8>>}};
sha1 ->
{error,
{unsupported_algorithm,
<<"Insecure algorithm. Please select Sha224, Sha256, Sha384, or Sha512."/utf8>>}};
_ ->
{ok, Alg}
end.
-file("src/pinkdf2.gleam", 120).
-spec compute_block(
fun((bitstring(), bitstring()) -> bitstring()),
bitstring(),
bitstring(),
integer(),
integer(),
integer(),
bitstring(),
bitstring()
) -> bitstring().
compute_block(Prf, Password, Salt, Iterations, Block_idx, Count, Prev, Acc) ->
case Count of
Count@1 when Count@1 > Iterations ->
Acc;
1 ->
Init = Prf(Password, <<Salt/bitstring, Block_idx:32/integer-big>>),
compute_block(
Prf,
Password,
Salt,
Iterations,
Block_idx,
2,
Init,
Init
);
_ ->
Next = Prf(Password, Prev),
compute_block(
Prf,
Password,
Salt,
Iterations,
Block_idx,
Count + 1,
Next,
crypto:exor(Next, Acc)
)
end.
-file("src/pinkdf2.gleam", 94).
-spec compute_key(
fun((bitstring(), bitstring()) -> bitstring()),
bitstring(),
bitstring(),
integer(),
integer(),
integer(),
bitstring()
) -> bitstring().
compute_key(Prf, Password, Salt, Iterations, D_len, Block_idx, Acc) ->
case erlang:byte_size(Acc) > D_len of
true ->
Bit_len = D_len * 8,
<<Key:Bit_len/bitstring, _/bitstring>> = case Acc of
<<_:Bit_len/bitstring, _/bitstring>> -> Acc;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
value => _assert_fail,
module => <<"pinkdf2"/utf8>>,
function => <<"compute_key"/utf8>>,
line => 106})
end,
Key;
false ->
Block = compute_block(
Prf,
Password,
Salt,
Iterations,
Block_idx,
1,
<<>>,
<<>>
),
compute_key(
Prf,
Password,
Salt,
Iterations,
D_len,
Block_idx + 1,
<<Block/bitstring, Acc/bitstring>>
)
end.
-file("src/pinkdf2.gleam", 42).
?DOC(
" Derives a key using the provided configuration.\n"
"\n"
" `alg` may be any algorithm from `crypto.HashAlgorithm` except for `Md5` and `Sha1`.\n"
" 'd_len' is the targeted derived key length in bytes.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" import gleam/crypto\n"
" import pinkdf2\n"
"\n"
" let salt = pinkdf2.get_salt()\n"
" let assert Ok(key) = pinkdf2.with_config(crypto.Sha512, \"password\", salt, 210_000, 32)\n"
" ```\n"
).
-spec with_config(
gleam@crypto:hash_algorithm(),
binary(),
binary(),
integer(),
integer()
) -> {ok, pbkdf2_keys()} | {error, pbkdf2_error()}.
with_config(Alg, Password, Salt, Iterations, D_len) ->
case allowed_algorithm(Alg) of
{error, E} ->
{error, E};
{ok, _} ->
Prf = new_prf(Alg),
case d_len_too_long(
begin
_pipe = Prf(<<Password/binary>>, <<Salt/binary>>),
erlang:byte_size(_pipe)
end,
D_len
) of
true ->
{error, key_derived_length_too_long};
false ->
Raw = compute_key(
Prf,
<<Password/binary>>,
<<Salt/binary>>,
Iterations,
D_len,
1,
<<>>
),
{ok,
{pbkdf2_keys,
Raw,
gleam_stdlib:bit_array_base64_encode(Raw, false)}}
end
end.
-file("src/pinkdf2.gleam", 83).
-spec with_defaults_(
gleam@crypto:hash_algorithm(),
bitstring(),
bitstring(),
integer()
) -> bitstring().
with_defaults_(Alg, Password, Salt, Iterations) ->
Prf = new_prf(Alg),
D_len = begin
_pipe = Prf(Password, Salt),
erlang:byte_size(_pipe)
end,
compute_key(Prf, Password, Salt, Iterations, D_len, 1, <<>>).
-file("src/pinkdf2.gleam", 19).
?DOC(
" Derives a key from a password and salt with default settings based on the\n"
" (OWASP recommendations)[https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2].\n"
).
-spec with_defaults(binary(), binary()) -> {ok, pbkdf2_keys()} |
{error, pbkdf2_error()}.
with_defaults(Password, Salt) ->
Raw = with_defaults_(sha256, <<Password/binary>>, <<Salt/binary>>, 600000),
{ok, {pbkdf2_keys, Raw, gleam_stdlib:bit_array_base64_encode(Raw, false)}}.