ASP.NET Identity의 디폴트 패스워드 해셔 - 어떻게 동작하며 안전한가?
MVC 5와 ASP에 부속되어 있는 UserManager에 디폴트로 실장되어 있는 패스워드 해셔가 궁금하네요.NET Identity Framework는 충분히 안전한가요?만약 그렇다면, 어떻게 작동하는지 설명해 주시겠어요?
IPasswordHasher 인터페이스는 다음과 같습니다.
public interface IPasswordHasher
{
string HashPassword(string password);
PasswordVerificationResult VerifyHashedPassword(string hashedPassword,
string providedPassword);
}
보시다시피 염분이 필요 없습니다만, 「Asp.net Identity Password Hashing」이라고 하는 스레드에 기재되어 있습니다.이것들은 뒤에서 염분 처리를 하고 있습니다.그래서 어떻게 하는 건지 궁금하네요.그리고 이 소금은 어디서 오는 걸까요?
제 걱정은 소금이 정전기여서 상당히 불안정하다는 것입니다.
디폴트 실장(ASP) 방법은 다음과 같습니다.NET Framework 또는 ASP.NET Core)가 동작합니다.해시를 생성하기 위해 임의의 소금이 포함된 키 파생 함수를 사용합니다.소금은 KDF의 출력물의 일부로 포함되어 있다.따라서 동일한 비밀번호를 "해시"할 때마다 다른 해시를 얻을 수 있습니다.해시를 확인하기 위해 출력이 salt로 분할되고 나머지 salt로 KDF가 다시 실행됩니다.결과가 초기 출력의 나머지 부분과 일치하면 해시가 검증됩니다.
해시:
public static string HashPassword(string password)
{
byte[] salt;
byte[] buffer2;
if (password == null)
{
throw new ArgumentNullException("password");
}
using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password, 0x10, 0x3e8))
{
salt = bytes.Salt;
buffer2 = bytes.GetBytes(0x20);
}
byte[] dst = new byte[0x31];
Buffer.BlockCopy(salt, 0, dst, 1, 0x10);
Buffer.BlockCopy(buffer2, 0, dst, 0x11, 0x20);
return Convert.ToBase64String(dst);
}
확인 중:
public static bool VerifyHashedPassword(string hashedPassword, string password)
{
byte[] buffer4;
if (hashedPassword == null)
{
return false;
}
if (password == null)
{
throw new ArgumentNullException("password");
}
byte[] src = Convert.FromBase64String(hashedPassword);
if ((src.Length != 0x31) || (src[0] != 0))
{
return false;
}
byte[] dst = new byte[0x10];
Buffer.BlockCopy(src, 1, dst, 0, 0x10);
byte[] buffer3 = new byte[0x20];
Buffer.BlockCopy(src, 0x11, buffer3, 0, 0x20);
using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password, dst, 0x3e8))
{
buffer4 = bytes.GetBytes(0x20);
}
return ByteArraysEqual(buffer3, buffer4);
}
왜냐하면 요즘 ASP.NET은 오픈 소스이며 GitHub: AspNet에서 찾을 수 있습니다.아이덴티티 3.0 및 AspNet아이덴티티 2.0
코멘트로부터:
/* =======================
* HASHED PASSWORD FORMATS
* =======================
*
* Version 2:
* PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations.
* (See also: SDL crypto guidelines v5.1, Part III)
* Format: { 0x00, salt, subkey }
*
* Version 3:
* PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations.
* Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey }
* (All UInt32s are stored big-endian.)
*/
나는 받아들여진 대답을 이해했고, 그것을 상향 투표했지만, 내 평신도의 답변을 여기에 버리려고 생각했다.
해시 생성
- salt는 해시 및 salt를 생성하는 Rfc2898DeriveBytes 함수를 사용하여 랜덤하게 생성됩니다.Rfc2898DeriveBytes 입력은 비밀번호, 생성하는 솔트의 크기 및 실행하는 해시 반복 횟수입니다.https://msdn.microsoft.com/en-us/library/h83s4e12(v=vs.110).aspx
- 그런 다음 소금과 해시를 함께 으깨고(소금 후 해시), 문자열로 인코딩합니다(소금은 해시에 인코딩됩니다).이 부호화된 해시(솔트 및 해시 포함)는 사용자에 대해 데이터베이스에 저장됩니다(일반적으로).
해시에 대한 비밀번호 확인
사용자가 입력한 비밀번호를 확인합니다.
- 저장된 해시된 비밀번호에서 salt가 추출됩니다.
- salt는 RFC 2898DeriveBytes 오버로드를 사용하여 사용자의 입력 비밀번호를 해시하기 위해 사용됩니다.이 오버로드에는 salt를 생성하는 대신 salt를 사용합니다.https://msdn.microsoft.com/en-us/library/yx129kfs(v=vs.110).aspx
- 그런 다음 저장된 해시와 테스트 해시를 비교합니다.
해시
여기서는 SHA1 해시함수(https://en.wikipedia.org/wiki/SHA-1)를 사용하여 해시가 생성됩니다.이 함수는 1000회 반복 호출됩니다(디폴트 ID 실장에서는).
이것은 왜 안전한가?
- 랜덤 솔트는 공격자가 미리 생성된 해시 테이블을 사용하여 암호를 해독할 수 없음을 의미합니다.모든 소금에 대해 해시 테이블을 생성해야 합니다(해커가 소금도 손상시켰다고 가정함).
- 2개의 패스워드가 같은 경우는, 해시가 다릅니다.(공격자가 '공통' 비밀번호를 추론할 수 없음)
- SHA1을 1000회 반복 호출하는 것은 공격자가 이 작업을 수행해야 함을 의미합니다.즉, 슈퍼컴퓨터를 사용할 시간이 없으면 해시에서 패스워드를 강제할 수 있는 리소스가 부족하다는 것입니다.특정 소금의 해시 테이블을 생성하는 데 걸리는 시간을 크게 줄일 수 있습니다.
처음 접하는 분들을 위해 const 코드와 바이트[]의 실제 비교 방법을 소개합니다.stackoverflow에서 이 모든 코드를 얻었지만 값을 변경할 수 있도록 const를 정의했습니다.
// 24 = 192 bits
private const int SaltByteSize = 24;
private const int HashByteSize = 24;
private const int HasingIterationsCount = 10101;
public static string HashPassword(string password)
{
// http://stackoverflow.com/questions/19957176/asp-net-identity-password-hashing
byte[] salt;
byte[] buffer2;
if (password == null)
{
throw new ArgumentNullException("password");
}
using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password, SaltByteSize, HasingIterationsCount))
{
salt = bytes.Salt;
buffer2 = bytes.GetBytes(HashByteSize);
}
byte[] dst = new byte[(SaltByteSize + HashByteSize) + 1];
Buffer.BlockCopy(salt, 0, dst, 1, SaltByteSize);
Buffer.BlockCopy(buffer2, 0, dst, SaltByteSize + 1, HashByteSize);
return Convert.ToBase64String(dst);
}
public static bool VerifyHashedPassword(string hashedPassword, string password)
{
byte[] _passwordHashBytes;
int _arrayLen = (SaltByteSize + HashByteSize) + 1;
if (hashedPassword == null)
{
return false;
}
if (password == null)
{
throw new ArgumentNullException("password");
}
byte[] src = Convert.FromBase64String(hashedPassword);
if ((src.Length != _arrayLen) || (src[0] != 0))
{
return false;
}
byte[] _currentSaltBytes = new byte[SaltByteSize];
Buffer.BlockCopy(src, 1, _currentSaltBytes, 0, SaltByteSize);
byte[] _currentHashBytes = new byte[HashByteSize];
Buffer.BlockCopy(src, SaltByteSize + 1, _currentHashBytes, 0, HashByteSize);
using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password, _currentSaltBytes, HasingIterationsCount))
{
_passwordHashBytes = bytes.GetBytes(SaltByteSize);
}
return AreHashesEqual(_currentHashBytes, _passwordHashBytes);
}
private static bool AreHashesEqual(byte[] firstHash, byte[] secondHash)
{
int _minHashLength = firstHash.Length <= secondHash.Length ? firstHash.Length : secondHash.Length;
var xor = firstHash.Length ^ secondHash.Length;
for (int i = 0; i < _minHashLength; i++)
xor |= firstHash[i] ^ secondHash[i];
return 0 == xor;
}
커스텀 ApplicationUserManager에서 PasswordHasher 속성을 위의 코드를 포함하는 클래스의 이름을 설정합니다.
클래스 Password Hasher는 .net6 Password Hasher docs 최신 버전(V3) https://github.com/dotnet/aspnetcore/blob/b56bb17db3ae73ce5a8664a2023a9b9af89499dd/src/Identity/Extensions.Core/src/PasswordHasher.cs에 기반하여 작성합니다.
namespace Utilities;
public class PasswordHasher
{
public const int Pbkdf2Iterations = 1000;
public static string HashPasswordV3(string password)
{
return Convert.ToBase64String(HashPasswordV3(password, RandomNumberGenerator.Create()
, prf: KeyDerivationPrf.HMACSHA512, iterCount: Pbkdf2Iterations, saltSize: 128 / 8
, numBytesRequested: 256 / 8));
}
public static bool VerifyHashedPasswordV3(string hashedPasswordStr, string password)
{
byte[] hashedPassword = Convert.FromBase64String(hashedPasswordStr);
var iterCount = default(int);
var prf = default(KeyDerivationPrf);
try
{
// Read header information
prf = (KeyDerivationPrf)ReadNetworkByteOrder(hashedPassword, 1);
iterCount = (int)ReadNetworkByteOrder(hashedPassword, 5);
int saltLength = (int)ReadNetworkByteOrder(hashedPassword, 9);
// Read the salt: must be >= 128 bits
if (saltLength < 128 / 8)
{
return false;
}
byte[] salt = new byte[saltLength];
Buffer.BlockCopy(hashedPassword, 13, salt, 0, salt.Length);
// Read the subkey (the rest of the payload): must be >= 128 bits
int subkeyLength = hashedPassword.Length - 13 - salt.Length;
if (subkeyLength < 128 / 8)
{
return false;
}
byte[] expectedSubkey = new byte[subkeyLength];
Buffer.BlockCopy(hashedPassword, 13 + salt.Length, expectedSubkey, 0, expectedSubkey.Length);
// Hash the incoming password and verify it
byte[] actualSubkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, subkeyLength);
#if NETSTANDARD2_0 || NETFRAMEWORK
return ByteArraysEqual(actualSubkey, expectedSubkey);
#elif NETCOREAPP
return CryptographicOperations.FixedTimeEquals(actualSubkey, expectedSubkey);
#else
#error Update target frameworks
#endif
}
catch
{
// This should never occur except in the case of a malformed payload, where
// we might go off the end of the array. Regardless, a malformed payload
// implies verification failed.
return false;
}
}
// privates
private static byte[] HashPasswordV3(string password, RandomNumberGenerator rng, KeyDerivationPrf prf, int iterCount, int saltSize, int numBytesRequested)
{
byte[] salt = new byte[saltSize];
rng.GetBytes(salt);
byte[] subkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, numBytesRequested);
var outputBytes = new byte[13 + salt.Length + subkey.Length];
outputBytes[0] = 0x01; // format marker
WriteNetworkByteOrder(outputBytes, 1, (uint)prf);
WriteNetworkByteOrder(outputBytes, 5, (uint)iterCount);
WriteNetworkByteOrder(outputBytes, 9, (uint)saltSize);
Buffer.BlockCopy(salt, 0, outputBytes, 13, salt.Length);
Buffer.BlockCopy(subkey, 0, outputBytes, 13 + saltSize, subkey.Length);
return outputBytes;
}
private static void WriteNetworkByteOrder(byte[] buffer, int offset, uint value)
{
buffer[offset + 0] = (byte)(value >> 24);
buffer[offset + 1] = (byte)(value >> 16);
buffer[offset + 2] = (byte)(value >> 8);
buffer[offset + 3] = (byte)(value >> 0);
}
private static uint ReadNetworkByteOrder(byte[] buffer, int offset)
{
return ((uint)(buffer[offset + 0]) << 24)
| ((uint)(buffer[offset + 1]) << 16)
| ((uint)(buffer[offset + 2]) << 8)
| ((uint)(buffer[offset + 3]));
}
}
UserController에서 사용:
namespace WebApi.Controllers.UserController;
[Route("api/[controller]/[action]")]
[ApiController]
public class UserController : ControllerBase
{
private readonly IUserService _userService;
public UserController(IUserService userService)
{
_userService = userService;
}
[HttpPost]
public async Task<IActionResult> Register(VmRegister model)
{
var user = new User
{
UserName = model.UserName,
PasswordHash = PasswordHasher.HashPasswordV3(model.Password),
FirstName = model.FirstName,
LastName = model.LastName,
Mobile = model.Mobile,
Email = model.Email,
};
await _userService.Add(user);
return StatusCode(201, user.Id);
}
[HttpPost]
public async Task<IActionResult> Login(VmLogin model)
{
var user = await _userService.GetByUserName(model.UserName);
if (user is null || !PasswordHasher.VerifyHashedPasswordV3(user.PasswordHash, model.Password))
throw new Exception("The UserName or Password is wrong.");
// generate token
return Ok();
}
}
https://github.com/mammadkoma/WebApi/tree/master/WebApi
Andrew Savinkh의 답변에 따라 다음과 같은 변경을 가했습니다.AspNet Identity로 구성된 기존 DB에서 Dapper를 사용하고 있습니다.
해 주세요.
PasswordHasherCompatibilityMode.IdentityV2
AspNet Identity 를 、 「 」 、 「 」AspNetCore ID 입니다.
이것은 전체 수업을 위한 GitHub Gist입니다.
언급URL : https://stackoverflow.com/questions/20621950/asp-net-identitys-default-password-hasher-how-does-it-work-and-is-it-secure
'sourcecode' 카테고리의 다른 글
CSS를 사용하여 div를 수직으로 스크롤할 수 있도록 하다 (0) | 2023.04.23 |
---|---|
Bash 배열에 대한 명령어 출력 읽기 (0) | 2023.04.23 |
암호를 입력하라는 메시지를 표시하지 않고 PowerShell 인증 정보 사용 (0) | 2023.04.23 |
Open XML 워크시트에 날짜를 삽입하려면 어떻게 해야 합니까? (0) | 2023.04.23 |
디버깅 버튼이 있는 표준 에러 메시지와 같은 에러 메시지를 표시하는 방법 (0) | 2023.04.23 |