-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, <>), 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, <> = 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, <> ) 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(<>, <>), erlang:byte_size(_pipe) end, D_len ) of true -> {error, key_derived_length_too_long}; false -> Raw = compute_key( Prf, <>, <>, 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, <>, <>, 600000), {ok, {pbkdf2_keys, Raw, gleam_stdlib:bit_array_base64_encode(Raw, false)}}.