C#でPBKDF2アルゴリズムを利用する

PBKDF2アルゴリズムは、ソルト(同一パスワードから同一ハッシュ値が生成されるのを避けるためのユーザーごとに異なる値)とストレッチ(= イテレーション)を備えたパスワードハッシュ方法の一つです。
C#でPBKDF2アルゴリズムを利用するには、System.Security.CryptographyRfc2898DeriveBytesという何とも直感的ではない名前のクラスを利用します。

github.com

Rfc2898DeriveBytesはランダムなソルトを生成する機能も持っています。
当然検証時には同一のソルトでなければ同じハッシュ値にならないのでハッシュ値と同様にソルトもDBに保存する必要があります(ストレッチ回数とハッシュアルゴリズム情報も保存する方が良いかもしれません)。
Rfc2898DeriveBytesはデフォルトのハッシュアルゴリズムSHA1とされています。SHA1は今日では安全とはみなされていないのでご注意ください。

public class HashedPassword
{
    public HashedPassword(byte[] has, byte[] salt, int iterations, string hashAlgorithmName)
    {
        Hash = has;
        Salt = salt;
        Iterations = iterations;
        HashAlgorithmName = hashAlgorithmName;
    }

    public byte[] Hash { get; }

    public byte[] Salt { get; }

    public int Iterations { get; }

    public string HashAlgorithmName { get; }
}

public static HashedPassword HashPassword(string password, int saltSize = 32, int iterations = 10000, string hashAlgorithmName = "SHA256")
{
    var hashAlgorithm = new HashAlgorithmName(hashAlgorithmName);
    using var rfc2898DeriveBytes = new Rfc2898DeriveBytes(password, saltSize, iterations, hashAlgorithm);
    var hash = rfc2898DeriveBytes.GetBytes(32);
    var salt = rfc2898DeriveBytes.Salt;

    return new HashedPassword(hash, salt, iterations, hashAlgorithm.Name);
}

public static HashedPassword HashPassword(string password, byte[] salt, int iterations, string hashAlgorithmName)
{
    var hashAlgorithm = new HashAlgorithmName(hashAlgorithmName);
    using var rfc2898DeriveBytes = new Rfc2898DeriveBytes(password, salt, iterations, hashAlgorithm);
    var hash = rfc2898DeriveBytes.GetBytes(32);

    return new HashedPassword(hash, salt, iterations, hashAlgorithm.Name);
}

public static bool VerifyHash(byte[] hashedPassword1, byte[] hashedPassword2)
{
    return hashedPassword1.SequenceEqual(hashedPassword2);
}

利用方法はこんな感じです。
入力パスワードを元にハッシュを生成しますが、初回はランダムなソルトも自動的に生成されます。
bcryptアルゴリズムなどとはことなり出力地にソルトやストレッチ回数などの情報は含まれていないので、各自でDBに保存する必要があります。
検証時には初回時に作成したハッシュ値とソルト(ストレッチ回数、ハッシュアルゴリズム)が必要になります。

var input_userId = "foo@bar.com";
var input_password = "YourSecretP@$$W0rd";
var hashedPassword = HashPassword(input_password);
            
Console.WriteLine($"hash: {Convert.ToBase64String(hashedPassword.Hash)}, salt: {Convert.ToBase64String(hashedPassword.Salt)}");
// -> hash: cHEmiYsijk9VTupKpreq0jxenkVrTZ2ZXk1pJavh7vY=, salt: phyMROcRnZt6sw2/FeYWrksH/6wxFGwkQqqcrmuXVFA=

// ユーザー情報と生成されたハッシュ情報(ソルト、ストレッチ回数、ハッシュアルゴリズム含む)をDBに保存しておく。今回は省略する
// SaveUserPasswordHash(input_userId, hashedPassword);

// 検証時にはDBからハッシュ情報をロード。今回は省略する
// var savedHash = GetUserPasswordHash(input_userId);
var savedHash = hashedPassword;

{
    // 入力されたパスワードとDBに保存されたソルト、ストレッチ回数、ハッシュアルゴリズムをもとにハッシュ値を生成する。
    var reproducedHash1 = HashPassword("YourSecretP@$$W0rd", savedHash.Salt, savedHash.Iterations, savedHash.HashAlgorithmName);
    Console.WriteLine($"hash: {Convert.ToBase64String(reproducedHash1.Hash)}");

    // 条件(入力されたパスワード、ソルト、ストレッチ回数、ハッシュアルゴリズム)が全て一致するので、同一のハッシュ値が生成される
    var isVerified = VerifyHash(reproducedHash1.Hash, savedHash.Hash);
    Console.WriteLine($"same password: {isVerified}");

    // -> hash: cHEmiYsijk9VTupKpreq0jxenkVrTZ2ZXk1pJavh7vY=
    // -> same password: True
}

{
    var reproducedHash2 = HashPassword("YourSecretPassword", savedHash.Salt, savedHash.Iterations, savedHash.HashAlgorithmName);
    Console.WriteLine($"hash: {Convert.ToBase64String(reproducedHash2.Hash)}");

    // 入力されたパスワードが異なるので、異なるハッシュ値が生成される
    var isVerified = VerifyHash(reproducedHash2.Hash, savedHash.Hash);
    Console.WriteLine($"same password: {isVerified}");

    // -> hash: zspK13LuQvVM5hFUHCckIPLS5SzVrgN3ghif+L2FuUQ=
    // -> same password: False
}

{
    // ソルトをちょっと変える(ユーザーごとに異なるソルトが発行されている)
    var salt = savedHash.Salt;
    salt[0] = 0x01;
    salt[1] = 0x23;
    var reproducedHash3 = HashPassword("YourSecretP@$$W0rd", salt, savedHash.Iterations, savedHash.HashAlgorithmName);
    Console.WriteLine($"hash: {Convert.ToBase64String(reproducedHash3.Hash)}, salt: {Convert.ToBase64String(reproducedHash3.Salt)}");

    // 入力されたパスワードが同一でも、ソルトが異なるので異なるハッシュ値が生成される
    var isVerified = VerifyHash(reproducedHash3.Hash, savedHash.Hash);
    Console.WriteLine($"same password: {isVerified}");

    // -> hash: UYr9Y2AXusTHgd4WLYo4/el16RyykRe4jKZex9pw0iA=, salt: ASOMROcRnZt6sw2/FeYWrksH/6wxFGwkQqqcrmuXVFA=
    // -> same password: False
}

{
    // ストレッチ回数をちょっと変える
    var iteration = savedHash.Iterations;
    iteration += 1;
    var reproducedHash4 = HashPassword("YourSecretP@$$W0rd", savedHash.Salt, iteration, savedHash.HashAlgorithmName);
    Console.WriteLine($"hash: {Convert.ToBase64String(reproducedHash4.Hash)}");

    // 入力されたパスワードが同一でも、ストレッチ回数が異なるので異なるハッシュ値が生成される
    var isVerified = VerifyHash(reproducedHash4.Hash, savedHash.Hash);
    Console.WriteLine($"same password: {isVerified}");

    // -> hash: aJoZRaX7wMBZk/dizs8TiuZJJDQo4vLA9FYElqBD4SU=
    // -> same password: False
}

本の紹介

セキュリティやパスワードハッシュに興味があるけどよく知らないという方にはこの本がお勧めです。
「ハッシュ?ソルト?ストレッチ?何それ美味しいの?」というレベルの方でもそれ以上を求める方でも得るものがあると思います。
このリンクから購入しても私には一円の得にもならないですが、良い本には変わりありません。