Comment hacher un mot de passe en Java?


176

J'ai besoin de hacher les mots de passe pour le stockage dans une base de données. Comment puis-je faire cela en Java?

J'espérais prendre le mot de passe en texte brut, ajouter un sel aléatoire, puis stocker le sel et le mot de passe haché dans la base de données.

Ensuite, lorsqu'un utilisateur voulait se connecter, je pouvais prendre son mot de passe soumis, ajouter le sel aléatoire à partir des informations de son compte, le hacher et voir s'il correspond au mot de passe de hachage stocké avec les informations de son compte.


11
@YGL ce n'est en fait pas une recombinaison de nos jours, les attaques GPU étant si bon marché, la famille SHA est en fait un très mauvais choix pour le hachage de mot de passe (trop rapide) même avec du sel. Utilisez bcrypt, scrypt ou PBKDF2
Eran Medan

11
Pourquoi cette question a-t-elle été close? C'est une question pour un vrai problème d'ingénierie, et les réponses sont inestimables. Le PO ne demande pas une bibliothèque, il demande comment résoudre le problème d'ingénierie.
stackoverflowuser2010

12
Juste incroyable. Cette question a 52 votes positifs, et quelqu'un décide de la fermer comme "hors sujet".
stackoverflowuser2010

1
Ouais, j'ai déjà posté sur Meta à propos de ce problème de fermetures, mais j'ai été battu assez mal.
Chris Dutrow

8
Cette question devrait être rouverte. C'est une question sur la façon d'écrire un programme pour résoudre le problème décrit (authentification par mot de passe), avec une solution de code court. Voir le mot déclencheur "bibliothèque" ne justifie pas la fermeture réflexive d'une question; il ne demande pas une recommandation de bibliothèque, il demande comment hacher les mots de passe. Edit: Là, corrigé.
erickson

Réponses:


157

Vous pouvez en fait utiliser une fonction intégrée au runtime Java pour ce faire. Le SunJCE dans Java 6 prend en charge PBKDF2, qui est un bon algorithme à utiliser pour le hachage de mot de passe.

byte[] salt = new byte[16];
random.nextBytes(salt);
KeySpec spec = new PBEKeySpec("password".toCharArray(), salt, 65536, 128);
SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
byte[] hash = f.generateSecret(spec).getEncoded();
Base64.Encoder enc = Base64.getEncoder();
System.out.printf("salt: %s%n", enc.encodeToString(salt));
System.out.printf("hash: %s%n", enc.encodeToString(hash));

Voici une classe d'utilitaire que vous pouvez utiliser pour l'authentification par mot de passe PBKDF2:

import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.Base64;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;

/**
 * Hash passwords for storage, and test passwords against password tokens.
 * 
 * Instances of this class can be used concurrently by multiple threads.
 *  
 * @author erickson
 * @see <a href="http://stackoverflow.com/a/2861125/3474">StackOverflow</a>
 */
public final class PasswordAuthentication
{

  /**
   * Each token produced by this class uses this identifier as a prefix.
   */
  public static final String ID = "$31$";

  /**
   * The minimum recommended cost, used by default
   */
  public static final int DEFAULT_COST = 16;

  private static final String ALGORITHM = "PBKDF2WithHmacSHA1";

  private static final int SIZE = 128;

  private static final Pattern layout = Pattern.compile("\\$31\\$(\\d\\d?)\\$(.{43})");

  private final SecureRandom random;

  private final int cost;

  public PasswordAuthentication()
  {
    this(DEFAULT_COST);
  }

  /**
   * Create a password manager with a specified cost
   * 
   * @param cost the exponential computational cost of hashing a password, 0 to 30
   */
  public PasswordAuthentication(int cost)
  {
    iterations(cost); /* Validate cost */
    this.cost = cost;
    this.random = new SecureRandom();
  }

  private static int iterations(int cost)
  {
    if ((cost < 0) || (cost > 30))
      throw new IllegalArgumentException("cost: " + cost);
    return 1 << cost;
  }

