재우니의 블로그

원문 (Hash passwords in ASP.NET Core)

 

https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/consumer-apis/password-hashing?view=aspnetcore-6.0 

 

Hash passwords in ASP.NET Core

Learn how to hash passwords using the ASP.NET Core Data Protection APIs.

docs.microsoft.com

 

 

데이터 보호 코드 베이스에는 암호화 키 파생 함수를 포함하는 Microsoft.AspNetCore.Cryptography.KeyDerivation 패키지가 포함되어 있습니다. 이 패키지는 독립 실행형 구성 요소이며 나머지 데이터 보호 시스템에 대한 종속성이 없습니다. 완전히 독립적으로 사용할 수 있습니다. 원본은 데이터 보호 코드 베이스와 함께 편의를 위해 존재합니다.

 

패키지는 현재 PBKDF2 알고리즘을 사용하여 암호를 해시할 수 있는 KeyDerivation.Pbkdf2 메서드를 제공합니다. 이 API는 .NET Framework 기존 Rfc2898DeriveBytesAPI와 매우 유사하지만 세 가지 중요한 차이점이 있습니다.

  1. KeyDerivation.Pbkdf2 메서드는 여러 PRF(현재 HMACSHA1, HMACSHA256 및 HMACSHA512)를 사용하도록 지원하지만 Rfc2898DeriveBytes 형식은 HMACSHA1을 지원합니다.

  2. KeyDerivation.Pbkdf2 메서드는 현재 운영 체제를 검색하고 가장 최적화된 루틴 구현을 선택하려고 시도하여 특정 경우에 훨씬 더 나은 성능을 제공합니다. (Windows 8에서 Rfc2898DeriveBytes 처리량의 10배를 제공합니다.)

  3. KeyDerivation.Pbkdf2 메서드에는 호출자가 모든 매개 변수(salt, PRF 및 반복 횟수)를 지정해야 합니다. Rfc2898DeriveBytes 형식은 이에 대한 기본값을 제공합니다.

 

 

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 cryptographically strong random sequence of nonzero values
        byte[] salt = new byte[128 / 8];
        using (var rngCsp = new RNGCryptoServiceProvider())
        {
            rngCsp.GetNonZeroBytes(salt);
        }
        Console.WriteLine($"Salt: {Convert.ToBase64String(salt)}");

        // derive a 256-bit subkey (use HMACSHA256 with 100,000 iterations)
        string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2(
            password: password,
            salt: salt,
            prf: KeyDerivationPrf.HMACSHA256,
            iterationCount: 100000,
            numBytesRequested: 256 / 8));
        Console.WriteLine($"Hashed: {hashed}");
    }
}

/*
 * SAMPLE OUTPUT
 *
 * Enter a password: Xtw9NMgx
 * Salt: CGYzqeN4plZekNC88Umm1Q==
 * Hashed: Gt9Yc4AiIvmsC1QQbe2RZsCIqvoYlst2xbz0Fs8aHnw=
 */

 

 

Asp.net Core 에서 오직 암호화 및 비교만 심플하게 사용할 경우 Helper 함수 만들어 활용해 보겠습니다.

 

이를 구현하기 위해서는 암호화 키 파생 함수를 포함하는 Microsoft.AspNetCore.Cryptography.KeyDerivation  System.Security.Cryptography 클래스를 사용해야 합니다.  

 

먼저 HMACSHA512 으로 하였으며, 회전 횟수는 10,000 번이고, Salt 는 랜덤 생성하여 이를 조합해서 비밀번호 암호화를 하였고, 암호 값은 salt.hash 형태의 값을 반환받게 하였습니다. 구분을 위해 . (포인트) 을 사용하였습니다.

 

 

 

비밀번호 맞는지 여부의 확인은 비밀번호 암호화 값에서 salt 와 hash 값을 분리하여 이를 다시 검증하는 방법으로 구현 되어 있습니다.

using Microsoft.AspNetCore.Cryptography.KeyDerivation;
using System;
using System.Security.Cryptography;
using System.Text;

namespace WebApplication3
{
    public class PasswordHasher
    {
        private static string HashPassword(string value, string salt)
        {
            var valueBytes = KeyDerivation.Pbkdf2(
                password: value,
                salt: Encoding.UTF8.GetBytes(salt),
                prf: KeyDerivationPrf.HMACSHA512,
                iterationCount: 10000,
                numBytesRequested: 256 / 8);

            return Convert.ToBase64String(valueBytes);
        }
        public static string HashPassword(string password)
        {
            var salt = GenerateSalt();
            var hash = HashPassword(password, salt);
            var result = $"{salt}.{hash}";
            Console.WriteLine("hash result:{0}", result);
            return result;
        }

        private static bool Validate(string password, string salt, string hash) => HashPassword(password, salt) == hash;

        public static bool VerifyHashedPassword(string password, string storePassword)
        {
            if (string.IsNullOrEmpty(password))
            {
                throw new ArgumentNullException(nameof(password));
            }

            if (string.IsNullOrEmpty(storePassword))
            {
                throw new ArgumentNullException(nameof(storePassword));
            }

            var parts = storePassword.Split('.');
            var salt = parts[0];
            var hash = parts[1];

            return Validate(password, salt, hash); ;
        }

        private static string GenerateSalt()
        {
            byte[] randomBytes = new byte[128 / 8];
            using (var generator = RandomNumberGenerator.Create())
            {
                generator.GetBytes(randomBytes);
                return Convert.ToBase64String(randomBytes);
            }
        }

    }
}

 

Asp.net Core 의 Controller 에서 구현하기

 

Controller 에서 이를 테스트 해봤습니다. 비밀번호 값을 5번 정도 작동 해봤으며 전부 다른 형태의 비밀번호 암호화 값을 제공해 주었습니다. result 값은 database 의 사용자 테이블에 저장해 두고 나서, 인증 할 때 이를 꺼내서 비교하는 방법을 구현하면 되겠습니다.

public IActionResult Index()
{
    var password = "Aspdotnet.tistory.com";

    for(int i = 0; i < 5; i++)
    {
        var result = PasswordHasher.HashPassword(password);
        var right = PasswordHasher.VerifyHashedPassword(password, result);

        Console.WriteLine("password is right : {0}", right);
    }

    return View();
}