Hash de Contraseñas y Derivación de Claves

Estas cuatro funciones resuelven dos problemas distintos que es fácil confundir.

 

Almacenar una contraseña necesita ser lento y exigente en memoria, de modo que robar la base de datos no entregue las contraseñas. Use Argon2id en primer lugar, scrypt en segundo lugar, y PBKDF2 solo cuando un estándar lo exija específicamente.

 

Derivar una clave a partir de un secreto ya aleatorio y de alta entropía, por ejemplo la salida de un acuerdo de claves X25519, necesita ser rápido: HKDF es la herramienta adecuada, y dirigir un hash de contraseñas lento hacia un secreto que ya es aleatorio desperdicia tiempo de CPU sin ningún beneficio de seguridad.

 

Todas las funciones residen en sgcCrypto_KDF, sgcCrypto_Scrypt, sgcCrypto_Argon2 y sgcCrypto_HKDF.

 

PBKDF2

 

Estira una contraseña repitiendo un HMAC muchas veces. Solo cuesta tiempo de CPU, de modo que el hardware personalizado (GPU, FPGA) lo ataca de forma mucho más barata de lo que le cuesta a un servidor legítimo comprobar una contraseña. Es el más débil de los tres hashes de contraseñas aquí, mantenido porque algunos estándares (WPA2, varios formatos PKCS) todavía lo exigen por su nombre.

 


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 es el parámetro de coste de memoria/CPU: 16384 con r=8 necesita aproximadamente 16 MB de RAM por hash, que es lo que hace que un ataque de GPU a gran escala resulte caro y no solo lento, porque el atacante tiene que pagar por memoria, no solo por núcleos de cómputo.

 


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 ganó la Password Hashing Competition de 2015 y es actualmente la primera opción para almacenar contraseñas. Argon2id es la variante que se debe usar: combina el direccionamiento dependiente de los datos de Argon2d (resistente frente al cracking por GPU) con la primera pasada independiente de los datos de Argon2i (resistente frente a un atacante de canal lateral que pueda observar los patrones de acceso a memoria). RFC 9106 recomienda al menos 19 MiB de memoria con 2 iteraciones para inicios de sesión interactivos, o 64 MiB con 3 iteraciones cuando el coste adicional sea aceptable.

 


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 y Argon2i también están disponibles a través del punto de entrada general para quienes necesiten una variante específica:

 


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

 

HKDF

 

HKDF divide un buen secreto en tantas claves independientes como necesite un protocolo. El parámetro aInfo es lo que las mantiene independientes: el mismo secreto con una cadena de información distinta produce una clave sin relación, que es exactamente lo que impide que una clave derivada para un propósito (por ejemplo, cifrar un mensaje) también pueda usarse para otro (por ejemplo, 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;

 

Los pasos de extracción y expansión detrás de sgcHKDF_SHA256 también están disponibles por separado, para protocolos que necesiten ejecutarlos en momentos distintos:

 


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

 

KDF1, KDF2 y ANSI X9.63

 

Tres funciones de derivación de claves en modo contador de ISO 18033-2 e IEEE 1363a, también en sgcCrypto_KDF. Son las que nombran RSA-KEM, ECIES y los esquemas ANSI más antiguos, que es el único motivo para recurrir a ellas. HKDF más arriba sigue siendo la mejor opción para cualquier cosa nueva, porque separa la extracción de la expansión en lugar de hashear el secreto directamente hacia la salida.

 

KDF1 y KDF2 difieren exactamente en un detalle, y es una trampa de interoperabilidad real: KDF1 empieza su contador en 0, KDF2 en 1. Las salidas son completamente distintas, cada función parece correcta aislada, y una especificación no siempre indica cuál de las dos quiere decir, así que compruebe la especificación en lugar de guiarse por el nombre. X9.63 es KDF2 con una cadena de información compartida añadida tras el contador, que es la forma que especifican ECIES y las suites TLS ECDH más antiguas.

 


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;