Hasher password predefinito di ASP.NET Identity: come funziona ed è sicuro?


162

Mi chiedo se il Password Hasher implementato di default nel UserManager fornito con MVC 5 e ASP.NET Identity Framework sia abbastanza sicuro? E se è così, se potessi spiegarmi come funziona?

L'interfaccia di IPasswordHasher è simile alla seguente:

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

Come puoi vedere, non ci vuole niente, ma è menzionato in questo thread: " Asp.net Identity password hashing " che infatti lo sale dietro le quinte. Quindi mi chiedo come fa? E da dove viene questo sale?

La mia preoccupazione è che il sale sia statico, rendendolo piuttosto insicuro.


Non credo che questo risponda direttamente alla tua domanda, ma Brock Allen ha scritto su alcune delle tue preoccupazioni qui => brockallen.com/2013/10/20/… e ha anche scritto una libreria di gestione e autenticazione dell'identità utente open source che ha vari caratteristiche della piastra della caldaia come reimpostazione password, hashing ecc ecc. github.com/brockallen/BrockAllen.MembershipReboot
Shiva

@Shiva Grazie, guarderò in biblioteca e il video sulla pagina. Ma preferirei non avere a che fare con una biblioteca esterna. Non se posso evitarlo.
André Snede Kock,

2
Cordiali saluti: l'equivalente stackoverflow per la sicurezza. Quindi, anche se spesso otterrai una risposta buona / corretta qui. Gli esperti sono su security.stackexchange.com in particolare il commento "è sicuro" Ho fatto una domanda simile e la profondità e la qualità della risposta è stata sorprendente.
Phil Soady,

@philsoady Grazie, questo ha ovviamente senso, sono già su alcuni degli altri "forum secondari", se non ottengo una risposta, posso usare, passerò a securiry.stackexchange.com. E grazie per il suggerimento!
André Snede Kock,

Risposte:


227

Ecco come funziona l'implementazione predefinita ( ASP.NET Framework o ASP.NET Core ). Utilizza una funzione di derivazione chiave con salt casuale per produrre l'hash. Il sale è incluso come parte dell'output del KDF. Pertanto, ogni volta che si "hash" la stessa password si ottengono hash diversi. Per verificare l'hash, l'output viene suddiviso nuovamente in salt e il resto e KDF viene eseguito nuovamente sulla password con il salt specificato. Se il risultato corrisponde al resto dell'output iniziale, l'hash viene verificato.

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

verifica:

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
Quindi, se lo capisco correttamente, la HashPasswordfunzione restituisce entrambi nella stessa stringa? E quando lo verifichi, lo divide di nuovo e lo hash della password in chiaro in arrivo, con il sale della divisione, e lo confronta con l'hash originale?
André Snede Kock,

9
@ AndréSnedeHansen, esattamente. E anche io ti consiglio di chiedere sulla sicurezza o sulla crittografia SE. La parte "è sicura" può essere affrontata meglio nei rispettivi contesti.
Andrew Savinykh,

1
@shajeerpuzhakkal come descritto nella risposta sopra.
Andrew Savinykh,

3
@AndrewSavinykh Lo so, ecco perché lo sto chiedendo: qual è il punto? Per rendere il codice più intelligente? ;) Perché per me contare cose usando numeri decimali è MOLTO più intuitivo (dopotutto abbiamo 10 dita - almeno la maggior parte di noi), quindi dichiarare un numero di qualcosa usando esadecimali sembra un inutile offuscamento del codice.
Andrew Cyrul,

1
@ MihaiAlexandru-Ionut var hashedPassword = HashPassword(password); var result = VerifyHashedPassword(hashedPassword, password);- è quello che devi fare. dopo ciò resultcontiene vero.
Andrew Savinykh,

43

Poiché in questi giorni ASP.NET è open source, è possibile trovarlo su GitHub: AspNet.Identity 3.0 e AspNet.Identity 2.0 .

Dai commenti:

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

Sì, e vale la pena notare, ci sono aggiunte all'algoritmo che zespri sta mostrando.
André Snede Kock,

1
La fonte su GitHub è Asp.Net.Identity 3.0 che è ancora in versione non definitiva. La fonte della funzione hash 2.0 è su CodePlex
David

1
La più recente implementazione può essere trovata sotto github.com/dotnet/aspnetcore/blob/master/src/Identity/… ora. Hanno archiviato l'altro repository;)
FranzHuber23 il

32

Capisco la risposta accettata e l'ho votata ma ho pensato di scaricare la risposta dei miei laici qui ...

