[TUTO] Connexion avec sel côté client et côté serveur

1 message   •   Page 1 sur 1
Avatar de l’utilisateur
ViPHP
AB
ViPHP | 5818 Messages

18 Déc 2014, 03:48

Bonjour,

Suite au premier tuto d'initiation sur l'inscription et la connexion dans un espace membre vous trouverez ci-dessous un exemple plus sécurisé pour protéger les mots de passe lors de leur transfert sur le réseau et pour leur stockage dans la base de donnée.

A/ Piratage base de donnée
Pour éviter de fournir trop facilement des informations en cas de piratage de la bdd il est conseillé de hasher les mots de passe avec un sel. On utilise pour cela préférentiellement la fonction crypt de php ou la fonction password_hash disponible avec php 5.5. Quelque soit la méthode employée le contenu du champ "pass" est donc composé suivant le principe :
$champ pass = hash($mot_de_passe.$sel_bdd)



B/ Piratage réseau
Si la transmission serveur n'est pas sécurisée avec une connexion de type "ssl", il y a un risque de se faire pirater les mots de passe lors d'une authentification/connexion par un pirate qui snifferait le réseau (administrateur réseau indélicat ou piratage d'une liaison wifi etc.).

Dans ce cas il est plus prudent de saler également les mots de passe côté client pour retourner au serveur le hash du mot de passe salé. Ainsi la transmission ne contient plus le mot de passe en clair, ou un simple hash sans sel qui permettrait l'utilisation de dictionnaires de hash. Pour construire en javascript (côté client) la chaine correspondant à la valeur du champ "pass" enregistré en bdd, nous feront une requête Ajax qui retournera le sel en fonction du login.

Nous apportons ainsi une protection supplémentaire aux mots de passe, mais pour autant le même sniffeur de réseau aurait accès à la valeur globale (hash du mot de passe salé) qui a elle seule permettrait de se connecter puisqu'il lui suffirait de la renvoyer sans besoin de connaître précisément le mot de passe. En plus du sel utilisé en bdd (qui est fixe) nous utiliseront donc conjointement un sel aléatoire pour garantir la confidentialité de la valeur globale (hash du mot de passe salé) et l'unicité du post.

Au total, et avant son envoi sur le serveur, on passera donc dans un champ du formulaire (nommé "reponse" dans le code en exemple plus bas) la valeur :
$reponse = hash($sel_aleatoire.hash($mot_de_passe.$sel_bdd)

Et évidemment on prendra soin de supprimer la valeur du mot de passe du formulaire sinon on a perdu notre temps... De même si on utilise ssl on en reste à la protection "A" car la transmission avec ssl est indéchiffrable (tant que les certificats ne sont pas piratés).


C/ Choix du hash
Il faut utiliser un hash qui soit compatible côté client et côté serveur (javascript/php) et pour ce faire j'ai choisi le sha512. Cela nous prive d'utiliser les fonctions dédiées "crypt" ou "password_hash" de php qui ont des sels embarqués mais on pourra toutefois générer des sels assez forts avec par exemple la fonction "openssl_random_pseudo_bytes". Pas de problème cependant si vous n'avez accès à cette fonction (souvent désactivée par défaut sur un serveur de test local wampserver) car une autre fonction utilisant "uniqid()" sera utilisée en remplacement.


D/ Déroulement de l'aventure
Le principe est donc de suivre les étapes suivantes :

1/ Avec php :
On génère un sel aléatoire pour assurer l'unicité du post et protéger le hash du mot de passe salé. Ce sel servira par la même occasion de token.

2 / Avec javascript :
A la soumission du formulaire on récupère le login, le mot de passe et on envoie une requête Ajax (avec le login comme seul paramètre) pour récupérer le sel de bdd correspondant au login. Le code de la requête Ajax est fourni après le code de connexion.
En retour de la requête ajax on pourra donc construire la chaine "$reponse" citée plus haut. Puis la transmettre dans le formulaire, puis supprimer la valeur du mot de passe et finalement envoyer le formulaire.

A noter que par facilité j'utilise jquery et ici avec un lien google. Je conseille plutôt (envers et contre tous ceux qui disent le contraire) de rapatrier jquery sur votre serveur car je conçois mal qu'on puisse se mettre sous la dépendance d'un autre serveur (quand bien même s'appellerait-il google qu'on a vu suffisamment de fois ramer au passage) pour les services importants d'un site.

3 / Avec php
A réception du formulaire, php reconstitue une chaine de caractères construite avec le sel aléatoire de session et le contenu du champ pass récupéré en bdd qui contient hash($mot_de_passe.$sel_bdd). Et l'on compare avec la valeur du champ "reponse" envoyée par le formulaire.

Prêt ? 8-| Ok c'est parti. Le code est commenté au fil de l'eau :)

Script du fichier de connexion :
<?php 
ini_set('session.use_only_cookies', true);
session_start();
header('Content-type: text/html; charset=UTF-8');

/* Activer openssl si possible (sur le serveur) pour pouvoir se servir de la fonction 'openssl_random_pseudo_bytes' */
function Unique_Sel()
{
return function_exists('openssl_random_pseudo_bytes')? hash("sha512",openssl_random_pseudo_bytes("128", $cstrong)) : hash("sha512",uniqid(rand(), true));
}

/* On part du principe que le mot de passe est enregistrer comme ci-dessous en bdd (définir un passe) : */
/*
$mot_de_passe = 'b';

// et rentrez ces valeurs en bdd (avec un login correspondant)
echo 'sel = '. $sel = Unique_Sel();
echo '<br>';
echo 'pass = '. hash("sha512",$mot_de_passe.$sel);
*/



/* $_SESSION["login"] est définie uniquement si l'authentification est réussie. Si l'on est déjà enregistré normalement on fait un header de redirection vers une page souhaitée par exemple "menu.php" */
if(isset($_SESSION["login"]))
{
/* Désactivé pour l'exemple. */
/*
header('Location: menu.php');
exit;
*/
}

/* Teste les retours attendus du formulaire */
$reponse = filter_input(INPUT_POST,'reponse', FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH);
$login = filter_input(INPUT_POST,'nom', FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW);

/* Définition et récupération du sel aléatoire de session */
$_SESSION["sel"] = isset($reponse,$_SESSION["sel"]) ? $_SESSION["sel"] : Unique_Sel();


/* Message par défaut */
$message = null;
/* Initialisation des erreurs */
$erreur = null;

/* Si le formulaire est envoyé */
if (isset($login,$reponse))
{
/* Connexion à la base de donnée, ici test sur une bdd en local */
/* normalement on enregistre ces variables dans un fichier que l'on inclus avec un require */
$hostname = "localhost";
$database = "ma_base";
$username = "root";
$password = "";

try
{
/* Configuration de PDO et connexion
récupération mode objet */
$pdo_options[PDO::ATTR_DEFAULT_FETCH_MODE] = PDO::FETCH_OBJ;

/* mode d'erreurs */
$pdo_options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION;

/* Indispensable pour ne pas avoir execute qui formate en string avec un tableau en paramètre (incompatible avec la clause limit) */
$pdo_options[PDO::ATTR_EMULATE_PREPARES] = false;

/* charset reconnu dans la connexion à partir de php 5.3.6 auquel cas on peut supprimer l'option MYSQL_ATTR_INIT_COMMAND ci-dessous */
/* $pdo_options[PDO::MYSQL_ATTR_INIT_COMMAND] = "SET NAMES utf8"; */

$connexion = new PDO('mysql:host='.$hostname.';dbname='.$database.';charset=utf8', $username, $password, $pdo_options);

/* Sélection du mot de passe correspondant au login (structure pour requête préparée) */
$query = "SELECT pass FROM test WHERE login = ?";

/* On prépare la requête */
$stmt = $connexion->prepare($query);
/* On lie la variable avec binParam */
$stmt->bindParam(1, $login);
/* Et on exécute */
$stmt->execute();

/* On récupère le tout dans un tableau (ce qui permet ici de compter facilement les résultats) */
$result = $stmt->fetchAll();

if (empty($result))
{
/* On sait ici que le login est non valide mais pas besoin de renseigner plus présicément les pirates */
throw new Exception("Identifiants non valides");
}

/* On ne devrait jamais rentrer dans la condition ci-dessous si la table est bien construite avec une clé primaire sur la colonne du login. Cela dit ce contrôle complémentaire ne coûte pas cher... */
if (count($result) > 1)
{
throw new Exception("Erreur d'unicité du login");
}

/* Récupération du pass (première ligne du tableau $result) qui est égal à : hash("sha512",mot_de_passe.sel_bdd); */
$pass = $result[0]->pass;

/* On reconstruit la même chaine que celle construite avec javascript pour renseigner le champ "reponse" */
$compare_bdd = hash("sha512",$_SESSION["sel"].$pass);

/* On compare les deux valeurs et si oui */
if($reponse == $compare_bdd)
{
/* On efface cette variable de session qui ne sert plus à rien */
unset($_SESSION["sel"]);

/* On régénère les identifiants de session */
session_regenerate_id(true);

/* Déclaration de la variable de session témoin de l'enregistrement */
$_SESSION["login"] = 1;

/* On envoie un message, sauf si on préfère la redirection mise en commentaire juste après */
$message = "Vous êtes maintenant connecté !";

/* Désactivé pour l'exemple. Normalement on fait un header de redirection vers une page souhaitée par exemple "menu.php" */
/*
header('Location: menu.php');
exit;
*/
}
else
{
/* On sait ici que le pass est non valide mais pas besoin de renseigner plus précisément les pirates */
throw new Exception("Identifiants non valides");
}
}
catch (PDOException $e) /* on récupère les erreurs PDO */
{
/* message en production */
$erreur = "Erreur dans la requête d'authentification";

/* message complet en développement -> !!!!!!! IMPORTANT mettre la ligne ci-dessous en commentaires pour la production (pas besoin de renseigner plus précisément les pirates !!!!!!!! */
$erreur .= ' : '.$e->getMessage();
}
catch (Exception $e)/* puis on récupère les autres erreurs générées avec throw */
{
$erreur = $e->getMessage();
}

$message = isset($erreur) ? $erreur : $message;
}

?>
<!DOCTYPE html>
<html lang="fr">

<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Authentification</title>
<meta name="Robots" content="noindex, nofollow" />

<!-- Inclusion de jquery depuis google pour les besoins du tuto. Je préfère normalement ne pas être dépendant de google et inclure jquery mon serveur -->
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>

<!-- Fonction javascript sha 512 (passer la lecture de ce code, ce n'est pas le sujet) -->
<script type="text/javascript">
var CryptoJS=CryptoJS||function(a,m){var r={},f=r.lib={},g=function(){},l=f.Base={extend:function(a){g.prototype=this;var b=new g;a&&b.mixIn(a);b.hasOwnProperty("init")||(b.init=function(){b.$super.init.apply(this,arguments)});b.init.prototype=b;b.$super=this;return b},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var b in a)a.hasOwnProperty(b)&&(this[b]=a[b]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}},
p=f.WordArray=l.extend({init:function(a,b){a=this.words=a||[];this.sigBytes=b!=m?b:4*a.length},toString:function(a){return(a||q).stringify(this)},concat:function(a){var b=this.words,d=a.words,c=this.sigBytes;a=a.sigBytes;this.clamp();if(c%4)for(var j=0;j<a;j++)b[c+j>>>2]|=(d[j>>>2]>>>24-8*(j%4)&255)<<24-8*((c+j)%4);else if(65535<d.length)for(j=0;j<a;j+=4)b[c+j>>>2]=d[j>>>2];else b.push.apply(b,d);this.sigBytes+=a;return this},clamp:function(){var n=this.words,b=this.sigBytes;n[b>>>2]&=4294967295<<
32-8*(b%4);n.length=a.ceil(b/4)},clone:function(){var a=l.clone.call(this);a.words=this.words.slice(0);return a},random:function(n){for(var b=[],d=0;d<n;d+=4)b.push(4294967296*a.random()|0);return new p.init(b,n)}}),y=r.enc={},q=y.Hex={stringify:function(a){var b=a.words;a=a.sigBytes;for(var d=[],c=0;c<a;c++){var j=b[c>>>2]>>>24-8*(c%4)&255;d.push((j>>>4).toString(16));d.push((j&15).toString(16))}return d.join("")},parse:function(a){for(var b=a.length,d=[],c=0;c<b;c+=2)d[c>>>3]|=parseInt(a.substr(c,
2),16)<<24-4*(c%8);return new p.init(d,b/2)}},G=y.Latin1={stringify:function(a){var b=a.words;a=a.sigBytes;for(var d=[],c=0;c<a;c++)d.push(String.fromCharCode(b[c>>>2]>>>24-8*(c%4)&255));return d.join("")},parse:function(a){for(var b=a.length,d=[],c=0;c<b;c++)d[c>>>2]|=(a.charCodeAt(c)&255)<<24-8*(c%4);return new p.init(d,b)}},fa=y.Utf8={stringify:function(a){try{return decodeURIComponent(escape(G.stringify(a)))}catch(b){throw Error("Malformed UTF-8 data");}},parse:function(a){return G.parse(unescape(encodeURIComponent(a)))}},
h=f.BufferedBlockAlgorithm=l.extend({reset:function(){this._data=new p.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=fa.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(n){var b=this._data,d=b.words,c=b.sigBytes,j=this.blockSize,l=c/(4*j),l=n?a.ceil(l):a.max((l|0)-this._minBufferSize,0);n=l*j;c=a.min(4*n,c);if(n){for(var h=0;h<n;h+=j)this._doProcessBlock(d,h);h=d.splice(0,n);b.sigBytes-=c}return new p.init(h,c)},clone:function(){var a=l.clone.call(this);
a._data=this._data.clone();return a},_minBufferSize:0});f.Hasher=h.extend({cfg:l.extend(),init:function(a){this.cfg=this.cfg.extend(a);this.reset()},reset:function(){h.reset.call(this);this._doReset()},update:function(a){this._append(a);this._process();return this},finalize:function(a){a&&this._append(a);return this._doFinalize()},blockSize:16,_createHelper:function(a){return function(b,d){return(new a.init(d)).finalize(b)}},_createHmacHelper:function(a){return function(b,d){return(new ga.HMAC.init(a,
d)).finalize(b)}}});var ga=r.algo={};return r}(Math);
(function(a){var m=CryptoJS,r=m.lib,f=r.Base,g=r.WordArray,m=m.x64={};m.Word=f.extend({init:function(a,p){this.high=a;this.low=p}});m.WordArray=f.extend({init:function(l,p){l=this.words=l||[];this.sigBytes=p!=a?p:8*l.length},toX32:function(){for(var a=this.words,p=a.length,f=[],q=0;q<p;q++){var G=a[q];f.push(G.high);f.push(G.low)}return g.create(f,this.sigBytes)},clone:function(){for(var a=f.clone.call(this),p=a.words=this.words.slice(0),g=p.length,q=0;q<g;q++)p[q]=p[q].clone();return a}})})();
(function(){function a(){return g.create.apply(g,arguments)}for(var m=CryptoJS,r=m.lib.Hasher,f=m.x64,g=f.Word,l=f.WordArray,f=m.algo,p=[a(1116352408,3609767458),a(1899447441,602891725),a(3049323471,3964484399),a(3921009573,2173295548),a(961987163,4081628472),a(1508970993,3053834265),a(2453635748,2937671579),a(2870763221,3664609560),a(3624381080,2734883394),a(310598401,1164996542),a(607225278,1323610764),a(1426881987,3590304994),a(1925078388,4068182383),a(2162078206,991336113),a(2614888103,633803317),
a(3248222580,3479774868),a(3835390401,2666613458),a(4022224774,944711139),a(264347078,2341262773),a(604807628,2007800933),a(770255983,1495990901),a(1249150122,1856431235),a(1555081692,3175218132),a(1996064986,2198950837),a(2554220882,3999719339),a(2821834349,766784016),a(2952996808,2566594879),a(3210313671,3203337956),a(3336571891,1034457026),a(3584528711,2466948901),a(113926993,3758326383),a(338241895,168717936),a(666307205,1188179964),a(773529912,1546045734),a(1294757372,1522805485),a(1396182291,
2643833823),a(1695183700,2343527390),a(1986661051,1014477480),a(2177026350,1206759142),a(2456956037,344077627),a(2730485921,1290863460),a(2820302411,3158454273),a(3259730800,3505952657),a(3345764771,106217008),a(3516065817,3606008344),a(3600352804,1432725776),a(4094571909,1467031594),a(275423344,851169720),a(430227734,3100823752),a(506948616,1363258195),a(659060556,3750685593),a(883997877,3785050280),a(958139571,3318307427),a(1322822218,3812723403),a(1537002063,2003034995),a(1747873779,3602036899),
a(1955562222,1575990012),a(2024104815,1125592928),a(2227730452,2716904306),a(2361852424,442776044),a(2428436474,593698344),a(2756734187,3733110249),a(3204031479,2999351573),a(3329325298,3815920427),a(3391569614,3928383900),a(3515267271,566280711),a(3940187606,3454069534),a(4118630271,4000239992),a(116418474,1914138554),a(174292421,2731055270),a(289380356,3203993006),a(460393269,320620315),a(685471733,587496836),a(852142971,1086792851),a(1017036298,365543100),a(1126000580,2618297676),a(1288033470,
3409855158),a(1501505948,4234509866),a(1607167915,987167468),a(1816402316,1246189591)],y=[],q=0;80>q;q++)y[q]=a();f=f.SHA512=r.extend({_doReset:function(){this._hash=new l.init([new g.init(1779033703,4089235720),new g.init(3144134277,2227873595),new g.init(1013904242,4271175723),new g.init(2773480762,1595750129),new g.init(1359893119,2917565137),new g.init(2600822924,725511199),new g.init(528734635,4215389547),new g.init(1541459225,327033209)])},_doProcessBlock:function(a,f){for(var h=this._hash.words,
g=h[0],n=h[1],b=h[2],d=h[3],c=h[4],j=h[5],l=h[6],h=h[7],q=g.high,m=g.low,r=n.high,N=n.low,Z=b.high,O=b.low,$=d.high,P=d.low,aa=c.high,Q=c.low,ba=j.high,R=j.low,ca=l.high,S=l.low,da=h.high,T=h.low,v=q,s=m,H=r,E=N,I=Z,F=O,W=$,J=P,w=aa,t=Q,U=ba,K=R,V=ca,L=S,X=da,M=T,x=0;80>x;x++){var B=y[x];if(16>x)var u=B.high=a[f+2*x]|0,e=B.low=a[f+2*x+1]|0;else{var u=y[x-15],e=u.high,z=u.low,u=(e>>>1|z<<31)^(e>>>8|z<<24)^e>>>7,z=(z>>>1|e<<31)^(z>>>8|e<<24)^(z>>>7|e<<25),D=y[x-2],e=D.high,k=D.low,D=(e>>>19|k<<13)^
(e<<3|k>>>29)^e>>>6,k=(k>>>19|e<<13)^(k<<3|e>>>29)^(k>>>6|e<<26),e=y[x-7],Y=e.high,C=y[x-16],A=C.high,C=C.low,e=z+e.low,u=u+Y+(e>>>0<z>>>0?1:0),e=e+k,u=u+D+(e>>>0<k>>>0?1:0),e=e+C,u=u+A+(e>>>0<C>>>0?1:0);B.high=u;B.low=e}var Y=w&U^~w&V,C=t&K^~t&L,B=v&H^v&I^H&I,ha=s&E^s&F^E&F,z=(v>>>28|s<<4)^(v<<30|s>>>2)^(v<<25|s>>>7),D=(s>>>28|v<<4)^(s<<30|v>>>2)^(s<<25|v>>>7),k=p[x],ia=k.high,ea=k.low,k=M+((t>>>14|w<<18)^(t>>>18|w<<14)^(t<<23|w>>>9)),A=X+((w>>>14|t<<18)^(w>>>18|t<<14)^(w<<23|t>>>9))+(k>>>0<M>>>
0?1:0),k=k+C,A=A+Y+(k>>>0<C>>>0?1:0),k=k+ea,A=A+ia+(k>>>0<ea>>>0?1:0),k=k+e,A=A+u+(k>>>0<e>>>0?1:0),e=D+ha,B=z+B+(e>>>0<D>>>0?1:0),X=V,M=L,V=U,L=K,U=w,K=t,t=J+k|0,w=W+A+(t>>>0<J>>>0?1:0)|0,W=I,J=F,I=H,F=E,H=v,E=s,s=k+e|0,v=A+B+(s>>>0<k>>>0?1:0)|0}m=g.low=m+s;g.high=q+v+(m>>>0<s>>>0?1:0);N=n.low=N+E;n.high=r+H+(N>>>0<E>>>0?1:0);O=b.low=O+F;b.high=Z+I+(O>>>0<F>>>0?1:0);P=d.low=P+J;d.high=$+W+(P>>>0<J>>>0?1:0);Q=c.low=Q+t;c.high=aa+w+(Q>>>0<t>>>0?1:0);R=j.low=R+K;j.high=ba+U+(R>>>0<K>>>0?1:0);S=l.low=
S+L;l.high=ca+V+(S>>>0<L>>>0?1:0);T=h.low=T+M;h.high=da+X+(T>>>0<M>>>0?1:0)},_doFinalize:function(){var a=this._data,f=a.words,h=8*this._nDataBytes,g=8*a.sigBytes;f[g>>>5]|=128<<24-g%32;f[(g+128>>>10<<5)+30]=Math.floor(h/4294967296);f[(g+128>>>10<<5)+31]=h;a.sigBytes=4*f.length;this._process();return this._hash.toX32()},clone:function(){var a=r.clone.call(this);a._hash=this._hash.clone();return a},blockSize:32});m.SHA512=r._createHelper(f);m.HmacSHA512=r._createHmacHelper(f)})();
</script>


<script type="text/javascript">

/* ICI commence le code javascript qui intercepte les données du formulaire, fait une requête ajax et modifie le formulaire avant son envoi */
$(function()
{
/* DESTINATION DE LA REQUETE AJAX */
var destination_ajax = 'recuperation_sel.php';

/* Trouve le formulaire en fonction de son id */
var formulaire = $("#authentification");

/* Trouve le champ "nom" du formulaire en fonction de son nom */
var nom = formulaire.find("input[name=nom]");

/* On met le focus dans le champ "nom" s'il existe */
if (nom.length > 0){nom.get(0).focus();}

/* Comportement à la soumission du formulaire */
formulaire.on('submit',function(event)
{
/* Suspend la soumission du formulaire */
event.preventDefault();

/* Trouve le champ "pass" du formulaire en fonction de son nom */
var pass = $(this).find("input[name=pass]");
/* Trouve le champ "reponse" du formulaire en fonction de son nom */
var reponse = $(this).find("input[name=reponse]");
/* Trouve le champ "message" du formulaire en fonction de son id */
var message = $(this).find("#message");

/* Vérification de l'existence des objets précédents */
if(pass.length == 0 || nom.length == 0 || reponse.length == 0 || message.length == 0)
{
/* Sinon on retourne le message suivant : */
message.text('Champs de formulaires non trouvés');
/* Et l'on sort */
return false;
}

/* Vérification des valeurs des objets précédents */
if($.trim(pass.val()) == "" || $.trim(nom.val()) == "")
{
/* Sinon on retourne le message suivant : */
message.text('Les deux champs doivent être remplis');
/* Et l'on sort */
return false;
}

/* Valeurs "token" et "login" que l'on envoie à la requête ajax.
- "token" sert pour s'assurer que la requête effectuée dans "destination_ajax" est faite sur demande de ce formulaire
- On les met dans un objet pour ensuite les sérialiser avec $.param */
var obj_post = {};

obj_post.token = '<?=isset($_SESSION["sel"])? $_SESSION["sel"]:''?>';
obj_post.login = nom.val();

/* sérialisation avec $.param */
var data_post = $.param(obj_post);

/* On envoie la requête ajax */
$.ajax({
/* type du retour attendu */
dataType: "json",
/* type de l'envoi */
type: "POST",
/* destination du fichier php */
url: destination_ajax,
/* données à envoyer */
data: data_post
})
/* en cas de succès de la requête ajax */
.done(function(result)
{
/* Cherche la présence des élements du tableau json renvoyé */
var sel_bdd = result.sel != undefined ? result.sel : null;
var erreur_requete = result.erreur != undefined ? result.erreur : null;

/* On fait une vérification sur le sel retourné */
if($.trim(sel_bdd) == '')
{
/* Si vide on retourne le message suivant : */
message.text('Jeton de vérification manquant');
/* Et l'on sort */
return false;
}

/* On fait une vérification sur le retour de la requête ajax */
if($.trim(erreur_requete) != '')
{
/* Si erreur on retourne le message envoyé par php comme retour de requête ajax */
message.text(erreur_requete);
/* Et l'on sort */
return false;
}

/* On construit la chaine qui sera comparée en php */
var str = CryptoJS.SHA512(obj_post.token+CryptoJS.SHA512(pass.val()+sel_bdd));

/* On efface le contenu du champ "pass" - NE PAS OUBLIER SINON TOUT CELA NE SERT A RIEN !!!! */
pass.val("");

/* On envoie la chaine dans le champ "reponse" du formulaire */
reponse.val(str);

/* On fait une dernière vérification avant l'envoi du formulaire */
if (pass.val() == '' && reponse.val().length === 128)
{
/* On désactive le comportement javascript on submit sinon on referait un tour de manège à la soumission du formulaire */
formulaire.off('submit');

/* Et on envoie le formulaire */
formulaire.submit();
}
})
/* en cas d'échec de la requête ajax (la plupart du temps c'est que l'adresse de destination du fichier php est erronée) */
.fail(function( jqXHR, textStatus)
{
message.text('Erreur de destination de la requête ajax');
});

return false;
});
});
</script>
<style type="text/css">
#blocentre {
position: relative;
width:450px;
margin:20px auto 0 auto;
text-align:left;
font-family: Arial, Helvetica, sans-serif;
font-size: 14px;
font-weight:bold;
background-color: #ccc;
color: #FFFFFF;
border:2px solid black;
border-radius:10px;
box-shadow: 0px 0px 3px 1px rgba(100, 100, 100, 0.7);
}

#authentification p {
margin:20px auto 20px auto;
width:360px;
}

