Hasher de mot de passe par défaut ASP.NET Identity - Comment ça marche et est-il sécurisé?

162

Je me demande si le Password Hasher qui est implémenté par défaut dans le UserManager fourni avec MVC 5 et ASP.NET Identity Framework est suffisamment sécurisé? Et si oui, pouvez-vous m'expliquer comment cela fonctionne?

L'interface IPasswordHasher ressemble à ceci:

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

Comme vous pouvez le voir, cela ne prend pas un sel, mais il est mentionné dans ce fil: " Asp.net Identity password hashing " qu'il le salit en fait dans les coulisses. Alors je me demande comment cela fait-il? Et d'où vient ce sel?

Ma préoccupation est que le sel est statique, ce qui le rend assez peu sûr.

André Snede Kock
la source
Je ne pense pas que cela réponde directement à votre question, mais Brock Allen a écrit sur certaines de vos préoccupations ici => brockallen.com/2013/10/20/… et a également écrit une bibliothèque open source de gestion des identités d'utilisateurs et d'authentification qui a divers fonctionnalités de base comme la réinitialisation du mot de passe, le hachage, etc. github.com/brockallen/BrockAllen.MembershipReboot
Shiva
@Shiva Merci, je vais regarder dans la bibliothèque et la vidéo sur la page. Mais je préfère ne pas avoir affaire à une bibliothèque externe. Pas si je peux l'éviter.
André Snede Kock
2
FYI: l'équivalent de stackoverflow pour la sécurité. Donc, bien que vous obteniez souvent une bonne / bonne réponse ici. Les experts sont sur security.stackexchange.com en particulier le commentaire "is it secure" J'ai posé une sorte de question similaire et la profondeur et la qualité de la réponse étaient incroyables.
phil soady
@philsoady Merci, cela a du sens bien sûr, je suis déjà sur quelques-uns des autres "sous-forums", si je n'obtiens pas de réponse, je peux utiliser, je vais passer à securiry.stackexchange.com. Et merci pour la pointe!
André Snede Kock

Réponses:

227

Voici comment fonctionne l'implémentation par défaut ( ASP.NET Framework ou ASP.NET Core ). Il utilise une fonction de dérivation de clé avec un sel aléatoire pour produire le hachage. Le sel est inclus dans la sortie du KDF. Ainsi, chaque fois que vous "hachez" le même mot de passe, vous obtiendrez différents hachages. Pour vérifier le hachage, la sortie est répartie entre le sel et le reste, et le KDF est à nouveau exécuté sur le mot de passe avec le sel spécifié. Si le résultat correspond au reste de la sortie initiale, le hachage est vérifié.

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

Vérification:

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);
}
Andrew Savinykh
la source
7
Donc, si je comprends bien, la HashPasswordfonction renvoie les deux dans la même chaîne? Et quand vous le vérifiez, il le divise à nouveau, et hache le mot de passe en clair entrant, avec le sel de la division, et le compare avec le hachage d'origine?
André Snede Kock
9
@ AndréSnedeHansen, exactement. Et je vous recommande aussi de poser des questions sur la sécurité ou sur la cryptographie SE. La partie «est-ce sécurisé» peut être mieux traitée dans ces contextes respectifs.
Andrew Savinykh
1
@shajeerpuzhakkal comme décrit dans la réponse ci-dessus.
Andrew Savinykh
3
@AndrewSavinykh Je sais, c'est pourquoi je demande - à quoi ça sert? Pour rendre le code plus intelligent? ;) Parce que pour moi, compter des trucs en utilisant des nombres décimaux est BEAUCOUP plus intuitif (nous avons 10 doigts après tout - au moins la plupart d'entre nous), donc déclarer un certain nombre de quelque chose en utilisant des hexadécimaux semble être une obfuscation inutile du code.
Andrew Cyrul
1
@ MihaiAlexandru-Ionut var hashedPassword = HashPassword(password); var result = VerifyHashedPassword(hashedPassword, password);- c'est ce que vous devez faire. après cela resultcontient vrai.
Andrew Savinykh
43

Parce que ces jours-ci, ASP.NET est open source, vous pouvez le trouver sur GitHub: AspNet.Identity 3.0 et AspNet.Identity 2.0 .

D'après les commentaires:

/* =======================
 * 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.)
 */
Knelis
la source
Oui, et il convient de noter, il y a des ajouts à l'algorithme que zespri montre.
André Snede Kock
1
La source sur GitHub est Asp.Net.Identity 3.0 qui est toujours en version préliminaire. La source de la fonction de hachage 2.0 est sur CodePlex
David
1
La dernière implémentation peut être trouvée sous github.com/dotnet/aspnetcore/blob/master/src/Identity/… maintenant. Ils ont archivé l'autre référentiel;)
FranzHuber23
32

Je comprends la réponse acceptée et je l'ai votée à la hausse, mais j'ai pensé que je laisserais la réponse de mes profanes ici ...