  /**
   * Hash a password for storage.
   * 
   * @return a secure authentication token to be stored for later authentication 
   */
  public String hash(char[] password)
  {
    byte[] salt = new byte[SIZE / 8];
    random.nextBytes(salt);
    byte[] dk = pbkdf2(password, salt, 1 << cost);
    byte[] hash = new byte[salt.length + dk.length];
    System.arraycopy(salt, 0, hash, 0, salt.length);
    System.arraycopy(dk, 0, hash, salt.length, dk.length);
    Base64.Encoder enc = Base64.getUrlEncoder().withoutPadding();
    return ID + cost + '$' + enc.encodeToString(hash);
  }

  /**
   * Authenticate with a password and a stored password token.
   * 
   * @return true if the password and token match
   */
  public boolean authenticate(char[] password, String token)
  {
    Matcher m = layout.matcher(token);
    if (!m.matches())
      throw new IllegalArgumentException("Invalid token format");
    int iterations = iterations(Integer.parseInt(m.group(1)));
    byte[] hash = Base64.getUrlDecoder().decode(m.group(2));
    byte[] salt = Arrays.copyOfRange(hash, 0, SIZE / 8);
    byte[] check = pbkdf2(password, salt, iterations);
    int zero = 0;
    for (int idx = 0; idx < check.length; ++idx)
      zero |= hash[salt.length + idx] ^ check[idx];
    return zero == 0;
  }

  private static byte[] pbkdf2(char[] password, byte[] salt, int iterations)
  {
    KeySpec spec = new PBEKeySpec(password, salt, iterations, SIZE);
    try {
      SecretKeyFactory f = SecretKeyFactory.getInstance(ALGORITHM);
      return f.generateSecret(spec).getEncoded();
    }
    catch (NoSuchAlgorithmException ex) {
      throw new IllegalStateException("Missing algorithm: " + ALGORITHM, ex);
    }
    catch (InvalidKeySpecException ex) {
      throw new IllegalStateException("Invalid SecretKeyFactory", ex);
    }
  }

  /**
   * Hash a password in an immutable {@code String}. 
   * 
   * <p>Passwords should be stored in a {@code char[]} so that it can be filled 
   * with zeros after use instead of lingering on the heap and elsewhere.
   * 
   * @deprecated Use {@link #hash(char[])} instead
   */
  @Deprecated
  public String hash(String password)
  {
    return hash(password.toCharArray());
  }

  /**
   * Authenticate with a password in an immutable {@code String} and a stored 
   * password token. 
   * 
   * @deprecated Use {@link #authenticate(char[],String)} instead.
   * @see #hash(String)
   */
  @Deprecated
  public boolean authenticate(String password, String token)
  {
    return authenticate(password.toCharArray(), token);
  }

}

11
Vous voudrez peut-être vous méfier un peu des conversions octet en hexadécimal avec BigInteger: les zéros non significatifs sont supprimés. C'est correct pour un débogage rapide, mais j'ai vu des bogues dans le code de production à cause de cet effet.
Thomas Pornin

24
@ thomas-pornin's souligne pourquoi nous avons besoin d'une bibliothèque , pas d'un bloc de code qui est presque là. Effrayant que la réponse acceptée ne réponde pas à la question sur un sujet aussi important.
Nilzor

9
Utilisez l'algorithme PBKDF2WithHmacSHA512 en commençant par Java 8. C'est un peu plus fort.
iwan.z

1
Remarque, ALGs existants ne sont pas supprimés dans les versions ultérieures: java_4: PBEWithMD5AndDES, DESede, DES java_5 / 6/7: PBKDF2WithHmacSHA1, PBE (uniquement en Java 5), PBEWithSHA1AndRC2_40, PBEWithSHA1And, PBEWithMD5AndTriple java_8: PBEWithHmacSHA224AndAES_128, PBEWithHmacSHA384AndAES_128, PBEWithHmacSHA512AndAES_128, RC4_40, PBKDF2WithHmacSHA256 , PBEWithHmacSHA1AndAES_128, RC4_128, PBKDF2WithHmacSHA224, PBEWithHmacSHA256AndAES_256, RC2_128, PBEWithHmacSHA224AndAES_256, PBEWithHmacSHA384AndAES_256, PBEWithHmacSHA512AndAES_256, PBKDF2WithHmacSHA512, PBEWithHmacSHA256AndAES_128, PBKDF2WithHmacSHA384, PBEWithHmacSHA1AndAES_256
iwan.z

