Secure authentication without SSL
February 18, 2008 1:26 pm PHP, Zend FrameworkLast days, I’ve implemented authentication in Wisss. The goal is to have a secure authentication without the need to use SSL. I’ve then choose a basic Challenge Response Authentication Mechanism which prevent to send password in clear text.
The mechanisme is simple :
- The server send a unique salt, which can be used only once
- The client answer with its username and SHA1(salt+SHA1(password))
- The server retrieves user’s password SHA1 from database and do SHA1(salt,sha1_password)
- if the two match, the user is authenticated
Here is the code for the server (it’s inside a Zend controller, but it can easily be implemented for all framework/language) :
The CRAM authentication adapter for Zend_Auth :
class Wisss_Auth_Adapter_BasicCram implements Zend_Auth_Adapter_Interface
protected $login ;
protected $digest ;
public funtion __construct($login,$digest)
{
$this->login = $login ;
$this->digest = $digest ;
}
public function authenticate()
{
$auth_session = new Zend_Session_Namespace('auth');
// retrieve salt from session and erase it since it's used once
$salt = $auth_session->salt ;
unset($auth_session->salt) ;
// digest received from client
$cram_from_client = $this->digest ;
// retrieve password sha1 digest from database for the user provided
require_once('Wisss/Dao/Factory/Abstract.php') ;
$dao_user = Wisss_Dao_Factory_Abstract::getDao('User','core') ;
$user = $dao_user->findBy(array('login' => $this->login)) ;
$sha1_password = $user[0]->getPassword() ;
// compute the CRAM digest
$cram_from_server = sha1($salt.$sha1_password) ;
if($cram_from_client == $cram_from_server) {
return new Zend_Auth_Result(Zend_Auth_Result::SUCCESS,$this->login) ;
} else {
return new Zend_Auth_Result(Zend_Auth_Result::FAILURE,$this->login) ;
}
}
}
The code of the controller :
class LoginController extends Zend_Controller_Action
{
public function indexAction()
{
$salt = md5(uniqid(rand(), true)) ;
$auth_session = new Zend_Session_Namespace('auth');
$this->view->assign('login_salt',$salt) ;
$auth_session->salt = $salt ;
$auth_session->referer = $_SERVER['HTTP_REFERER'] ;
}
public function authenticateAction()
{
$auth_session = new Zend_Session_Namespace('auth');
require_once 'Zend/Auth.php';
$auth = Zend_Auth::getInstance();
// Set up the authentication adapter
require_once('Wisss/Auth/Adapter/BasicCram.php') ;
$authAdapter = new Wisss_Auth_Adapter_BasicCram($this->_getParam('login'), $this->_getParam('password'));
// Attempt authentication, saving the result
$result = $auth->authenticate($authAdapter);
if (!$result->isValid()) {
// Authentication failed
$this->_redirect('/login') ;
} else {
// Authentification succeeded
// regenerate id to prevent session fixation after successfull authentication
Zend_Session::regenerateId();
$referer = $auth_session->referer ;
unset($auth_session->referer) ;
if($referer != $_SERVER['HTTP_REFERER']) {
$this->_redirect($referer) ;
}
}
}
}
and finally, the client code (it requires the Prototype Javascript framework) :
<div>
<form action="login/authenticate" method="POST" onSubmit="cram()">
<?php echo $this->formHidden('login_salt',$this->login_salt) ?>
<fieldset>
<div><label for="login">Login : </label><?php echo $this->formText('login') ?></div>
<div><label for="password">Password : </label><?php echo $this->formPassword('password') ?></div>
<div><input type="submit" name="authenticate" value="login"/></div>
</fieldset>
</form>
</div>
And the associated js :
function cram()
{
$('password').value = sha1Hash($('login_salt').value+sha1Hash($('password').value)) ;
$('login_salt').value = "" ;
}
// © 2002-2005 Chris Veness
// http://www.movable-type.co.uk/scripts/sha1.html
function sha1Hash(msg)
{
// constants [§4.2.1]
var K = [0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xca62c1d6];
// PREPROCESSING
msg += String.fromCharCode(0x80); // add trailing '1' bit to string [§5.1.1]
// convert string msg into 512-bit/16-integer blocks arrays of ints [§5.2.1]
var l = Math.ceil(msg.length/4) + 2; // long enough to contain msg plus 2-word length
var N = Math.ceil(l/16); // in N 16-int blocks
var M = new Array(N);
for (var i=0; i<N; i++) {
M[i] = new Array(16);
for (var j=0; j<16; j++) { // encode 4 chars per integer, big-endian encoding
M[i][j] = (msg.charCodeAt(i*64+j*4)<<24) | (msg.charCodeAt(i*64+j*4+1)<<16) |
(msg.charCodeAt(i*64+j*4+2)<<8) | (msg.charCodeAt(i*64+j*4+3));
}
}
// add length (in bits) into final pair of 32-bit integers (big-endian) [5.1.1]
// note: most significant word would be ((len-1)*8 >>> 32, but since JS converts
// bitwise-op args to 32 bits, we need to simulate this by arithmetic operators
M[N-1][14] = ((msg.length-1)*8) / Math.pow(2, 32); M[N-1][14] = Math.floor(M[N-1][14])
M[N-1][15] = ((msg.length-1)*8) & 0xffffffff;
// set initial hash value [§5.3.1]
var H0 = 0x67452301;
var H1 = 0xefcdab89;
var H2 = 0x98badcfe;
var H3 = 0x10325476;
var H4 = 0xc3d2e1f0;
// HASH COMPUTATION [§6.1.2]
var W = new Array(80); var a, b, c, d, e;
for (var i=0; i<N; i++) {
// 1 - prepare message schedule 'W'
for (var t=0; t<16; t++) W[t] = M[i][t];
for (var t=16; t<80; t++) W[t] = ROTL(W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16], 1);
// 2 - initialise five working variables a, b, c, d, e with previous hash value
a = H0; b = H1; c = H2; d = H3; e = H4;
// 3 - main loop
for (var t=0; t<80; t++) {
var s = Math.floor(t/20); // seq for blocks of 'f' functions and 'K' constants
var T = (ROTL(a,5) + f(s,b,c,d) + e + K[s] + W[t]) & 0xffffffff;
e = d;
d = c;
c = ROTL(b, 30);
b = a;
a = T;
}
// 4 - compute the new intermediate hash value
H0 = (H0+a) & 0xffffffff; // note 'addition modulo 2^32'
H1 = (H1+b) & 0xffffffff;
H2 = (H2+c) & 0xffffffff;
H3 = (H3+d) & 0xffffffff;
H4 = (H4+e) & 0xffffffff;
}
return H0.toHexStr() + H1.toHexStr() + H2.toHexStr() + H3.toHexStr() + H4.toHexStr();
}
//
// function 'f' [§4.1.1]
//
function f(s, x, y, z)
{
switch (s) {
case 0: return (x & y) ^ (~x & z); // Ch()
case 1: return x ^ y ^ z; // Parity()
case 2: return (x & y) ^ (x & z) ^ (y & z); // Maj()
case 3: return x ^ y ^ z; // Parity()
}
}
//
// rotate left (circular left shift) value x by n positions [§3.2.5]
//
function ROTL(x, n)
{
return (x<<n) | (x>>>(32-n));
}
//
// extend Number class with a tailored hex-string method
// (note toString(16) is implementation-dependant, and
// in IE returns signed numbers when used on full words)
//
Number.prototype.toHexStr = function()
{
var s="", v;
for (var i=7; i>=0; i--) { v = (this>>>(i*4)) & 0xf; s += v.toString(16); }
return s;
}

Bram :
Date: February 20, 2008 @ 10:35 pm
I’ve been considering doing something similar for a project I’m working on. Only, if I were you, I’d include something like a hidden profile field to indicate whether you’re POSTing the password in cleartext or in the hashed format. That way, users without javascript can still log in, albeit less secure. Simply use a simple javascript to change the value of the hidden field for users who do have JS enabled.
Otherwise, I like it, and I’ll definitely borrow an idea or two from your setup for my project. Thanks!
Alf :
Date: February 21, 2008 @ 11:07 am
Be careful, I’m writing a second post since we discovered a miss in this approach. Your proposition to let the javascript set up the type of authentication (plain text or hash) is a good point, I will add it later.