パスワードをハッシュする方法


117

パスワードのハッシュを電話に保存したいのですが、方法がわかりません。暗号化の方法しか見つけられないようです。パスワードはどのように適切にハッシュされるべきですか?

回答:


62

更新この回答は深刻に古くなっています。代わりに、https://stackoverflow.com/a/10402129/251311の推奨事項を使用してください

あなたはどちらかを使うことができます

var md5 = new MD5CryptoServiceProvider();
var md5data = md5.ComputeHash(data);

または

var sha1 = new SHA1CryptoServiceProvider();
var sha1data = sha1.ComputeHash(data);

dataあなたが使用できるバイト配列として取得するには

var data = Encoding.ASCII.GetBytes(password);

md5dataまたはから文字列を取得するsha1data

var hashedPassword = ASCIIEncoding.GetString(md5data);

11
私は本当にSHA1を使用することをお勧めします。MD5は、既存のシステムとの下位互換性を維持している場合を除いて、いいえです。さらに、実装の使用が終わったら、usingステートメントに入れるか、呼び出しClear()てください。
vcsjones 2010年

3
@vcsjones:私はここで聖戦したくありませんがmd5、ほとんどすべての種類のタスクには十分です。その脆弱性は非常に特定の状況にも関係しており、攻撃者が暗号化について多くのことを知っている必要があります。
zerkms 2010年

4
@zerkmsポイントが取られましたが、下位互換性の理由がない場合、MD5を使用する理由はありません。"転ばぬ先の杖"。
vcsjones 2010年

4
この時点でMD5を使用する理由はありません。計算時間は重要ではないので、既存のシステムとの互換性を除いてMD5を使用する理由はありません。MD5が「十分に良い」としても、はるかに安全なSHAをユーザーが使用してもコストはかかりません。私はzerkmsがこのコメントが質問者のためのものであることを知っていると確信しています。
ジェラルドデイビス

11
3つの大きな間違い:1)ASCIIは異常な文字を含むパスワードを静かに低下させます2)プレーンMD5 / SHA-1 / SHA-2は高速です。3)塩が必要です。| 代わりにPBKDF2、bcryptまたはscryptを使用してください。PBKDF2はRfc2898DeriveBytesクラスで最も簡単です(WP7に存在するかどうかは不明です)
CodesInChaos

298

ここでの他の回答のほとんどは、今日のベストプラクティスでは多少古くなっています。そのため、ここにPBKDF2 / Rfc2898DeriveBytesを使用してパスワードを保存および検証するアプリケーションがあります。次のコードは、この投稿のスタンドアロンクラスにあります。ソルトパスワードハッシュを格納する方法の別の例。。基本は本当に簡単なので、ここで分解します:

ステップ1暗号化PRNGを使用してソルト値を作成します。

byte[] salt;
new RNGCryptoServiceProvider().GetBytes(salt = new byte[16]);

ステップ2 Rfc2898DeriveBytesを作成し、ハッシュ値を取得します。

var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100000);
byte[] hash = pbkdf2.GetBytes(20);

ステップ3後で使用するためにソルトとパスワードのバイトを結合します。

byte[] hashBytes = new byte[36];
Array.Copy(salt, 0, hashBytes, 0, 16);
Array.Copy(hash, 0, hashBytes, 16, 20);

ステップ4組み合わせたsalt + hashを保存用の文字列に変換します

string savedPasswordHash = Convert.ToBase64String(hashBytes);
DBContext.AddUser(new User { ..., Password = savedPasswordHash });

ステップ5ユーザーが入力したパスワードを、保存されているパスワードと照合します。

/* Fetch the stored value */
string savedPasswordHash = DBContext.GetUser(u => u.UserName == user).Password;
/* Extract the bytes */
byte[] hashBytes = Convert.FromBase64String(savedPasswordHash);
/* Get the salt */
byte[] salt = new byte[16];
Array.Copy(hashBytes, 0, salt, 0, 16);
/* Compute the hash on the password the user entered */
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100000);
byte[] hash = pbkdf2.GetBytes(20);
/* Compare the results */
for (int i=0; i < 20; i++)
    if (hashBytes[i+16] != hash[i])
        throw new UnauthorizedAccessException();

注:特定のアプリケーションのパフォーマンス要件によっては、値100000を減らすことができます。最小値はおよそ10000です。


8
@Danielは基本的に、ハッシュだけよりも安全なものを使用することを目的としています。saltを使用してパスワードを単純にハッシュすると、ユーザーにパスワードを変更するように指示する前に、ユーザーのパスワードが危険にさらされ(おそらく販売/公開され)てしまいます。上記のコードを使用して、開発者にとって容易ではなく、攻撃者にとって困難にします。
csharptest.net 2014

