Password Hashing & Key Derivation

These four functions solve two different problems that are easy to confuse.

Storing a password needs to be slow and memory hard, so that stealing the database does not hand over the passwords. Use Argon2id first, scrypt second, and PBKDF2 only where a standard specifically requires it.

Deriving a key from an already random, high entropy secret, for example the output of an X25519 key agreement, needs to be fast: HKDF is the right tool, and pointing a slow password hash at a secret that is already random wastes CPU time for no security benefit.

All functions live in sgcCrypto_KDF, sgcCrypto_Scrypt, sgcCrypto_Argon2 and sgcCrypto_HKDF.

PBKDF2

Stretches a password by repeating an HMAC many times. It only costs CPU time, so custom hardware (GPUs, FPGAs) attacks it far more cheaply than it costs a legitimate server to check one password. It is the weakest of the three password hashes here, kept because some standards (WPA2, several PKCS formats) still require it by name.


function sgcPBKDF2_SHA1(const aPassword, aSalt: TBytes; aIterations, aDKLen: Integer): TBytes;
function sgcPBKDF2_SHA256(const aPassword, aSalt: TBytes; aIterations, aDKLen: Integer): TBytes;
function sgcPBKDF2_SHA384(const aPassword, aSalt: TBytes; aIterations, aDKLen: Integer): TBytes;
function sgcPBKDF2_SHA512(const aPassword, aSalt: TBytes; aIterations, aDKLen: Integer): TBytes;

scrypt

N is the memory/CPU cost parameter: 16384 with r=8 needs roughly 16 MB of RAM per hash, which is what makes a large scale GPU attack expensive rather than merely slow, because the attacker has to pay for memory, not only for compute cores.


function sgcScrypt(const aPassword, aSalt: TBytes; aN, aR, aP, aDKLen: Integer): TBytes;

var
  oHash: TBytes;
begin
  oHash := sgcScrypt(TBytes(oPassword), oSalt, 16384, 8, 1, 32);
end;

Argon2

Argon2 won the 2015 Password Hashing Competition and is the current first choice for storing passwords. Argon2id is the variant to use: it mixes the data dependent addressing of Argon2d (strong against GPU cracking) with the data independent first pass of Argon2i (resistant to a side channel attacker who can observe memory access patterns). RFC 9106 recommends at least 19 MiB of memory with 2 iterations for interactive logins, or 64 MiB with 3 iterations where the extra cost is acceptable.


function sgcArgon2id(const aPassword, aSalt: TBytes; aMemoryKB, aIterations, aParallelism, aTagLen: Integer): TBytes;

var
  oStored, oCandidate: TBytes;
begin
  { at signup }
  oStored := sgcArgon2id(TBytes(oPassword), oSalt, 19456, 2, 1, 32);

  { at login }
  oCandidate := sgcArgon2id(TBytes(oTypedPassword), oSalt, 19456, 2, 1, 32);
  if sgcConstantTimeEquals(oStored, oCandidate) then
    { password accepted };
end;

Argon2d and Argon2i are also available through the general entry point for callers that need a specific variant:


function sgcArgon2(aKind: TsgcArgon2Kind; const aPassword, aSalt, aSecret, aAssociated: TBytes;
  aMemoryKB, aIterations, aParallelism, aTagLen: Integer): TBytes;

HKDF

HKDF splits one good secret into as many independent keys as a protocol needs. The aInfo parameter is what keeps them independent: the same secret with a different info string produces an unrelated key, which is exactly what stops a key derived for one purpose (say, encrypting a message) from also being usable for another (say, authenticating one).


function sgcHKDF_SHA256(const aIKM, aSalt, aInfo: TBytes; aLen: Integer): TBytes;
function sgcHKDF_SHA384(const aIKM, aSalt, aInfo: TBytes; aLen: Integer): TBytes;

var
  oSharedSecret, oSalt, oClientKey, oServerKey: TBytes;
begin
  oClientKey := sgcHKDF_SHA256(oSharedSecret, oSalt, TBytes(AnsiString('client write key')), 32);
  oServerKey := sgcHKDF_SHA256(oSharedSecret, oSalt, TBytes(AnsiString('server write key')), 32);
end;

The extract and expand steps behind sgcHKDF_SHA256 are also available separately, for protocols that need to run them at different times:


function sgcHKDF_Extract_SHA256(const aSalt, aIKM: TBytes): TBytes;
function sgcHKDF_Expand_SHA256(const aPRK, aInfo: TBytes; aLen: Integer): TBytes;

KDF1, KDF2 and ANSI X9.63

Three counter mode key derivation functions from ISO 18033-2 and IEEE 1363a, also in sgcCrypto_KDF. They are the ones RSA-KEM, ECIES and the older ANSI schemes name, which is the only reason to reach for them. HKDF above remains the better choice for anything new, because it separates extraction from expansion instead of hashing the secret straight into the output.

KDF1 and KDF2 differ in exactly one detail, and it is a real interoperability trap: KDF1 starts its counter at 0, KDF2 at 1. The outputs are completely different, each function looks correct in isolation, and a specification does not always spell out which one it means, so check the spec rather than the name. X9.63 is KDF2 with a shared-info string appended after the counter, which is the form ECIES and the older ECDH TLS suites specify.


function sgcKDF1(const aSeed: TBytes; aLen: Integer; aHash: TsgcKDFHash): TBytes;
function sgcKDF2(const aSeed: TBytes; aLen: Integer; aHash: TsgcKDFHash): TBytes;
function sgcKDF_X963(const aSharedSecret, aSharedInfo: TBytes; aLen: Integer; aHash: TsgcKDFHash): TBytes;

var
  oSharedSecret, oSharedInfo, oKey: TBytes;
begin
  { ECIES: the ECDH X coordinate plus the shared info the scheme defines }
  oKey := sgcKDF_X963(oSharedSecret, oSharedInfo, 32, khSHA256);
end;