Skip to content
This repository has been archived by the owner on Apr 3, 2019. It is now read-only.

refactored crypto/password.js and added verifierVersion config parameter #507

Merged
merged 2 commits into from
Jan 21, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,10 @@ module.exports = function (fs, path, url, convict) {
passwordChangeToken: {
default: 1000 * 60 * 15
}
},
verifierVersion: {
doc: 'verifer version for new and changed passwords',
default: 1
}
})

Expand Down
3 changes: 2 additions & 1 deletion config/dev.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
},
"log": {
"level": "trace"
}
},
"verifierVersion": 0
}
72 changes: 48 additions & 24 deletions crypto/password.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,63 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

var P = require('../promise')
var hkdf = require('./hkdf')
var scrypt = require('./scrypt')
var butil = require('./butil')

function stretch(authPW, authSalt) {
return scrypt.hash(authPW, authSalt)
var hashVersions = {
0: function (authPW, authSalt) {
return P(butil.xorBuffers(authPW, authSalt))
},
1: function (authPW, authSalt) {
return scrypt.hash(authPW, authSalt, 65536, 8, 1, 32)
}
}

function verify(stretched, verifyHash) {
return hkdf(stretched, 'verifyHash', null, 32)
.then(
function (hash) {
if (!verifyHash) { return hash }
return butil.buffersAreEqual(hash, verifyHash) ? hash : false
}
)
function Password(authPW, authSalt, version) {
version = typeof(version) === 'number' ? version : 1
this.authPW = authPW
this.authSalt = authSalt
this.version = version
this.stretchPromise = hashVersions[version](authPW, authSalt)
this.verifyHashPromise = this.stretchPromise.then(hkdfVerify)
}

Password.prototype.stretchedPassword = function () {
return this.stretchPromise
}

Password.prototype.verifyHash = function () {
return this.verifyHashPromise
}

function verifyHash(authPW, authSalt) {
return stretch(authPW, authSalt)
.then(verify)
Password.prototype.matches = function (verifyHash) {
return this.verifyHash().then(
function (hash) {
return butil.buffersAreEqual(hash, verifyHash)
}
)
}

function wrapKb(stretched, wrapWrapKb) {
return hkdf(stretched, 'wrapwrapKey', null, 32)
.then(
function (wrapwrapKey) {
return butil.xorBuffers(wrapwrapKey, wrapWrapKb)
}
)
Password.prototype.unwrap = function (wrapped, context) {
context = context || 'wrapwrapKey'
return this.stretchedPassword().then(
function (stretched) {
return hkdf(stretched, context, null, 32)
.then(
function (wrapper) {
return butil.xorBuffers(wrapper, wrapped)
}
)
}
)
}
Password.prototype.wrap = Password.prototype.unwrap

function hkdfVerify(stretched) {
return hkdf(stretched, 'verifyHash', null, 32)
}


module.exports.verifyHash = verifyHash
module.exports.stretch = stretch
module.exports.verify = verify
module.exports.wrapKb = wrapKb
module.exports = Password
4 changes: 2 additions & 2 deletions crypto/scrypt.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ var scrypt = require('scrypt-hash')
* @param {Buffer} salt The salt for the hash
* @returns {Object} d.promise Deferred promise
*/
function hash(input, salt) {
function hash(input, salt, N, r, p, len) {
var d = P.defer()
scrypt(input, salt, 65536, 8, 1, 32,
scrypt(input, salt, N, r, p, len,
function (err, hash) {
return err ? d.reject(err) : d.resolve(hash.toString('hex'))
}
Expand Down
199 changes: 104 additions & 95 deletions routes/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ var validators = require('./validators')
var HEX_STRING = validators.HEX_STRING
var LAZY_EMAIL = validators.LAZY_EMAIL

var password = require('../crypto/password')
var Password = require('../crypto/password')
var butil = require('../crypto/butil')

module.exports = function (
Expand All @@ -19,6 +19,7 @@ module.exports = function (
db,
mailer,
redirectDomain,
verifierVersion,
isProduction
) {

Expand All @@ -38,7 +39,10 @@ module.exports = function (
authPW: isA.String().min(64).max(64).regex(HEX_STRING).required(),
preVerified: isA.Boolean(),
service: isA.String().max(16).alphanum().optional(),
redirectTo: isA.String().max(512).regex(validators.domainRegex(redirectDomain)).optional()
redirectTo: isA.String()
.max(512)
.regex(validators.domainRegex(redirectDomain))
.optional()
}
},
handler: function accountCreate(request) {
Expand All @@ -54,25 +58,26 @@ module.exports = function (
if (exists) {
throw error.accountExists(email)
}
}
)
.then(password.verifyHash.bind(null, authPW, authSalt))
.then(
function (verifyHash) {
return db.createAccount(
{
uid: uuid.v4('binary'),
email: email,
emailCode: crypto.randomBytes(16),
emailVerified: form.preVerified || false,
kA: crypto.randomBytes(32),
wrapWrapKb: crypto.randomBytes(32),
devices: {},
accountResetToken: null,
passwordForgotToken: null,
authSalt: authSalt,
verifierVersion: 1,
verifyHash: verifyHash
var password = new Password(authPW, authSalt, verifierVersion)
return password.verifyHash()
.then(
function (verifyHash) {
return db.createAccount(
{
uid: uuid.v4('binary'),
email: email,
emailCode: crypto.randomBytes(16),
emailVerified: form.preVerified || false,
kA: crypto.randomBytes(32),
wrapWrapKb: crypto.randomBytes(32),
devices: {},
accountResetToken: null,
passwordForgotToken: null,
authSalt: authSalt,
verifierVersion: password.version,
verifyHash: verifyHash
}
)
}
)
}
Expand Down Expand Up @@ -131,68 +136,68 @@ module.exports = function (
db.emailRecord(form.email)
.then(
function (emailRecord) {
return password.stretch(authPW, emailRecord.authSalt)
.then(
function (stretched) {
return password.verify(stretched, emailRecord.verifyHash)
.then(
function (verifyHash) {
if (!verifyHash) {
throw error.incorrectPassword(emailRecord.email)
}
return db.createSessionToken(
{
uid: emailRecord.uid,
email: emailRecord.email,
emailCode: emailRecord.emailCode,
emailVerified: emailRecord.emailVerified
}
)
}
)
.then(
function (sessionToken) {
log.security({ event: 'login-success', uid: sessionToken.uid })
log.security({ event: 'session-create' })
return sessionToken
},
function (err) {
log.security({ event: 'login-failure', err: err, email: form.email })
throw err
}
)
.then(
function (sessionToken) {
if (request.query.keys !== 'true') {
return P({
sessionToken: sessionToken
})
}
return password.wrapKb(stretched, emailRecord.wrapWrapKb)
.then(
function (wrapKb) {
return db.createKeyFetchToken(
{
uid: emailRecord.uid,
kA: emailRecord.kA,
wrapKb: wrapKb,
emailVerified: emailRecord.emailVerified
}
)
}
)
.then(
function (keyFetchToken) {
return {
sessionToken: sessionToken,
keyFetchToken: keyFetchToken
}
}
)
var password = new Password(
authPW,
emailRecord.authSalt,
emailRecord.verifierVersion
)
return password.matches(emailRecord.verifyHash)
.then(
function (match) {
if (!match) {
throw error.incorrectPassword(emailRecord.email)
}
return db.createSessionToken(
{
uid: emailRecord.uid,
email: emailRecord.email,
emailCode: emailRecord.emailCode,
emailVerified: emailRecord.emailVerified
}
)
}
)
.then(
function (sessionToken) {
log.security({ event: 'login-success', uid: sessionToken.uid })
log.security({ event: 'session-create' })
return sessionToken
},
function (err) {
log.security({ event: 'login-failure', err: err, email: form.email })
throw err
}
)
.then(
function (sessionToken) {
if (request.query.keys !== 'true') {
return P({
sessionToken: sessionToken
})
}
return password.unwrap(emailRecord.wrapWrapKb)
.then(
function (wrapKb) {
return db.createKeyFetchToken(
{
uid: emailRecord.uid,
kA: emailRecord.kA,
wrapKb: wrapKb,
emailVerified: emailRecord.emailVerified
}
)
}
)
}
)
.then(
function (keyFetchToken) {
return {
sessionToken: sessionToken,
keyFetchToken: keyFetchToken
}
}
)
}
)
}
)
.done(
Expand All @@ -201,7 +206,9 @@ module.exports = function (
{
uid: tokens.sessionToken.uid.toString('hex'),
sessionToken: tokens.sessionToken.data.toString('hex'),
keyFetchToken: tokens.keyFetchToken ? tokens.keyFetchToken.data.toString('hex') : undefined,
keyFetchToken: tokens.keyFetchToken ?
tokens.keyFetchToken.data.toString('hex')
: undefined,
verified: tokens.sessionToken.emailVerified
}
)
Expand Down Expand Up @@ -343,7 +350,10 @@ module.exports = function (
validate: {
payload: {
service: isA.String().max(16).alphanum().optional(),
redirectTo: isA.String().max(512).regex(validators.domainRegex(redirectDomain)).optional()
redirectTo: isA.String()
.max(512)
.regex(validators.domainRegex(redirectDomain))
.optional()
}
},
tags: ["account", "recovery"],
Expand Down Expand Up @@ -433,7 +443,8 @@ module.exports = function (
var accountResetToken = request.auth.credentials
var authPW = Buffer(request.payload.authPW, 'hex')
var authSalt = crypto.randomBytes(32)
return password.verifyHash(authPW, authSalt)
var password = new Password(authPW, authSalt, verifierVersion)
return password.verifyHash()
.then(
function (verifyHash) {
return db.resetAccount(
Expand Down Expand Up @@ -473,18 +484,16 @@ module.exports = function (
db.emailRecord(form.email)
.then(
function (emailRecord) {
if (!emailRecord) {
throw error.unknownAccount(form.email)
}
return password.stretch(authPW, emailRecord.authSalt)
.then(
function (stretched) {
return password.verify(stretched, emailRecord.verifyHash)
}
)
var password = new Password(
authPW,
emailRecord.authSalt,
emailRecord.verifierVersion
)

return password.matches(emailRecord.verifyHash)
.then(
function (verifyHash) {
if (!verifyHash) {
function (match) {
if (!match) {
throw error.incorrectPassword(emailRecord.email)
}
return db.deleteAccount(emailRecord)
Expand Down