2
@DatVMいいえ、ハッシュを保存するたびに新しいソルトが使用されます。これが、パスワードを検証できるように、保存用のハッシュと組み合わされる理由です。
csharptest.net

9
@CiprianJijie全体のポイントはあなたができるとは思いません。
csharptest.net 2016年

9
誰かがVerifyPasswordメソッドを実行している場合、Linqとブール値の短い呼び出しを使用したい場合は、次のようにします。return hash.SequenceEqual(hashBytes.Skip(_saltSize));
カスティーヨ

2
@ csharptest.netどのような配列サイズをお勧めしますか?配列のサイズはとにかくセキュリティに大きく影響しますか?ハッシュ/暗号化についてはあまり知らない
lennyy '17

71

csharptest.netのすばらしい答えに基づいて、私はこれのためのクラスを書きました:

public static class SecurePasswordHasher
{
    /// <summary>
    /// Size of salt.
    /// </summary>
    private const int SaltSize = 16;

    /// <summary>
    /// Size of hash.
    /// </summary>
    private const int HashSize = 20;

    /// <summary>
    /// Creates a hash from a password.
    /// </summary>
    /// <param name="password">The password.</param>
    /// <param name="iterations">Number of iterations.</param>
    /// <returns>The hash.</returns>
    public static string Hash(string password, int iterations)
    {
        // Create salt
        byte[] salt;
        new RNGCryptoServiceProvider().GetBytes(salt = new byte[SaltSize]);

        // Create hash
        var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations);
        var hash = pbkdf2.GetBytes(HashSize);

        // Combine salt and hash
        var hashBytes = new byte[SaltSize + HashSize];
        Array.Copy(salt, 0, hashBytes, 0, SaltSize);
        Array.Copy(hash, 0, hashBytes, SaltSize, HashSize);

        // Convert to base64
        var base64Hash = Convert.ToBase64String(hashBytes);

        // Format hash with extra information
        return string.Format("$MYHASH$V1${0}${1}", iterations, base64Hash);
    }

    /// <summary>
    /// Creates a hash from a password with 10000 iterations
    /// </summary>
    /// <param name="password">The password.</param>
    /// <returns>The hash.</returns>
    public static string Hash(string password)
    {
        return Hash(password, 10000);
    }

    /// <summary>
    /// Checks if hash is supported.
    /// </summary>
    /// <param name="hashString">The hash.</param>
    /// <returns>Is supported?</returns>
    public static bool IsHashSupported(string hashString)
    {
        return hashString.Contains("$MYHASH$V1$");
    }

    /// <summary>
    /// Verifies a password against a hash.
    /// </summary>
    /// <param name="password">The password.</param>
    /// <param name="hashedPassword">The hash.</param>
    /// <returns>Could be verified?</returns>
    public static bool Verify(string password, string hashedPassword)
    {
        // Check hash
        if (!IsHashSupported(hashedPassword))
        {
            throw new NotSupportedException("The hashtype is not supported");
        }

        // Extract iteration and Base64 string
        var splittedHashString = hashedPassword.Replace("$MYHASH$V1$", "").Split('$');
        var iterations = int.Parse(splittedHashString[0]);
        var base64Hash = splittedHashString[1];

        // Get hash bytes
        var hashBytes = Convert.FromBase64String(base64Hash);

        // Get salt
        var salt = new byte[SaltSize];
        Array.Copy(hashBytes, 0, salt, 0, SaltSize);

        // Create hash with given salt
        var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations);
        byte[] hash = pbkdf2.GetBytes(HashSize);

        // Get result
        for (var i = 0; i < HashSize; i++)
        {
            if (hashBytes[i + SaltSize] != hash[i])
            {
                return false;
            }
        }
        return true;
    }
}

使用法:

// Hash
var hash = SecurePasswordHasher.Hash("mypassword");

// Verify
var result = SecurePasswordHasher.Verify("mypassword", hash);

サンプルハッシュは次のようになります。

$MYHASH$V1$10000$Qhxzi6GNu/Lpy3iUqkeqR/J1hh8y/h5KPDjrv89KzfCVrubn

ご覧のとおり、簡単に使用できるようにハッシュに反復を含めており、アップグレードが必要な場合にこれをアップグレードする可能性もあります。


.netコアに興味がある場合は、Code Reviewに .netコアバージョンもあります。


1
確認のために、ハッシュエンジンをアップグレードした場合、ハッシュのV1セクションとそれからキーを増やしますか?
Mike Cole

