20
27

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-06-27

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

※ パスワードハッシュや 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 を使用。
    • NIST 推奨1のハッシュ関数。セキュリティ強度 128 bits2
    • 本来はよりセキュリティ強度の高い HMAC-SHA256(セキュリティ強度 256 bits2) 以上がベター。ターゲットフレームワークが絞られる34ので今回は妥協。
  • Modular Crypt Format ハッシュ文字列を出力。
  • Salt / ストレッチ回数の既定値は適当。salt=16 (128 bits) は NIST SP800-132 の推奨最小値6iterations=10000NIST SP800-63B の推奨最小値7(目標とするハッシュ計算時間によりけり)。
  • passlib.hash.pbkdf2_sha1 互換(のつもり)。
    • dkLen8(PBKDF2 で導出されるキー長) を hLen8(内部 PRF8 の出力長) に揃えてる9。ここで PRF は HMAC-SHA1 なので 160 bits。

Usage

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

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 に上げました。

References


  1. "表 3: SP800-57 と SP800-131A での代表的な暗号アルゴリズムの取り扱いについて" IPA テクニカルウォッチ 『暗号をめぐる最近の話題』に関するレポート 

  2. "表 3 : 目標とするセキュリティ強度を提供するために利用できるハッシュ関数 " NIST SP 800-57 Part 1 Rev. 4 鍵管理における 推奨事項 - IPA 

  3. .NET Framework 4.7.2+ / .NET Core 2.0+ をターゲットとすれば、HashAlgorithmName を引数に取る Rfc2898DeriveBytes コンストラクタを使用できる。 

  4. Rfc2898DeriveBytes の代わりに Microsoft.AspNetCore.Cryptography.KeyDerivation.KeyDerivation.Pbkdf2() の使用も考えられる。 

  5. Microsoft.AspNetCore.Identity.PasswordHasher もパラメーター込みの固有形式 HASHED PASSWORD FORMATS で出力する。 

  6. OWASP の推奨値は 32 or 64 bytes。 

  7. Microsoft.AspNetCore.Identity.PasswordHasher の既定値も 10,000 回、Microsoft Threat Modeling Tool ではなんと 150,000 回以上を推奨! 

  8. https://tools.ietf.org/html/rfc2898#section-2 

  9. 「鍵導出関数からの出力を選択する際,根拠となる一方向関数の出力と長さが同じであるべきである(SHOULD).」NIST SP800-63B 

20
27
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
27