이 네 가지 함수는 혼동하기 쉬운 두 가지 서로 다른 문제를 해결합니다.
비밀번호 저장은 느리고 메모리 소모가 커야 합니다. 그래야 데이터베이스를 훔치더라도 비밀번호가 그대로 넘어가지 않습니다. Argon2id를 우선으로, scrypt를 차선으로 사용하고, PBKDF2는 표준이 명시적으로 요구하는 경우에만 사용하십시오.
키 유도는 이미 무작위이고 엔트로피가 높은 비밀 값(예를 들어 X25519 키 합의의 출력)으로부터 이루어지며, 빨라야 합니다: HKDF가 적합한 도구이며, 이미 무작위인 비밀 값에 느린 비밀번호 해시를 적용하는 것은 보안상 이득 없이 CPU 시간만 낭비합니다.
모든 함수는 sgcCrypto_KDF, sgcCrypto_Scrypt, sgcCrypto_Argon2, sgcCrypto_HKDF에 들어 있습니다.
PBKDF2
HMAC을 여러 번 반복해 비밀번호를 늘립니다. CPU 시간만 소모하기 때문에, 전용 하드웨어(GPU, FPGA)를 이용한 공격은 정상적인 서버가 비밀번호 하나를 확인하는 비용보다 훨씬 저렴하게 이루어집니다. 여기 소개된 세 가지 비밀번호 해시 중 가장 약하지만, 일부 표준(WPA2, 여러 PKCS 형식)이 여전히 이름으로 명시하기 때문에 남아 있습니다.
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은 메모리/CPU 비용 매개변수입니다: r=8일 때 16384는 해시 하나당 대략 16MB의 RAM을 필요로 하며, 이것이 대규모 GPU 공격을 단순히 느리게 만드는 데 그치지 않고 비용을 크게 만드는 이유입니다. 공격자는 연산 코어뿐 아니라 메모리에도 비용을 지불해야 하기 때문입니다.
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는 2015년 Password Hashing Competition에서 우승했으며, 현재 비밀번호를 저장할 때 첫 번째로 고려해야 할 선택입니다. 사용해야 할 변형은 Argon2id입니다: GPU 크래킹에 강한 Argon2d의 데이터 종속 주소 지정과, 메모리 접근 패턴을 관찰할 수 있는 사이드 채널 공격자에 강한 Argon2i의 데이터 독립적인 첫 패스를 결합합니다. RFC 9106은 대화형 로그인에는 반복 2회에 최소 19MiB의 메모리를, 추가 비용을 감수할 수 있다면 반복 3회에 64MiB를 권장합니다.
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와 Argon2i는 특정 변형이 필요한 호출자를 위해 범용 진입점을 통해서도 사용할 수 있습니다:
function sgcArgon2(aKind: TsgcArgon2Kind; const aPassword, aSalt, aSecret, aAssociated: TBytes;
aMemoryKB, aIterations, aParallelism, aTagLen: Integer): TBytes;
HKDF
HKDF는 하나의 좋은 비밀 값을 프로토콜이 필요로 하는 만큼의 독립적인 키로 나눕니다. 이들을 독립적으로 유지하는 것은 aInfo 매개변수입니다: 같은 비밀 값이라도 정보 문자열이 다르면 서로 무관한 키가 만들어지며, 이것이 바로 한 목적(예를 들어 메시지 암호화)을 위해 유도된 키가 다른 목적(예를 들어 메시지 인증)에도 사용될 수 있는 것을 막는 방법입니다.
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;
sgcHKDF_SHA256 뒤에 있는 추출(extract) 및 확장(expand) 단계는, 서로 다른 시점에 실행해야 하는 프로토콜을 위해 별도로도 제공됩니다:
function sgcHKDF_Extract_SHA256(const aSalt, aIKM: TBytes): TBytes;
function sgcHKDF_Expand_SHA256(const aPRK, aInfo: TBytes; aLen: Integer): TBytes;
KDF1, KDF2, ANSI X9.63
ISO 18033-2와 IEEE 1363a에서 나온 세 가지 카운터 모드 키 유도 함수로, 역시 sgcCrypto_KDF에 들어 있습니다. 이들은 RSA-KEM, ECIES, 그리고 오래된 ANSI 방식들이 이름으로 명시하는 것들이며, 이것이 이들을 사용해야 하는 유일한 이유입니다. 새로운 용도로는 위의 HKDF가 여전히 더 나은 선택인데, 비밀 값을 곧바로 출력으로 해시하는 대신 추출과 확장을 분리하기 때문입니다.
KDF1과 KDF2는 정확히 한 가지 세부 사항에서만 다르며, 이는 실제 상호 운용성 함정입니다: KDF1은 카운터를 0에서 시작하고, KDF2는 1에서 시작합니다. 출력은 완전히 다르고, 각 함수는 따로 놓고 보면 모두 올바른 것처럼 보이며, 사양이 어느 쪽을 의미하는지 항상 명시하는 것도 아니므로, 이름이 아니라 사양을 확인하십시오. X9.63은 카운터 뒤에 공유 정보 문자열을 덧붙인 KDF2이며, 이는 ECIES와 오래된 ECDH 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: the ECDH X coordinate plus the shared info the scheme defines }
oKey := sgcKDF_X963(oSharedSecret, oSharedInfo, 32, khSHA256);
end;