Secure authentication without SSL (part2)

by alf

21 02 2008

After chatting with my colleagues, we noticed that my CRAM can be worth than nothing.

First, I have forgotten to precise what was the goal. It doesn’t prevent from stealing session by a man in the middle. The goal was to avoid seeing password on the network (if you use the same for all websites) and to prevent replay attack. The CRAM prevent replay by asking for a response with a different salt each time but it implies that the client and the server share the password. I was mistaken with this approach.

It’s a known and good practice to store a password hash instead of plain text password since a read access to the database doesn’t allow an attacker to authenticate since he sends the password in plain text and the server makes the hash. With my CRAM, even if you only have the hash, you can authenticate. That’s the security hole. Since you can’t prevent from a man in the middle attack without SSL, the CRAM seems useless.

But we still want to prevent replay and showing password. First, let’s come back to a basic authentication : we send username and password in plain text, the server makes the hash of the password and compare it with the one in database. To prevent the plain text exchange, we will store a double hash of the password in database : sha1(sha1(password)). The client sends sha1(password) and the server apply a new sha1 before comparing. Concerning the replay, we will use the CRAM. The server sends a unique salt for each try and the client still sends sha1(password) for authentication. The client will also send sha1(salt+md5(password)) as the challenge response. Then the server can authenticate the client with sha1(password) and ensure uniqueness of the challenge with the challenge-response.This implies to store sha1(sha1(password)) and md5(password) in database.

We have the best of the two approach. A man in the middle won’t see the password in plain text and won’t be able to replay the authentication because of the CRAM. He still is able to steal the session, but as long as it is valid. If the valid user logs out, the session becomes invalid. If an attacker has access to the database, he will find md5(password) and sha1(sha1(password)) what will allow him to answer to the CRAM but not to the basic authentication (which wait for sha1(password)).

This method remains low secure but I can’t see anything better without SSL. Like Bram sugessted, I should add a basic plain text authentication by default if javascript is disabled.



Secure authentication without SSL

by alf

18 02 2008

Last 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 :

  1. The server send a unique salt, which can be used only once
  2. The client answer with its username and SHA1(salt+SHA1(password))
  3. The server retrieves user’s password SHA1 from database and do SHA1(salt,sha1_password)
  4. 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;
}