#authentification #pass,#authentification #Valider {
margin-left:20px;
}

#authentification #labpass {
margin-left:90px;
}

#authentification #Valider {
font-weight:bold;
cursor:pointer;
}

#authentification form,#authentification input {
margin: 0;
}

#authentification input {
background-color:#FDFDF7;
width:100px;
border-radius:3px;
}

#message {
text-align:center;
padding:0.1em;
color: #333;
}
</style>
</head>
<body>

<div id="blocentre">
<form action="#" method="post" id="authentification">
<p><label>Login</label><label id="labpass">&nbsp;Mot de passe</label></p>
<p>
<input name="nom" id="nom" type="text" size="20" maxlength="30" />
<input id="pass" name="pass" type="password" size="20" maxlength="255" />
<input type="submit" name="Submit" value="Valider" id="Valider" />
<input type="hidden" name="reponse" id="reponse" value="" />
<p>
<p id="message"><?=$message ?></p>
</form>


<noscript>
<h2>- Javascript est désactivé. Vous devez réactiver Javascript pour l'authentification.</h2>
</noscript>
</div>
</body>
</html>


Le code de connexion ci-dessus fait une requête ajax à destination d'un fichier nommé "recuperation_sel.php" qui doit se trouver au même niveau (dans le même répertoire) sur le serveur et contenir le code suivant :
<?php
session_start();

