ASP.NET Identity의 기본 암호 해셔-어떻게 작동하며 안전합니까?


162

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 암호 해싱 "은 실제로 배후에서 소금을 뿌립니다. 그래서 이것이 어떻게되는지 궁금합니다. 그리고이 소금은 어디에서 왔습니까?

저의 염려는 소금이 정체되어있어 매우 불안합니다.


나는 이것이 귀하의 질문에 직접 대답한다고 생각하지 않지만 Brock Allen은 귀하의 우려 사항에 대해 여기에 작성했습니다 => brockallen.com/2013/10/20/… 또한 다양한 오픈 소스 사용자 ID 관리 및 인증 라이브러리 비밀번호 재설정, 해싱 등과 같은 보일러 플레이트 기능 github.com/brockallen/BrockAllen.MembershipReboot
Shiva

@Shiva 감사합니다. 페이지의 라이브러리와 비디오를 살펴 보겠습니다. 그러나 외부 라이브러리를 다루지 않아도됩니다. 내가 피할 수 있다면 아닙니다.
André Snede Kock

2
참고로, 보안과 동등한 스택 오버 플로우. 따라서 종종 좋은 대답과 올바른 대답을 얻을 수 있습니다. 전문가들은 security.stackexchange.com 에 댓글을 달았습니다. "보안 적"이라는 질문에 비슷한 종류의 질문을했으며 답변의 깊이와 품질은 놀랍습니다.
phil soady

@philsoady 감사합니다. 당연히, 다른 "서브 포럼"중 일부에 이미 답변을 얻지 못하면 사용할 수 있습니다 securiry.stackexchange.com. 그리고 팁 주셔서 감사합니다!
André Snede Kock

답변:


227

기본 구현 ( 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);
}

7
그래서 이것을 올바르게 이해하면 HashPassword함수는 동일한 문자열로 둘 다 반환합니까? 그리고 확인하면 다시 다시 분할하고 들어오는 일반 텍스트 암호를 분할 된 소금으로 해시하고 원래 해시와 비교합니까?
André Snede Kock

9
@ AndréSnedeHansen, 정확히. 또한 보안 또는 암호화 SE에 대해 문의하는 것이 좋습니다. "안전한"부분은 각각의 상황에서 더 잘 다룰 수 있습니다.
앤드류 Savinykh

1
위의 답변에서 설명한대로 @shajeerpuzhakkal.
Andrew Savinykh

3
@AndrewSavinykh 내가 알고있는 이유는 무엇입니까?-요점이 무엇입니까? 코드를 더 똑똑하게 보이게하려면? ;) 10 진수를 사용하여 물건을 세는 이유는 훨씬 직관적입니다 (최소한 대부분의 사람들은 10 손가락을 가지고 있습니다). 따라서 16 진수를 사용하여 많은 것을 선언하는 것은 불필요한 코드 난독 화처럼 보입니다.
Andrew Cyrul

1
@ MihaiAlexandru-Ionut- var hashedPassword = HashPassword(password); var result = VerifyHashedPassword(hashedPassword, password);당신이해야 할 일입니다. 그 후에 result는 참 이 포함됩니다.
Andrew Savinykh

43

요즘 ASP.NET은 오픈 소스이므로 GitHub에서 찾을 수 있습니다 : AspNet.Identity 3.0AspNet.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.)
 */

그렇습니다. 주목할 가치가있는 것은 zespri가 보여주는 알고리즘에 추가 된 것들입니다.
André Snede Kock 2016 년

1
GitHub의 소스는 여전히 시험판에있는 Asp.Net.Identity 3.0입니다. 2.0 해시 함수의 소스는 CodePlex
David

1
최신 구현은 github.com/dotnet/aspnetcore/blob/master/src/Identity/…에 있습니다. 그들은 다른 저장소를 보관했다;)
FranzHuber23

32

나는 받아 들인 대답을 이해하고 투표를했지만 여기에 평신도의 대답을 버릴 것이라고 생각했습니다 ...

해시 만들기

  1. 솔트는 해시와 솔트 를 생성하는 Rfc2898DeriveBytes 함수를 사용하여 임의로 생성됩니다 . Rfc2898DeriveBytes의 입력 은 비밀번호, 생성 할 솔트의 크기 및 수행 할 해시 반복 횟수입니다. https://msdn.microsoft.com/en-us/library/h83s4e12(v=vs.110).aspx
  2. 그런 다음 솔트와 해시는 함께 으깬 다음 (소금이 먼저 소금에 이어) 문자열로 인코딩됩니다 (솔트는 해시로 인코딩됩니다). 그런 다음이 인코딩 된 해시 (소금과 해시가 포함됨)는 일반적으로 사용자에 대해 데이터베이스에 저장됩니다.

