Merge pull request #1737 from eordano/fix/passphrase

Passphrase getting generated correctly
This commit is contained in:
Matias Alejo Garcia 2014-11-10 20:12:30 -03:00
commit e8a376cf31
5 changed files with 242 additions and 10 deletions

View File

@ -1,8 +1,11 @@
var request = require('request');
var cryptoUtil = require('../util/crypto');
var buffers = require('buffer');
var querystring = require('querystring');
var Identity = require('../models/Identity');
var SEPARATOR = '|';
function InsightStorage(config) {
this.type = 'DB';
this.storeUrl = config.url || 'https://test-insight.bitpay.com:443/api/email';
@ -14,10 +17,12 @@ InsightStorage.prototype.init = function () {};
InsightStorage.prototype.setCredentials = function(email, password, opts) {
this.email = email;
this.password = password;
this._cachedKey = null;
};
InsightStorage.prototype.createItem = function(name, value, callback) {
var self = this;
this.getItem(name, function(err, retrieved) {
if (err || !retrieved) {
return self.setItem(name, value, callback);
@ -27,12 +32,34 @@ InsightStorage.prototype.createItem = function(name, value, callback) {
});
};
function mayBeOldPassword(password) {
// Test for base64
return /^[a-zA-Z0-9\/=\+]+$/.test(password);
}
InsightStorage.prototype.getItem = function(name, callback) {
var key = cryptoUtil.kdf(this.password + this.email);
var secret = cryptoUtil.kdf(key, this.password);
var encodedEmail = encodeURIComponent(this.email);
var retrieveUrl = this.storeUrl + '/retrieve/' + encodedEmail;
this.request.get(retrieveUrl + '?' + querystring.encode({secret: secret, key: name}),
var passphrase = this.getPassphrase();
var self = this;
this._makeGetRequest(passphrase, name, function(err, body) {
if (err && err.indexOf('PNOTFOUND') !== -1 && mayBeOldPassword(self.password)) {
return self._brokenGetItem(name, callback);
}
return callback(err, body);
});
};
InsightStorage.prototype.getPassphrase = function() {
return cryptoUtil.hmac(this.getKey(), this.password);
};
InsightStorage.prototype._makeGetRequest = function(passphrase, key, callback) {
var authHeader = new buffers.Buffer(this.email + ':' + passphrase).toString('base64');
var retrieveUrl = this.storeUrl + '/retrieve';
this.request.get({
url: retrieveUrl + '?' + querystring.encode({key: key}),
headers: {'Authorization': authHeader}
},
function(err, response, body) {
if (err) {
return callback('Connection error');
@ -48,16 +75,69 @@ InsightStorage.prototype.getItem = function(name, callback) {
);
};
InsightStorage.prototype.setItem = function(name, value, callback) {
InsightStorage.prototype._brokenGetItem = function(name, callback) {
var passphrase = this._makeBrokenSecret();
var self = this;
this._makeGetRequest(passphrase, name, function(err, body) {
if (!err) {
return self._changePassphrase(function(err) {
if (err) {
return callback(err);
}
return callback(null, body);
});
}
return callback(err);
});
};
InsightStorage.prototype.getKey = function() {
if (!this._cachedKey) {
this._cachedKey = cryptoUtil.kdf(this.password + SEPARATOR + this.email);
}
return this._cachedKey;
};
InsightStorage.prototype._makeBrokenSecret = function() {
var key = cryptoUtil.kdf(this.password + this.email);
var secret = cryptoUtil.kdf(key, this.password);
var registerUrl = this.storeUrl + '/register';
return cryptoUtil.kdf(key, this.password);
};
InsightStorage.prototype._changePassphrase = function(callback) {
var passphrase = this._makeBrokenSecret();
var newPassphrase = this.getPassphrase();
var authHeader = new buffers.Buffer(this.email + ':' + passphrase).toString('base64');
var url = this.storeUrl + '/change_passphrase';
this.request.post({
url: url,
headers: {'Authorization': authHeader},
body: querystring.encode({
newPassphrase: newPassphrase
})
}, function(err, response, body) {
if (err) {
return callback('Connection error');
}
if (response.statusCode === 409) {
return callback('BADCREDENTIALS: Invalid username or password');
}
if (response.statusCode !== 200) {
return callback('Unable to store data on insight');
}
return callback();
});
};
InsightStorage.prototype.setItem = function(name, value, callback) {
var passphrase = this.getPassphrase();
var authHeader = new buffers.Buffer(this.email + ':' + passphrase).toString('base64');
var registerUrl = this.storeUrl + '/save';
this.request.post({
url: registerUrl,
headers: {'Authorization': authHeader},
body: querystring.encode({
key: name,
email: this.email,
secret: secret,
record: value
})
}, function(err, response, body) {

View File

@ -24,6 +24,17 @@ module.exports = {
);
},
/**
* @param {string} key
* @param {string} data
* @return {string} base64 encoded hmac
*/
hmac: function(key, data) {
return sjcl.codec.base64.fromBits(
new sjcl.misc.hmac(key, sjcl.hash.sha256).encrypt(data)
);
},
/**
* @param {string} password
* @param {string} salt - base64 encoded, defaults to 'mjuBtGybi/4='

132
test/plugin.insight.js Normal file
View File

@ -0,0 +1,132 @@
var InsightStorage = require('../js/plugins/InsightStorage');
var assert = require('assert');
var querystring = require('querystring');
describe('insight storage plugin', function() {
var requestMock = sinon.stub();
var storage = new InsightStorage({request: requestMock});
var email = 'john@doe.com';
var password = '1234';
var data = '{"random": true}';
var namespace = 'profile::0000000000000000000000000000000000000000';
var oldSecret = 'rFA+F/N+ZvKXp717zBdfCKYQ5v9Fjry0W6tautj5etIH'
+ 'KLQliZBEYXA7AXjTJ9K3DglzGWJKost3QJUCMbhM/A=='
var newSecret = '+72pwnQ/ukrXVXZ/L4vFeiykwn522uVz0J6p81TGXvI=';
var setupStorageCredentials = function() {
storage.setCredentials(email, password);
};
beforeEach(function() {
requestMock.reset();
requestMock.get = sinon.stub();
requestMock.post = sinon.stub();
setupStorageCredentials();
});
var setupForCreation = function() {
requestMock.get.onFirstCall().callsArgWith(1, 'Not found');
requestMock.post.onFirstCall().callsArgWith(1, null, {statusCode: 200});
};
it('should be able to create a namespace for storage', function(done) {
setupForCreation();
storage.createItem(namespace, data, function(err) {
assert(!err);
return done();
});
});
var setupForRetrieval = function() {
requestMock.get.onFirstCall().callsArgWith(1, null, {statusCode: 200}, data);
};
it('should be able to retrieve data in a namespace', function(done) {
setupForRetrieval();
storage.getItem(namespace, function(err, retrieved) {
assert(!err);
assert(retrieved === data);
return done();
});
});
var setupForSave = function () {
requestMock.post.onFirstCall().callsArgWith(1, null, {statusCode: 200});
};
it('should be able to overwrite data when using same password', function(done) {
setupForSave();
storage.setItem(namespace, data, function(err) {
assert(!err);
assert(requestMock.post.firstCall.args[0].url.indexOf('save') !== -1);
return done();
});
});
it('won\'t make an unnecessary request if old password can\'t work', function(done) {
storage.setCredentials(email, '!');
setupForRetrieval();
storage.getItem(namespace, function(err, retrieved) {
assert(requestMock.get.firstCall);
assert(!requestMock.get.secondCall);
return done();
});
});
it('shouldn\'t be able to create a namespace twice', function(done) {
setupForRetrieval();
storage.createItem(namespace, data, function(err) {
assert(err);
assert(requestMock.get.firstCall.args[0].url.indexOf('retrieve') !== -1);
assert(!requestMock.post.firstCall);
return done();
});
});
var setupForOldData = function() {
requestMock.get = sinon.stub();
requestMock.get.onFirstCall().callsArgWith(1, null, {statusCode: 403});
requestMock.get.onSecondCall().callsArgWith(1, null, {statusCode: 200}, data);
requestMock.post = sinon.stub();
requestMock.post.onFirstCall().callsArgWith(1, null, {statusCode: 200});
}
it('should be able to restore 0.7.2 data', function(done) {
setupForOldData();
storage.getItem(namespace, function(error, dataReturned) {
assert(!error);
done();
});
});
it('should change the remote passphrase if retrieved with 0.7.2 passphrase',
function(done) {
setupForOldData();
storage.getItem(namespace, function(error, dataReturned) {
var receivedArgs = requestMock.post.firstCall.args[0].body;
var url = requestMock.post.firstCall.args[0].url;
var args = querystring.decode(receivedArgs);
assert(url.indexOf('change_passphrase') !== -1);
assert(requestMock.post.firstCall.args[0].headers.Authorization
===
new Buffer(email + ':' + oldSecret).toString('base64'));
assert(args.newPassphrase === newSecret);
done();
});
}
);
});

View File

@ -48,6 +48,12 @@ describe('crypto utils', function() {
var phrase = cryptoUtils.kdf(t.word, t.salt, t.iterations);
phrase.should.equal(t.phrase);
});
it('should generate a passphrase from weird chars', function() {
var phrase = cryptoUtils.kdf('Pwd123!@#$%^&*(){}[]\|/?.>,<=+-_`~åéþ䲤þçæ¶');
var expected = 'CZwb5KdikvZHVsEoZUdJckAy+yyzGnd++XhyqxJXbc30'
+ 'pEoO+WqHgqBbdf0gn2wiyWZv3zymB+7L75Xnz3uSlg==';
phrase.should.equal(expected);
});
});

View File

@ -88,6 +88,9 @@ var createBundle = function(opts) {
b.require('./js/plugins/InsightStorage', {
expose: '../plugins/InsightStorage'
});
b.require('./js/plugins/InsightStorage', {
expose: '../js/plugins/InsightStorage'
});
b.require('./js/plugins/LocalStorage', {
expose: '../plugins/LocalStorage'
});