Creare un hash

  1. Il salt viene generato casualmente utilizzando la funzione Rfc2898DeriveBytes che genera un hash e un salt. Input per Rfc2898DeriveBytes sono la password, la dimensione del salt da generare e il numero di iterazioni di hashing da eseguire. https://msdn.microsoft.com/en-us/library/h83s4e12(v=vs.110).aspx
  2. Il sale e l'hash vengono quindi uniti insieme (il sale viene seguito dall'hash) e codificato come una stringa (quindi il sale viene codificato nell'hash). Questo hash codificato (che contiene salt e hash) viene quindi archiviato (in genere) nel database contro l'utente.

Verifica di una password rispetto a un hash

Per controllare una password immessa da un utente.

  1. Il salt viene estratto dalla password con hash memorizzata.
  2. Il salt viene usato per eseguire il hash della password di input dell'utente utilizzando un sovraccarico di Rfc2898DeriveBytes che richiede un salt anziché generarne uno. https://msdn.microsoft.com/en-us/library/yx129kfs(v=vs.110).aspx
  3. L'hash memorizzato e l'hash di test vengono quindi confrontati.

L'hash

Sotto le coperte l'hash è generato usando la funzione hash SHA1 ( https://en.wikipedia.org/wiki/SHA-1 ). Questa funzione viene chiamata iterativamente 1000 volte (nell'implementazione Identity predefinita)

Perché è sicuro?

  • Sali casuali significa che un utente malintenzionato non può utilizzare una tabella pre-generata di hash per provare a violare le password. Avrebbero bisogno di generare una tabella di hash per ogni sale. (Supponendo qui che l'hacker abbia anche compromesso il tuo sale)
  • Se 2 password sono identiche, avranno hash diversi. (il che significa che gli aggressori non possono dedurre password "comuni")
  • Chiamare iterativamente SHA1 1000 volte significa che anche l'attaccante deve farlo. L'idea è che a meno che non abbiano tempo su un supercomputer non avranno abbastanza risorse per forzare la password dall'hash. Rallenterebbe enormemente il tempo per generare una tabella hash per un dato sale.

Grazie per la tua spiegazione. Nel "Creazione di un hash 2." dici che il sale e l'hash sono uniti insieme, sai se questo è memorizzato nel PasswordHash nella tabella AspNetUsers. Il sale è conservato ovunque per me vedere?
unicorn2

1
@ unicorn2 Se dai un'occhiata alla risposta di Andrew Savinykh ... Nella sezione sull'hash sembra che il salt sia memorizzato nei primi 16 byte dell'array di byte che è codificato Base64 e scritto nel database. Potresti vedere questa stringa codificata Base64 nella tabella PasswordHash. Tutto quello che puoi dire sulla stringa Base64 è che all'incirca il primo terzo è il sale. Il significato significativo sono i primi 16 byte della versione decodificata Base64 dell'intera stringa memorizzata nella tabella PasswordHash
Nattrass

@Nattrass, La mia comprensione di hash e sali è piuttosto rudimentale, ma se il sale viene facilmente estratto dalla password con hash, qual è il punto di salare in primo luogo. Pensavo che il sale fosse un ulteriore input per l'algoritmo di hashing che non poteva essere facilmente intuito.
NSouth,

1
@NSouth Il sale unico rende l'hash univoco per una determinata password. Quindi due password identiche avranno hash diversi. Avere accesso al tuo hash e salt non ottiene ancora l'attaccante che la tua password ricorda. L'hash non è reversibile. Avrebbero comunque bisogno di forzare la forza lì attraverso ogni possibile password. Il salt unico significa solo che l'hacker non può inferire una password comune facendo un'analisi di frequenza su hash specifici se sono riusciti a ottenere l'intera tabella utente.
Nattrass,

8

Per quelli come me che sono nuovi di zecca, ecco il codice con const e un modo reale per confrontare i byte []. Ho ottenuto tutto questo codice da StackOverflow, ma ho definito i costi in modo che i valori possano essere modificati e anche

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

Nel tuo ApplicationUserManager personalizzato, imposta la proprietà PasswordHasher il nome della classe che contiene il codice sopra.


Per questo .. _passwordHashBytes = bytes.GetBytes(SaltByteSize); Immagino tu intendessi questo _passwordHashBytes = bytes.GetBytes(HashByteSize);.. Non importa nel tuo scenario poiché entrambi hanno le stesse dimensioni ma in generale ..
Akshatha,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.