Stockage sécurisé de mots de passe
Article du 01/01/2008
Introduction
La plupart des applications modernes utilisent le mécanisme d'authentification par mot de passe. La majorité des utilisateurs partagent le même mot de passe sur toutes leurs applications. Si quelqu'un dérobe le mot de passe d'un utilisateur celui-ci pourra accéder à tout type d'applications ou site web auquel l'utilisateur a accés.
On constate souvent que les mots de passe sont stockés directement en base de donnée de manière lisible. Ainsi le mot de passe peut etre dérobé par un DBA, un utilisateur ou encore pire un attaquent via une injection de SQL.
Les sauvagardes des bases de données (bandes magnétique etc.) sont aussi vulnérables. Afin de résoudre ce problème, les mots de passes doivent être stockés chiffrés.
Deux types de chiffrement existent :
- Le chiffrement faisant appel à des fonction à sens unique (SHA-256, SHA1, MD5) aussi connu sous le nom de fonction de hachage.
- Le chiffrement classique réversible (DES, AES, ....).
Les propriétés de reversibilité du chiffrement ne sont pas souhaitable lorsqu'on stocke des mots de passe (cf le guide OWASP v2.0.1) :
Définition d'une fonction de hachage cryptographique :
Une fonction de hachage créée une emprunte de petite taille (appelé empreinte digitale ou message digest) a partir d'une chaine de longueur illimitée.
hash(X) ->Y
X est un ensemble illimité et Y est un ensemble fini.
Une bonne fonction de hachage cryptographique doit avoir en plus les propriétés suivantes :
- Résistance à une attaque sur la première preimage : A partir d'une empreinte Y il doit être impossible de retrouver la chaine initiale X.
- Résistance à une attaque sur la deuxième preimage : A parti d'une chaine X1 il doit être impossible de trouver une autre chaine X2 (différente de X1) tel que hash(X1)=hash(X2)
- Résistance aux collisions : Il doit être difficile de trouver deux chaines X1 et X2 (X1<>X2) tel que hash(X1)=hash(X2).
Exemple de code java pour le calcul d'une empreinte SHA-1 :
import java.security.MessageDigest;
public byte[] getHash(String password)
throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.reset();
byte[] input = digest.digest(password.getBytes("UTF-8"));
}
Stockage de mot de passe.
Grace à la première propriété, si un mot de passe est stocké haché dans une base de donnée un attaquant ne pourra pas retrouver simplement le mot de passe. Seule une attaque par recherche exhaustive permettra de retrouver un mot de passe (ie. calculer l'empreinte de tous les mots de passes possibles ou de ceux d'un dictionnaire de mot de passe).
Pourquoi ajouter un sel au mot de passe.
Il existe deux effets de bord au stockage de l'empreinte d'un mot de passe :
- Il est facile d'identifier deux utilisateurs ayant le même mot de passe.
- A cause du du paradoxe des anniversaire, l'attaquant peut trouver un mot de passe assez rapidement surtout si le nombre de mot de passe est important. Afin de resoudre ce problème, un sel peu etre concaténé au mot de passe afin de ralentir l'opération de hachage.
Un sel (salt) est un nombre aléatoire d'une longueur fixe. Le sel doit être différent pour chaque mot de passe. Il doit être stocké en clair à coté du mot de passe chiffré.
Dans cette nouvelle configuration, un attaquant doit mener une attaque exhaustive sur chaque mot de passe. La base de mot de passe ne peut plus être attaqué grace au paradoxe des anniversaires.
Un sel d'une longueur de 64 bits est recommandé dans la norme PKCS#5
Exemple de code java pour le calcul d'une empreinte SHA-1 avec un sel:
import java.security.MessageDigest;
public byte[] getHash(String password, byte[] salt)
throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
digest.reset();
digest.update(salt);
return digest.digest(password.getBytes("UTF-8"));
}
Rendre la tache de l'attaquant encore plus complexe
Afin de ralentir ecore un peu plus le travail de l'attaquant, il est recommandé d'effectuer l'opération de hachage un certain nombre de fois. Même si appliquer une fonction de hachage plusieurs fois peut ralentir à la fois un utilisateur et un attaquant, un utilisateur légitime ne le remarquera pas étant donné que le temps de hachage est très faible comparé au temps total durant lequel il intéragit avec le système. L'attaquant quand à lui passe 100% de son temps à hacher des mots de passe, ainsi hacher n fois un mot de passe le relentira d'un facteur n.
Un minimum de 1000 opération de hachage successive est recommandé par la norme PKCS#5.
Le mot de passe stocké sera le résultat de la fonction suivante: Hash(hash(hash(hash(……….hash(password||salt)))))))))))))))
Afin d'authentifier un utilisateur, la même opération devra être réalisée, suivi par la comparaison de l'empreinte stockée en base avec l'empreinte calculée.
La fonction de hachage choisi dépends de la politique de sécurité de votre système. Pour un stockage sur une longue période de temps, SHA-256 ou SHA-512 sont recommandés.
Exemple de code :
import java.security.*;
public byte[] getHash(int iterationNb, String password, byte[] salt)
throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.reset();
digest.update(salt);
byte[] input = digest.digest(password.getBytes("UTF-8"));
for (int i = 0; i < iterationNb; i++) {
digest.reset();
input = digest.digest(input);
}
return input;
}
Exemple de code complet :
Afin de créer la table nécessaire à l'application, appelez la méthode creerTable. Celle-ci créée une table appelée CREDENTIAL avec les champs suivants :
- LOGIN VARCHAR (100) PRIMARY KEY
- PASSWORD VARCHAR (32)
- SALT VARCHAR (32)
Dans cette table, le sel et le mot de passe sont stockés en Base64.
La méthode authenticate est utilisé afin d'identifier un utilisateur, la méthode createUser permet de crééer un nouvel utilisateur.
package org.psafix.memopwd;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.io.IOException;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
import java.sql.*;
import java.util.Arrays;
import java.security.SecureRandom;
public class Owasp {
private final static int ITERATION_NUMBER = 1000;
public Owasp() {
}
/**
* Authenticates the user with a given login and password
* If password and/or login is null then always returns false.
* If the user does not exist in the database returns false.
* @param con Connection An open connection to a databse
* @param login String The login of the user
* @param password String The password of the user
* @return boolean Returns true if the user is
* authenticated, false otherwise
* @throws SQLException If the database is inconsistent or
* unavailable (
* (Two users with the same login, salt or
* digested password altered etc.)
* @throws NoSuchAlgorithmException If the algorithm SHA-1 is not
* supported by the JVM
*/
public boolean authenticate(Connection con, String login, String password)
throws SQLException, NoSuchAlgorithmException{
boolean authenticated=false;
PreparedStatement ps = null;
ResultSet rs = null;
try {
boolean userExist = true;
// INPUT VALIDATION
if (login==null||password==null){
// TIME RESISTANT ATTACK
// Computation time is equal to the time
//needed by a legitimate user
userExist = false;
login="";
password="";
}
ps = con.prepareStatement("SELECT PASSWORD, SALT "+
"FROM CREDENTIAL WHERE LOGIN =?");
ps.setString(1, login);
rs = ps.executeQuery();
String digest, salt;
if (rs.next()) {
digest = rs.getString("PASSWORD");
salt = rs.getString("SALT");
// DATABASE VALIDATION
if (digest == null || salt == null) {
throw new SQLException("Database inconsistant Salt "+
"or Digested Password altered");
}
if (rs.next()) {
// Should not append, because login is the primary key
throw new SQLException("Database inconsistent two "+
"CREDENTIALS with the same LOGIN");
}
} else {
// TIME RESISTANT ATTACK (Even if the user does not exist the
// Computation time is equal to the time needed
// for a legitimate user
digest = "000000000000000000000000000=";
salt = "00000000000=";
userExist = false;
}
byte[] bDigest = base64ToByte(digest);
byte[] bSalt = base64ToByte(salt);
// Compute the new DIGEST
byte[] proposedDigest = getHash(ITERATION_NUMBER,
password, bSalt);
return Arrays.equals(proposedDigest, bDigest) && userExist;
} catch (IOException ex){
throw new SQLException("Database inconsistant Salt or Digested "+
"Password altered");
}
finally{
close(rs);
close(ps);
}
}
/**
* Inserts a new user in the database
* @param con Connection An open connection to a databse
* @param login String The login of the user
* @param password String The password of the user
* @return boolean Returns true if the login and password are ok (not null
* and length(login)<=100
* @throws SQLException If the database is unavailable
* @throws NoSuchAlgorithmException If the algorithm SHA-1 or the SecureRandom
* is not supported by the JVM
*/
public boolean createUser(Connection con, String login, String password)
throws SQLException, NoSuchAlgorithmException
{
PreparedStatement ps = null;
try {
if (login!=null&&password!=null&&login.length()<=100){
// Uses a secure Random not a simple Random
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
// Salt generation 64 bits long
byte[] bSalt = new byte[8];
random.nextBytes(bSalt);
// Digest computation
byte[] bDigest = getHash(ITERATION_NUMBER,password,bSalt);
String sDigest = byteToBase64(bDigest);
String sSalt = byteToBase64(bSalt);
ps = con.prepareStatement("INSERT INTO CREDENTIAL "+
"(LOGIN, PASSWORD, SALT) VALUES (?,?,?)");
ps.setString(1,login);
ps.setString(2,sDigest);
ps.setString(3,sSalt);
ps.executeUpdate();
return true;
} else {
return false;
}
} finally {
close(ps);
}
}
/**
* From a password, a number of iterations and a salt,
* returns the corresponding digest
* @param iterationNb int The number of iterations of the algorithm
* @param password String The password to encrypt
* @param salt byte[] The salt
* @return byte[] The digested password
* @throws NoSuchAlgorithmException If the algorithm doesn't exist
*/
public byte[] getHash(int iterationNb, String password, byte[] salt)
throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.reset();
digest.update(salt);
byte[] input = digest.digest(password.getBytes("UTF-8"));
for (int i = 0; i < iterationNb; i++) {
digest.reset();
input = digest.digest(input);
}
return input;
}
public void creerTable(Connection con) throws SQLException{
Statement st = null;
try {
st = con.createStatement();
st.execute("CREATE TABLE CREDENTIAL "+
"(LOGIN VARCHAR(100) PRIMARY KEY, "+
"PASSWORD VARCHAR(32) NOT NULL, "+
"SALT VARCHAR(32) NOT NULL)");
} finally {
close(st);
}
}
/**
* Closes the current statement
* @param ps Statement
*/
public void close(Statement ps) {
if (ps!=null){
try {
ps.close();
} catch (SQLException ignore) {
}
}
}
/**
* Closes the current resultset
* @param ps Statement
*/
public void close(ResultSet rs) {
if (rs!=null){
try {
rs.close();
} catch (SQLException ignore) {
}
}
}
/**
* From a base 64 representation, returns the corresponding byte[]
* @param data String The base64 representation
* @return byte[]
* @throws IOException
*/
public static byte[] base64ToByte(String data) throws IOException {
BASE64Decoder decoder = new BASE64Decoder();
return decoder.decodeBuffer(data);
}
/**
* From a byte[] returns a base 64 representation
* @param data byte[]
* @return String
* @throws IOException
*/
public static String byteToBase64(byte[] data){
BASE64Encoder endecoder = new BASE64Encoder();
return endecoder.encode(data);
}
}
NB : Cet article est la traduction française d'une contribution de Safe Pic Technologies à l'OWASP. La version anglaise peut être trouvé sur leur site. http://www.owasp.org/index.php/Hashing_Java