Wednesday, Apr 3rd, 2013, 12:24 PM

Some time ago, I stumbled across the concept of "security by obesity" - the gist of it being that you made passwords as difficult as possible to link to specific users. I've been working on a practical implementation of this concept in node for the last couple of months off and on across a few other projects, and ended up revisiting the original blog post by Jeremy Spilman only to discover a follow-up post on the topic.

Having had to re-engineer my code in development in order to take advantage of his double-hash idea, I thought I'd share how to pull it off in node with everyone else.


As a minor note, if you choose to use scrypt over bcrypt, there's an excellent package for doing so on github, published under npm package name scrypt.

Another is here, but it seems to be poorly documented and not very well maintained. I would recommend the former over the latter.


First off, table structure. A basic MariaDB table schema would be as follows:

CREATE TABLE hashes (
    hash CHAR(32) BINARY NOT NULL,
    rsalt CHAR(29) BINARY NOT NULL,
    PRIMARY KEY (hash)
);

CREATE TABLE users (
    id INT UNSIGNED AUTO_INCREMENT NOT NULL,
    -- your own columns here...
    salt CHAR(29) BINARY NOT NULL,
    rhash CHAR(32) BINARY NOT NULL,
    PRIMARY KEY (id)
) CHARACTER SET = utf8 COLLATE = utf8_bin;

Note that, in the above, we are not using foreign keys between the users and hashes table at all. Remember, the goal is to not link users to specific hashes.

Now, for our code to handle passwords. You could use the asynchronous functions within bcrypt for everything here like I do in my own code (I use it with async.waterfall specifically), but the goal here is to illustrate how to do it as simple as possible.

var bcrypt = require('bcrypt')

// @ tl note:
// user will be an instantiated as an object fleshed out to provide user-management utilties
// and store user data in specific properties...
function user() {
    // ...
}

// @ tl note:
// good way to make yourself an easy to obtain object constant
Object.defineProperty(user, 'SALT_ROUNDS', {
    enumerable: false,
    writable: false,
    value: 10,
})

user.prototype.setPassword = function(password) {
    var self = this, salt, rsalt, hash, rhash

    // @ tl note:
    // the salt generation below will throw an exception if it fails. be prepared.
    salt = bcrypt.genSaltSync(this.SALT_ROUNDS)
    rsalt = bcrypt.genSaltSync(this.SALT_ROUNDS),

    // @ tl note:
    // salts generated, onto step 2. generating hashes.
    // we're also going to strip the salts from the hashes right off the bat.
    // this is necessary because of how bcrypt handles storage

    hash = bcrypt.hashSync(salt, password).substr(salt.length)
    rhash = bcrypt.hashSync(rsalt, password).substr(rsalt.length)
    password = ''
    // ^ as soon as you have your hashes, take appropriate measures to not leak the unhashed password data.

    // @ tl note:
    // next step is database storage.
    // to prevent tracing the hash back to the user easily, you should be storing as follows:
    //        {salt, rhash} in "users" table
    //        {rsalt, hash} in "hashes" table
}

All done and good. Now, to check passwords - query your user's table for $username, load up appropriate row. Be careful though - if the username does not exist in the database, you should still be burning CPU cycles so that an attacker can't figure out which usernames are valid and which are not (assuming that you don't provide if it's the username or the password that was incorrect); if you do not, you could be exposing yourself to a timing attack, which has been growing more and more viable in today's world. The link provided was written regarding PHP-based applications, but the information within is still very relevant.