4
@TheTosters Oui, le temps d'exécution sera plus long pour les mots de passe incorrects ; plus précisément, les mauvais mots de passe prendront le même temps que les bons mots de passe. Cela empêche les attaques de chronométrage, même si j'avoue que je ne peux pas penser à un moyen pratique d'exploiter une telle vulnérabilité dans ce cas. Mais vous ne coupez pas les coins. Ce n'est pas parce que je ne peux pas le voir qu'un esprit plus sournois ne le fera pas.
erickson

97

Voici une implémentation complète avec deux méthodes faisant exactement ce que vous voulez:

String getSaltedHash(String password)
boolean checkPassword(String password, String stored)

Le fait est que même si un attaquant accède à la fois à votre base de données et au code source, les mots de passe sont toujours sûrs.

import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.SecureRandom;
import org.apache.commons.codec.binary.Base64;

public class Password {
    // The higher the number of iterations the more 
    // expensive computing the hash is for us and
    // also for an attacker.
    private static final int iterations = 20*1000;
    private static final int saltLen = 32;
    private static final int desiredKeyLen = 256;

    /** Computes a salted PBKDF2 hash of given plaintext password
        suitable for storing in a database. 
        Empty passwords are not supported. */
    public static String getSaltedHash(String password) throws Exception {
        byte[] salt = SecureRandom.getInstance("SHA1PRNG").generateSeed(saltLen);
        // store the salt with the password
        return Base64.encodeBase64String(salt) + "$" + hash(password, salt);
    }

    /** Checks whether given plaintext password corresponds 
        to a stored salted hash of the password. */
    public static boolean check(String password, String stored) throws Exception{
        String[] saltAndHash = stored.split("\\$");
        if (saltAndHash.length != 2) {
            throw new IllegalStateException(
                "The stored password must have the form 'salt$hash'");
        }
        String hashOfInput = hash(password, Base64.decodeBase64(saltAndHash[0]));
        return hashOfInput.equals(saltAndHash[1]);
    }

    // using PBKDF2 from Sun, an alternative is https://github.com/wg/scrypt
    // cf. http://www.unlimitednovelty.com/2012/03/dont-use-bcrypt.html
    private static String hash(String password, byte[] salt) throws Exception {
        if (password == null || password.length() == 0)
            throw new IllegalArgumentException("Empty passwords are not supported.");
        SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
        SecretKey key = f.generateSecret(new PBEKeySpec(
            password.toCharArray(), salt, iterations, desiredKeyLen));
        return Base64.encodeBase64String(key.getEncoded());
    }
}

Nous stockons 'salt$iterated_hash(password, salt)'. Le sel est composé de 32 octets aléatoires et son but est que si deux personnes différentes choisissent le même mot de passe, les mots de passe stockés seront toujours différents.

Le iterated_hash, ce qui hash(hash(hash(... hash(password, salt) ...)))rend fondamentalement très coûteux pour un attaquant potentiel qui a accès à votre base de données de deviner les mots de passe, de les hacher et de rechercher des hachages dans la base de données. Vous devez calculer cela iterated_hashchaque fois qu'un utilisateur se connecte, mais cela ne vous coûte pas beaucoup par rapport à l'attaquant qui passe près de 100% de son temps à calculer des hachages.


14
Désolé de harceler, mais pourquoi devrais-je choisir cela sur une bibliothèque existante? Une bibliothèque a probablement plus de chances d'être examinée en profondeur. Je doute que chacun des 14 votes positifs ait analysé le code pour tout problème.
Joachim Sauer

2
@JoachimSauer Il s'agit essentiellement d'utiliser simplement une bibliothèque (javax.crypto) mais vous avez raison - les mots de passe vides ne sont pas pris en charge. Ajout d'une exception pour le rendre explicite. Merci!
Martin Konicek

3
Vous devriez probablement changer les signatures de méthodes à la char[] passwordplace de String password.
assylias

