암호를 해시하는 방법


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 가이 의견이 질문자에게 더 많은 것을 알고 있다고 확신합니다.
Gerald Davis

11
세 가지 큰 실수 : 1) ASCII는 비정상적인 문자로 암호를 조용히 저하시킵니다. 2) 일반 MD5 / SHA-1 / SHA-2는 빠릅니다. 3) 소금이 필요합니다. | 대신 PBKDF2, bcrypt 또는 scrypt를 사용하십시오. PBKDF2는 Rfc2898DeriveBytes 클래스에서 가장 쉽습니다 (WP7에 있는지 확실하지 않음)
CodesInChaos

299

여기에있는 대부분의 다른 답변은 오늘날의 모범 사례에 비해 다소 오래된 것입니다. 여기에 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);

STEP 4 소금 + 해시를 합쳐서 보관 용 끈으로

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

STEP 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 기본적으로 게시물은 해시보다 더 안전한 것을 사용하는 것입니다. 솔트를 사용하더라도 단순히 암호를 해시하면 사용자에게 암호를 변경하라고 지시하기 전에 사용자 암호가 손상 될 수 있습니다 (판매 / 게시 될 가능성이 있음). 위의 코드를 사용하여 공격자가 어렵게 만들고 개발자에게는 쉽지 않습니다.
csharptest.net 2014

2
@DatVM 아니요, 해시를 저장할 때마다 새로운 솔트가 있습니다. 이것이 저장 용 해시와 결합되어 암호를 확인할 수있는 이유입니다.
csharptest.net

9
@CiprianJijie 요점은 당신이 할 수 있다고 생각하지 않습니다.
csharptest.net

9
누군가 VerifyPassword 메서드를 수행하는 경우 Linq를 사용하고 부울에 대한 더 짧은 호출을 사용하려면 다음과 같이합니다. return hash.SequenceEqual (hashBytes.Skip (_saltSize));
Jesú Castillo

2
@ csharptest.net 어떤 종류의 어레이 크기를 권장합니까? 어레이의 크기가 보안에 많은 영향을 미칩니 까? 나는 해싱 / 암호화에 대해 그렇게 많이 알고하지 않습니다
lennyy

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
네, 그게 계획입니다. 그런 다음에 따라 결정하는 것 V1하고 V2있는 검증 당신이 필요로하는 방법.
Christian Gollhardt

답장과 수업에 감사드립니다. 우리가 말하는대로 구현하고 있습니다.
Mike Cole

2
네 @NelsonSilva. 그것은 소금 때문입니다 .
Christian Gollhardt

1
이 코드 (나를 포함하여)의 모든 복사 / 붙여 넣기를 통해 누군가가 문제를 발견하면 게시물이 수정되기를 바랍니다. :)
pettys

14

암호 암호화에 해시와 솔트를 사용합니다 (Asp.Net Membership에서 사용하는 것과 동일한 해시).

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
-1은 빠른 일반 SHA-1을 사용합니다. PBKDF2, bcrypt 또는 scrypt와 같은 느린 키 파생 기능을 사용하십시오.
CodesInChaos

1
  1. 소금 만들기,
  2. 솔트로 해시 비밀번호 만들기
  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의 답변은 훌륭합니다. 대단히 감사합니다. 그러나 수백만 개의 레코드로 프로덕션에서이 코드를 실행 한 후 메모리 누수가 있음을 발견했습니다. RNGCryptoServiceProviderRfc2898DeriveBytes 클래스는 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
BTW, 이전 답변 stackoverflow.com/a/57508528/11603057 은 정확하고 유해하지 않습니다. 이것은 암호 해싱이 아닌 해싱의 예입니다. 키 유도 프로세스 동안 의사 랜덤 함수의 반복이어야합니다. 없습니다. 댓글을 달거나 반대 투표를 할 수 없습니다 (낮은 평판). 오답을 놓치지 마세요!
Albert Lyubarsky 19 년
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.