header('Content-type: text/html; charset=UTF-8');

/* On récupère les valeurs envoyées par la requête ajax. */
$token = filter_input(INPUT_POST,'token', FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH);
$login = filter_input(INPUT_POST,'login', FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW);


/* Pour renvoyer un minimum d'informations significatives, en cas d'erreur je renvoie un sel bidon */
$sel_retour = hash("sha512",(uniqid(rand(), true)));

$erreur = null;

/* On vérifie le token et les variables, par sécurité et permet d'éviter des requêtes inutiles si on appelait ce fichier directement */
if(isset($login,$_SESSION["sel"]) && !empty($token) && $token == $_SESSION["sel"])
{
/* On utilise rawurldecode car les valeurs ont été encodées avec $.param de jquery qui utilise encodeURIComponent */
$token = rawurldecode($token);
$login = rawurldecode($login);

/* A modifier avec vos valeurs. Normalement on enregistre ces variables dans un fichier que l'on inclus avec un require */
$hostname = "localhost";
$database = "ma_base";
$username = "root";
$password = "";

try
{
/* récupération mode objet */
$pdo_options[PDO::ATTR_DEFAULT_FETCH_MODE] = PDO::FETCH_OBJ;

/*/ mode d'erreurs */
$pdo_options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION;

/* Indispensable pour ne pas avoir execute qui formate en string avec un tableau en paramètre (incompatible avec la clause limit) */
$pdo_options[PDO::ATTR_EMULATE_PREPARES] = false;

/* charset reconnu dans la connexion à partir de php 5.3.6 auquel cas on peut supprimer l'option MYSQL_ATTR_INIT_COMMAND ci-dessous */
/*$pdo_options[PDO::MYSQL_ATTR_INIT_COMMAND] = "SET NAMES utf8"; */

$connexion = new PDO('mysql:host='.$hostname.';dbname='.$database.';charset=utf8', $username, $password, $pdo_options);

/* Sélection du mot de passe correspondant au login (structure pour requête préparée) */
$query = "SELECT sel FROM test WHERE login = ?";

$stmt = $connexion->prepare($query);
$stmt->bindParam(1, $login);
$stmt->execute();

/* On récupère le tout dans un tableau (ce qui permet ici de compter facilement les résultats) */
$result = $stmt->fetchAll();

/* Si un seul résultat */
if (!empty($result) && count($result) == 1)
{
$sel_bdd = $result[0]->sel;

if(trim($sel_bdd) != '')
{
$sel_retour = $sel_bdd;
}
}
}
catch (PDOException $e)/* on récupère les erreurs PDO */
{
/* on renvoie une erreur qui signale une erreur dans la requête du sel de bdd */
$erreur = 'Problème : "A la conquête du sel"';
}
}

