Haszowanie haseł i wyprowadzanie kluczy

Te cztery funkcje rozwiązują dwa różne problemy, które łatwo ze sobą pomylić.

 

Przechowywanie hasła musi być powolne i wymagające dużo pamięci, tak aby kradzież bazy danych nie wydała haseł. Używaj w pierwszej kolejności Argon2id, w drugiej scrypt, a PBKDF2 tylko tam, gdzie konkretny standard tego wymaga.

 

Wyprowadzanie klucza z już losowego, wysokoentropijnego sekretu, na przykład wyniku uzgodnienia klucza X25519, musi być szybkie: HKDF jest tu właściwym narzędziem, a kierowanie powolnego skrótu hasła na sekret, który już jest losowy, marnuje czas CPU bez żadnej korzyści dla bezpieczeństwa.

 

Wszystkie funkcje znajdują się w jednostkach sgcCrypto_KDF, sgcCrypto_Scrypt, sgcCrypto_Argon2 i sgcCrypto_HKDF.

 

PBKDF2

 

Rozciąga hasło, wielokrotnie powtarzając HMAC. Kosztuje wyłącznie czas CPU, więc niestandardowy sprzęt (GPU, FPGA) atakuje go znacznie taniej, niż kosztuje legalny serwer sprawdzenie jednego hasła. Jest najsłabszym z trzech opisanych tu skrótów haseł, zachowanym dlatego, że niektóre standardy (WPA2, kilka formatów PKCS) nadal wymagają go z nazwy.

 


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 to parametr kosztu pamięci/CPU: 16384 przy r=8 wymaga w przybliżeniu 16 MB pamięci RAM na jeden skrót, co sprawia, że atak GPU na dużą skalę jest kosztowny, a nie tylko powolny, ponieważ atakujący musi zapłacić za pamięć, a nie tylko za rdzenie obliczeniowe.

 


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 wygrał konkurs Password Hashing Competition w 2015 roku i jest obecnie pierwszym wyborem do przechowywania haseł. Argon2id to wariant, którego należy używać: łączy adresowanie zależne od danych z Argon2d (odporne na łamanie za pomocą GPU) z niezależnym od danych pierwszym przebiegiem z Argon2i (odporne na atakującego wykorzystującego kanał boczny, który może obserwować wzorce dostępu do pamięci). RFC 9106 zaleca co najmniej 19 MiB pamięci przy 2 iteracjach dla logowań interaktywnych albo 64 MiB przy 3 iteracjach tam, gdzie dodatkowy koszt jest akceptowalny.

 


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 i Argon2i są również dostępne przez ogólny punkt wejścia dla wywołujących, którzy potrzebują konkretnego wariantu:

 


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

 

HKDF

 

HKDF dzieli jeden dobry sekret na tyle niezależnych kluczy, ile potrzebuje protokół. Parametr aInfo jest tym, co utrzymuje ich niezależność: ten sam sekret z innym ciągiem info daje niepowiązany klucz, co dokładnie uniemożliwia, aby klucz wyprowadzony do jednego celu (powiedzmy, szyfrowania wiadomości) był też przydatny do innego (powiedzmy, uwierzytelniania tej wiadomości).

 


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;

 

Kroki extract i expand, na których opiera się sgcHKDF_SHA256, są również dostępne osobno, dla protokołów, które muszą je wykonywać w różnych momentach:

 


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

 

KDF1, KDF2 i ANSI X9.63

 

Trzy funkcje wyprowadzania klucza w trybie licznikowym z ISO 18033-2 i IEEE 1363a, również w sgcCrypto_KDF. To te, które wymieniają z nazwy RSA-KEM, ECIES i starsze schematy ANSI, i to jedyny powód, by po nie sięgać. HKDF powyżej pozostaje lepszym wyborem do wszystkiego nowego, ponieważ oddziela ekstrakcję od rozwinięcia, zamiast haszować sekret wprost na wyjście.

 

KDF1 i KDF2 różnią się dokładnie jednym szczegółem, i jest to prawdziwa pułapka interoperacyjności: KDF1 zaczyna licznik od 0, KDF2 od 1. Wyniki są zupełnie różne, każda z funkcji wygląda poprawnie w oderwaniu od drugiej, a specyfikacja nie zawsze precyzuje, o którą z nich chodzi, więc sprawdzaj specyfikację, a nie nazwę. X9.63 to KDF2 z ciągiem shared-info doklejonym po liczniku, w postaci, jaką określają ECIES i starsze zestawy szyfrów ECDH w TLS.

 


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: współrzędna X z ECDH plus shared info zdefiniowane przez schemat }
  oKey := sgcKDF_X963(oSharedSecret, oSharedInfo, 32, khSHA256);
end;