[C#] 암호를 해시하는 방법

비밀번호 해시를 전화에 저장하고 싶은데 어떻게해야할지 모르겠습니다. 암호화 방법 만 찾을 수있는 것 같습니다. 암호는 어떻게 올바르게 해시해야합니까?



답변

업데이트 : 이 답변은 매우 오래되었습니다 . 대신 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);


답변

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


답변

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 코어 버전도 있습니다 .


답변

암호 암호화에 해시와 솔트를 사용합니다 (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);
}


답변

  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);
      }
 }


답변

@ 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);


답변

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 컴퓨터 성능)이 있습니다. 암호 해싱은 “많은 시간”을 ” 불합리한 시간”으로 전환하기 위해 암호 학적으로 강력해야합니다 .

추가 할 점 하나 더

해시 확인에는 시간이 걸립니다 (좋습니다). 사용자가 잘못된 사용자 이름을 입력하면 사용자 이름이 잘못된 지 확인하는 데 시간이 걸리지 않습니다. 사용자 이름이 정확하면 암호 확인을 시작합니다. 이는 비교적 긴 과정입니다.

해커의 경우 사용자가 있는지 여부를 이해하기가 매우 쉽습니다.

사용자 이름이 틀렸을 때 즉시 응답하지 않도록하십시오.

말할 필요도없이, 무엇이 잘못되었는지 절대로 대답하지 마십시오. 일반적인 “자격 증명이 잘못되었습니다”.