2
Bien qu'il semble que la référence ne fasse pas l'unanimité. Voir aussi ceci: security.stackexchange.com/a/20369/12614
assylias

3
Êtes-vous sûr que .equals () sur les chaînes ne court-circuite pas (c'est-à-dire: arrêtez la boucle quand il trouve deux octets qui ne sont pas égaux)? Si c'est le cas, il y a un risque qu'une attaque de synchronisation fuit des informations sur le hachage du mot de passe.
bobpoekert


7

Vous pouvez utiliser l' implémentation de la bibliothèque Shiro (anciennement JSecurity ) de ce qui est décrit par OWASP .

Il semble également que la bibliothèque JASYPT dispose d'un utilitaire similaire .


C'est en fait ce que j'utilisais. Mais puisque nous avons décidé de ne pas utiliser Shiro, il y avait une certaine inquiétude concernant l'inefficacité d'avoir à inclure toute la bibliothèque Shiro pour un seul paquet.
Chris Dutrow

Je ne connais pas de bibliothèque composée uniquement d'un utilitaire de hachage de mot de passe. Vous feriez probablement mieux de rouler vous-même si les dépendances sont un problème. La réponse d'Erickson me semble plutôt bonne. Ou copiez simplement le code de ce lien OWASP que j'ai référencé si vous préférez utiliser SHA de manière sécurisée.
laz

7

Vous pouvez calculer les hachages en utilisant MessageDigest, mais c'est faux en termes de sécurité. Les hachages ne doivent pas être utilisés pour stocker les mots de passe, car ils sont facilement cassables.

Vous devez utiliser un autre algorithme comme bcrypt, PBKDF2 et scrypt pour stocker vos mots de passe. Regardez ici .


3
Comment hacheriez-vous le mot de passe lors de la connexion sans stocker le sel dans la base de données?
ZZ Coder

9
Utiliser le nom d'utilisateur comme sel n'est pas un défaut fatal, mais c'est loin d'être aussi bon que d'utiliser un sel d'un RNG cryptographique. Et il n'y a absolument aucun problème pour stocker le sel dans la base de données. Le sel n'est pas un secret.
erickson

1
Le nom d'utilisateur et l'e-mail ne seraient-ils pas également stockés dans la base de données?
Chris Dutrow

@ZZ Coder, @erickson correct, j'ai en quelque sorte supposé que ce serait un sel pour tous les mots de passe, ce qui conduirait à une table arc-en-ciel facilement calculable.
Bozho

13
Un problème avec l'utilisation du nom d'utilisateur (ou d'un autre identifiant comme l'e-mail) en tant que sel est que vous ne pouvez pas modifier l'identifiant sans que l'utilisateur définisse également un nouveau mot de passe.
Lawrence Dol

6

En plus de bcrypt et PBKDF2 mentionnés dans d'autres réponses, je recommanderais de regarder scrypt

MD5 et SHA-1 ne sont pas recommandés car ils sont relativement rapides, donc en utilisant un calcul distribué "loyer à l'heure" (par exemple EC2) ou un GPU haut de gamme moderne, on peut "cracker" des mots de passe en utilisant des attaques par force brute / dictionnaire à des coûts relativement bas et raisonnables temps.

Si vous devez les utiliser, effectuez au moins une itération de l'algorithme un nombre de fois significatif prédéfini (1000+).


6

Tout à fait d'accord avec Erickson que PBKDF2 est la réponse.

Si vous n'avez pas cette option, ou si vous avez seulement besoin d'utiliser un hachage, Apache Commons DigestUtils est beaucoup plus facile que d'obtenir le bon code JCE: https://commons.apache.org/proper/commons-codec/apidocs/org/apache /commons/codec/digest/DigestUtils.html

Si vous utilisez un hachage, utilisez sha256 ou sha512. Cette page contient de bonnes recommandations sur la gestion et le hachage des mots de passe (notez qu'elle ne recommande pas le hachage pour la gestion des mots de passe): http://www.daemonology.net/blog/2009-06-11-cryptographic-right-answers.html


il convient de noter que SHA512 n'est pas meilleur que SHA256 (à cet effet) simplement parce que le nombre est plus grand.
Azsgy

5

Vous pouvez utiliser Spring Security Crypto (n'a que 2 dépendances de compilation facultatives ), qui prend en charge le cryptage des mots de passe PBKDF2 , BCrypt , SCrypt et Argon2 .

Argon2PasswordEncoder argon2PasswordEncoder = new Argon2PasswordEncoder();
String aCryptedPassword = argon2PasswordEncoder.encode("password");
boolean passwordIsValid = argon2PasswordEncoder.matches("password", aCryptedPassword);
SCryptPasswordEncoder sCryptPasswordEncoder = new SCryptPasswordEncoder();
String sCryptedPassword = sCryptPasswordEncoder.encode("password");
boolean passwordIsValid = sCryptPasswordEncoder.matches("password", sCryptedPassword);
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String bCryptedPassword = bCryptPasswordEncoder.encode("password");
boolean passwordIsValid = bCryptPasswordEncoder.matches("password", bCryptedPassword);
Pbkdf2PasswordEncoder pbkdf2PasswordEncoder = new Pbkdf2PasswordEncoder();
String pbkdf2CryptedPassword = pbkdf2PasswordEncoder.encode("password");
boolean passwordIsValid = pbkdf2PasswordEncoder.matches("password", pbkdf2CryptedPassword);

4

Bien que la recommandation du NIST PBKDF2 ait déjà été mentionnée, je tiens à souligner qu'il y a eu un concours de hachage de mot de passe public qui s'est déroulé de 2013 à 2015. En fin de compte, Argon2 a été choisi comme fonction de hachage de mot de passe recommandée.

Il existe une liaison Java assez bien adoptée pour la bibliothèque d'origine (C native) que vous pouvez utiliser.

Dans le cas d'utilisation moyen, je ne pense pas qu'il soit important du point de vue de la sécurité si vous choisissez PBKDF2 sur Argon2 ou vice-versa. Si vous avez des exigences de sécurité strictes, je vous recommande de considérer Argon2 dans votre évaluation.

Pour plus d'informations sur la sécurité des fonctions de hachage de mot de passe, consultez security.se .


@zaph J'ai édité la réponse pour être plus objective. Veuillez noter que la recommandation du NIST n'est peut-être pas toujours le meilleur choix (voir ici un exemple) - bien sûr, cela est vrai pour tout ce qui est recommandé ailleurs. Par conséquent, je pense que cette réponse apporte une valeur à cette question.
Qw3ry

2

Ici, vous avez deux liens pour le hachage MD5 et d'autres méthodes de hachage:

API Javadoc: https://docs.oracle.com/javase/1.5.0/docs/api/java/security/MessageDigest.html

Tutoriel: http://www.twmacinta.com/myjava/fast_md5.php


3
Gardez simplement à l'esprit que pour le hachage de mot de passe, il est préférable de ralentir. Vous devez utiliser des milliers d'itérations de la fonction de hachage comme technique de "renforcement des clés". De plus, le sel est impératif.
erickson

J'avais l'impression que plusieurs itérations d'un algorithme de hachage de qualité produiraient à peu près la même sécurité qu'une itération puisque la longueur des octets serait toujours la même?
Chris Dutrow

@erickson Il serait préférable de ralentir explicitement les attaquants.
démon

6
À propos du renforcement des clés: les sels existent pour rendre les hachages précalculés inutilisables. Mais les attaquants n'ont pas à pré-calculer. Les attaquants peuvent simplement hacher les chaînes + sel "à la volée" jusqu'à ce qu'ils trouvent la bonne. Mais si vous répétez des milliers de fois pour vos hachages, ils devront faire de même. Votre serveur ne sera pas beaucoup affecté par les itérations de 10 000 car cela ne se produit pas souvent. Les attaquants auront besoin de 10 000 fois la puissance de calcul.
zockman

2
@Simon aujourd'hui MD5 est considéré comme inutile pour le hachage de mot de passe car il peut être craqué en quelques secondes à l'aide d'attaques GPU par force brute / dictionnaire. Voir ici: codahale.com/how-to-safely-store-a-password
Eran Medan

1

Parmi tous les schémas de hachage standard, LDAP ssha est le plus sûr à utiliser,

http://www.openldap.org/faq/data/cache/347.html

Je suivrais simplement les algorithmes spécifiés ici et utiliserais MessageDigest pour faire le hachage.

Vous devez stocker le sel dans votre base de données comme vous l'avez suggéré.


1
Parce que SSHA n'itère pas la fonction de hachage, elle est trop rapide. Cela permet aux attaquants d'essayer les mots de passe plus rapidement. De meilleurs algorithmes comme Bcrypt, PBBKDF1 et PBKDF2 utilisent des techniques de «renforcement de clé» pour ralentir les attaquants au point où un mot de passe devrait expirer avant de pouvoir forcer brutalement même un espace de mot de passe de 8 lettres.
erickson

Le problème avec tous ces mécanismes est que vous n'obtenez pas de support client. Le problème avec le mot de passe haché est que vous ne pouvez pas prendre en charge le mot de passe haché avec un autre algorithme. Avec ssha, au moins tous les clients LDAP le prennent en charge.
ZZ Coder

Ce n'est pas «le plus sûr», c'est simplement «assez compatible». bcrypt / scrypt sont beaucoup plus intensifs en ressources.
eckes

1

À partir de 2020, l'algorithme le plus crédible et le plus flexible en usage,

celui qui est le plus susceptible d'optimiser sa solidité quel que soit le matériel,

est Argon2id ou Argon2i .

Il fournit l'outil d'étalonnage nécessaire pour trouver des paramètres de résistance optimisés en fonction d'un temps de hachage cible et du matériel utilisé.

  • Argon2i est spécialisé dans le hachage gourmand en mémoire
  • Argon2d est spécialisé dans le hachage gourmand en CPU
  • Argon2id utilise les deux méthodes.

Le hachage gourmand en mémoire aiderait à éviter l'utilisation du GPU pour le cracking.

L'implémentation de Spring security / Bouncy Castle n'est pas optimisée et relativement semaine compte tenu de ce que l'attaquant pourrait utiliser. cf: documentation Spring

L'implémentation actuelle utilise Bouncy Castle qui n'exploite pas le parallélisme / optimisations que les craqueurs de mots de passe le feront, il y a donc une asymétrie inutile entre l'attaquant et le défenseur.

L'implémentation la plus crédible utilisée pour java est celle de mkammerer ,

un jar / bibliothèque wrapper de l'implémentation native officielle écrite en Rust.

Il est bien écrit et simple à utiliser.

La version intégrée fournit des versions natives pour Linux, Windows et OSX.

À titre d'exemple, il est utilisé par jpmorganchase dans son projet de sécurité tessera utilisé pour sécuriser Quorum , son implémentation de cryptocurency Ethereum.

Voici l'exemple de code de tessera.

L'étalonnage peut être effectué à l'aide de de.mkammerer.argon2.Argon2Helper # findIterations

Les algorithmes SCRYPT et Pbkdf2 peuvent également être calibrés en écrivant un simple benchmark, mais les valeurs d'itérations sûres minimales actuelles nécessiteront des temps de hachage plus élevés.


0

Je me suis appuyé sur une vidéo sur udemy et je l'ai modifié pour être un mot de passe aléatoire plus fort

}

private String pass() {
        String passswet="1234567890zxcvbbnmasdfghjklop[iuytrtewq@#$%^&*" ;

        char icon1;
        char[] t=new char[20];

         int rand1=(int)(Math.random()*6)+38;//to make a random within the range of special characters

            icon1=passswet.charAt(rand1);//will produce char with a special character

        int i=0;
        while( i <11) {

             int rand=(int)(Math.random()*passswet.length());
             //notice (int) as the original value of Math>random() is double

             t[i] =passswet.charAt(rand);

             i++;
                t[10]=icon1;
//to replace the specified item with icon1
         }
        return new String(t);
}






}

Je suis ouvert à être corrigé, mais je pense que vous ne devriez pas utiliser de nombres aléatoires lors du hachage. Ceci afin que votre fonction de hachage reste déterministe; c'est-à-dire que si vous hachez une chaîne plusieurs fois, vous obtiendrez toujours la même valeur de hachage pour cette chaîne.
duldi
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.