Hashing delle password e derivazione delle chiavi

Queste quattro funzioni risolvono due problemi diversi che è facile confondere.

Memorizzare una password deve essere lento e memory hard, in modo che il furto del database non consegni le password. Utilizzare Argon2id come prima scelta, scrypt come seconda, e PBKDF2 solo dove uno standard lo richiede specificamente.

Derivare una chiave da un segreto già casuale e ad alta entropia, ad esempio l'output di un key agreement X25519, deve essere veloce: HKDF è lo strumento giusto, e puntare un hash di password lento verso un segreto già casuale spreca tempo di CPU senza alcun beneficio in termini di sicurezza.

Tutte le funzioni risiedono in sgcCrypto_KDF, sgcCrypto_Scrypt, sgcCrypto_Argon2 e sgcCrypto_HKDF.

PBKDF2

Rafforza una password ripetendo un HMAC molte volte. Costa solo tempo di CPU, quindi hardware dedicato (GPU, FPGA) la attacca a un costo molto più basso di quanto costi a un server legittimo verificare una password. È il più debole dei tre hash di password qui presenti, mantenuto perché alcuni standard (WPA2, diversi formati PKCS) lo richiedono ancora per nome.


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 è il parametro di costo memoria/CPU: 16384 con r=8 richiede circa 16 MB di RAM per hash, il che è ciò che rende costoso, e non solo lento, un attacco su larga scala con GPU, perché l'attaccante deve pagare per la memoria, non solo per i core di calcolo.


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 ha vinto la Password Hashing Competition del 2015 ed è attualmente la prima scelta per memorizzare le password. Argon2id è la variante da utilizzare: unisce l'indirizzamento dipendente dai dati di Argon2d (efficace contro il cracking su GPU) con il primo passaggio indipendente dai dati di Argon2i (resistente a un attaccante side-channel in grado di osservare i pattern di accesso alla memoria). L'RFC 9106 raccomanda almeno 19 MiB di memoria con 2 iterazioni per i login interattivi, oppure 64 MiB con 3 iterazioni dove il costo aggiuntivo è accettabile.


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 e Argon2i sono disponibili anche tramite l'entry point generale per i chiamanti che necessitano di una variante specifica:


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

HKDF

HKDF suddivide un unico buon segreto in tante chiavi indipendenti quante ne richiede un protocollo. Il parametro aInfo è ciò che le mantiene indipendenti: lo stesso segreto con una stringa info diversa produce una chiave non correlata, il che è esattamente ciò che impedisce a una chiave derivata per uno scopo (ad esempio, cifrare un messaggio) di essere utilizzabile anche per un altro (ad esempio, autenticarlo).


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;

I passaggi di extract ed expand dietro sgcHKDF_SHA256 sono disponibili anche separatamente, per i protocolli che devono eseguirli in momenti diversi:


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

KDF1, KDF2 e ANSI X9.63

Tre funzioni di derivazione delle chiavi in modalità contatore, dall'ISO 18033-2 e dall'IEEE 1363a, anch'esse in sgcCrypto_KDF. Sono quelle che RSA-KEM, ECIES e i più datati schemi ANSI indicano per nome, il che è l'unico motivo per cui sceglierle. HKDF sopra resta la scelta migliore per qualsiasi cosa nuova, perché separa l'estrazione dall'espansione anziché eseguire l'hash del segreto direttamente sull'output.

KDF1 e KDF2 differiscono in un solo dettaglio, ed è una vera trappola di interoperabilità: KDF1 fa partire il proprio contatore da 0, KDF2 da 1. Gli output sono completamente diversi, ciascuna funzione appare corretta se considerata isolatamente, e una specifica non sempre chiarisce quale delle due intenda, quindi verificare la specifica anziché il nome. X9.63 è KDF2 con una stringa shared-info aggiunta dopo il contatore, la forma che ECIES e i più datati TLS suite ECDH specificano.


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: la coordinata X dell'ECDH più lo shared info definito dallo schema }
  oKey := sgcKDF_X963(oSharedSecret, oSharedInfo, 32, khSHA256);
end;