Première revue de mon livre en anglais !

Merci à Gregor d’avoir pris le temps d’écrire ceci : http://gregorsuttie.com/2012/06/10/book-review-practical-performance-improving-the-efficiency-of-net-code/

Posted in Retours | Tagged | Leave a comment

Apprendre MVC3 quand vous êtes expert et débutant à la fois

On a parfois du mal à trouver de la documentation adaptée quand on a une bonne expérience en programmation, mais qu’on ne connait pas du tout une technologie.

Ou alors on tombe sur des documentations niveau débutant, mais qui réexplique tout le contexte qui nous parait évident, et on a alors l’impression de perdre notre temps à lire beaucoup pour n’apprendre que des bribes.

Ou bien on tape un peu plus haut dans la techno cherchée, mais comme on n’a pas les bases, la simple application d’une commande demande de la recherche.

J’ai eu le cas pour me mettre à MVC3. Je connais bien le modèle ASP.NET, un peu moins WebForms, mais je me débrouille suffisamment en HTML. Par contre, MVC3, rien du tout, à part le principe, et encore de 10 000 mètres de haut… Pas facile donc de trouver la bonne doc. Mais j’ai ce qu’il vous faut si vous êtes dans le même cas que moi :

http://codesamplez.com/development/introduction-to-asp-net-mvc3-and-razor-in-c-sharp

Posted in .NET | Tagged | Leave a comment

L’affaire des hash SHA-1 fuités de LinkedIn

image This article is also available in English.

Le contexte

Vous êtes sûrement au courant du fait qu’un pirate a réussi à voler une liste de 6,5 millions de mots de passe à LinkedIn récemment. En pratique, ces mots de passe n’étaient pas lisibles en clair : il s’agit d’un hash SHA-1 des mots de passe. Toutefois, l’algorithme avait été utilisé de manière un peu légère, car sans sel.

Pour expliquer en deux minutes, le principe d’un hash est de transformer un mot en un autre sans qu’il y ait possibilité mathématique de le retrouver. Ainsi, le hash de “password” est par exemple “5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8”, et il n’existe pas de moyen théorique de retrouver “password” à partir de ce nombre.

image

Ceci est intéressant pour les mots de passe, car c’est censé garantir que, si on stocke le hash, même un administrateur ne peut pas en déduire votre mot de passe. Par contre, lorsqu’une application reçoit une tentative de connexion, elle est capable de dire si le mot de passe envoyé est le bon car, si c’est le cas, il aura le même hash que celui qui est stocké dans sa base de données. A l’inverse, un léger changement dans le mot de passe proposé provoquera un grand changement dans le hash calculé, et l’application saura alors que le mot de passe n’est pas correct. Cette grande instabilité est importante, car elle permet de rendre statistiquement impossible de modifier petit à petit des tentatives pour approcher d’un hash voulu : si on change ne serait-ce qu’un caractère, le hash s’en trouve tout retourné, et il n’y a donc aucune chance qu’on puisse approcher le bon mot par tâtonnements.

Voila pour la théorie. Dans la pratique, le mécanisme de hash pose un problème, à savoir que comme “password” donne toujours un hash de “5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8” (et c’est heureux sinon nous ne pourrions pas nous en servir pour valider les mots de passe), cela veut dire qu’il est possible de pré-calculer des dictionnaires de hash sur des mots de passe “courants”, et de comparer ensuite directement les hash, pour en déduire le mot de passe utilisé.

image

La contre-mesure pour ceci est d’utiliser ce qu’on appelle un “sel”. “Saler l’algorithme de hash” consiste à ajouter du “bruit” connu autour du mot de passe à hasher. Ainsi, au lieu de calculer le hash de “password”, on calculerait par exemple celui de “PréfIXE32password67coucou”, qui a beaucoup moins de chance de se trouver dans un dictionnaire de hash, car il s’agit pour le moins d’un mot de passe peu courant. Pour ce qui est de la validation des mots de passe, pas de problème, car on sait qu’avant de comparer le hash, il faut toujours entourer la tentative avec “PréFIXE32” et “67coucou”.

image

La boulette de LinkedIn a été de ne pas saler leur algorithme de hash SHA-1 (en tout cas à une époque, dont ce fichier date). Du coup, quand le fichier en question a fuité, n’importe qui a pu y trouver des hash de mots de passe connus, comme ceux de “password”, “123456”, etc.

L’hypothèse à valider

