.NET におけるパスワードハッシュのベストプラクティス

というより、パスワードハッシュを簡易に使えるテンプレという側面が強いかも。
サポート対象を絞ればより良い解がありますが、今回は間口を広げたプラクティスです。

※ パスワードハッシュや Salt ってなんぞやって方は、下記の記事で解りやすく解説されています。是非ご参照ください。
- [パスワードはハッシュ化するだけで十分? | NTTデータ](http://www.nttdata.com/jp/ja/insights/blog/20170914.html)
- [ユーザーのパスワードを安全に保管する方法について - 11月 - 2013 - ソフォス プレス リリース、セキュリティニュース、ソフォスに関するニュース記事 - Sophos Press Office | Sophos News and Press Releases](https://www.sophos.com/ja-jp/press-office/press-releases/2013/11/ns-serious-security-how-to-store-your-users-passwords-safely.aspx)

## Points
- .NET Framework 4.5+ / .NET Core 1.0+ / .NET Standard 1.3+ サポート。
- Pure .NET。コピペで直ぐ試せる。サードパーティ不使用。
- [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2)-SHA1 を使用。
    - [NIST](https://www.ipa.go.jp/security/publications/nist/) 推奨[^1]のハッシュ関数。セキュリティ強度 128 bits[^2]。
    - 本来はよりセキュリティ強度の高い HMAC-SHA256（セキュリティ強度 256 bits[^2]） 以上がベター。ターゲットフレームワークが絞られる[^3][^4]ので今回は妥協。
- [Modular Crypt Format](http://passlib.readthedocs.io/en/stable/modular_crypt_format.html) ハッシュ文字列を出力。
    - Salt やストレッチ回数込みの文字列を出力[^5]する為、ハッシュ管理が容易。
    - [パスワードハッシュのアップグレード](https://www.owasp.org/index.php/Password_Storage_Cheat_Sheet#Upgrading_your_existing_password_hashing_solution)への備え。
- Salt / ストレッチ回数の既定値は適当。`salt=16` (128 bits) は [NIST SP800-132](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf) の推奨最小値[^6]。 `iterations=10000` は [NIST SP800-63B](https://openid-foundation-japan.github.io/800-63-3-final/sp800-63b.ja.html#sec5) の推奨最小値[^7]（目標とするハッシュ計算時間によりけり）。
- [passlib.hash.pbkdf2_sha1](http://passlib.readthedocs.io/en/stable/lib/passlib.hash.pbkdf2_digest.html#passlib.hash.pbkdf2_sha1) 互換（のつもり）。
    - dkLen[^8]（PBKDF2 で導出されるキー長） を hLen[^8]（内部 PRF[^8] の出力長） に揃えてる[^9]。ここで PRF は HMAC-SHA1 なので 160 bits。

[^1]: "表 3: SP800-57 と SP800-131A での代表的な暗号アルゴリズムの取り扱いについて" [IPA テクニカルウォッチ 『暗号をめぐる最近の話題』に関するレポート](https://www.ipa.go.jp/files/000009369.pdf)
[^2]: "表 3 ： 目標とするセキュリティ強度を提供するために利用できるハッシュ関数 " [NIST SP 800-57 Part 1 Rev. 4 鍵管理における 推奨事項 - IPA](https://www.ipa.go.jp/files/000055490.pdf)
[^3]: .NET Framework 4.7.2+ / .NET Core 2.0+ をターゲットとすれば、[HashAlgorithmName](https://docs.microsoft.com/ja-jp/dotnet/api/system.security.cryptography.hashalgorithmname) を引数に取る [Rfc2898DeriveBytes](https://docs.microsoft.com/ja-jp/dotnet/api/system.security.cryptography.rfc2898derivebytes.-ctor?view=netframework-4.7.2) コンストラクタを使用できる。
[^4]: [Rfc2898DeriveBytes](https://docs.microsoft.com/ja-jp/dotnet/api/system.security.cryptography.rfc2898derivebytes) の代わりに [Microsoft.AspNetCore.Cryptography.KeyDerivation.KeyDerivation.Pbkdf2()](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.cryptography.keyderivation.keyderivation.pbkdf2?view=aspnetcore-2.1#Microsoft_AspNetCore_Cryptography_KeyDerivation_KeyDerivation_Pbkdf2_System_String_System_Byte___Microsoft_AspNetCore_Cryptography_KeyDerivation_KeyDerivationPrf_System_Int32_System_Int32_) の使用も考えられる。
[^5]: [Microsoft.AspNetCore.Identity.PasswordHasher](https://github.com/aspnet/Identity/blob/dev/src/Core/PasswordHasher.cs) もパラメーター込みの固有形式 `HASHED PASSWORD FORMATS` で出力する。
[^6]: [OWASP](https://www.owasp.org/index.php/Password_Storage_Cheat_Sheet#Use_a_cryptographically_strong_credential-specific_salt) の推奨値は 32 or 64 bytes。
[^7]: [Microsoft.AspNetCore.Identity.PasswordHasher](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordhasheroptions.iterationcount) の既定値も 10,000 回、[Microsoft Threat Modeling Tool](https://docs.microsoft.com/ja-jp/azure/security/azure-security-threat-modeling-tool-sensitive-data#salted-hash) ではなんと 150,000 回以上を推奨！
[^8]: https://tools.ietf.org/html/rfc2898#section-2
[^9]: 「鍵導出関数からの出力を選択する際，根拠となる一方向関数の出力と長さが同じであるべきである(SHOULD)．」[NIST SP800-63B](https://openid-foundation-japan.github.io/800-63-3-final/sp800-63b.ja.html#memsecretver)

## Usage
```cs:Program.cs
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
```cs:PBKDF2.cs
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](https://github.com/in-async/PasswordHash.NET) に上げました。

## References
- [ユーザーのパスワードを安全に保管する方法について - 11月 - 2013 - ソフォス プレス リリース、セキュリティニュース、ソフォスに関するニュース記事 - Sophos Press Office | Sophos News and Press Releases](https://www.sophos.com/ja-jp/press-office/press-releases/2013/11/ns-serious-security-how-to-store-your-users-passwords-safely.aspx)
- [パスワードはハッシュ化するだけで十分? | NTTデータ](http://www.nttdata.com/jp/ja/insights/blog/20170914.html)
- [Password Storage Cheat Sheet - OWASP](https://www.owasp.org/index.php/Password_Storage_Cheat_Sheet)
- [Modular Crypt Format &#8212; Passlib v1.7.1 Documentation](http://passlib.readthedocs.io/en/stable/modular_crypt_format.html)
- [PHC string format Specification](https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md)
- [passlib.hash.pbkdf2_digest - Generic PBKDF2 Hashes &#8212; Passlib v1.7.1 Documentation](http://passlib.readthedocs.io/en/stable/lib/passlib.hash.pbkdf2_digest.html)
- [NIST SP 800-57 Part 1 Rev. 4 鍵管理における 推奨事項 - IPA](https://www.ipa.go.jp/files/000055490.pdf)
- [NIST SP 800-63B Digital Identity Guidelines (翻訳版)](https://openid-foundation-japan.github.io/800-63-3-final/sp800-63b.ja.html)
- [NIST SP 800-132 Recommendation for Password-Based Key Derivation](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf)
- [IPA テクニカルウォッチ 『暗号をめぐる最近の話題』に関するレポート](https://www.ipa.go.jp/files/000009369.pdf)
- [RFC 2898 - PKCS #5: Password-Based Cryptography Specification Version 2.0 > 5.2 PBKDF2](https://tools.ietf.org/html/rfc2898#section-5.2)
- [ASP.NET Core でパスワードをハッシュ | Microsoft Docs](https://docs.microsoft.com/ja-jp/aspnet/core/security/data-protection/consumer-apis/password-hashing)
- [軽減策 - Microsoft Threat Modeling Tool - Azure | Microsoft Docs](https://docs.microsoft.com/ja-jp/azure/security/azure-security-threat-modeling-tool-mitigations)