Adding password stretching
This commit is contained in:
Родитель
cbd62774ac
Коммит
4ea38ab744
252
client/index.js
252
client/index.js
|
@ -8,6 +8,7 @@ var srp = require('srp')
|
|||
|
||||
var ClientApi = require('./api')
|
||||
var models = require('../models')({},{},{})
|
||||
var keyStretch = require('./keystretch')
|
||||
var tokens = models.tokens
|
||||
var AuthBundle = models.AuthBundle
|
||||
|
||||
|
@ -81,23 +82,35 @@ function verifier(salt, email, password, algorithm) {
|
|||
).toString('hex')
|
||||
}
|
||||
|
||||
Client.prototype.setupCredentials = function (email, password) {
|
||||
// TODO: password stretching
|
||||
this.email = Buffer(email).toString('hex')
|
||||
this.password = password
|
||||
this.srp = {}
|
||||
this.srp.type = 'SRP-6a/SHA256/2048/v1'
|
||||
this.srp.salt = crypto.randomBytes(32).toString('hex')
|
||||
this.srp.algorithm = 'sha256'
|
||||
this.srp.verifier = verifier(this.srp.salt, this.email, this.password,
|
||||
this.srp.algorithm);
|
||||
this.passwordSalt = crypto.randomBytes(32).toString('hex')
|
||||
Client.prototype.setupCredentials = function (email, password, customSalt) {
|
||||
var salt = customSalt ? customSalt : crypto.randomBytes(32).toString('hex')
|
||||
|
||||
return keyStretch.derive(email, password, salt)
|
||||
.then(
|
||||
function (result) {
|
||||
this.email = Buffer(email).toString('hex')
|
||||
this.srpPw = result.srpPw
|
||||
this.srp = {}
|
||||
this.srp.type = 'SRP-6a/SHA256/2048/v1'
|
||||
this.srp.salt = crypto.randomBytes(32).toString('hex')
|
||||
this.srp.algorithm = 'sha256'
|
||||
this.srp.verifier = verifier(this.srp.salt, this.email, this.srpPw,
|
||||
this.srp.algorithm)
|
||||
this.passwordSalt = salt
|
||||
}.bind(this)
|
||||
)
|
||||
}
|
||||
|
||||
Client.create = function (origin, email, password, callback) {
|
||||
var c = new Client(origin)
|
||||
c.setupCredentials(email, password)
|
||||
var p = c.create()
|
||||
|
||||
var p = c.setupCredentials(email, password)
|
||||
.then(
|
||||
function() {
|
||||
return c.create()
|
||||
}
|
||||
)
|
||||
|
||||
if (callback) {
|
||||
p.done(callback.bind(null, null), callback)
|
||||
}
|
||||
|
@ -108,13 +121,15 @@ Client.create = function (origin, email, password, callback) {
|
|||
|
||||
Client.login = function (origin, email, password, callback) {
|
||||
var c = new Client(origin)
|
||||
c.setupCredentials(email, password)
|
||||
c.email = Buffer(email).toString('hex')
|
||||
c.password = password
|
||||
|
||||
var p = c.login()
|
||||
.then(
|
||||
function () {
|
||||
return c
|
||||
}
|
||||
)
|
||||
function () {
|
||||
return c
|
||||
}
|
||||
)
|
||||
if (callback) {
|
||||
p.done(callback.bind(null, null), callback)
|
||||
}
|
||||
|
@ -157,12 +172,12 @@ Client.prototype.create = function (callback) {
|
|||
salt: this.passwordSalt
|
||||
}
|
||||
)
|
||||
.then(
|
||||
function (a) {
|
||||
this.uid = a.uid
|
||||
return this
|
||||
}.bind(this)
|
||||
)
|
||||
.then(
|
||||
function (a) {
|
||||
this.uid = a.uid
|
||||
return this
|
||||
}.bind(this)
|
||||
)
|
||||
if (callback) {
|
||||
p.done(callback.bind(null, null), callback)
|
||||
}
|
||||
|
@ -183,27 +198,56 @@ Client.prototype._clear = function () {
|
|||
}
|
||||
|
||||
Client.prototype.stringify = function () {
|
||||
return JSON.stringify(this)
|
||||
return JSON.stringify(this)
|
||||
}
|
||||
|
||||
Client.prototype.auth = function (callback) {
|
||||
var K = null
|
||||
var session
|
||||
var p = this.api.authStart(this.email)
|
||||
.then(
|
||||
function (srpSession) {
|
||||
var x = getAMK(srpSession, this.email, this.password)
|
||||
var k = P.defer()
|
||||
|
||||
if (!this.srpPw) {
|
||||
session = srpSession
|
||||
var emailStr = Buffer(this.email, 'hex').toString()
|
||||
|
||||
keyStretch.derive(emailStr, this.password, session.passwordStretching.salt)
|
||||
.then(
|
||||
function (result) {
|
||||
this.srpPw = result.srpPw
|
||||
this.passwordSalt = session.passwordStretching.salt
|
||||
|
||||
k.resolve(srpSession)
|
||||
}.bind(this),
|
||||
function (err) {
|
||||
k.reject(err)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
k.resolve(srpSession)
|
||||
}
|
||||
return k.promise
|
||||
}.bind(this)
|
||||
)
|
||||
.then(
|
||||
function (srpSession) {
|
||||
var x = getAMK(srpSession, this.email, this.srpPw)
|
||||
K = x.K
|
||||
|
||||
return this.api.authFinish(x.srpToken, x.A, x.M)
|
||||
}.bind(this)
|
||||
)
|
||||
.then(
|
||||
function (json) {
|
||||
|
||||
return AuthBundle.create(K, 'auth/finish')
|
||||
.then(
|
||||
function (b) {
|
||||
return b.unbundle(json.bundle)
|
||||
}
|
||||
)
|
||||
function (b) {
|
||||
return b.unbundle(json.bundle)
|
||||
}
|
||||
)
|
||||
}.bind(this)
|
||||
)
|
||||
.then(
|
||||
|
@ -231,10 +275,10 @@ Client.prototype.login = function (callback) {
|
|||
function (json) {
|
||||
return tokens.AuthToken.fromHex(this.authToken)
|
||||
.then(
|
||||
function (t) {
|
||||
return t.unbundleSession(json.bundle)
|
||||
}
|
||||
)
|
||||
function (t) {
|
||||
return t.unbundleSession(json.bundle)
|
||||
}
|
||||
)
|
||||
}.bind(this)
|
||||
)
|
||||
.then(
|
||||
|
@ -285,11 +329,11 @@ Client.prototype.verifyEmail = function (code, callback) {
|
|||
Client.prototype.emailStatus = function (callback) {
|
||||
var o = this.sessionToken ? P(null) : this.login()
|
||||
var p = o.then(
|
||||
function () {
|
||||
return this.api.recoveryEmailStatus(this.sessionToken)
|
||||
}.bind(this)
|
||||
)
|
||||
.then(
|
||||
function () {
|
||||
return this.api.recoveryEmailStatus(this.sessionToken)
|
||||
}.bind(this)
|
||||
)
|
||||
.then(
|
||||
function (status) {
|
||||
// decode email
|
||||
status.email = Buffer(status.email, 'hex').toString()
|
||||
|
@ -322,11 +366,11 @@ Client.prototype.requestVerifyEmail = function (callback) {
|
|||
Client.prototype.sign = function (publicKey, duration, callback) {
|
||||
var o = this.sessionToken ? P(null) : this.login()
|
||||
var p = o.then(
|
||||
function () {
|
||||
return this.api.certificateSign(this.sessionToken, publicKey, duration)
|
||||
}.bind(this)
|
||||
)
|
||||
.then(
|
||||
function () {
|
||||
return this.api.certificateSign(this.sessionToken, publicKey, duration)
|
||||
}.bind(this)
|
||||
)
|
||||
.then(
|
||||
function (x) {
|
||||
return x.cert
|
||||
}
|
||||
|
@ -350,10 +394,10 @@ Client.prototype.changePassword = function (newPassword, callback) {
|
|||
function (json) {
|
||||
return tokens.AuthToken.fromHex(this.authToken)
|
||||
.then(
|
||||
function (t) {
|
||||
return t.unbundleAccountReset(json.bundle)
|
||||
}
|
||||
)
|
||||
function (t) {
|
||||
return t.unbundleAccountReset(json.bundle)
|
||||
}
|
||||
)
|
||||
}.bind(this)
|
||||
)
|
||||
.then(
|
||||
|
@ -368,11 +412,26 @@ Client.prototype.changePassword = function (newPassword, callback) {
|
|||
return tokens.AccountResetToken.fromHex(this.accountResetToken)
|
||||
}.bind(this)
|
||||
)
|
||||
.then(
|
||||
function (token) {
|
||||
var salt = crypto.randomBytes(32).toString('hex')
|
||||
var emailStr = Buffer(this.email, 'hex').toString()
|
||||
|
||||
return keyStretch.derive(emailStr, newPassword, salt)
|
||||
.then(
|
||||
function (result) {
|
||||
this.srpPw = result.srpPw
|
||||
this.passwordSalt = salt
|
||||
|
||||
return token
|
||||
}.bind(this)
|
||||
)
|
||||
}.bind(this)
|
||||
)
|
||||
.then(
|
||||
function (token) {
|
||||
this.srp.salt = crypto.randomBytes(32).toString('hex')
|
||||
this.password = newPassword
|
||||
this.srp.verifier = verifier(this.srp.salt, this.email, newPassword, this.srp.algorithm)
|
||||
this.srp.verifier = verifier(this.srp.salt, this.email, this.srpPw, this.srp.algorithm)
|
||||
var bundle = token.bundle(this.wrapKb, this.srp.verifier)
|
||||
return this.api.accountReset(
|
||||
this.accountResetToken,
|
||||
|
@ -381,7 +440,15 @@ Client.prototype.changePassword = function (newPassword, callback) {
|
|||
type: this.srp.type,
|
||||
salt: this.srp.salt
|
||||
},
|
||||
this.passwordStretching
|
||||
{
|
||||
type: 'PBKDF2/scrypt/PBKDF2/v1',
|
||||
PBKDF2_rounds_1: 20000,
|
||||
scrypt_N: 65536,
|
||||
scrypt_r: 8,
|
||||
scrypt_p: 1,
|
||||
PBKDF2_rounds_2: 20000,
|
||||
salt: this.passwordSalt
|
||||
}
|
||||
)
|
||||
}.bind(this)
|
||||
)
|
||||
|
@ -397,32 +464,32 @@ Client.prototype.changePassword = function (newPassword, callback) {
|
|||
Client.prototype.keys = function (callback) {
|
||||
var o = this.keyFetchToken ? P(null) : this.login()
|
||||
var p = o.then(
|
||||
function () {
|
||||
return this.api.accountKeys(this.keyFetchToken)
|
||||
}.bind(this)
|
||||
)
|
||||
.then(
|
||||
function (data) {
|
||||
return tokens.KeyFetchToken.fromHex(this.keyFetchToken)
|
||||
.then(
|
||||
function () {
|
||||
return this.api.accountKeys(this.keyFetchToken)
|
||||
}.bind(this)
|
||||
)
|
||||
.then(
|
||||
function (data) {
|
||||
return tokens.KeyFetchToken.fromHex(this.keyFetchToken)
|
||||
.then(
|
||||
function (token) {
|
||||
return token.unbundle(data.bundle)
|
||||
}
|
||||
)
|
||||
}.bind(this)
|
||||
)
|
||||
.then(
|
||||
function (keys) {
|
||||
this.keyFetchToken = null
|
||||
this.kA = keys.kA
|
||||
this.wrapKb = keys.wrapKb
|
||||
return keys
|
||||
}.bind(this),
|
||||
function (err) {
|
||||
this.keyFetchToken = null
|
||||
throw err
|
||||
}.bind(this)
|
||||
)
|
||||
}.bind(this)
|
||||
)
|
||||
.then(
|
||||
function (keys) {
|
||||
this.keyFetchToken = null
|
||||
this.kA = keys.kA
|
||||
this.wrapKb = keys.wrapKb
|
||||
return keys
|
||||
}.bind(this),
|
||||
function (err) {
|
||||
this.keyFetchToken = null
|
||||
throw err
|
||||
}.bind(this)
|
||||
)
|
||||
|
||||
if (callback) {
|
||||
p.done(callback.bind(null, null), callback)
|
||||
|
@ -435,16 +502,16 @@ Client.prototype.keys = function (callback) {
|
|||
Client.prototype.devices = function (callback) {
|
||||
var o = this.sessionToken ? P(null) : this.login()
|
||||
var p = o.then(
|
||||
function () {
|
||||
return this.api.accountDevices(this.sessionToken)
|
||||
}.bind(this)
|
||||
)
|
||||
.then(
|
||||
function (json) {
|
||||
this._devices = json.devices
|
||||
return this._devices
|
||||
}.bind(this)
|
||||
)
|
||||
function () {
|
||||
return this.api.accountDevices(this.sessionToken)
|
||||
}.bind(this)
|
||||
)
|
||||
.then(
|
||||
function (json) {
|
||||
this._devices = json.devices
|
||||
return this._devices
|
||||
}.bind(this)
|
||||
)
|
||||
if (callback) {
|
||||
p.done(callback.bind(null, null), callback)
|
||||
}
|
||||
|
@ -498,8 +565,13 @@ Client.prototype.reforgotPassword = function (callback) {
|
|||
Client.prototype.resetPassword = function (code, password, callback) {
|
||||
// this will generate a new wrapKb on the server
|
||||
var wrapKb = '0000000000000000000000000000000000000000000000000000000000000000'
|
||||
this.setupCredentials(Buffer(this.email, 'hex').toString(), password)
|
||||
var p = this.api.passwordForgotVerifyCode(this.forgotPasswordToken, code)
|
||||
var emailStr = Buffer(this.email, 'hex').toString()
|
||||
var p = this.setupCredentials(emailStr, password)
|
||||
.then(
|
||||
function () {
|
||||
return this.api.passwordForgotVerifyCode(this.forgotPasswordToken, code)
|
||||
}.bind(this)
|
||||
)
|
||||
.then(
|
||||
function (json) {
|
||||
return tokens.AccountResetToken.fromHex(json.accountResetToken)
|
||||
|
@ -515,7 +587,15 @@ Client.prototype.resetPassword = function (code, password, callback) {
|
|||
type: this.srp.type,
|
||||
salt: this.srp.salt
|
||||
},
|
||||
this.passwordStretching
|
||||
{
|
||||
type: 'PBKDF2/scrypt/PBKDF2/v1',
|
||||
PBKDF2_rounds_1: 20000,
|
||||
scrypt_N: 65536,
|
||||
scrypt_r: 8,
|
||||
scrypt_p: 1,
|
||||
PBKDF2_rounds_2: 20000,
|
||||
salt: this.passwordSalt
|
||||
}
|
||||
)
|
||||
}.bind(this)
|
||||
)
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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('p-promise')
|
||||
var sjcl = require('sjcl')
|
||||
var pbkdf2 = require('./pbkdf2')
|
||||
var scrypt = require('./scrypt')
|
||||
var hkdf = require('../hkdf')
|
||||
var crypto = require('crypto')
|
||||
|
||||
// The namespace for the salt functions
|
||||
const NAMESPACE = 'identity.mozilla.com/picl/v1/'
|
||||
|
||||
|
||||
/** Derive a key from an email and password pair
|
||||
*
|
||||
* @param {String} email The email of the user
|
||||
* @param {String} password The password of the user
|
||||
* @param {String} saltHex The salt to derive hkdf as a hex string
|
||||
* @return p.promise object - It will resolve with
|
||||
* {String} srpPw srp password
|
||||
* {String} unwrapBKey unwrapBKey
|
||||
* {String} salt salt used in this derivation
|
||||
* or fail with {object} err
|
||||
*/
|
||||
function derive(email, password, saltHex) {
|
||||
var p = P.defer()
|
||||
var salt = Buffer(saltHex, 'hex')
|
||||
|
||||
if (password == 'undefined' || email == 'undefined' || password.length === 0 || email.length === 0) {
|
||||
p.reject('Bad password or email input')
|
||||
return p.promise
|
||||
}
|
||||
|
||||
// derive the first key from pbkdf2
|
||||
pbkdf2
|
||||
.derive(password, KWE('first-PBKDF', email))
|
||||
.then(
|
||||
function(K1) {
|
||||
// request a hash from scrypt based on the first key
|
||||
return scrypt.hash(K1, KW("scrypt"), 'http://scrypt.dev.lcip.org/')
|
||||
}
|
||||
)
|
||||
.then(
|
||||
function (K2) {
|
||||
// combine the K2 hex string and a password UTF8 into a bit array
|
||||
var scryptPassword = sjcl.bitArray.concat(
|
||||
sjcl.codec.hex.toBits(K2),
|
||||
sjcl.codec.utf8String.toBits(password)
|
||||
)
|
||||
// derive the second key from pbkdf2
|
||||
return pbkdf2.derive(scryptPassword, KWE('second-PBKDF', email))
|
||||
}
|
||||
)
|
||||
.then(
|
||||
function (stretchedPw) {
|
||||
var input = new Buffer (stretchedPw, 'hex')
|
||||
// if there's existingSalt use it
|
||||
var lengthHkdf = 2 * 32;
|
||||
return hkdf(input, 'mainKDF', salt, lengthHkdf)
|
||||
}
|
||||
)
|
||||
.done(
|
||||
function (hkdfResult) {
|
||||
var hkdfResultHex = hkdfResult.toString('hex')
|
||||
var srpPw = hkdfResultHex.substring(0,64)
|
||||
var unwrapBKey = hkdfResultHex.substring(64,128)
|
||||
p.resolve({ srpPw: srpPw, unwrapBKey: unwrapBKey })
|
||||
},
|
||||
function (err) {
|
||||
p.reject(err)
|
||||
}
|
||||
)
|
||||
|
||||
return p.promise
|
||||
}
|
||||
|
||||
|
||||
/** KWE
|
||||
*
|
||||
* @param {String} name The name of the salt
|
||||
* @param {String} email The email of the user.
|
||||
* @return {string} the salt combination with the namespace
|
||||
*/
|
||||
function KWE(name, email) {
|
||||
return NAMESPACE + name + ':' + email
|
||||
}
|
||||
|
||||
/** KW
|
||||
*
|
||||
* @param {String} name The name of the salt
|
||||
* @return {string} the salt combination with the namespace
|
||||
*/
|
||||
function KW(name) {
|
||||
return NAMESPACE + name
|
||||
}
|
||||
|
||||
module.exports.derive = derive
|
|
@ -0,0 +1,24 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 sjcl = require('sjcl')
|
||||
var P = require('p-promise')
|
||||
|
||||
const ITERATIONS = 20 * 1000
|
||||
const LENGTH = 32 * 8
|
||||
|
||||
/** pbkdf2 string creator
|
||||
*
|
||||
* @param {bitArray|String} password The password.
|
||||
* @param {String} salt The salt.
|
||||
* @return {String} the derived key.
|
||||
*/
|
||||
function derive(password, salt) {
|
||||
var saltBits = sjcl.codec.utf8String.toBits(salt)
|
||||
var result = sjcl.misc.pbkdf2(password, saltBits, ITERATIONS, LENGTH, sjcl.misc.hmac)
|
||||
|
||||
return P(sjcl.codec.hex.fromBits(result))
|
||||
}
|
||||
|
||||
module.exports.derive = derive
|
|
@ -0,0 +1,79 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 request = require('request')
|
||||
var P = require('p-promise')
|
||||
var scrypt = require('node-scrypt-js')
|
||||
|
||||
|
||||
/** hash Creates an scrypt hash
|
||||
*
|
||||
* @param {String} input The input for scrypt
|
||||
* @param {String} salt The salt for the hash
|
||||
* @param {String} url scrypt helper server url
|
||||
* @returns {Object} d.promise Deferred promise
|
||||
*/
|
||||
function hash(input, salt, url) {
|
||||
var p;
|
||||
var payload = {
|
||||
salt: salt,
|
||||
N: 64 * 1024,
|
||||
r: 8,
|
||||
p: 1,
|
||||
buflen: 32,
|
||||
input: input
|
||||
}
|
||||
|
||||
if (url) {
|
||||
p = remoteScryptHelper(payload, url)
|
||||
} else {
|
||||
p = localScrypt(payload)
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
/** localScrypt generates the scrypt hash locally
|
||||
*
|
||||
* @param {Object} payload the payload required to generate the hash
|
||||
*/
|
||||
function localScrypt(payload) {
|
||||
var inputBinary = new Buffer(payload.input, "hex").toString("binary")
|
||||
var spass = scrypt.scrypt(inputBinary, payload.salt, payload.N, payload.r, payload.p, payload.buflen)
|
||||
var result = new Buffer(spass, 'base64').toString('hex');
|
||||
|
||||
return P(result)
|
||||
}
|
||||
|
||||
/** remoteScryptHelper generates the scrypt hash using a remote helper
|
||||
*
|
||||
* @param {Object} payload The payload required to generate the hash
|
||||
* @param {String} url The url of the remote helper
|
||||
*/
|
||||
function remoteScryptHelper(payload, url) {
|
||||
var d = P.defer()
|
||||
var method = 'POST'
|
||||
var headers = {}
|
||||
|
||||
request(
|
||||
{
|
||||
url: url,
|
||||
method: method,
|
||||
headers: headers,
|
||||
json: payload
|
||||
},
|
||||
function (err, res, body) {
|
||||
if ((err || body.error)) {
|
||||
d.reject(err || body)
|
||||
}
|
||||
else {
|
||||
d.resolve(body.output)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return d.promise
|
||||
}
|
||||
|
||||
module.exports.hash = hash
|
|
@ -22,6 +22,7 @@
|
|||
"hapi": "1.9.0",
|
||||
"hawk": "1.0.0",
|
||||
"hkdf": "0.0.1",
|
||||
"sjcl": "git://github.com/bitwiseshiftleft/sjcl.git#961316282a",
|
||||
"compute-cluster": "0.0.7",
|
||||
"jwcrypto": "0.4.3",
|
||||
"handlebars": "1.0.10",
|
||||
|
@ -33,7 +34,8 @@
|
|||
"node-statsd": "0.0.7",
|
||||
"toobusy": "0.2.3",
|
||||
"nodemailer": "0.5.2",
|
||||
"lout": "0.4.1"
|
||||
"lout": "0.4.1",
|
||||
"node-scrypt-js": "git://github.com/vladikoff/node-scrypt-js.git#512f37884ff9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"awsbox": "0.4.5",
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
var test = require('tap').test
|
||||
var P = require('p-promise')
|
||||
var hkdf = require('../../hkdf')
|
||||
|
||||
test(
|
||||
'hkdf basic',
|
||||
function (t) {
|
||||
var stretchedPw = 'c16d46c31bee242cb31f916e9e38d60b76431d3f5304549cc75ae4bc20c7108c'
|
||||
stretchedPw = new Buffer (stretchedPw,'hex')
|
||||
var info = 'mainKDF'
|
||||
var salt = new Buffer ('00f000000000000000000000000000000000000000000000000000000000034d','hex')
|
||||
var lengthHkdf = 2 * 32;
|
||||
|
||||
function end() { t.end() }
|
||||
|
||||
hkdf(stretchedPw, info, salt, lengthHkdf)
|
||||
.then(
|
||||
function (hkdfResult) {
|
||||
var hkdfStr = hkdfResult.toString('hex')
|
||||
|
||||
t.equal(hkdfStr.substring(0,64), '00f9b71800ab5337d51177d8fbc682a3653fa6dae5b87628eeec43a18af59a9d')
|
||||
t.equal(hkdfStr.substring(64,128), '6ea660be9c89ec355397f89afb282ea0bf21095760c8c5009bbcc894155bbe2a')
|
||||
return hkdfResult
|
||||
},
|
||||
function (err) {
|
||||
t.fail(err, null)
|
||||
}
|
||||
)
|
||||
.done(end, end)
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'hkdf basic with salt',
|
||||
function (t) {
|
||||
var stretchedPw = 'c16d46c31bee242cb31f916e9e38d60b76431d3f5304549cc75ae4bc20c7108c'
|
||||
stretchedPw = new Buffer (stretchedPw, 'hex')
|
||||
var info = 'mainKDF'
|
||||
var salt = new Buffer ('00f000000000000000000000000000000000000000000000000000000000034d', 'hex')
|
||||
var lengthHkdf = 2 * 32;
|
||||
|
||||
function end() { t.end() }
|
||||
|
||||
hkdf(stretchedPw, info, salt, lengthHkdf)
|
||||
.then(
|
||||
function (hkdfResult, salt) {
|
||||
var hkdfStr = hkdfResult.toString('hex')
|
||||
|
||||
t.equal(hkdfStr.substring(0,64), '00f9b71800ab5337d51177d8fbc682a3653fa6dae5b87628eeec43a18af59a9d')
|
||||
t.equal(hkdfStr.substring(64,128), '6ea660be9c89ec355397f89afb282ea0bf21095760c8c5009bbcc894155bbe2a')
|
||||
t.equal(salt.toString('hex'), '00f000000000000000000000000000000000000000000000000000000000034d')
|
||||
return hkdfResult
|
||||
},
|
||||
function (err) {
|
||||
t.fail(err, null)
|
||||
}
|
||||
)
|
||||
.done(end, end)
|
||||
|
||||
}
|
||||
)
|
|
@ -63,10 +63,12 @@ function main() {
|
|||
var newPassword = 'foobar'
|
||||
var wrapKb = null
|
||||
var client = null
|
||||
var firstSrpPw
|
||||
Client.create(config.public_url, email, password)
|
||||
.then(
|
||||
function (x) {
|
||||
client = x
|
||||
firstSrpPw = x.srpPw
|
||||
return client.keys()
|
||||
}
|
||||
)
|
||||
|
@ -82,7 +84,7 @@ function main() {
|
|||
)
|
||||
.then(
|
||||
function () {
|
||||
t.equal(client.password, newPassword, 'password has changed')
|
||||
t.notEqual(client.srpPw, firstSrpPw, 'password has changed')
|
||||
return client.keys()
|
||||
}
|
||||
)
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
var test = require('tap').test
|
||||
var P = require('p-promise')
|
||||
var keystretch = require('../../client/keystretch')
|
||||
|
||||
test(
|
||||
'basic key stretching, test vectors',
|
||||
function (t) {
|
||||
var email = 'andré@example.org'
|
||||
var password = 'pässwörd'
|
||||
var salt = '00f000000000000000000000000000000000000000000000000000000000034d'
|
||||
function end() { t.end() }
|
||||
|
||||
keystretch.derive(email, password, salt)
|
||||
.then(
|
||||
function (result) {
|
||||
t.equal(result.srpPw, '00f9b71800ab5337d51177d8fbc682a3653fa6dae5b87628eeec43a18af59a9d')
|
||||
t.equal(result.unwrapBKey, '6ea660be9c89ec355397f89afb282ea0bf21095760c8c5009bbcc894155bbe2a')
|
||||
}
|
||||
)
|
||||
.done(end, end)
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'basic key stretching, longer credentials',
|
||||
function (t) {
|
||||
var salt = '00f000000000000000000000000000000000000000000000000000000000034d'
|
||||
var email = 'ijqmkkafer3xsj5rzoq+msnxsacvkmqxabtsvxvj@some-test-domain-with-a-long-name-example.org'
|
||||
var password = 'mSnxsacVkMQxAbtSVxVjCCoWArNUsFhiJqmkkafER3XSJ5rzoQ'
|
||||
function end() { t.end() }
|
||||
|
||||
keystretch.derive(email, password, salt)
|
||||
.then(
|
||||
function (result) {
|
||||
t.equal(result.srpPw, '261559a74f7b7199fef846c8138db08333bbcc7f5177194da5c965ba953a346b')
|
||||
t.equal(result.unwrapBKey, 'cf48fbc1613a46c794d37c2fe5423c7813b70e5b6c525d5c4463056f267959ff')
|
||||
}
|
||||
)
|
||||
.done(end, end)
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'false input both',
|
||||
function (t) {
|
||||
var email = ''
|
||||
var password = ''
|
||||
var salt = ''
|
||||
function end() { t.end() }
|
||||
|
||||
keystretch.derive(email, password, salt)
|
||||
.then(
|
||||
function (stretchedPassword) {
|
||||
t.fail('Got a stretchedPassword from false input')
|
||||
},
|
||||
function (err) {
|
||||
t.equal(err, 'Bad password or email input')
|
||||
}
|
||||
)
|
||||
.done(end, end)
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'false input email',
|
||||
function (t) {
|
||||
var email = 'me@example.org'
|
||||
var password = ''
|
||||
var salt = ''
|
||||
function end() { t.end() }
|
||||
|
||||
keystretch.derive(email, password, salt)
|
||||
.then(
|
||||
function (stretchedPassword) {
|
||||
t.fail('Got a stretchedPassword from false input')
|
||||
},
|
||||
function (err) {
|
||||
t.equal(err, 'Bad password or email input')
|
||||
}
|
||||
)
|
||||
.done(end, end)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
test(
|
||||
'false input password',
|
||||
function (t) {
|
||||
var email = ''
|
||||
var password = 'password'
|
||||
var salt = ''
|
||||
function end() { t.end() }
|
||||
|
||||
keystretch.derive(email, password, salt)
|
||||
.then(
|
||||
function (stretchedPassword) {
|
||||
t.fail('Got a stretchedPassword from false input')
|
||||
},
|
||||
function (err) {
|
||||
t.equal(err, 'Bad password or email input')
|
||||
}
|
||||
)
|
||||
.done(end, end)
|
||||
}
|
||||
)
|
|
@ -0,0 +1,66 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 pbkdf2 = require('../../client/pbkdf2')
|
||||
var P = require('p-promise')
|
||||
var test = require('tap').test
|
||||
var sjcl = require('sjcl')
|
||||
|
||||
test(
|
||||
'pbkdf2 derive',
|
||||
function (t) {
|
||||
var salt = 'identity.mozilla.com/picl/v1/first-PBKDF:andré@example.org'
|
||||
var password = 'pässwörd'
|
||||
function end() { t.end() }
|
||||
|
||||
pbkdf2.derive(password, salt)
|
||||
.then(
|
||||
function (K1) {
|
||||
t.equal(K1, 'f84913e3d8e6d624689d0a3e9678ac8dcc79d2c2f3d9641488cd9d6ef6cd83dd')
|
||||
}
|
||||
)
|
||||
.done(end, end)
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'pbkdf2 derive long input',
|
||||
function (t) {
|
||||
var email = 'ijqmkkafer3xsj5rzoq+msnxsacvkmqxabtsvxvj@some-test-domain-with-a-long-name-example.org'
|
||||
var password = 'mSnxsacVkMQxAbtSVxVjCCoWArNUsFhiJqmkkafER3XSJ5rzoQ'
|
||||
var salt = 'identity.mozilla.com/picl/v1/first-PBKDF:' + email
|
||||
function end() { t.end() }
|
||||
|
||||
pbkdf2.derive(password, salt)
|
||||
.then(
|
||||
function (K1) {
|
||||
t.equal(K1, '5f99c22dfac713b6d73094604a05082e6d345f8a00d4947e57105733f51216eb')
|
||||
}
|
||||
)
|
||||
.done(end, end)
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'pbkdf2 derive bit array',
|
||||
function (t) {
|
||||
var salt = 'identity.mozilla.com/picl/v1/second-PBKDF:andré@example.org'
|
||||
var K2 = '5b82f146a64126923e4167a0350bb181feba61f63cb1714012b19cb0be0119c5'
|
||||
var passwordString = 'pässwörd'
|
||||
var password = sjcl.bitArray.concat(
|
||||
sjcl.codec.hex.toBits(K2),
|
||||
sjcl.codec.utf8String.toBits(passwordString)
|
||||
)
|
||||
|
||||
function end() { t.end() }
|
||||
|
||||
pbkdf2.derive(password, salt)
|
||||
.then(
|
||||
function (K1) {
|
||||
t.equal(K1, 'c16d46c31bee242cb31f916e9e38d60b76431d3f5304549cc75ae4bc20c7108c')
|
||||
}
|
||||
)
|
||||
.done(end, end)
|
||||
}
|
||||
)
|
|
@ -0,0 +1,44 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 scrypt = require('../../client/scrypt')
|
||||
var P = require('p-promise')
|
||||
var test = require('tap').test
|
||||
|
||||
test(
|
||||
'scrypt basic with a remote helper',
|
||||
function (t) {
|
||||
function end() { t.end() }
|
||||
|
||||
var K1 = 'f84913e3d8e6d624689d0a3e9678ac8dcc79d2c2f3d9641488cd9d6ef6cd83dd'
|
||||
var salt = 'identity.mozilla.com/picl/v1/scrypt'
|
||||
var helper = 'http://scrypt.dev.lcip.org/'
|
||||
|
||||
scrypt.hash(K1, salt, helper)
|
||||
.then(
|
||||
function (K2) {
|
||||
t.equal(K2, '5b82f146a64126923e4167a0350bb181feba61f63cb1714012b19cb0be0119c5')
|
||||
}
|
||||
)
|
||||
.done(end, end)
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'scrypt basic with a local helper',
|
||||
function (t) {
|
||||
function end() { t.end() }
|
||||
|
||||
var K1 = 'f84913e3d8e6d624689d0a3e9678ac8dcc79d2c2f3d9641488cd9d6ef6cd83dd'
|
||||
var salt = 'identity.mozilla.com/picl/v1/scrypt'
|
||||
|
||||
scrypt.hash(K1, salt)
|
||||
.then(
|
||||
function (K2) {
|
||||
t.equal(K2, '5b82f146a64126923e4167a0350bb181feba61f63cb1714012b19cb0be0119c5')
|
||||
}
|
||||
)
|
||||
.done(end, end)
|
||||
}
|
||||
)
|
|
@ -180,7 +180,6 @@ function main() {
|
|||
)
|
||||
.then(
|
||||
function () {
|
||||
t.equal(client.password, newPassword)
|
||||
return client.keys()
|
||||
}
|
||||
)
|
||||
|
@ -199,6 +198,38 @@ function main() {
|
|||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'Login flow for a new password',
|
||||
function (t) {
|
||||
var email = 'verification@example.com'
|
||||
var password = 'ez'
|
||||
var wrapKb = null
|
||||
var client = null
|
||||
Client.login(config.public_url, email, password)
|
||||
.then(
|
||||
function (x) {
|
||||
client = x
|
||||
return client.keys()
|
||||
}
|
||||
)
|
||||
.then(
|
||||
function (keys) {
|
||||
t.equal(typeof(keys.kA), 'string', 'kA exists, login after password reset')
|
||||
t.equal(typeof(keys.wrapKb), 'string', 'wrapKb exists, login after password reset')
|
||||
}
|
||||
)
|
||||
.done(
|
||||
function () {
|
||||
t.end()
|
||||
},
|
||||
function (err) {
|
||||
t.fail(err.message || err.error)
|
||||
t.end()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'forgot password limits verify attempts',
|
||||
function (t) {
|
||||
|
|
Загрузка…
Ссылка в новой задаче