Skip to content

Commit 4ccc934

Browse files
author
Eric Elliott
committed
Extensible hashes (breaking change)
Extensible hashes allow you to keep up with Moore's law by changing the work units required to hash passwords over time, or swapping out the hashing algorithm when security flaws are discovered. Now, each password hash contains a JSON-encoded record of: * salt * hash * hashMethod (algorithm) * iterations (or work units) * keylength This allows the verifier to verify the password, even if the format changes over time, and allows the system administrator to scan the user database for vulnerable users who need to change their passwords and upgrade to the latest password hashing standard.
1 parent 62fd4b3 commit 4ccc934

File tree

3 files changed

+80
-28
lines changed

3 files changed

+80
-28
lines changed

credential.js

+53-21
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,20 @@ var crypto = require('crypto'),
3636
* @param {[type]} salt
3737
* @param {Function} callback err, buffer
3838
*/
39-
pbkdf2 = function pbkdf2(password, salt, callback) {
39+
pbkdf2 = function pbkdf2(password, salt, iterations, keylength, callback) {
4040
crypto.pbkdf2(password, salt,
41-
this.iterations, this.keylength, function (err, buff) {
41+
iterations, keylength, function (err, hash) {
4242
if (err) {
4343
return callback(err);
4444
}
45-
callback(null, buff.toString('base64'));
45+
callback(null, new Buffer(hash).toString('base64'));
4646
});
4747
},
4848

49+
hashMethods = {
50+
pbkdf2: pbkdf2
51+
},
52+
4953
/**
5054
* createSalt(callback)
5155
*
@@ -56,8 +60,8 @@ var crypto = require('crypto'),
5660
* @param {Function} callback [description]
5761
* @return {[type]} [description]
5862
*/
59-
createSalt = function createSalt(callback) {
60-
crypto.randomBytes(this.keylength, function (err, buff) {
63+
createSalt = function createSalt(keylength, callback) {
64+
crypto.randomBytes(keylength, function (err, buff) {
6165
if (err) {
6266
return callback(err);
6367
}
@@ -77,22 +81,33 @@ var crypto = require('crypto'),
7781
*/
7882
toHash = function toHash(password,
7983
callback) {
84+
var hashMethod = this.hashMethod,
85+
keylength = this.keylength,
86+
iterations = this.iterations;
8087

8188
// Create the salt
82-
createSalt.call(this, function (err, salt) {
89+
createSalt(keylength, function (err, salt) {
8390
if (err) {
8491
return callback(err);
8592
}
8693

87-
salt = salt.toString('base64');
88-
8994
// Then create the hash
90-
pbkdf2.call(this, password, salt, function (err, hash) {
95+
hashMethods[hashMethod](password, salt,
96+
iterations, keylength,
97+
function (err, hash) {
98+
9199
if (err) {
92100
return callback(err);
93101
}
94102

95-
callback(null, salt + '$' + hash.toString('base64'));
103+
callback(null, JSON.stringify({
104+
salt: salt,
105+
hash: hash,
106+
hashMethod: hashMethod,
107+
iterations: iterations,
108+
keylength: keylength
109+
}));
110+
96111
});
97112
}.bind(this));
98113
},
@@ -117,6 +132,14 @@ var crypto = require('crypto'),
117132
return result;
118133
},
119134

135+
parseHash = function parseHash(encodedHash) {
136+
try {
137+
return JSON.parse(encodedHash);
138+
} catch (err) {
139+
return err;
140+
}
141+
},
142+
120143
/**
121144
* verify(hash, input, callback)
122145
*
@@ -129,15 +152,21 @@ var crypto = require('crypto'),
129152
* @param {Function} callback callback(err, isValid)
130153
*/
131154
verify = function verify(hash, input, callback) {
132-
var oldHash = hash,
133-
salt = hash.slice(0, 88);
155+
var storedHash = parseHash(hash);
156+
157+
if (!hashMethods[storedHash.hashMethod]) {
158+
return callback(new Error('Couldn\'t parse stored ' +
159+
'hash.'));
160+
}
161+
162+
hashMethods[storedHash.hashMethod](input, storedHash.salt, storedHash.iterations,
163+
storedHash.keylength, function (err, newHash) {
134164

135-
pbkdf2.call(this, input, salt, function (err, newHash) {
136165
var result;
137166
if (err) {
138167
return callback(err);
139168
}
140-
callback(null, constantEquals(salt + '$' + newHash, oldHash));
169+
callback(null, constantEquals(newHash, storedHash.hash));
141170
});
142171
},
143172

@@ -154,15 +183,18 @@ var crypto = require('crypto'),
154183
* @return {Object} credential object
155184
*/
156185
configure = function configure(options) {
157-
var overrides = pick(options, ['keylength', 'iterations']);
158-
mixIn(this, overrides);
186+
mixIn(this, this.defaults, options);
159187
return this;
188+
},
189+
190+
defaults = {
191+
keylength: 66,
192+
iterations: 80000,
193+
hashMethod: 'pbkdf2'
160194
};
161195

162-
module.exports = {
196+
module.exports = mixIn({}, defaults, {
163197
hash: toHash,
164198
verify: verify,
165-
configure: configure,
166-
keylength: 66,
167-
iterations: 80000
168-
};
199+
configure: configure
200+
});

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "credential",
3-
"version": "0.1.4",
3+
"version": "0.2.0",
44
"description": "Easy password hashing and verification in Node. Protects against brute force, rainbow tables, and timing attacks.",
55
"main": "credential.js",
66
"directories": {

test/credential-test.js

+26-6
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ test('hash', function (t) {
88
t.equal(typeof hash, 'string',
99
'should produce a hash string.');
1010

11-
t.equal(hash.length, 177,
12-
'should produce an 177 character hash string.');
11+
t.ok(JSON.parse(hash).hash,
12+
'should a json object representing the hash.');
1313

1414
t.end();
1515
});
@@ -50,6 +50,9 @@ test('verify with right pw', function (t) {
5050

5151
pw.hash(pass, function (err, storedHash) {
5252
pw.verify(storedHash, pass, function (err, isValid) {
53+
t.error(err,
54+
'should not cause error.');
55+
5356
t.ok(isValid,
5457
'should return true for matching password.');
5558
t.end();
@@ -58,6 +61,21 @@ test('verify with right pw', function (t) {
5861

5962
});
6063

64+
test('verify with broken stored hash', function (t) {
65+
var pass = 'foo',
66+
storedHash = 'aoeuntkh;kbanotehudil,.prcgidax$aoesnitd,riouxbx;qjkwmoeuicgr';
67+
68+
pw.verify(storedHash, pass, function (err, isValid) {
69+
70+
t.ok(err,
71+
'should cause error.');
72+
73+
t.end();
74+
});
75+
76+
});
77+
78+
6179
test('verify with wrong pw', function (t) {
6280
var pass = 'foo';
6381

@@ -73,17 +91,19 @@ test('verify with wrong pw', function (t) {
7391

7492

7593
test('overrides', function (t) {
94+
var iterations = 1;
95+
var keylength = 12;
7696
pw.configure({
77-
iterations: 1,
78-
keylength: 12
97+
iterations: iterations,
98+
keylength: keylength
7999
});
80100

81101
pw.hash('foo', function (err, hash) {
82102

83-
t.equal(pw.iterations, 1,
103+
t.equal(pw.iterations, iterations,
84104
'should allow iterations override');
85105

86-
t.equal(hash.length, 33,
106+
t.equal(JSON.parse(hash).keylength, keylength,
87107
'should allow keylength override');
88108
t.end();
89109
});

0 commit comments

Comments
 (0)