1
はいそれは計画です。あなたは、その後に基づいて決めるだろうV1V2あなたが必要とする検証方法います。
クリスチャンゴルハート

返事をありがとう、そしてクラス。私たちが話すように私はそれを実装しています。
Mike Cole

2
はい@NelsonSilva。それはのせいです。
クリスチャンゴルハート2017年

1
このコード(私を含む)のすべてのコピーと貼り付けにより、誰かが発言して、問題が見つかった場合は投稿が改訂されることを願っています!:)
pettys

14

私はパスワードの暗号化にハッシュとソルトを使用します(Asp.Netメンバーシップが使用するのと同じハッシュです):

private string PasswordSalt
{
   get
   {
      var rng = new RNGCryptoServiceProvider();
      var buff = new byte[32];
      rng.GetBytes(buff);
      return Convert.ToBase64String(buff);
   }
}

private string EncodePassword(string password, string salt)
{
   byte[] bytes = Encoding.Unicode.GetBytes(password);
   byte[] src = Encoding.Unicode.GetBytes(salt);
   byte[] dst = new byte[src.Length + bytes.Length];
   Buffer.BlockCopy(src, 0, dst, 0, src.Length);
   Buffer.BlockCopy(bytes, 0, dst, src.Length, bytes.Length);
   HashAlgorithm algorithm = HashAlgorithm.Create("SHA1");
   byte[] inarray = algorithm.ComputeHash(dst);
   return Convert.ToBase64String(inarray);
}

16
高速なプレーンSHA-1を使用する場合は-1。PBKDF2、bcrypt、scryptなどの低速な鍵導出関数を使用します。
CodesInChaos

1
  1. 塩を作り、
  2. saltでハッシュパスワードを作成する
  3. ハッシュとソルトの両方を保存
  4. パスワードとソルトで復号化...開発者はパスワードを復号化できません
public class CryptographyProcessor
{
    public string CreateSalt(int size)
    {
        //Generate a cryptographic random number.
          RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
         byte[] buff = new byte[size];
         rng.GetBytes(buff);
         return Convert.ToBase64String(buff);
    }


      public string GenerateHash(string input, string salt)
      { 
         byte[] bytes = Encoding.UTF8.GetBytes(input + salt);
         SHA256Managed sHA256ManagedString = new SHA256Managed();
         byte[] hash = sHA256ManagedString.ComputeHash(bytes);
         return Convert.ToBase64String(hash);
      }

      public bool AreEqual(string plainTextInput, string hashedInput, string salt)
      {
           string newHashedPin = GenerateHash(plainTextInput, salt);
           return newHashedPin.Equals(hashedInput); 
      }
 }

1

@ csharptest.netChristian Gollhardtの回答は素晴らしいです。ありがとうございました。しかし、このコードを何百万ものレコードを含む本番環境で実行した後、メモリリークがあることに気付きました。RNGCryptoServiceProviderクラスとRfc2898DeriveBytesクラスはIDisposableから派生していますが、それらは破棄しません。誰かが処分バージョンを必要とする場合、私は答えとして私の解決策を書きます。

public static class SecurePasswordHasher
{
    /// <summary>
    /// Size of salt.
    /// </summary>
    private const int SaltSize = 16;

    /// <summary>
    /// Size of hash.
    /// </summary>
    private const int HashSize = 20;

