というより、パスワードハッシュを簡易に使えるテンプレという側面が強いかも。
サポート対象を絞ればより良い解がありますが、今回は間口を広げたプラクティスです。
※ パスワードハッシュや Salt ってなんぞやって方は、下記の記事で解りやすく解説されています。是非ご参照ください。
- パスワードはハッシュ化するだけで十分? | NTTデータ
- ユーザーのパスワードを安全に保管する方法について - 11月 - 2013 - ソフォス プレス リリース、セキュリティニュース、ソフォスに関するニュース記事 - Sophos Press Office | Sophos News and Press Releases
Points
- .NET Framework 4.5+ / .NET Core 1.0+ / .NET Standard 1.3+ サポート。
- Pure .NET。コピペで直ぐ試せる。サードパーティ不使用。
- PBKDF2-SHA1 を使用。
-
Modular Crypt Format ハッシュ文字列を出力。
- Salt やストレッチ回数込みの文字列を出力5する為、ハッシュ管理が容易。
- パスワードハッシュのアップグレードへの備え。
- Salt / ストレッチ回数の既定値は適当。
salt=16
(128 bits) は NIST SP800-132 の推奨最小値6。iterations=10000
は NIST SP800-63B の推奨最小値7(目標とするハッシュ計算時間によりけり)。 - passlib.hash.pbkdf2_sha1 互換(のつもり)。
Usage
class Program {
static void Main(string[] args) {
var password = "abc";
// サインアップ
var hashStr = SignUp(password);
Console.WriteLine(hashStr);
// サインイン
Console.WriteLine(SignIn(password, hashStr));
}
static string SignUp(string password) {
return PBKDF2.Hash(password).ToString();
}
static bool SignIn(string password, string hashStr) {
return PBKDF2.Verify(password, hashStr);
}
}
Code
using System;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
namespace InAsync.Security.PasswordHashing {
public static class PBKDF2 {
private const int DerivedKeyLength = 160 / 8;
public static PBKDF2Hash Hash(string password, int saltSize = 16, int iterations = 10000) {
using (var deriveBytes = new Rfc2898DeriveBytes(password, saltSize: saltSize, iterations: iterations)) {
var dk = deriveBytes.GetBytes(DerivedKeyLength);
return new PBKDF2Hash(deriveBytes.IterationCount, deriveBytes.Salt, dk);
}
}
public static bool Verify(string password, string hashStr) {
if (password == null) throw new ArgumentNullException(nameof(password));
if (hashStr == null) throw new ArgumentNullException(nameof(hashStr));
if (PBKDF2Hash.TryParse(hashStr, out var hash) == false) throw new FormatException(nameof(hashStr));
return Verify(password, hash);
}
public static bool Verify(string password, PBKDF2Hash hash) {
if (password == null) throw new ArgumentNullException(nameof(password));
if (hash == null) throw new ArgumentNullException(nameof(hash));
using (var deriveBytes = new Rfc2898DeriveBytes(password, salt: hash.Salt, iterations: hash.IterationCount)) {
var dk = deriveBytes.GetBytes(DerivedKeyLength);
return hash.DerivedKey.SequenceEqual(dk);
}
}
}
public class PBKDF2Hash {
private const string HashId = "pbkdf2";
public PBKDF2Hash(int iterationCount, byte[] salt, byte[] derivedKey) {
IterationCount = iterationCount;
Salt = salt;
DerivedKey = derivedKey;
}
public int IterationCount { get; }
public byte[] Salt { get; }
public byte[] DerivedKey { get; }
public override string ToString() {
return $"${HashId}${IterationCount}${AdaptedBase64Encode(Salt)}${AdaptedBase64Encode(DerivedKey)}";
}
public static bool TryParse(string hashStr, out PBKDF2Hash result) {
result = null;
if (hashStr == null) return false;
if (hashStr.StartsWith("$") == false) return false;
var elems = hashStr.Split(new[] { '$' }, StringSplitOptions.RemoveEmptyEntries);
if (elems.Length != 4) return false;
if (elems[0] != HashId) return false;
if (int.TryParse(elems[1], out var iterationCount) == false) return false;
var salt = AdaptedBase64Decode(elems[2]);
var dk = AdaptedBase64Decode(elems[3]);
result = new PBKDF2Hash(iterationCount, salt, dk);
return true;
}
#region Helper
/// <summary>
/// adapted base64 encoding
/// http://nullege.com/codes/search/passlib.utils.ab64_encode
/// </summary>
/// <param name="bin"></param>
/// <returns></returns>
private static string AdaptedBase64Encode(byte[] bin) {
return Convert.ToBase64String(bin).TrimEnd('=').Replace('+', '.');
}
/// <summary>
/// http://nullege.com/codes/search/passlib.utils.ab64_decode
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
private static byte[] AdaptedBase64Decode(string value) {
var paddingLen = (4 - value.Length % 4) & 0x3;
var bldr = new StringBuilder(value);
bldr.Replace('.', '+');
bldr.Append('=', paddingLen);
return Convert.FromBase64String(bldr.ToString());
}
#endregion Helper
}
}
Postscript
読み易いようコードを要点のみに絞っているので、そのうちブラッシュアップして GitHub に上げる。
2018/06/29 追記: GitHub に上げました。
References
- ユーザーのパスワードを安全に保管する方法について - 11月 - 2013 - ソフォス プレス リリース、セキュリティニュース、ソフォスに関するニュース記事 - Sophos Press Office | Sophos News and Press Releases
- パスワードはハッシュ化するだけで十分? | NTTデータ
- Password Storage Cheat Sheet - OWASP
- Modular Crypt Format — Passlib v1.7.1 Documentation
- PHC string format Specification
- passlib.hash.pbkdf2_digest - Generic PBKDF2 Hashes — Passlib v1.7.1 Documentation
- NIST SP 800-57 Part 1 Rev. 4 鍵管理における 推奨事項 - IPA
- NIST SP 800-63B Digital Identity Guidelines (翻訳版)
- NIST SP 800-132 Recommendation for Password-Based Key Derivation
- IPA テクニカルウォッチ 『暗号をめぐる最近の話題』に関するレポート
- RFC 2898 - PKCS #5: Password-Based Cryptography Specification Version 2.0 > 5.2 PBKDF2
- ASP.NET Core でパスワードをハッシュ | Microsoft Docs
- 軽減策 - Microsoft Threat Modeling Tool - Azure | Microsoft Docs
-
"表 3: SP800-57 と SP800-131A での代表的な暗号アルゴリズムの取り扱いについて" IPA テクニカルウォッチ 『暗号をめぐる最近の話題』に関するレポート ↩
-
"表 3 : 目標とするセキュリティ強度を提供するために利用できるハッシュ関数 " NIST SP 800-57 Part 1 Rev. 4 鍵管理における 推奨事項 - IPA ↩ ↩2
-
.NET Framework 4.7.2+ / .NET Core 2.0+ をターゲットとすれば、HashAlgorithmName を引数に取る Rfc2898DeriveBytes コンストラクタを使用できる。 ↩
-
Rfc2898DeriveBytes の代わりに Microsoft.AspNetCore.Cryptography.KeyDerivation.KeyDerivation.Pbkdf2() の使用も考えられる。 ↩
-
Microsoft.AspNetCore.Identity.PasswordHasher もパラメーター込みの固有形式
HASHED PASSWORD FORMATS
で出力する。 ↩ -
Microsoft.AspNetCore.Identity.PasswordHasher の既定値も 10,000 回、Microsoft Threat Modeling Tool ではなんと 150,000 回以上を推奨! ↩
-
「鍵導出関数からの出力を選択する際,根拠となる一方向関数の出力と長さが同じであるべきである(SHOULD).」NIST SP800-63B ↩