function(err, row, passwordAttempt, callback) {
    var salt, rsalt, hash, rhash
    // assume we've just queried here, user data is loaded into $row,
    // and if the user does not exist, err is not null
    if(err) {
        // @ tl note:
        // we need to waste time here to avoid timing attacks
        // our intention is to specifically create a failing comparison
        // primarily to just burn time in the request.
        // for reference, some of our data generated would be like the below.
        // you'll want to generate your own "bum" data to use:
        //        password = 'fail'
        //        salt = '$2a$10$zdoo5iDejVtMaKTYV5G2ze'
        //        rsalt = '$2a$10$70TeMj.egmpFVAIQNcNw3.'
        //        hash = '5MDIuhVOBJsm16mhaRyKQVoRAvckijW'
        //        rhash = '5Y2ytet4BA6YtfUF5AXmNnzCpaAEUgK'
        salt = '$2a$10$zdoo5iDejVtMaKTYV5G2ze', rsalt = '$2a$10$70TeMj.egmpFVAIQNcNw3.',
            hash = salt + '5MDIuhVOBJsm16mhaRyKQVoRAvckijW', rhash = rsalt + '5Y2ytet4BA6YtfUF5AXmNnzCpaAEUgK'
        bcrypt.compareSync('nope', hash)
        bcrypt.compareSync('nope', rhash)
        // remember, don't pay any attention to the return values here. the point is to waste time.
        // it may feel wrong, but it's for the best.

        callback('invalid username or password')
        return
        // or error out somehow. handle as necessary on your own.
    }

    salt = row.salt
    hash = bcrypt.hashSync(passwordAttempt, salt).substr(salt.length)
    // ^ strip salt off the beginning, we're storing in hashes without the salt

    db.query() // ...
}

Now, take the hash, query against the database. If it's found, great! Move onto the next step. Otherwise, you'll need to run a single dummy hash attempt (again, be wary of timing attacks) and then tell the user they did something wrong.

// assume we're inheriting the scope from above function
var row, passwordAttempt, callback

// again, if err is not null, the hash doesn't exist and thus the authentication failed.
function(err, hashes, passwordAttempt) {
    var rsalt, rhash

    if(err) {
        // we'll reuse the "bum" data from above again
        rsalt = '$2a$10$70TeMj.egmpFVAIQNcNw3.', rhash = rsalt + '5Y2ytet4BA6YtfUF5AXmNnzCpaAEUgK'
        bcrypt.compareSync('nope', rhash)

        callback('invalid username or password')
        return
    }

    rsalt = hashes.rsalt
    rhash = rsalt + row.rhash
    if(!bcrypt.compare(passwordAttempt, rhash)) {
        // failed.
        callback('invalid username or password')
    } else {
        // success!
        callback(null, row)
    }
}

So, authentication is handled. We're done, right?

NO.

As explained in the first link, you need faux data to store to bloat your passwords table, making it harder to obtain discretely. You should be stretching the table as much as possible, making most of it fake hashes in order to cause as many problems with cracking your hashes as possible. We want to become an absolute jackass in this situation.

The best way we can do this is to grab npm module async and leave a script on overnight to insert as much as possible.

The code that follows is merely a suggestion - you should expand upon it yourself and ensure that no salts collide with that of any users.

var async = require('async'),
    bcrypt = require('bcrypt'),
    crypt = require('crypto')


// ------ configuration
var randByteSize = 12, // bytes of crypto.pseudoRandomBytes to use for "random password strings"
    rotateSalt = 100, // after 100 runs, generate new salts (rsalts will be regenerated every time)
    generateTarget = 10000, // ye grande total of hashes to insert.
    queueSize = 4, // change for more stuff at once
    saltRounds = 10, // salt rounds, as per previous
// ------ end configuration

var randomPW = function() {
        // using pseudoRandomBytes because we don't absolutely need random
        // using /dev/urandom is fine, we just need noise in the DB.
        return crypto.pseudoRandomBytes(randByteSize).toString('base64')
    },
    randomSalt = function() {
        var salt = bcrypt.genSaltSync(saltRounds)
        // probably should check if `salt` is already in use and try again if so.
        return salt
    }

var queue = async.queue(function(data, callback) {
        var password = randomPW(), rsalt = bcrypt.genSaltSync(saltRounds)
        hash = bcrypt.hashSync(data.salt, password).substr(data.salt.length)
        // insert {hash, rsalt} into database, then be sure to call callback() !
        callback(null)
    }, queueSize)

for(var i = 0, salt = randomSalt(), saltUses = 0; i < generateTarget; i++, saltUses++) {
    // rotate salt every so often
    if(saltUses > rotateSalt) {
        salt = randomSalt()
        saltUses = 0
    }

    // using async.queue so that we don't start any fires.
    queue.push({ salt: salt }, function(err) {
        if(err) console.error('encountered error: ' + err)
    })
}

You'll need to adapt it of course, but it's a good start.


As always, comments are welcome, with the exception of spambots; you bastard robots can go pack sand.

// non.odios.us

comments powered by Disqus