/* on renvoie le résultat */
echo json_encode(array('sel'=>$sel_retour,'erreur'=>$erreur));
exit;
?>


Attention ! dans les deux codes ci-dessus vous devrez renseigner les lignes suivantes avec vos valeurs personnelles.
$hostname = "localhost";
$database = "ma_base";
$username = "root";
$password = "";


Par ailleurs le code des requêtes est testé avec cette table de bdd nommée "test" qui inclus pour l'instant une seule ligne avec comme login "a" et comme mot de passe "b", ou plus précisément le hash salé de "b" si vous avez bien suivi =D>
(à copier dans une fenêtre sql de phpMyadmin pour faire fonctionner le script et les requêtes sur la table "test")
CREATE TABLE IF NOT EXISTS `test` (
`login` tinytext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
`pass` tinytext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
`sel` tinytext NOT NULL,
PRIMARY KEY (`login`(100))
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

INSERT INTO `test` (`login`, `pass`, `sel`) VALUES
('a', '504cee9ec03554f5b35b2e5024660376cfac0d5c0499ee22e6e9cff24cdecdbf90fb33247945e67b6650b5bec1b2f9a0e8bca3189a431c94378127f88735b01b', '7383f1dedde7d348493c06465ef59879d5f20c3683b86d2e42f28921685e8e1f1ac139b7644674fa2e9e5bf8bac13552c6cf9170efbdde737144b98dc9f7936c');


Plus d'infos pour la création des mots de passe et du sel dans ce sujet.

Et Voilà :)


Remarque : et si j'ai la chance d'avoir une connexion ssl ...

Vous pourriez utiliser ce code en pensant qu'il vous protégera en cas de vol des certificats ssl. C'est vrai mais le vol de certificats est très rare. Et il faut prendre en considération que le hashage sha512 côté javascript nous prive d'utiliser la fonction "crypt" de php. Or celle ci est plus puissante qu'un hashage standard car elle permet de définir un coût machine plus important pour le hashage ce qui donne plus de résistance à un pirate qui voudrait utiliser la force brute pour connaître le mot de passe suite à un piratage de la bdd. On choisira donc le plus souvent de garder le maximum de résistance possible côté bdd car le risque de piratage de la connexion ssl est vraiment très faible.

1 message   •   Page 1 sur 1