この4つの関数は、混同されがちな2つの異なる問題を解決します。
パスワードの保存は、データベースを盗まれてもパスワードそのものが渡らないように、低速でメモリを多く必要とするものでなければなりません。まず Argon2id を、次に scrypt を使用し、PBKDF2 は標準が明示的に要求している場合にのみ使用してください。
既にランダムで高いエントロピーを持つ秘密(たとえば X25519 鍵合意の出力)から鍵を導出する場合は、高速である必要があります。HKDF が適したツールであり、既にランダムな秘密に低速なパスワードハッシュを適用することは、セキュリティ上の利点を何も生まないまま CPU 時間を浪費するだけです。
すべての関数はsgcCrypto_KDF、sgcCrypto_Scrypt、sgcCrypto_Argon2およびsgcCrypto_HKDFに含まれています。
PBKDF2
HMAC を何度も繰り返すことでパスワードをストレッチします。かかるコストは CPU 時間だけであるため、専用ハードウェア(GPU、FPGA)による攻撃は、正規のサーバーが1回のパスワード確認にかけるコストよりもはるかに安価に行われます。ここで挙げる3つのパスワードハッシュの中では最も弱いものですが、一部の標準(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 では 1 回のハッシュ計算におよそ 16 MB の 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 のデータ非依存な第1パスを組み合わせたものです。RFC 9106 では、対話的なログイン用途では 2 回の反復で少なくとも 19 MiB のメモリを、追加コストが許容できる場合は 3 回の反復で 64 MiB を推奨しています。
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 は、1つの良質な秘密を、プロトコルが必要とする数だけの独立した鍵に分割します。aInfoパラメータが、これらを独立させている要因です。同じ秘密であっても異なる info 文字列を使えば無関係な鍵が生成されます。これにより、あるひとつの目的(たとえばメッセージの暗号化)のために導出された鍵が、別の目的(たとえば認証)にも使えてしまうことを防いでいます。
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 による、3種類のカウンターモード鍵導出関数で、これもsgcCrypto_KDFに含まれています。これらは RSA-KEM、ECIES、そして古い ANSI 方式が名指しで要求するものであり、それがこれらを使う唯一の理由です。新規の用途では、上記の HKDF のほうが引き続き優れた選択です。抽出と拡張を分離しており、秘密をそのまま出力にハッシュ化するわけではないためです。
KDF1 と KDF2 は、たった1つの詳細だけが異なりますが、それは実際の相互運用性の落とし穴です。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;