Créer un hachage

  1. Le sel est généré aléatoirement à l'aide de la fonction Rfc2898DeriveBytes qui génère un hachage et un sel. Les entrées de Rfc2898DeriveBytes sont le mot de passe, la taille du sel à générer et le nombre d'itérations de hachage à effectuer. https://msdn.microsoft.com/en-us/library/h83s4e12(v=vs.110).aspx
  2. Le sel et le hachage sont ensuite écrasés ensemble (sel d'abord suivi du hachage) et codés sous forme de chaîne (le sel est donc codé dans le hachage). Ce hachage codé (qui contient le sel et le hachage) est ensuite stocké (généralement) dans la base de données contre l'utilisateur.

Vérification d'un mot de passe par rapport à un hachage

Pour vérifier un mot de passe saisi par un utilisateur.

  1. Le sel est extrait du mot de passe haché stocké.
  2. Le salt est utilisé pour hacher le mot de passe d'entrée des utilisateurs en utilisant une surcharge de Rfc2898DeriveBytes qui prend un salt au lieu d'en générer un. https://msdn.microsoft.com/en-us/library/yx129kfs(v=vs.110).aspx
  3. Le hachage stocké et le hachage de test sont ensuite comparés.

Le hachage

Sous les couvertures, le hachage est généré à l'aide de la fonction de hachage SHA1 ( https://en.wikipedia.org/wiki/SHA-1 ). Cette fonction est appelée de manière itérative 1000 fois (dans l'implémentation d'identité par défaut)

Pourquoi est-ce sécurisé

  • Les sels aléatoires signifient qu'un attaquant ne peut pas utiliser une table de hachage pré-générée pour essayer de casser les mots de passe. Ils auraient besoin de générer une table de hachage pour chaque sel. (En supposant ici que le hacker a également compromis votre sel)
  • Si 2 mots de passe sont identiques, ils auront des hachages différents. (ce qui signifie que les attaquants ne peuvent pas déduire de mots de passe `` communs '')
  • L'appel itératif de SHA1 1000 fois signifie que l'attaquant doit également le faire. L'idée étant qu'à moins d'avoir du temps sur un supercalculateur, ils n'auront pas assez de ressources pour forcer brutalement le mot de passe à partir du hachage. Cela ralentirait massivement le temps de génération d'une table de hachage pour un sel donné.
Nattrass
la source
Merci pour votre explication. Dans le "Création d'un hachage 2." vous mentionnez que le sel et le hachage sont écrasés ensemble, savez-vous si cela est stocké dans le PasswordHash dans la table AspNetUsers. Le sel est-il stocké quelque part pour que je puisse le voir?
unicorn2
1
@ unicorn2 Si vous regardez la réponse d'Andrew Savinykh ... Dans la section sur le hachage, il semble que le sel soit stocké dans les 16 premiers octets du tableau d'octets qui est encodé en Base64 et écrit dans la base de données. Vous pourrez voir cette chaîne encodée en Base64 dans la table PasswordHash. Tout ce que vous pouvez dire à propos de la chaîne Base64, c'est qu'en gros le premier tiers est le sel. Le sel significatif est les 16 premiers octets de la version décodée en Base64 de la chaîne complète stockée dans la table PasswordHash
Nattrass
@Nattrass, Ma compréhension des hachages et des sels est plutôt rudimentaire, mais si le sel est facilement extrait du mot de passe haché, quel est l'intérêt du salage en premier lieu. Je pensais que le sel était censé être une entrée supplémentaire pour l'algorithme de hachage qui ne pouvait pas être facilement deviné.
NSouth
1
@NSouth Le sel unique rend le hachage unique pour un mot de passe donné. Donc, deux mots de passe identiques auront des hachages différents. L'accès à votre hachage et à votre sel ne permet toujours pas à l'attaquant de se souvenir de votre mot de passe. Le hachage n'est pas réversible. Ils auraient encore besoin de force brute pour chaque mot de passe possible. Le sel unique signifie simplement que le pirate ne peut pas déduire un mot de passe commun en effectuant une analyse de fréquence sur des hachages spécifiques s'il a réussi à obtenir l'ensemble de votre table utilisateur.
Nattrass
8

Pour ceux comme moi qui sont tout nouveaux dans ce domaine, voici le code avec const et un moyen réel de comparer les octets []. J'ai obtenu tout ce code de stackoverflow mais j'ai défini des consts pour que les valeurs puissent être modifiées et aussi

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

Dans votre ApplicationUserManager personnalisé, vous définissez la propriété PasswordHasher le nom de la classe qui contient le code ci-dessus.

kfrosty
la source
Pour cela .. _passwordHashBytes = bytes.GetBytes(SaltByteSize); Je suppose que vous vouliez dire ceci _passwordHashBytes = bytes.GetBytes(HashByteSize);.. Peu importe dans votre scénario puisque les deux sont de la même taille mais en général ..
Akshatha