Evidemment, ceci est une erreur de la part de LinkedIn. C’est bien gentil d’expliquer que leur équipe contient des experts de la sécurité (http://blog.linkedin.com/2012/06/09/an-update-on-taking-steps-to-protect-our-members/), si c’est pour oublier de saler leur algo de hash – ce qu’un informaticien qui s’intéresse un tant soit peu à la sécurité sait – ce n’est pas la peine de faire trop de pub…

Pourtant, je fais l’hypothèse qu’un sel, appliqué de manière unique à tous les hash découverts dans le fichier, n’aurait en fait pas changé grand chose à la situation.

En effet, dans un tel fichier avec 6.5 millions de mots de passe, il y a toutes les chances de trouver bon nombre de mots de passe “traditionnels” comme “password”, “123456”, etc. Du coup, si le mécanisme de hash est commun, il suffit de lancer une attaque de force brute sur le mécanisme de sel, tout en restreignant à ces quelques mots de passe cible, pour retrouver celui-ci. Cela prend évidemment du temps en plus, mais mon hypothèse est que cela ne fait que multiplier par deux le temps nécessaire, et non pas par le nombre de chaînes possibles. En bref, avec des unités de temps relatives :

image

Je vous propose de me suivre dans cet article à rallonge, qui relate mes différentes expérimentations sur cette hypothèse. Le code est en C#, mais vous n’aurez pas de mal à retrouver sa signification même si vous êtes habitués à d’autres langages.

Trouver les fichiers

Avant de commencer à pouvoir jouer avec, il s’agit de trouver le fichier avec les fameux hash. Le lien original ne fonctionne plus :

image

Mais en suivant un peu les conversations sur https://news.ycombinator.com/item?id=4073309, on retrouve rapidement la trace d’une copie… Il doit y en avoir quelques milliers dans le monde, j’imagine. Du coup, j’avoue ne pas voir l’intérêt de bloquer le premier lien. A la rigueur, ça ne fait qu’encourager la consommation disque pour rien, puisque de toute façon, on le retrouve en deux temps trois mouvements.

Bref, nous voici sur un autre site pour récupérer le fichier :

image

20 minutes de téléchargement plus tard, on jette un œil dans le fichier, et ça semble être le bon, avec une partie des hash dont le début est remplacé par cinq caractères “0”, caractéristique abondamment discutée sur http://dropsafe.crypticide.com/article/7235.

image

Ensuite, on récupère un fichier de mots de passe retrouvé sur DazzlePod, et on commence par faire un petit test rapide sur de façon à vérifier qu’on a bien les “grands standards” :

        [TestMethod]
        public void PresenceMotsDePasseSimplesDansFichierReference()
        {
            List<string> mots = new List<string>() { "password", "123456", "superman" };
            string ligne;
            int motsTrouves = 0;
            int nbLignesAnalysees = 0;
            using (StreamReader lecteur = new StreamReader(@"D:\Projets\Brèche LinkedIn SHA1\passwords.txt"))
            {
                while ((ligne = lecteur.ReadLine()) != null)
                {
                    nbLignesAnalysees++;
                    if (mots.Contains(ligne))
                        motsTrouves++;
                    if (motsTrouves == mots.Count)
                        break;
                }
            }
            Assert.AreEqual(mots.Count, motsTrouves);
        }

Ce fichier contient un peu plus de deux millions de mots de passe, qui ont du être moissonnés je ne sais comment, mais qui correspondent à des cas réels. Quoi qu’il en soit, on retrouve bien dedans nos mots de passe les plus courants, comme testés ci-dessus. Nous allons nous en servir pour mener une attaque ciblée sur les hash du fichier LinkedIn.

Rechercher les hash SHA-1 par force brute

L’étape suivante consiste à regarder si on retrouve les SHA-1 de ces mots de passe :

        [TestMethod]
        public void PresenceSHA1ReduitDeMotsDePasseSimples()
        {
            List<string> mots = new List<string>() { "password", "123456", "superman" };
            SHA1 moteur = SHA1CryptoServiceProvider.Create();
            List<string> hashes = mots.ConvertAll(m => BitConverter.ToString(
                moteur.ComputeHash(Encoding.UTF8.GetBytes(m))).ToLower().Replace("-", string.Empty));

            string ligne;
            int hashesTrouves = 0;
            int nbLignesAnalysees = 0;
            using (StreamReader lecteur = new StreamReader(@"D:\Projets\Brèche LinkedIn SHA1\SHA1.txt"))
            {
                while ((ligne = lecteur.ReadLine()) != null)
                {
                    nbLignesAnalysees++;
                    if (hashes.Contains(ligne))
                        hashesTrouves++;
                    if (hashesTrouves == mots.Count)
                        break;
                }
            }
            Assert.AreEqual(hashes.Count, hashesTrouves); // Plante : on ne trouve rien
        }

On ne les trouve pas en direct, mais c’est là qu’interviennent les 00000 en préfixe, et si on les rajoute, pas de problème, on fait carton plein (je ne montre ci-dessous que le code modifié par rapport au listing précédent) :

List<string> hashes = mots.ConvertAll(m => string.Concat("00000", BitConverter.ToString(
    moteur.ComputeHash(Encoding.UTF8.GetBytes(m))).ToLower().Replace("-", string.Empty).Substring(5)));

Du coup, on peut passer aux choses plus sérieuses en croisant le contenu complet des fichiers, de façon à moissonner tous les mots de passe “simples” dans le fichier fuité de LinkedIn. Pour réaliser ceci, on travaille évidemment par force brute. Et quand je dis “brute”, c’est vraiment au pied de la lettre, à savoir que je me contente de bêtement boucler, sans optimisation aucune. Dans la pratique, une personne mal intentionnée se servirait d’outils beaucoup plus sophistiqués comme John The Ripper (surtout qu’une extension destinée à la manipulation des hash en provenance de LinkedIn, avec les 00000 en préfixes, est spécialement sortie).

Le temps de taper ces quelques lignes, ce code pas du tout optimisé (sur une bonne bécane, toutefois : Xeon, beaucoup de RAM, SSD) a trouvé une vingtaine de mots de passe :

image

A priori, en laissant tourner toute une nuit, on devrait donc retrouver suffisamment d’instances pour pouvoir mener le reste de nos expériences avec une étendue statistiquement suffisante. Le code est donc modifié pour plutôt ressortir les résultats dans un fichier CSV :

    [TestClass]
    public class TestHashes
    {
        private SHA1 moteur = SHA1CryptoServiceProvider.Create();

        private string CalculerHashReduit(string motDePasse)
        {
            return string.Concat("00000", BitConverter.ToString(
                moteur.ComputeHash(Encoding.UTF8.GetBytes(motDePasse))).ToLower()
                .Replace("-", string.Empty).Substring(5));
        }

        [TestMethod]
        public void RechercheMotsDePasseUsuelsDansFichierFuiteLinkedInVersionToutEnMemoire()
        {
            string fichierSortie = Path.Combine(@"D:\Projets\Brèche LinkedIn SHA1", DateTime.Now.ToString("yyyy-MM-dd-hh-mm-ss"));

            string[] tableau = File.ReadAllLines(@"D:\Projets\Brèche LinkedIn SHA1\SHA1.txt");
            List<string> hashes = new List<string>(tableau);

            string ligne;
            int index = 0;
            Stopwatch chrono = Stopwatch.StartNew();
            using (StreamReader lecteur = new StreamReader(@"D:\Projets\Brèche LinkedIn SHA1\passwords.txt"))
            {
                while ((ligne = lecteur.ReadLine()) != null)
                {
                    index++;
                    string hash = CalculerHashReduit(ligne);
                    if (hashes.Contains(hash))
                    {
                        Debug.WriteLine(string.Format("{0} trouvé sous le hash {1} - {2} / 2 151 220 - {3}",
                            ligne, hash, index, chrono.Elapsed));
                        File.AppendAllText(fichierSortie, string.Concat(ligne, Environment.NewLine));
                    }
                }
            }
        }
    }

Ensuite, il ne reste plus qu’à attendre. Un peu moins de 24 heures plus tard, les résultats sont les suivants :

image

42 799 hash ont été trouvés dans le fichier LinkedIn, sur 412 116 mots de passe testés parmi les 2 151 220 que contient le dictionnaire de mots de passe.

Passage à la vitesse supérieure

Bon, c’est bien sympa, tout ça, mais je ne vais pas laisser ma bécane allumée pendant trois jours juste pour me faire un corpus de mots de passe dans le but de tester une hypothèse sur la présence du sel ! Et 40 000 mots de passe, c’est déjà pas mal, mais j’aimerais bien travailler sur tout ce que je peux retrouver pour valider au mieux mon hypothèse. Du coup, je modifie le code pour utiliser la puissance de mes huit cœurs, et ça va pousser un peu plus Sourire.

image

Le code pour mettre ceci en place est le suivant :

    [TestClass]
    public class TestSHA1Breach
    {
        private List<string> hashes;
        private string identifiant;
        private string repSortie;

        [TestMethod]
        public void TestParallelBreachSHA1LinkedIn()
        {
            identifiant = DateTime.Now.ToString("yyyy-MM-dd-hh-mm-ss");
            repSortie = string.Concat(@"D:\Projets\Brèche LinkedIn SHA1\ParallelResults-", identifiant);
            Directory.CreateDirectory(repSortie);
            
            string[] tableau = File.ReadAllLines(@"D:\Projets\Brèche LinkedIn SHA1\SHA1.txt");
            hashes = new List<string>(tableau);

            using (StreamReader lecteur = new StreamReader(@"D:\Projets\Brèche LinkedIn SHA1\passwords - Copie.txt"))
            {
                List<Task> taches = new List<Task>();
                int limite = 1000;
                string ligne;

                List<string> mots = new List<string>();
                while ((ligne = lecteur.ReadLine()) != null && limite-- >= 0)
                {
                    mots.Add(ligne);
                    if (mots.Count == 100)
                    {
                        Task t = new Task(new Action<object>(Traitement), new List<string>(mots));
                        taches.Add(t);
                        t.Start();
                        mots.Clear();
                    }
                }

                foreach (Task t in taches)
                    t.Wait();

                string fichierComplet = Path.Combine(@"D:\Projets\Brèche LinkedIn SHA1",
                    string.Concat("parallel-", identifiant));
                foreach (string fichier in Directory.GetFiles(repSortie))
                    File.AppendAllLines(fichierComplet, File.ReadAllLines(fichier));
                Directory.Delete(repSortie, true);
            }
        }

        // On pourrait aussi faire avec des BlockingCollection, mais l'avantage des fichiers est que si le test
        // plante en cours de route pour un lock mal géré ou quelque chose du genre, on ne perd pas tous les résultats.
        //private BlockingCollection<string> Resultats = new BlockingCollection<string>();

        private void Traitement(object mots)
        {
            SHA1 moteur = SHA1CryptoServiceProvider.Create();
            List<string> Resultats = new List<string>();
            foreach (string mot in mots as List<string>)
            {
                string hash = string.Concat("00000", BitConverter.ToString(
                    moteur.ComputeHash(Encoding.UTF8.GetBytes(mot))).ToLower()
                    .Replace("-", string.Empty).Substring(5));

                if (hashes.Contains(hash))
                {
                    Debug.WriteLine(string.Format("{0} trouvé sous le hash {1}", mot, hash));
                    Resultats.Add(mot);
                }
            }
            string fichierSortie = Path.Combine(repSortie, Guid.NewGuid().ToString());
            File.AppendAllLines(fichierSortie, Resultats.ToArray());
        }
    }

Je ne rentre pas dans les détails, mais ce code va à peu près huit fois plus vite que le précédent, car il utilise à pleine puissance les huit cœurs CPU de ma machine. On pourrait aller encore plus vite en utilisant du GPGPU, mais ça serait pas mal de boulot, et là, les estimations sont que le calcul durera en gros 15 heures, donc ça ne vaut pas le coup. Encore une fois, si le but était juste de cracker le plus vite possible le maximum de mots de passe, on utiliserait John The Ripper, qui d’ailleurs a un plugin pour fonctionner en OpenCL. Avec les possibilités du cloud, il est même quasi-sûr qu’il existe déjà des outils pour utiliser la puissance ponctuelle énorme d’une telle architecture de façon à gagner encore un ou deux ordres de grandeur dans la performance de la méthode par force brute.

Au final, on obtient bien notre fichier avec tous les mots de passe dont les hash ont été retrouvés dans le fichier fuité de LinkedIn. Sur 6,5 millions de hash, cette méthode nous a permis de retrouver 600 000 mots de passe, ce qui est déjà pas mal, sachant que pour certains très simples, le nombre de duplications doit être très important. Le tout a mis finalement 18 heures au lieu des 15 estimées…

On passe aux choses sérieuses

L’étape suivante nous rapproche enfin de l’hypothèse à tester. Jusqu’à maintenant, nous avons juste moissonné de l’information pour tester cette hypothèse. Maintenant, nous allons nous servir des mots de passe trouvés comme d’un nouveau corpus pour les études suivantes. Pour cela, nous créons un fichier de hash des mots de passe, mais cette fois-ci avec un sel, et nous allons vérifier si la connaissance statistiques de mots de passe “traditionnels” nous permet effectivement de réaliser une attaque de force brute non pas sur les mots de passe, mais sur le sel utilisé.

Pour faire le test, je vais utiliser le sel que je propose en TD à mes étudiants, à savoir préfixer le mot de passe par “zorglub”. Je recalcule donc mon fichier de hash avec ce sel, et nous allons tester si la méthode statistique proposée permet d’accélérer l’attaque du sel par force brute.

Pour cela, à chaque hypothèse de sel, nous ne testerons que les 25 mots de passe qui sont les plus utilisés (voir http://splashdata.com/splashid/worst-passwords/index.htm) :

  • password
  • 123456
  • 12345678
  • qwerty
  • abc123
  • monkey
  • 1234567
  • letmein
  • trustno1
  • dragon
  • baseball
  • 111111
  • iloveyou
  • master
  • sunshine
  • ashley
  • bailey
  • passw0rd
  • shadow
  • 123123
  • 654321
  • superman
  • qazwsx
  • michael
  • football

L’idée est donc de boucler sur des hypothèses de sel, de calculer les hash de ces 25 mots de passe, et de regarder si l’un d’entre eux au moins est dans le fichier de hash cible. Si c’est le cas, il y a des chances qu’on ait trouvé notre sel, et nous pouvons donc revenir à une attaque par force brute hors-sel, dont nous avons montré plus haut l’efficacité, même sur des gros volumes à analyser.

La fréquence des premiers mots de passe est telle que nous pourrions nous contenter de quelques mots de passe seulement. Pour mes tests, je vais même commencer avec un seul, à savoir “password”. En fait (et c’est là la faille d’utiliser un même sel sur un ensemble de plusieurs millions de mots de passe), le corpus est tellement grand qu’on est quasiment assuré de tomber sur au moins un mot de passe égal à cette valeur “traditionnelle”. Le code est le suivant :

    [TestClass]
    public class TestAttaqueSel
    {
        private List<string> hashes;

        [TestMethod]
        public void TestParallelRechercheSel()
        {
            string sel = "zorglub";
            int limite = int.MaxValue;

            // Préparation du fichier des hash selon le sel choisi
            string fichierMotsDePasseLinkedInObtenusParForceBrute = Path.Combine(
                @"D:\Projets\Brèche LinkedIn SHA1",
                "FichierObtenuSansParallelisme.txt");
            string fichierHashCible = Path.Combine(
                @"D:\Projets\Brèche LinkedIn SHA1",
                "FichierHashesSelEnCours.txt");
            SHA1 moteur = SHA1CryptoServiceProvider.Create();
            using (StreamReader lecteur = new StreamReader(fichierMotsDePasseLinkedInObtenusParForceBrute))
            using (StreamWriter scribe = new StreamWriter(fichierHashCible))
            {
                string ligne;
                while ((ligne = lecteur.ReadLine()) != null)
                {
                    scribe.WriteLine(BitConverter.ToString(moteur.ComputeHash(
                        Encoding.UTF8.GetBytes(string.Concat(sel, ligne)))));
                }
            }

            // Stockage de toutes ces valeurs en mémoire pour aller plus vite dans nos recherches
            hashes = new List<string>(File.ReadAllLines(fichierHashCible));

            // On lance des tâches de traitement de recherche pour toutes les hypothèses de sel. Comme hypothèses de sel,
            // on part sur la plus simple en premier, à savoir que le sel est un préfixe, et qu'il a des chances d'être
            // un mot courant. On repart donc d'un dictionnaire des mots de passe les plus utilisés. Si cela ne suffit pas,
            // on ferait ensuite la même analyse avec un suffixe, les deux, etc. Il est évident que dans ces cas, les temps
            // augmentent, mais le but est justement de faire voir qu'un simple sel n'apporte pas la sécurité imaginée,
            // et qu'il faut justement un mécanisme de sel complexe, ou en tout cas non unique, pour augmenter ce temps
            // à des valeurs suffisamment dissuasives.
            List<Task> taches = new List<Task>();
            using (StreamReader lecteur = new StreamReader(@"D:\Projets\Brèche LinkedIn SHA1\passwords.txt"))
            {
                string ligne;
                List<string> hypotheses = new List<string>();
                while ((ligne = lecteur.ReadLine()) != null && limite-- >= 0)
                {
                    hypotheses.Add(ligne);
                    if (hypotheses.Count == 100)
                    {
                        Task t = new Task(new Action<object>(Traitement), new List<string>(hypotheses));
                        taches.Add(t);
                        t.Start();
                        hypotheses.Clear();
                    }
                }
            }

            foreach (Task t in taches)
                t.Wait();
        }

        private void Traitement(object hypotheses)
        {
            // Le traitement de chaque hypothèse de sel consiste à regarder si, en appliquant ce sel sur "password",
            // on obtient un hash présent dans le fichier cible. Si c'est le cas, il y a statistiquement de bonnes
            // chances pour qu'on ait trouvé le hash. Et si ce n'est pas le cas, on trouvera rapidement car on a alors
            // trouvé une combinaison de sel et de mot de passe connue. Il suffit alors de déplacer le curseur sur les
            // caractères de la chaîne en testant les autres décomposition jusqu'à trouver la bonne.
            SHA1 moteur = SHA1CryptoServiceProvider.Create();
            List<string> Resultats = new List<string>();
            foreach (string hypothese in hypotheses as List<string>)
            {
                string hash = BitConverter.ToString(moteur.ComputeHash(
                    Encoding.UTF8.GetBytes(string.Concat(hypothese, "password"))));

                if (hashes.Contains(hash))
                {
                    Debug.WriteLine(string.Format("Il y a de bonnes chances pour que {0} soit le sel", hypothese));
                    File.AppendAllText(string.Concat(
                        @"D:\Projets\Brèche LinkedIn SHA1\ResultatRechercheSel-", 
                        DateTime.Now.ToString("yyyy-MM-dd-hh-mm-ss")), hypothese);
                    Environment.Exit(0);
                }
            }
        }
    }

La toute première vague d’hypothèses pour le sel, pour une attaque en force brute, est bien sûr de repartir directement sur les valeurs en provenance du dictionnaire de mots de passe et de s’en servir comme sel de type préfixe. Ensuite, on passerait au sel de suffixe, à la composition des deux, à l’ajout de symboles supplémentaires, etc.

Résultats

Cette première approche nous ressort le sel par force brute en à peine quelques minutes. Il se trouve que le préfixe choisi faisait partie du dictionnaire de mot de passe, et a donc été trouvé dès la 1 698 685 ème tentative.

Première attaque en force brute d’un sel simple sur le fichier de 6.5 millions de hash : solution trouvée en 4 minutes !

Nous pourrions bien sûr rendre le sel plus complexe, comme “Zorglub64” en préfixe et “Batman28” en suffixe, mais il y aurait quelque chose d’artificiel à adapter l’algorithme en fonction de ce qu’on recherche.

Surtout, ce que nous souhaitions démontrer l’est déjà avec cet exemple : à partir du moment où on a de bonnes chances de connaitre un des mots de passe et qu’on dispose d’un corpus de hash assez grand, utiliser un sel, s’il est unique pour tous les hash, ne sert pas à grand chose, car au lieu de démultiplier les cas de recherche, il ne fait qu’ajouter une étape de force brute. Dans ces cas statistiques limites, on pourra donc au mieux multiplier par deux le temps d’attaque, ce qui est bien évidemment insuffisant.

Pour résumer

Un petit tableau résumant la situation :

image

Bref, si vous exposez un grand nombre de hash et que certains mots de passe sont de mauvaise qualité (par exemple parmi les 25 plus utilisés), et ce même si leur proportion est infime, un sel simple ne vous apportera que très peu de sécurité supplémentaire.

Toute l’idée est que le volume de données, au lieu de nous retarder dans les calculs, nous donne au contraire plus de chances de valider l’hypothèse du sel. La conclusion de tout ceci est qu’un sel ne devrait pas être unique pour des gros volumes, sinon il perd de son efficacité.

La solution

Autant la démonstration est complexe, autant la solution est relativement simple : il ne faut pas utiliser un seul et même sel de hash pour des grands volumes de mots de passe. Du coup, on peut imaginer d’utiliser un sel variable (par exemple basé sur un hash de la date de création du mot de passe), voire même une variable exogène, comme par exemple un identifiant unique de compte (mais surtout pas le login : la seule chose qui a évité une catastrophe à LinkedIn était justement que les logins n’étaient pas présents dans ce même fichier de hash de mots de passe).

Voici une proposition de notation sur vingt des différents modes d’utilisation d’un sel (le mot de passe à hasher est en vert, et le sel en gris) :

image

Pas de 20/20, car aucune méthode n’est infaillible. Même le mécanisme de cryptographie SHA-1 n’est pas exempt de tout risque : des algorithmes de génération de collisions ont été trouvés qui sont suffisamment rapides pour représenter un risque (http://people.csail.mit.edu/yiqun/SHA1AttackProceedingVersion.pdf, ou pour quelque chose de plus facile à lire, http://www.rsa.com/rsalabs/node.asp?id=2927). Si vous utilisez les hash dans le cas d’une fourniture de signature numérique, il peut être intéressant de passer à une méthode plus moderne (SHA-256, par exemple).

Attention à ne pas tomber dans le piège de la fausse sécurité avec des méthodes exotiques comme inverser les lettres du mot de passe, etc. Quoi qu’on fasse, ces méthodes sont prédictibles par quelqu’un avec un peu d’imagination et les personnes mal-intentionnées en ont malheureusement beaucoup. Une fois une liste de ces méthodes inclue dans un outil, c’en sera fait d’elles…

Posted in Sécurité | Tagged , | 3 Comments

Bug de culture sur KinectExplorer

Microsoft a visiblement fait des efforts pour internationaliser Kinect : la reconnaissance vocale est désormais supportée dans de nombreuses langues additionnelles, dont le français. Toutefois, il reste quelques petits bugs dans les programmes exemples. C’est en particulier le cas de KinectExplorer.

image

Si vous le lancez tel quel (bouton Run), vous obtiendrez un plantage :

image

Ainsi bien sûr que l’assistant de compatibilité :

image

Il faut donc plutôt installer le programme exemple (bouton Install), puis ouvrir la solution Visual Studio .NET et lancer en mode débogage pour mieux voir le problème. Dans le menu Déboguer / Exceptions, activer le traçage des exceptions CLR levées :

image

Lors de l’initialisation, on tombe sur l’exception suivante :

image

La valeur envoyée dans la méthode ObjectToDouble de la classe Converters est une chaîne avec un séparateur de décimales qui est codé en point en dur. Et comme la culture par défaut de nos machines françaises utilise une virgule, ça ne marche pas.

Pour bien faire, il faudrait rentrer dans le code pour voir d’où vient le point codé en dur, mais la pile des appels montre du code inconnu, et on ne voit pas quelle fonction est à l’origine du problème. Peut-être le toolkit Kinect… Je regarderai un de ces jours, mais pour l’instant, le but est juste de faire marcher l’exemple. On rajoute donc simplement les deux lignes ci-dessous dans App.xaml.cs :

public partial class App : Application
{
    private void Application_Startup(object sender, StartupEventArgs e)
    {
        System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-US");
        System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("en-US");
    }
}

Et du coup, ça passe (à part la reconnaissance de squelette qui part un peu en vrille quand on est assis et qu’on n’active pas le mode correspondant au lieu de rester en mode squelette complet debout) :

image

Posted in .NET, C#, Retours | Tagged | Leave a comment

Affectation de propriétés dans le constructeur : à savoir…

Depuis .NET 3.0, il est possible d’affecter des propriétés dans la même instruction que le constructeur. On peut ainsi écrire :

Rectangle r = new Rectangle() { Largeur = 2, Longueur = 3 };

Ceci s’avère très pratique, car on peut choisir les propriétés à affecter, et éviter ainsi de devoir créer de nombreux constructeurs pour les différents cas possibles :

public Rectangle()
    : this(1, 1)
{
}

public Rectangle(int Longueur)
    : this(Longueur, 1)
{
}

public Rectangle(int Longueur, int Largeur)
{
    this.Longueur = Longueur;
    this.Largeur = Largeur;
}

Attention toutefois, car il y a une petite différence entre le dernier constructeur juste au-dessus et la première ligne de code : la première grammaire exécute le code supplémentaire, mais pas dans le cadre du constructeur lui-même. Ce code est exécuté après. On pourrait se dire que ce n’est pas essentiel, mais imaginons que nous faisons un calcul lors de la mise en place du constructeur :

public Rectangle(int Longueur, int Largeur)
{
    this.Longueur = Longueur;
    this.Largeur = Largeur;
    this.Aire = this.Longueur * this.Largeur;
}

Si on appelle alors le constructeur vide avec les instructions d’affectation après, ceci est complètement équivalent à :

Rectangle r = new Rectangle();
r.Largeur = 2;
r.Longueur = 3;

Et dans ce cas, l’aire reste à 0, alors que la largeur et la hauteur ne sont pas nulles, car le calcul de l’aire est réalisé avant que les valeurs de propriété ne soient affectées.

On pourra objecter, par contre, deux choses : la première est que, lorsqu’on affecte la largeur ou la hauteur, il faut de toute façon relancer le calcul d’aire. La seconde est que ce genre de calcul ne devrait en théorie pas être réalisé dans un constructeur, qui est censé être très rapide et ne jamais remonter d’exception. Dans notre cas, il suffirait que le calcul soit une division au lieu d’une multiplication pour permettre ce comportement, qui est à proscrire absolument : un développeur ne doit pas avoir à anticiper une exception sur un constructeur.

Au final, il ne faut donc pas se passer de cette facilité d’écriture, mais tout simplement bien prendre soin de traiter tous les cas. Et proposer un constructeur complet est de toute façon une bonne pratique pour que le développeur utilisant votre classe sache à quoi s’en tenir. Par contre, inutile de créer toutes les surcharges des signatures de constructeur Sourire

Le code permettant de régler ces problèmes est le suivant :

public class Rectangle
{
    private int _Longueur = 1;

    public int Longueur
    {
        get { return _Longueur; }
        set
        {
            if (value < 0) throw new ArgumentException("Valeur négative interdite", "Longueur");
            _Longueur = value;
            CalculerAire();
        }
    }

    private int _Largeur = 1;

    public int Largeur
    {
        get { return _Largeur; }
        set
        {
            if (value < 0) throw new ArgumentException("Valeur négative interdite", "Largeur");
            _Largeur = value;
            CalculerAire();
        }
    }

    protected void CalculerAire()
    {
        this.Aire = this.Longueur * this.Largeur;
    }

    public int Aire { get; private set; }

    public Rectangle()
    {
    }

    public Rectangle(int Longueur, int Largeur)
        : this()
    {
        this.Longueur = Longueur;
        this.Largeur = Largeur;
    }
}

Note : dans ce cas simpliste, il faudrait bien sûr plutôt une structure, ce qui permettrait d’obtenir une encapsulation forte par immutabilité, et donc un risque de bug réduit. Le but ici est simplement de faire voir le fonctionnement des affectations suivant le constructeur, et de montrer qu’une mauvaise utilisation peut poser problème. De la même manière, le calcul de l’aire est tellement simple qu’on aurait plutôt tendance à le refaire à chaque appel de la propriété correspondante.

Posted in .NET, C# | Leave a comment

Sur le titre du blog

Je ne sais pas si vous avez fait attention, mais l’image en haut de mon blog montre une requête en Linq. J’adore cette technologie, et l’idée était de faire voir que la grammaire associée est tellement expressive qu’on peut écrire au final en C# des phrases qui pourraient presque être du langage de tous les jours.

Il m’a été remonté par un autre canal les quelques remarques suivantes :

  1. “List<string> est lourd, et devrait être remplacé par un array.”
  2. “Un simple Select plutôt qu’un ConvertAll est préférable.”
  3. “Le constructeur BlogEntry ne fournit pas de Published, donc le RemoveAll n’est pas obligatoirement correct.”
  4. (et le plus intéressant à étudier, à mon avis) “Le RemoveAll devrait être fait avant le FindAll pour améliorer les performances”.

Mise en place du banc de test

Vu que je suis plutôt du genre Saint Thomas, je me propose de vérifier tout ceci sur le code en question :

[TestMethod]
public void TestEnteteBlog()
{
    new List<string>() { "C#", "GPGPU", "F#", "Object Prevalence" }
        .ConvertAll<BlogEntry>(delegate(string subject) { return new BlogEntry(subject); })
        .FindAll(post => Industry.ShowsInterest(post))
        .RemoveAll(post => !Industry.WillUseBefore(post.Published + TimeSpan.FromDays(365)));
}

Bien sûr, pour que ceci compile, il nous faudra en plus une classe BlogEntry :

public class BlogEntry
{
    public string Subject { get; set; }
        
    public DateTime Published { get; set; }

    public BlogEntry(string subject)
    {
        this.Subject = subject;
        Published = DateTime.Now;
    }
}

Ainsi qu’une classe Industry, que je définis comme ceci :

public class Industry
{
    public static Random moteur = new Random();

    public static bool ShowsInterest(BlogEntry article)
    {
        return article.Subject.Contains("#");
    }

    public static bool WillUseBefore(DateTime endDate)
    {
        return moteur.Next(2) > 0;
    }
}

Avant de prendre en détail chacun de ces points, juste une remarque préliminaire : je ne traite ici que des aspects techniques et non de l’expressivité du code. Cette dernière qualité est en effet très subjective, et un code peut très bien paraitre expressif à l’un et lourd à un autre…

Liste ou array

image

Du point de vue de la syntaxe, ConvertAll ne peut pas être utilisée sur un array, mais uniquement sous sa forme statique sur la classe System.Array. Et lorsqu’on corrige ceci, on arrive sur un autre problème, à savoir que le RemoveAll n’existe pas sur un array. Le but ici était bien de faire voir le chaînage des méthodes typique de Linq.

J’imagine que la lourdeur dont parle la première remarque fait référence au fait que les array vont utiliser la pile plutôt que le tas. En pratique, comme les chaines sont de toute façon sur le tas, seule la structure fera la différence.

De manière plus générale, je n’ai personnellement jamais été confronté à des problèmes de performance ou d’utilisation mémoire avec les génériques, quelle que soit la classe utilisée. Je suis intéressé par toute documentation / cas d’étude sur ce point si vous en avez. Je parle bien sûr de retours argumentés et, si possible, chiffré.

Select préférable à ConvertAll

image

La remarque n’était pas plus précise, donc j’en suis réduit à penser que c’est pour des raisons de performance, parce que Select renvoie un IEnumerable plutôt qu’une List. Dans notre cas, on sera obligés de repasser en List pour le RemoveAll qui suit, donc ça ne change rien… J’ai fait le test, et les temps sont absolument similaires.

Là où c’est plus intéressant, c’est si la totalité des données ne vont pas être consommées (pagination, etc.), parce que la projection ne sera alors appliquée qu’à la portion des données consommées, et là, on gagne effectivement en performance.

Enfin, Select est effectivement plus intéressant dans la syntaxe courte de Linq, car il permet de mimer le comportement d’une requête SQL, ce qui rend Linq encore plus expressif pour quelqu’un qui ne vient pas de la programmation mais des bases de données :

var resultats = from entree in Liste
                where entree.Contains("#")
                select new { auto = true, contenu = entree.Substring(1, 2) };

Cet exemple est d’ailleurs intéressant pour montrer une autre fonctionnalité de C# très liée à Linq, à savoir les constructeurs anonymes. Le new sans nom de classe après est important justement pour mimer SQL, et ne pas avoir à créer une classe réceptacle pour toutes les projections possibles.

Pas de Published dans la construction de BlogEntry : problème possible

image

Pour le coup, pas besoin de chercher. Ce n’est pas parce que le constructeur de BlogEntry ne prend pas en paramètre un Published que cette propriété n’est pas accessible, et l’exemple de classe pour BlogEntry en début d’article le montre : tout compile bien, et la classe assure qu’il y aura systématiquement une valeur.

C’est d’ailleurs l’intérêt de ne pas utiliser trop systématiquement les classes anonymes dont nous avons parlé juste avant : on pourrait alors oublier de mettre la propriété. Mais le compilateur ne l’oublierait pas, lui !

Inversion de RemoveAll et FindAll pour la performance

image

Autant des améliorations de performance peuvent être trouvées sur l’ordonnancement en fonction des nombres d’entrées listées, de la proportion de ce qui est parcouru, de si on travaille sur des listes ou des énumérateurs, autant il est impossible de faire une généralité sur ceci. De plus, sur le cas particulier dont nous parlons ici, c’est faux, comme le montre une comparaison des deux tests ci-dessous…

Le premier reproduit le code tel qu’il apparait dans le titre du blog :

[TestMethod]
public void TestPerformance()
{
    Stopwatch chrono = Stopwatch.StartNew();
    string contenu = null;

    var etape1 = Liste.ConvertAll<BlogEntry>(delegate(string subject) { return new BlogEntry(subject); });
    var etape2 = etape1.FindAll(post => Industry.ShowsInterest(post));
    etape2.RemoveAll(post => !Industry.WillUseBefore(post.Published + TimeSpan.FromDays(365)));
            
    foreach (BlogEntry b in etape2)
        contenu = b.Subject;

    chrono.Stop();
    Debug.WriteLine("Non optimisé : {0}", chrono.Elapsed);
}

Le second utilise l’optimisation proposée :

[TestMethod]
public void TestPerformanceInversion()
{
    Stopwatch chrono = Stopwatch.StartNew();
    string contenu = null;

    var etape1 = Liste.ConvertAll<BlogEntry>(delegate(string subject) { return new BlogEntry(subject); });
    etape1.RemoveAll(post => !Industry.WillUseBefore(post.Published + TimeSpan.FromDays(365)));
    var etape2 = etape1.FindAll(post => Industry.ShowsInterest(post));

    foreach (BlogEntry b in etape2)
        contenu = b.Subject;

    chrono.Stop();
    Debug.WriteLine("Optimisé : {0}", chrono.Elapsed);
}

Pour que tout ceci fonctionne, il faut bien sûr une fonction pour initialiser la liste, qui est la suivante :

private List<string> Liste;

[TestInitialize]
public void Initialisation()
{
    Random moteur = new Random();

    Stopwatch chrono = Stopwatch.StartNew();

    Liste = new List<string>();
    for (int i = 0; i < 1000000; i++)
        Liste.Add(new string(
            moteur.Next(5) > 3 ? '#' : 'a',
            moteur.Next(10)));

    chrono.Stop();
    Debug.WriteLine("Initialisation : {0}", chrono.Elapsed);
}

Les résultats sont les suivants :

image

Après, on peut bien sûr argumenter sur la proportion des symboles # qui font changer le résultat de la fonction ShowsInterest, etc. Mais visiblement, on ne peut pas parler de gain de performance en règle général.

Je suis d’accord pour dire que c’est un bon principe de filtrer une liste le plus en amont possible, mais ceci n’est vrai que si on applique des traitements plus longs que le filtre, de façon à ne pas effectuer des traitements qui ne serviront pas. Or, ici, le traitement suivant est un Find qui va également filtrer, donc ça ne change rien qu’on le fasse dans un ordre ou dans un autre.

Une remarque de plus

Allez, une dernière remarque (mais qui vient de moi-même), plus sur la forme. J’avais dit que je ne parlerai pas d’expressivité, mais celle-là, ça me gêne Sourire 

Il s’agit de la double négation sur le RemoveAll : plutôt que de dire qu’on va supprimer tout ce qui n’est pas “WillUseBefore”, il serait plus logique de faire un FindAll / Select sur la version sans opérateur “!”. Surtout que ça permettrait dans ce cas d’avoir le Select dans sa position standard en Linq, c’est-à-dire en fin de traitement.

Conclusion

De mon point de vue, la balance entre expressivité du code et aspects purement techniques n’est pas suffisamment déplacée : mon titre de blog reste.

Posted in .NET, Performance | Leave a comment

BreizhCamp 2012 : migrer vers un moteur Prévalence Objet

Vous trouvez que le NoSQL est limité en termes de structures disponibles, ou trop compliqué à cause d’une gestion de schémas ? Vous aimeriez pouvoir utiliser directement vos objets ? Vous en avez marre des ORM et vous voudriez pouvoir persister un modèle sans vous poser la moindre question ? Vous voudriez pouvoir travailler en mode DDD et ne faire aucun compromis sur votre POO pour de viles raisons de stockage ?

La prévalence objet est faite pour vous : persistance transparente, tout en RAM… Performances au taquet, simplicité de développement maximale.

Jusqu’à maintenant, un seul obstacle (à part celui psychologique de tirer une croix sur des licences coutant une fortune pour utiliser un produit Open Source qui fait moins d’un Mo) restait à l’adoption de la prévalence objet : l’absence de support de SQL.

J’ai le plaisir de vous annoncer que ce problème est en passe d’être résolu, par le développement d’un provider ADO.NET pour Bamboo en cours de réalisation au sein de la société MGDIS. J’ai usé deux stagiaires dessus, mais ça prend forme, et je vous donne rendez-vous au BreizhCamp 2012 pour vous le montrer, en avant-première mondiale Sourire

Plus d’infos sur la prévalence objet dans http://gouigoux.com/blog-fr/?tag=bamboo

Posted in Prevalence | Tagged | 2 Comments

Wébinaire “Astuces de profilage de performances en .NET” téléchargeable

Il y a quelques jours, j’ai réalisé avec Red Gate un wébinaire montrant quelques techniques “de base” de profilage. Vous pouvez retrouver l’enregistrement sur ce lien. Je préviens juste que c’est en anglais… mais avec mon accent franchouillard, vous devriez comprendre sans trop de problème Sourire

Posted in .NET, Performance | Leave a comment

e-book gratuit !

Bon, je sais que le mot “gratuit”, surtout sur internet, ça veut souvent dire ‘”gratuit maintenant, mais on vous aura au tournant”, mais là, c’est vraiment complètement gratuit : Red Gate vous offre mon dernier livre sur les performances .NET et le profilage (en anglais, par contre). C’est par ici.

Pour ceux qui préfèrent lire en français, ce n’est pas gratuit, mais ce n’est pas cher. Et c’est ici, chez ENI.

Bonne lecture !

Posted in .NET, Performance | 2 Comments

Mon livre sur le profilage de performance .NET en anglais est sorti !

image

Vous pouvez le trouver sur le site de Simple Talk, à l’adresse suivante :

http://www.simple-talk.com/books/.net-books/practical-performance-profiling-improving-the-efficiency-of-.net-code/

Ce n’est pas sans une certaine fierté que je fais cette annonce, car j’en ai vraiment bavé pour réaliser cette version pour diffusion internationale de mon premier livre. Au début, je pensais m’engager dans un simple exercice de traduction, et comme j’ai passé plusieurs années en pays anglophones, ça n’était pas censé prendre plus de quelques mois.

Mais je me suis ensuite rendu compte qu’il allait falloir aussi réécrire l’application exemple, donc réinstaller tout en anglais pour les captures d’écran. Et puis, c’était l’occasion de passer à .NET 4.0. Ce qui incluait aussi de compléter certains chapitres. Certains autres ont été partiellement repris suite aux excellentes remarques de mon relecteur technique Paul Glavitch. Enfin, mon éditrice Marianne Crowder était aussi perfectionniste que moi, et certains passages ont nécessité une dizaine d’aller-retour avant que nous soyons tous deux satisfaits. Au final, j’ai passé plus de temps qu’à écrire le livre initialement en français !

Pour le lancement du livre, je réalise avec Red Gate un webinaire sur le sujet : voir http://gouigoux.com/blog/?p=17 pour plus de détails.

Posted in .NET, Performance | 2 Comments