Mots de passe, limiter les risques
Dans la majeure partie des cas et pour la plupart des projets on vous demandera de créer un système d’authentification pour que les membres puissent se connecter et accéder à une interface spécifique. Rien de très complexe, une session et un petit gestionnaire de droit feront humblement l’affaire. Le point épineux dans tout ça c’est belle et bien le mot de passe, car même si celui-ci est complexe, il suffit d’une erreur dans votre code pour que tout s’écroule. Vous l’aurez compris, il ne faut surtout pas stocker les mots de passe en clair, plus votre application sera grande plus le risque d’erreur le sera, et croyez-moi si vous stockez vos mots de passe en clair il y a de fortes chances que l’application comporte des failles aussi. C’est une erreur que j’ai faite dans les projets informatiques de ma jeunesse, je vous déconseille de le faire. On retrouve par ailleurs un Commit Strip sur le sujet ici.
Une autre technique naïve serait encore de chiffrer le mot de passe avec une clé sous le principe de la cryptographie symétrique. À première vue c’est mieux, les mots de passe sont illisibles en base de données et c’est un niveau de sécurité supérieur qui a été atteint. Mais là encore, si votre clé est découverte, ce sont tous vos mots de passe qui le seront aussi, exemple ci-dessous avec PHP.
<?php
function encrypt($password, $key)
{
return rtrim(
base64_encode(
mcrypt_encrypt(
MCRYPT_RIJNDAEL_256,
$key, $password,
MCRYPT_MODE_ECB,
mcrypt_create_iv(
mcrypt_get_iv_size(
MCRYPT_RIJNDAEL_256,
MCRYPT_MODE_ECB
),
MCRYPT_RAND)
)
), "\0"
);
}
function decrypt($password, $key)
{
return rtrim(
mcrypt_decrypt(
MCRYPT_RIJNDAEL_256,
$key,
base64_decode($password),
MCRYPT_MODE_ECB,
mcrypt_create_iv(
mcrypt_get_iv_size(
MCRYPT_RIJNDAEL_256,
MCRYPT_MODE_ECB
),
MCRYPT_RAND
)
), "\0"
);
}
DEFINE('ENDL', "\n".'<br/>');
$key = 'Ju%2^:77"1(4#<s[sm+}19Hi';
$password = 'Cec!€$tùnM0t2P@$seCoMplèx€';
echo 'Mot de passe avant chiffrage : ' . $password.ENDL;
$password = encrypt($password, $key);
echo 'Mot de passe après chiffrage : ' . $password.ENDL;
$password = decrypt($password, $key);
echo 'Mot de passe après déchiffrage : ' . $password;
Ensuite il y a le hachage qui fonctionnait pas mal il y a quelques années. Là tout dépend de la complexité de l’algorithme que vous utilisez. Pour casser un hash il faut essayer toutes les possibilités de mot de passe jusqu’à trouver le même hash ce qui peut prendre beaucoup de temps. Maintenant il existe beaucoup de solutions pour accélérer le processus. Google est malgré lui devenu un bon dictionnaire, il vous suffit de mettre votre mot de passe haché dans la recherche et vous serez probablement servis, exemple ici et là. Pour les mots de passe moins communs, vous trouverez les Rainbow Table qui permettent un gros gain de temps machine au dépourvu de l’espace disque. Une Rainbow Table est une base de données de mots de passe avec leurs hashs respectifs pré compilés, la base de données est monstrueuse mais vous trouverez un mot de passe en très peu de temps. Si vous souhaitez vous en tenir à ça, évitez MD5 qui est maintenant peu fiable et limitez les usages de SHA1 dont la fin est proche. SHA515 est un bon compromis mais il est plus long.
Le problème avec le Hachage c’est que même avec le meilleur algorithme du monde, si 100 de vos membres ont le même mot de passe (ce qui arrive extrêmement couramment), les 100 hashs seront les mêmes, donc pourquoi chercher le mot de passe unique d’un seul compte alors que je peux en avoir 100 pour le prix d’un ? Il est donc recommandé d’ajouter un second élément au mot de passe, il faut que celui-ci soit fixe et dépendant de l’utilisateur, le pseudo et l’id sont généralement de bons candidats. Cette technique s’appelle le salage. Dans l’exemple ci-dessous on peut voir que les mots de passe chiffrés sont tous différents, ils seront donc théoriquement plus complexes à trouver. Pour les éléments de salage il est aussi possible d’ajouter une unique à clé d’application, le nom d’utilisateur haché… Le tout devant être ré-applicable une seconde fois pour vérifier la validité du mot de passe par la suite.
<?php
$users = array(
array('id'=>0, 'name'=>'jean', 'password'=>'1M0Td€P@$Se'),
array('id'=>1, 'name'=>'patrice', 'password'=>'1M0Td€P@$Se'),
array('id'=>3, 'name'=>'henri', 'password'=>'1M0Td€P@$Se')
);
DEFINE('ENDL', "\n".'<br/>');
echo 'Sans sallage'.ENDL;
foreach($users as $user)
{
echo 'hash("sha512", $user[\'password\']) == '.hash("sha512", $user['password']).ENDL;
}
echo 'Avec sallage'.ENDL;
foreach($users as $user)
{
echo 'hash("sha512", $user[\'id\'].$user[\'password\'].$user[\'name\']) == '.hash("sha512", $user['id'].$user['password'].$user['name']).ENDL;
}
echo 'Avec sallage haché'.ENDL;
foreach($users as $user)
{
echo 'hash("sha512", md5($user[\'id\']).hash(\'whirlpool\', $user[\'password\']).sha1($user[\'name\'])) == '.hash("sha512", md5($user['id']).hash('whirlpool', $user['password']).sha1($user['name'])).ENDL;
}
Il est aussi possible de créer plusieurs techniques de hachage et de les utiliser en fonction de celle définie pour l’utilisateur.
<?php
function customHash($user)
{
switch($user['tech'])
{
case 1:
return sha1($user['name'].$user['password']);
break;
case 2:
return md5($user['password'].sha1($user['name']));
break;
case 3:
return hash('ripemd256', $user['password'].sha1($user['id']));
break;
//etc...
}
}
foreach($users as $user)
echo customHash($user).ENDL;
Malheureusement les Rainbow Tables existeront toujours et ce peut importer l’algorithme que vous utilisez. La solution est de rendre le hachage des mots de passe suffisamment unique et long pour que le temps pour le trouver soit très coûteux en temps.
À titre d’exemple je vous propose cette classe que j’ai rédigée, je ne l’utilise pas et je vous déconseille de l’utiliser, ce type de code doit rester privé.
<?php
DEFINE('API_KEY', '"@%\'),46aO81*;)_S]]~)J52@%;5y5@.|6)kY1~>$^8,sQ$n1$,7]:%@fO&>:-._J](^74L[b14/6y+(g(<.+xX}^j.,=~#$:');
class User{
public $id;
public $userName;
public $password;
public $hashMode;
}
class Pasword
{
private static function getSalt(User $user)
{
$salt='';
switch($user->hashMode)
{
case 0:
$salt=hash('snefru', $user->id.$user->userName);
break;
case 5:
$salt=hash('adler32', $user->userName.$user->id);
break;
case 160:
$salt=hash('crc32b', $user->id.md5($user->id).sha1($user->userName));
break;
// et bien d'autres
}
$salt=hash('sha256', API_KEY.$salt);
}
public static function getHash(User $user, $password)
{
$salt = self::getSalt($user);
switch($user->hashMode)
{
case 0:
return hash('snefru', $salt.$password);
break;
case 5:
return hash('adler32', $password.$salt);
break;
case 160:
return hash('sha512', md5($salt).sha1($password));
break;
// et bien d'autres
}
}
public static function checkPassword(User $user, $password)
{
return $user->password === self::getHash($user, $password);
}
}
$user = new User();
$user->id = uniqid();
$user->hashMode = 160;
$user->userName = 'My userName';
$user->password = Pasword::getHash($user, 'JeanDeL@LùN€');
var_dump($user);
var_dump(Pasword::checkPassword($user, 'JeanDeL@LùN€'));
Si vous voyez une méthode que j’aurais pu oublier de traiter dans ce tutoriel n’hésitez pas à m’en faire part dans les commentaires. Ce tutoriel a été rédigé le 23/12/2013, ces conseils ne seront peut-être plus valables dans quelques années voire quelques mois.
Edit (17/06/2015)
Vous pouvez aussi passer cette phase en boucle et de tronquer le résultat à chaque itération. Il sera alors plus compliqué de retrouver le mot de passe directement dans une Rainbow table.