Hasher de senha padrão da identidade do ASP.NET - Como funciona e é seguro?


162

Gostaria de saber se o Password Hasher, que é o padrão implementado no UserManager que acompanha o MVC 5 e o ASP.NET Identity Framework, é seguro o suficiente? E se sim, se você pudesse me explicar como isso funciona?

A interface IPasswordHasher tem esta aparência:

public interface IPasswordHasher
{
    string HashPassword(string password);
    PasswordVerificationResult VerifyHashedPassword(string hashedPassword, 
                                                       string providedPassword);
}

Como você pode ver, ele não precisa de muito sal, mas é mencionado neste tópico: " Hash de senha de identidade do Asp.net ", que realmente o sal nos bastidores. Então, eu estou querendo saber como isso faz isso? E de onde vem esse sal?

Minha preocupação é que o sal seja estático, tornando-o bastante inseguro.


Não acho que isso responda diretamente à sua pergunta, mas Brock Allen escreveu sobre algumas de suas preocupações aqui => brockallen.com/2013/10/20/… e também escreveu uma biblioteca de gerenciamento e autenticação de identidade de usuário de código aberto que possui várias caldeira de placa apresenta como redefinição de senha, hashing etc etc. github.com/brockallen/BrockAllen.MembershipReboot
Shiva

@ Shiva Obrigado, vou dar uma olhada na biblioteca e no vídeo na página. Mas eu preferiria não ter que lidar com uma biblioteca externa. Não se eu puder evitá-lo.
André Snede Kock

2
FYI: o stackoverflow equivalente à segurança. Portanto, embora muitas vezes você obtenha uma resposta boa / correta aqui. Os especialistas estão no security.stackexchange.com, especialmente o comentário "é seguro"? Eu fiz um tipo semelhante de pergunta e a profundidade e a qualidade da resposta foram surpreendentes.
phil soady

@philsoady Obrigado, isso faz sentido, claro, eu já estou em alguns dos outros "sub-fóruns", se eu não receber uma resposta, posso usar, vou passar para securiry.stackexchange.com. E obrigado pela dica!
André Snede Kock

Respostas:


227

Aqui está como a implementação padrão ( ASP.NET Framework ou ASP.NET Core ) funciona. Ele usa uma Função de Derivação de Chave com sal aleatório para produzir o hash. O sal é incluído como parte da produção do KDF. Assim, cada vez que você "hash" a mesma senha, você obterá hashes diferentes. Para verificar o hash, a saída é dividida novamente no salt e no restante, e o KDF é executado novamente na senha com o salt especificado. Se o resultado corresponder ao restante da saída inicial, o hash será verificado.

Hashing:

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

Verificando:

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
Então, se eu entendi isso corretamente, a HashPasswordfunção retorna os dois na mesma string? E quando você o verifica, o divide novamente e faz o hash da senha de texto não criptografado, com o sal da divisão, e a compara com o hash original?
André Snede Kock

9
@ AndréSnedeHansen, exatamente. E eu também recomendo que você pergunte sobre segurança ou criptografia SE. A parte "é seguro" pode ser tratada melhor nesses contextos respectivos.
Andrew Savinykh

1
@shajeerpuzhakkal, conforme descrito na resposta acima.
Andrew Savinykh

3
@AndrewSavinykh Eu sei, é por isso que estou perguntando - qual é o sentido? Para tornar o código mais inteligente? ;) Para mim, contar coisas usando números decimais é MUITO mais intuitivo (afinal, temos 10 dedos - pelo menos a maioria de nós); portanto, declarar várias coisas usando hexadecimais parece uma ofuscação desnecessária do código.
precisa saber é o seguinte

1
@ MihaiAlexandru-Ionut var hashedPassword = HashPassword(password); var result = VerifyHashedPassword(hashedPassword, password);- é o que você precisa fazer. depois disso resultcontém true.
Andrew Savinykh

43

Como atualmente o ASP.NET é de código aberto, você pode encontrá-lo no GitHub: AspNet.Identity 3.0 e AspNet.Identity 2.0 .

Dos comentários:

/* =======================
 * 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.)
 */

Sim, e vale a pena notar, existem adições ao algoritmo que o zespri está mostrando.
André Snede Kock 12/01

1
A fonte no GitHub é o Asp.Net.Identity 3.0, que ainda está em pré-lançamento. A fonte da função de hash 2.0 está no CodePlex
David

1
A implementação mais recente pode ser encontrada em github.com/dotnet/aspnetcore/blob/master/src/Identity/… agora. Eles
arquivaram