해시에 대한 비밀번호 확인

사용자가 입력 한 비밀번호를 확인합니다.

  1. 소금은 저장된 해시 비밀번호에서 추출됩니다.
  2. 솔트는 Rfc2898DeriveBytes 의 오버로드를 사용하여 사용자 입력 비밀번호를 해시하는 데 사용됩니다 . https://msdn.microsoft.com/en-us/library/yx129kfs(v=vs.110).aspx
  3. 저장된 해시와 테스트 해시가 비교됩니다.

해시

커버 아래에서 해시는 SHA1 해시 함수 ( https://en.wikipedia.org/wiki/SHA-1 )를 사용하여 생성됩니다 . 이 함수는 반복적으로 1000 번 호출됩니다 (기본 아이디 구현에서)

이것이 왜 안전한가요?

  • 임의의 소금은 공격자가 미리 생성 된 해시 테이블을 사용하여 암호를 시도 및 차단할 수 없음을 의미합니다. 모든 소금에 대해 해시 테이블을 생성해야합니다. (여기서 해커가 소금을 손상 시켰다고 가정)
  • 두 개의 암호가 동일하면 해시가 다릅니다. (공격자가 '일반적인'암호를 유추 할 수 없음을 의미)
  • SHA1을 1000 번 반복해서 호출하면 공격자도이를 수행해야합니다. 아이디어는 슈퍼 컴퓨터에 시간이 없으면 해시에서 암호를 무차별 처리 할 수있는 충분한 리소스가 없다는 것입니다. 주어진 소금에 대한 해시 테이블을 생성하는 데 시간이 크게 느려질 것입니다.

설명해 주셔서 감사합니다. "해시 만들기 2"에서 당신은 소금과 해시가 으깬다고 언급했는데 이것이 AspNetUsers 테이블의 PasswordHash에 저장되어 있는지 알고 있습니다. 내가 볼 수있는 곳에 소금이 저장되어 있습니까?
unicorn2

1
@ unicorn2 Andrew Savinykh의 대답을 보면 해싱에 대한 섹션에서 소금이 바이트 배열의 첫 16 바이트에 Base64로 인코딩되어 데이터베이스에 기록되는 것처럼 보입니다. PasswordHash 테이블에서이 Base64 인코딩 문자열을 볼 수 있습니다. Base64 스트링에 대해 말할 수있는 것은 대략 1/3이 소금이라는 것입니다. 의미있는 소금은 Base64로의 처음 16 바이트가 PasswordHash 테이블에 저장된 전체 문자열의 버전 디코딩입니다
나트라 스

@Nattrass, 해시와 소금에 대한 나의 이해는 다소 기초적인 것이지만, 해시 된 암호에서 소금을 쉽게 추출 할 수 있다면, 처음부터 소금에 절이는 요점은 무엇입니까? 나는 소금이 쉽게 추측 할 수없는 해싱 알고리즘에 대한 추가 입력이라고 생각했습니다.
NSouth

1
@NSouth 고유 한 솔트는 주어진 암호에 대해 해시를 고유하게 만듭니다. 따라서 두 개의 동일한 암호에는 다른 해시가 있습니다. 해시와 솔트에 액세스해도 여전히 암호를 기억하지 못합니다. 해시는 뒤집을 수 없습니다. 그들은 가능한 모든 암호를 통해 무차별 대입해야합니다. 고유 한 솔트는 해커가 전체 사용자 테이블을 확보 할 수있는 경우 특정 해시에 대한 빈도 분석을 수행하여 공통 비밀번호를 유추 할 수 없음을 의미합니다.
Nattrass

8

저와 같은 새로운 사람들에게는 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 특성을 위 코드가 포함 된 클래스 이름으로 설정하십시오.


이를 위해 .. _passwordHashBytes = bytes.GetBytes(SaltByteSize); 나는 당신이 이것을 의미 한 것 같아 _passwordHashBytes = bytes.GetBytes(HashByteSize);.. 둘 다 같은 크기이지만 일반적으로 당신의 시나리오에서 중요하지 않습니다 ..
Akshatha
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.