MVC 5 및 ASP.NET Identity Framework와 함께 제공 되는 UserManager 에서 기본적으로 구현 된 Password Hasher가 충분히 안전한지 궁금합니다. 그렇다면 어떻게 작동하는지 설명해 주시겠습니까?
IPasswordHasher 인터페이스는 다음과 같습니다.
public interface IPasswordHasher
{
string HashPassword(string password);
PasswordVerificationResult VerifyHashedPassword(string hashedPassword,
string providedPassword);
}
보시다시피 소금이 필요하지는 않지만이 스레드에서 언급됩니다. ” Asp.net ID 암호 해싱 “은 실제로 배후에서 소금을 뿌립니다. 그래서 이것이 어떻게되는지 궁금합니다. 그리고이 소금은 어디에서 왔습니까?
저의 염려는 소금이 정체되어있어 매우 불안합니다.
답변
기본 구현 ( ASP.NET Framework 또는 ASP.NET Core ) 작동 방식은 다음과 같습니다 . 그것은 사용 키 유도 기능을 해시를 생성하는 임의의 소금. 소금은 KDF 출력의 일부로 포함됩니다. 따라서 동일한 비밀번호를 “해시”할 때마다 다른 해시를 얻게됩니다. 해시를 확인하기 위해 출력이 솔트와 나머지로 다시 분할되고 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.Identity 3.0 및 AspNet.Identity 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.)
*/
답변
나는 받아 들인 대답을 이해하고 투표를했지만 여기에 평신도의 대답을 버릴 것이라고 생각했습니다 …
해시 만들기
- 솔트는 해시와 솔트 를 생성하는 Rfc2898DeriveBytes 함수를 사용하여 임의로 생성됩니다
. Rfc2898DeriveBytes의 입력 은 비밀번호, 생성 할 솔트의 크기 및 수행 할 해시 반복 횟수입니다.
https://msdn.microsoft.com/en-us/library/h83s4e12(v=vs.110).aspx - 그런 다음 솔트와 해시는 함께 으깬 다음 (소금이 먼저 소금에 이어) 문자열로 인코딩됩니다 (솔트는 해시로 인코딩됩니다). 그런 다음이 인코딩 된 해시 (소금과 해시가 포함됨)는 일반적으로 사용자에 대해 데이터베이스에 저장됩니다.
해시에 대한 비밀번호 확인
사용자가 입력 한 비밀번호를 확인합니다.
- 소금은 저장된 해시 비밀번호에서 추출됩니다.
- 솔트는 Rfc2898DeriveBytes 의 오버로드를 사용하여 사용자 입력 비밀번호를 해시하는 데 사용됩니다 . https://msdn.microsoft.com/en-us/library/yx129kfs(v=vs.110).aspx
- 저장된 해시와 테스트 해시가 비교됩니다.
해시
커버 아래에서 해시는 SHA1 해시 함수 ( https://en.wikipedia.org/wiki/SHA-1 )를 사용하여 생성됩니다 . 이 함수는 반복적으로 1000 번 호출됩니다 (기본 아이디 구현에서)
이것이 왜 안전한가요?
- 임의의 소금은 공격자가 미리 생성 된 해시 테이블을 사용하여 암호를 시도 및 차단할 수 없음을 의미합니다. 모든 소금에 대해 해시 테이블을 생성해야합니다. (여기서 해커가 소금을 손상 시켰다고 가정)
- 두 개의 암호가 동일하면 해시가 다릅니다. (공격자가 ‘일반적인’암호를 유추 할 수 없음을 의미)
- SHA1을 1000 번 반복해서 호출하면 공격자도이를 수행해야합니다. 아이디어는 슈퍼 컴퓨터에 시간이 없으면 해시에서 암호를 무차별 처리 할 수있는 충분한 리소스가 없다는 것입니다. 주어진 소금에 대한 해시 테이블을 생성하는 데 시간이 크게 느려질 것입니다.
답변
저와 같은 새로운 사람들에게는 const가있는 코드와 byte []를 비교하는 실제 방법이 있습니다. 이 코드는 모두 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 특성을 위 코드가 포함 된 클래스 이름으로 설정하십시오.