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

BcryptアルゴリズムはPBKDF2アルゴリズムと同様に、ソルト(同一パスワードから同一ハッシュ値が生成されるのを避けるためのユーザーごとに異なる値)とストレッチ(= イテレーション)を備えたパスワードハッシュ方法の一つです。

関連記事:

mrgchr.hatenablog.com

PBKDF2アルゴリズムとの違い

利用者目線で見たBcryptアルゴリズムとPBKDF2アルゴリズムとの大きな違いは、Bcryptアルゴリズムの出力結果であるハッシュされた文字列には、「ソルトやストレッチ回数などの情報が含まれている」という点です。

下記はBcryptアルゴリズムの出力結果の一例です。

$2a$13$RqClfEoHTEBq381gT/.zFuZTzJoBcp.poMqkoU/x1k2SFkZX3zAGG

先頭の$2a$の部分はアルゴリズムバージョンで、この場合はバージョン2aが用いられています。
次の13$は、ストレッチ回数(=イテレーション回数)で「2の冪乗」回を表しています、この場合は13なので、2^13(=8,192)回のストレッチをします。
次のRqClfEoHTEBq381gT/.zFuZTzJoBcp.poMqkoU/x1k2SFkZX3zAGGは22文字のソルトとハッシュ値となっています。RqClfEoHTEBq381gT/.zFuがソルトで、続くZTzJoBcp.poMqkoU/x1k2SFkZX3zAGGハッシュ値です。
このように出力結果の文字列に様々な情報が含まれているため、DBに格納するのが非常に楽です。パスワードの検証もライブラリがやってくれます。

BcryptアルゴリズムC#にて利用する

.NET フレームワーク .NET Coreには、標準のBcrypt実装が無いようなので、NuGetより追加します。
今回は、BCrypt.Net-Nextライブラリを利用しました。

github.com

使い方も非常に簡単で、基本的にはBCrypt.HashPasswordでパスワードからハッシュを作成し、BCrypt.Verifyでパスワードとハッシュ値の一致を確認します。

var input_userId = "foo@bar.com";
var input_password = "YourSecretP@$$W0rd";
var hashedPassword = BCrypt.Net.BCrypt.HashPassword(input_password, workFactor:13);

Console.WriteLine($"hash: {hashedPassword}");
//hash: $2a$13$RqClfEoHTEBq381gT/.zFuZTzJoBcp.poMqkoU/x1k2SFkZX3zAGG

// ユーザー情報と生成されたハッシュ文字列(ソルト、ストレッチ回数、ハッシュアルゴリズムが文字列中に含まれている)をDBに保存しておく。
// SaveUserPasswordHash(input_userId, hashedPassword);

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

var savedHash = hashedPassword;

{
    var password = "YourSecretP@$$W0rd";
    // 入力パスワードとハッシュ文字列を検証するメソッドが提供されている(ハッシュ文字列中にソルトやストレッチ回数が含まれているため)。
    var isVerified = BCrypt.Net.BCrypt.Verify(password, savedHash);
    Console.WriteLine($"same password: {isVerified}");

    // -> same password: True
}

{
    var password = "YourSecretPassword";
    // パスワードが異なるので、検証結果もFalseになる
    var isVerified = BCrypt.Net.BCrypt.Verify(password, savedHash);
    Console.WriteLine($"same password: {isVerified}");

    // -> same password: False
}

workFactor:13がストレッチ回数指定に相当し、この場合は2^13回です。そこそこ計算時間がかかります。31まで指定できるらしいですが、2^31=21億強なので、現実的ではないでしょう。
またハッシュアルゴリズムはデフォルトではSHA384が用いられています。
Bcryptアルゴリズムでは内部でBlowfishと呼ばれるブロック暗号アルゴリズムが用いられているとのことですが、よく知られた制限として「多くのBlowfish実装は72バイトより大きい入力を無視する」というものがあります。
そしてそれはこのBCrypt.Net-Nextライブラリでも例外ではなく、入力パスワードの73文字目以降は無視されています。

var input_userId = "foo@bar.com";
var input_password = "123456789A123456789B123456789C123456789D123456789E123456789F123456789G123";
var hashedPassword = BCrypt.Net.BCrypt.HashPassword(input_password, workFactor: 13);

Console.WriteLine($"hash: {hashedPassword}");

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

{
    var password = "123456789A123456789B123456789C123456789D123456789E123456789F123456789G12XYZ";
    // パスワードの73文字以降が異なるが72文字目までは一致しているので検証結果はTrueとみなされる。
    var isVerified = BCrypt.Net.BCrypt.Verify(password, savedHash);
    Console.WriteLine($"same password: {isVerified}");
    // -> same password: True
}

72よりも長いパスワードをサポートするため*1には、下記のようにenhancedEntropyを指定する必要があります。

var input_userId = "foo@bar.com";
var input_password = "123456789A123456789B123456789C123456789D123456789E123456789F123456789G123";
var hashedPassword = BCrypt.Net.BCrypt.HashPassword(input_password, workFactor: 13, enhancedEntropy: true);
// または、BCrypt.Net.BCrypt.EnhancedHashPassword(input_password, workFactor: 13);

Console.WriteLine($"hash: {hashedPassword}");

var savedHash = hashedPassword;
{
    var password = "123456789A123456789B123456789C123456789D123456789E123456789F123456789G123";
    // 条件(入力されたパスワード、ソルト、ストレッチ回数、ハッシュアルゴリズム)が全て一致するので、同一のハッシュ値が生成される
    var isVerified = BCrypt.Net.BCrypt.Verify(password, savedHash, enhancedEntropy: true);
    //または、 BCrypt.Net.BCrypt.EnhancedVerify(password, savedHash)
    Console.WriteLine($"same password: {isVerified}");
    // -> same password: True
}

{
    var password = "123456789A123456789B123456789C123456789D123456789E123456789F123456789G12XYZ";
    // enhancedEntropyが指定されているため、73文字目以降も無視されない
    var isVerified = BCrypt.Net.BCrypt.Verify(password, savedHash, enhancedEntropy: true);
    //または、 BCrypt.Net.BCrypt.EnhancedVerify(password, savedHash)
    Console.WriteLine($"same password: {isVerified}");
    // -> same password: False
}

*1:例えばAmazonのパスワードは最大128文字