32

Entendo a resposta aceita e a votei de forma positiva, mas pensei em dar uma resposta aqui para a resposta dos meus leigos ...

Criando um hash

  1. O sal é gerado aleatoriamente usando a função Rfc2898DeriveBytes, que gera um hash e um sal. As entradas para Rfc2898DeriveBytes são a senha, o tamanho do salt a ser gerado e o número de iterações de hash a serem executadas. https://msdn.microsoft.com/en-us/library/h83s4e12(v=vs.110).aspx
  2. O sal e o hash são então triturados juntos (sal primeiro seguido pelo hash) e codificados como uma string (para que o sal seja codificado no hash). Esse hash codificado (que contém o salt e o hash) é então armazenado (normalmente) no banco de dados contra o usuário.

Verificando uma senha contra um hash

Para verificar uma senha inserida por um usuário.

  1. O sal é extraído da senha de hash armazenada.
  2. O salt é usado para fazer hash na senha de entrada dos usuários usando uma sobrecarga de Rfc2898DeriveBytes que usa um salt em vez de gerar um. https://msdn.microsoft.com/en-us/library/yx129kfs(v=vs.110).aspx
  3. O hash armazenado e o hash de teste são comparados.

The Hash

Nos bastidores, o hash é gerado usando a função de hash SHA1 ( https://en.wikipedia.org/wiki/SHA-1 ). Essa função é chamada iterativamente 1000 vezes (na implementação de identidade padrão)

Por que isso é seguro

  • Sais aleatórios significa que um invasor não pode usar uma tabela pré-gerada de hashs para tentar quebrar senhas. Eles precisariam gerar uma tabela de hash para cada sal. (Assumindo aqui que o hacker também comprometeu o seu sal)
  • Se duas senhas forem idênticas, elas terão hashes diferentes. (o que significa que invasores não podem inferir senhas "comuns")
  • Chamar iterativamente o SHA1 1000 vezes significa que o invasor também precisa fazer isso. A idéia é que, a menos que eles tenham tempo em um supercomputador, eles não terão recursos suficientes para forçar brutalmente a senha do hash. Reduziria enormemente o tempo para gerar uma tabela de hash para um determinado sal.

Obrigado pela sua explicação. No "Criando um hash 2." você menciona que o salt e o hash são misturados, você sabe se isso está armazenado no PasswordHash na tabela AspNetUsers. O sal está armazenado em algum lugar para eu ver?
precisa saber é

1
@ unicorn2 Se der uma olhada na resposta de Andrew Savinykh ... Na seção sobre hash, parece que o salt é armazenado nos primeiros 16 bytes da matriz de bytes, codificados em Base64 e gravados no banco de dados. Você seria capaz de ver essa sequência codificada Base64 na tabela PasswordHash. Tudo o que você pode dizer sobre a string Base64 é que aproximadamente o primeiro terço é o sal. O sal significativa é os primeiros 16 bytes do Base64 decodificado versão da string completo armazenado na tabela de PasswordHash
Nattrass

@ Nattrass, meu entendimento de hashes e sais é bastante rudimentar, mas se o sal é facilmente extraído da senha do hash, qual é o sentido da salga em primeiro lugar. Eu pensei que o sal era para ser uma entrada extra para o algoritmo de hash que não podia ser facilmente adivinhado.
NSouth 27/05/19

1
@NSouth O sal exclusivo torna o hash exclusivo para uma determinada senha. Portanto, duas senhas idênticas terão hash diferentes. Ter acesso ao seu hash e salt ainda não faz com que o invasor se lembre da senha. O hash não é reversível. Eles ainda precisariam usar força bruta para passar por todas as senhas possíveis. O salt exclusivo significa apenas que o hacker não pode inferir senhas comuns fazendo uma análise de frequência em hash específicos se tiver conseguido se apossar de toda a tabela de usuários.
Nattrass

8

Para aqueles como eu que são novatos nisso, aqui está o código com const e uma maneira real de comparar os bytes []. Eu recebi todo esse código do stackoverflow, mas defini consts para que os valores pudessem ser alterados e também

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

No ApplicationUserManager personalizado, você define a propriedade PasswordHasher como o nome da classe que contém o código acima.


Para isso .. _passwordHashBytes = bytes.GetBytes(SaltByteSize); Eu acho que você quis dizer isso _passwordHashBytes = bytes.GetBytes(HashByteSize);.. não importa em seu cenário uma vez que ambos são do mesmo tamanho, mas em geral ..
Akshatha
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.