    /// <summary>
    /// Creates a hash from a password.
    /// </summary>
    /// <param name="password">The password.</param>
    /// <param name="iterations">Number of iterations.</param>
    /// <returns>The hash.</returns>
    public static string Hash(string password, int iterations)
    {
        // Create salt
        using (var rng = new RNGCryptoServiceProvider())
        {
            byte[] salt;
            rng.GetBytes(salt = new byte[SaltSize]);
            using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations))
            {
                var hash = pbkdf2.GetBytes(HashSize);
                // Combine salt and hash
                var hashBytes = new byte[SaltSize + HashSize];
                Array.Copy(salt, 0, hashBytes, 0, SaltSize);
                Array.Copy(hash, 0, hashBytes, SaltSize, HashSize);
                // Convert to base64
                var base64Hash = Convert.ToBase64String(hashBytes);

                // Format hash with extra information
                return $"$HASH|V1${iterations}${base64Hash}";
            }
        }

    }

    /// <summary>
    /// Creates a hash from a password with 10000 iterations
    /// </summary>
    /// <param name="password">The password.</param>
    /// <returns>The hash.</returns>
    public static string Hash(string password)
    {
        return Hash(password, 10000);
    }

    /// <summary>
    /// Checks if hash is supported.
    /// </summary>
    /// <param name="hashString">The hash.</param>
    /// <returns>Is supported?</returns>
    public static bool IsHashSupported(string hashString)
    {
        return hashString.Contains("HASH|V1$");
    }

    /// <summary>
    /// Verifies a password against a hash.
    /// </summary>
    /// <param name="password">The password.</param>
    /// <param name="hashedPassword">The hash.</param>
    /// <returns>Could be verified?</returns>
    public static bool Verify(string password, string hashedPassword)
    {
        // Check hash
        if (!IsHashSupported(hashedPassword))
        {
            throw new NotSupportedException("The hashtype is not supported");
        }

        // Extract iteration and Base64 string
        var splittedHashString = hashedPassword.Replace("$HASH|V1$", "").Split('$');
        var iterations = int.Parse(splittedHashString[0]);
        var base64Hash = splittedHashString[1];

        // Get hash bytes
        var hashBytes = Convert.FromBase64String(base64Hash);

        // Get salt
        var salt = new byte[SaltSize];
        Array.Copy(hashBytes, 0, salt, 0, SaltSize);

        // Create hash with given salt
        using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations))
        {
            byte[] hash = pbkdf2.GetBytes(HashSize);

            // Get result
            for (var i = 0; i < HashSize; i++)
            {
                if (hashBytes[i + SaltSize] != hash[i])
                {
                    return false;
                }
            }

            return true;
        }

    }
}

使用法:

// Hash
var hash = SecurePasswordHasher.Hash("mypassword");

// Verify
var result = SecurePasswordHasher.Verify("mypassword", hash);

0

KeyDerivation.Pbkdf2の使用はRfc2898DeriveBytesよりも優れていると思います。

例と説明: ASP.NET Coreのハッシュパスワード

using System;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
 
public class Program
{
    public static void Main(string[] args)
    {
        Console.Write("Enter a password: ");
        string password = Console.ReadLine();
 
        // generate a 128-bit salt using a secure PRNG
        byte[] salt = new byte[128 / 8];
        using (var rng = RandomNumberGenerator.Create())
        {
            rng.GetBytes(salt);
        }
        Console.WriteLine($"Salt: {Convert.ToBase64String(salt)}");
 
        // derive a 256-bit subkey (use HMACSHA1 with 10,000 iterations)
        string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2(
            password: password,
            salt: salt,
            prf: KeyDerivationPrf.HMACSHA1,
            iterationCount: 10000,
            numBytesRequested: 256 / 8));
        Console.WriteLine($"Hashed: {hashed}");
    }
}
 
/*
 * SAMPLE OUTPUT
 *
 * Enter a password: Xtw9NMgx
 * Salt: NZsP6NnmfBuYeJrrAKNuVQ==
 * Hashed: /OOoOer10+tGwTRDTrQSoeCxVTFr6dtYly7d0cPxIak=
 */

これは、記事のサンプルコードです。そしてそれは最低限のセキュリティレベルです。これを増やすには、KeyDerivationPrf.HMACSHA1パラメータの代わりに使用します

KeyDerivationPrf.HMACSHA256またはKeyDerivationPrf.HMACSHA512。

パスワードハッシュについて妥協しないでください。パスワードハッシュハッキングを最適化するには、数学的に適切な方法が多数あります。結果は悲惨な可能性があります。悪意のあるユーザーがユーザーのパスワードハッシュテーブルを手に入れたら、アルゴリズムが弱いか、実装が正しくない場合、比較的簡単にパスワードを解読できます。彼はパスワードを解読するために多くの時間(時間xコンピュータ能力)を持っています。「大量の時間」を「不合理な時間」に変えるために、パスワードのハッシュは暗号的に強力でなければなりません。

追加するもう1つのポイント

ハッシュの検証には時間がかかります(そしてそれは良いことです)。ユーザーが間違ったユーザー名を入力した場合、ユーザー名が正しくないことを確認するのに時間がかかりません。ユーザー名が正しい場合、パスワードの検証を開始します。これは比較的長いプロセスです。

ハッカーにとって、ユーザーが存在するかどうかを理解するのは非常に簡単です。

ユーザー名が間違っている場合は、すぐに返答しないようにしてください。

言うまでもないことですが、何が悪いのか答えてはいけません。一般的な「資格情報が間違っている」だけです。


1
ところで、以前の回答stackoverflow.com/a/57508528/11603057は正しくなく、有害です。これはパスワードのハッシュではなく、ハッシュの例です。鍵導出プロセス中の疑似ランダム関数の反復でなければなりません。ありません。コメントしたり、反対票を投じたりすることはできません(私の評判が低い)。不正解をお見逃しなく!
Albert Lyubarsky
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.