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

Commit

Permalink
Merge pull request #507 from dannycoates/verifierVersion
Browse files Browse the repository at this point in the history
refactored crypto/password.js and added verifierVersion config parameter
  • Loading branch information
dannycoates committed Jan 21, 2014
2 parents e1ad70d + 3af3136 commit 101cd84
Show file tree
Hide file tree
Showing 8 changed files with 226 additions and 175 deletions.
4 changes: 4 additions & 0 deletions config/config.js
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
Expand Up @@ -10,5 +10,6 @@
},
"log": {
"level": "trace"
}
},
"verifierVersion": 0
}
72 changes: 48 additions & 24 deletions crypto/password.js
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
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
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

0 comments on commit 101cd84

Please sign in to comment.