mirror of
https://github.com/moparisthebest/mail
synced 2024-11-25 18:32:20 -05:00
refactor to generate and store random salt for PBKDF2
This commit is contained in:
parent
cb0e974fea
commit
1eb14d1e11
@ -5,6 +5,7 @@ define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var $ = require('jquery'),
|
||||
util = require('cryptoLib/util'),
|
||||
ImapClient = require('imap-client'),
|
||||
SmtpClient = require('smtp-client'),
|
||||
EmailDAO = require('js/dao/email-dao'),
|
||||
@ -18,7 +19,7 @@ define(function(require) {
|
||||
var self = {};
|
||||
|
||||
/**
|
||||
* Start the application by loading the view templates
|
||||
* Start the application
|
||||
*/
|
||||
self.start = function(callback) {
|
||||
// are we running in native app or in browser?
|
||||
@ -32,7 +33,9 @@ define(function(require) {
|
||||
|
||||
function onDeviceReady() {
|
||||
console.log('Starting app.');
|
||||
callback();
|
||||
// init app config storage
|
||||
self._appConfigStore = new DeviceStorageDAO();
|
||||
self._appConfigStore.init('app-config', callback);
|
||||
}
|
||||
};
|
||||
|
||||
@ -62,10 +65,17 @@ define(function(require) {
|
||||
return;
|
||||
}
|
||||
|
||||
// login using the received email address
|
||||
self.login(emailAddress, password, token, function(err) {
|
||||
// send email address to sandbox
|
||||
callback(err, emailAddress);
|
||||
self.getSalt(function(err, salt) {
|
||||
if (err || !salt) {
|
||||
callback({
|
||||
errMsg: 'Error gettin salt on login!',
|
||||
err: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// login using the received email address
|
||||
self.login(emailAddress, password, salt, token, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -76,25 +86,21 @@ define(function(require) {
|
||||
* Lookup the user's email address. Check local cache if available, otherwise query google's token info api to learn the user's email address
|
||||
*/
|
||||
self.queryEmailAddress = function(token, callback) {
|
||||
var deviceStorage, key = 'emailaddress';
|
||||
var itemKey = 'emailaddress';
|
||||
|
||||
// check device storage
|
||||
deviceStorage = new DeviceStorageDAO();
|
||||
deviceStorage.init('app-config', function() {
|
||||
deviceStorage.listItems(key, 0, null, function(err, cachedItems) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
self._appConfigStore.listItems(itemKey, 0, null, function(err, cachedItems) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// do roundtrip to google api if no email address is cached yet
|
||||
if (!cachedItems || cachedItems.length < 1) {
|
||||
queryGoogleApi();
|
||||
return;
|
||||
}
|
||||
// do roundtrip to google api if no email address is cached yet
|
||||
if (!cachedItems || cachedItems.length < 1) {
|
||||
queryGoogleApi();
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, cachedItems[0]);
|
||||
});
|
||||
callback(null, cachedItems[0]);
|
||||
});
|
||||
|
||||
function queryGoogleApi() {
|
||||
@ -112,7 +118,7 @@ define(function(require) {
|
||||
}
|
||||
|
||||
// cache the email address on the device
|
||||
deviceStorage.storeList([info.email], key, function(err) {
|
||||
self._appConfigStore.storeList([info.email], itemKey, function(err) {
|
||||
callback(err, info.email);
|
||||
});
|
||||
},
|
||||
@ -126,12 +132,45 @@ define(function(require) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch a random salt from the app storage or generate a new one
|
||||
*/
|
||||
self.getSalt = function(callback) {
|
||||
var itemKey = 'salt',
|
||||
salt;
|
||||
|
||||
self._appConfigStore.listItems(itemKey, 0, null, function(err, cachedItems) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// generate random salt if non exists
|
||||
if (!cachedItems || cachedItems.length < 1) {
|
||||
salt = util.random(config.symKeySize);
|
||||
|
||||
// store the salt locally
|
||||
self._appConfigStore.storeList([salt], itemKey, function(err) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, salt);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, cachedItems[0]);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Instanciate the mail email data access object and its dependencies. Login to imap on init.
|
||||
*/
|
||||
self.login = function(userId, password, token, callback) {
|
||||
self.login = function(userId, password, salt, token, callback) {
|
||||
var auth, imapOptions, smtpOptions,
|
||||
keychain, imapClient, smtpClient, crypto, deviceStorage;
|
||||
keychain, imapClient, smtpClient, crypto, userStorage;
|
||||
|
||||
// create mail credentials objects for imap/smtp
|
||||
auth = {
|
||||
@ -159,15 +198,16 @@ define(function(require) {
|
||||
imapClient = new ImapClient(imapOptions);
|
||||
smtpClient = new SmtpClient(smtpOptions);
|
||||
crypto = new Crypto();
|
||||
deviceStorage = new DeviceStorageDAO();
|
||||
self._emailDao = new EmailDAO(keychain, imapClient, smtpClient, crypto, deviceStorage);
|
||||
userStorage = new DeviceStorageDAO();
|
||||
self._emailDao = new EmailDAO(keychain, imapClient, smtpClient, crypto, userStorage);
|
||||
|
||||
// init email dao
|
||||
var account = {
|
||||
emailAddress: userId,
|
||||
symKeySize: config.symKeySize,
|
||||
symIvSize: config.symIvSize,
|
||||
asymKeySize: config.asymKeySize
|
||||
asymKeySize: config.asymKeySize,
|
||||
salt: salt
|
||||
};
|
||||
self._emailDao.init(account, password, callback);
|
||||
};
|
||||
|
@ -4,23 +4,37 @@ define(function(require) {
|
||||
var appController = require('js/app-controller');
|
||||
|
||||
var LoginCtrl = function($scope, $location) {
|
||||
var nextPath = '/desktop';
|
||||
|
||||
if (window.chrome && chrome.identity) {
|
||||
// start the main app controller
|
||||
appController.fetchOAuthToken('passphrase', function(err) {
|
||||
// start the main app controller
|
||||
appController.start(function(err) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.chrome && chrome.identity) {
|
||||
login('passphrase', onLogin);
|
||||
return;
|
||||
}
|
||||
|
||||
onLogin();
|
||||
});
|
||||
|
||||
function login(password, callback) {
|
||||
appController.fetchOAuthToken(password, function(err) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
$location.path(nextPath);
|
||||
$scope.$apply();
|
||||
callback();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
$location.path(nextPath);
|
||||
function onLogin() {
|
||||
$location.path('/desktop');
|
||||
$scope.$apply();
|
||||
}
|
||||
};
|
||||
|
||||
return LoginCtrl;
|
||||
|
@ -15,15 +15,29 @@ define(function(require) {
|
||||
var WriteCtrl = function($scope) {
|
||||
$scope.signature = str.signature;
|
||||
|
||||
if (window.chrome && chrome.identity) {
|
||||
// start the main app controller
|
||||
appController.fetchOAuthToken('passphrase', function(err) {
|
||||
// start the main app controller
|
||||
appController.start(function(err) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.chrome && chrome.identity) {
|
||||
login('passphrase', function() {
|
||||
emailDao = appController._emailDao;
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
function login(password, callback) {
|
||||
appController.fetchOAuthToken(password, function(err) {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
emailDao = appController._emailDao;
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,7 @@ define(function(require) {
|
||||
var self = this;
|
||||
|
||||
// valdiate input
|
||||
if (!args.emailAddress || !args.keySize || !args.rsaKeySize) {
|
||||
if (!args.emailAddress || !args.keySize || !args.rsaKeySize || typeof args.password !== 'string' || !args.salt) {
|
||||
callback({
|
||||
errMsg: 'Crypto init failed. Not all args set!'
|
||||
});
|
||||
@ -41,7 +41,7 @@ define(function(require) {
|
||||
self.rsaKeySize = args.rsaKeySize;
|
||||
|
||||
// derive PBKDF2 from password in web worker thread
|
||||
self.deriveKey(args.password, self.keySize, function(err, derivedKey) {
|
||||
self.deriveKey(args.password, args.salt, self.keySize, function(err, derivedKey) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
@ -124,16 +124,17 @@ define(function(require) {
|
||||
/**
|
||||
* Do PBKDF2 key derivation in a WebWorker thread
|
||||
*/
|
||||
Crypto.prototype.deriveKey = function(password, keySize, callback) {
|
||||
Crypto.prototype.deriveKey = function(password, salt, keySize, callback) {
|
||||
startWorker({
|
||||
script: PBKDF2_WORKER,
|
||||
args: {
|
||||
password: password,
|
||||
salt: salt,
|
||||
keySize: keySize
|
||||
},
|
||||
callback: callback,
|
||||
noWorker: function() {
|
||||
return pbkdf2.getKey(password, keySize);
|
||||
return pbkdf2.getKey(password, salt, keySize);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -1,38 +1,38 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
// import web worker dependencies
|
||||
importScripts('../../lib/require.js');
|
||||
// import web worker dependencies
|
||||
importScripts('../../lib/require.js');
|
||||
|
||||
/**
|
||||
* In the web worker thread context, 'this' and 'self' can be used as a global
|
||||
* variable namespace similar to the 'window' object in the main thread
|
||||
*/
|
||||
self.onmessage = function(e) {
|
||||
// fetch dependencies via require.js
|
||||
require(['../../require-config'], function() {
|
||||
require.config({
|
||||
baseUrl: '../../lib'
|
||||
});
|
||||
/**
|
||||
* In the web worker thread context, 'this' and 'self' can be used as a global
|
||||
* variable namespace similar to the 'window' object in the main thread
|
||||
*/
|
||||
self.onmessage = function(e) {
|
||||
// fetch dependencies via require.js
|
||||
require(['../../require-config'], function() {
|
||||
require.config({
|
||||
baseUrl: '../../lib'
|
||||
});
|
||||
|
||||
require(['js/crypto/pbkdf2'], function(pbkdf2) {
|
||||
require(['js/crypto/pbkdf2'], function(pbkdf2) {
|
||||
|
||||
var i = e.data,
|
||||
key = null;
|
||||
var i = e.data,
|
||||
key = null;
|
||||
|
||||
if (i.password && i.keySize) {
|
||||
// start deriving key
|
||||
key = pbkdf2.getKey(i.password, i.keySize);
|
||||
if (i.password && i.salt && i.keySize) {
|
||||
// start deriving key
|
||||
key = pbkdf2.getKey(i.password, i.salt, i.keySize);
|
||||
|
||||
} else {
|
||||
throw 'Not all arguments for web worker crypto are defined!';
|
||||
}
|
||||
} else {
|
||||
throw 'Not all arguments for web worker crypto are defined!';
|
||||
}
|
||||
|
||||
// pass output back to main thread
|
||||
self.postMessage(key);
|
||||
// pass output back to main thread
|
||||
self.postMessage(key);
|
||||
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
}());
|
@ -2,23 +2,23 @@
|
||||
* A Wrapper for Forge's PBKDF2 function
|
||||
*/
|
||||
define(['node-forge'], function(forge) {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
var self = {};
|
||||
var self = {};
|
||||
|
||||
/**
|
||||
* PBKDF2-HMAC-SHA1 key derivation with a constant salt and 1000 iterations
|
||||
* @param password [String] The password in UTF8
|
||||
* @param keySize [Number] The key size in bits
|
||||
* @return [String] The base64 encoded key
|
||||
*/
|
||||
self.getKey = function(password, keySize) {
|
||||
var salt = forge.util.decode64("vbhmLjC+Ub6MSbhS6/CkOwxB25wvwRkSLP2DzDtYb+4=");
|
||||
var key = forge.pkcs5.pbkdf2(password, salt, 1000, keySize / 8);
|
||||
var keyBase64 = forge.util.encode64(key);
|
||||
/**
|
||||
* PBKDF2-HMAC-SHA1 key derivation with a constant salt and 1000 iterations
|
||||
* @param password [String] The password in UTF8
|
||||
* @param salt [String] The base64 encoded salt
|
||||
* @param keySize [Number] The key size in bits
|
||||
* @return [String] The base64 encoded key
|
||||
*/
|
||||
self.getKey = function(password, salt, keySize) {
|
||||
var key = forge.pkcs5.pbkdf2(password, forge.util.decode64(salt), 1000, keySize / 8);
|
||||
var keyBase64 = forge.util.encode64(key);
|
||||
|
||||
return keyBase64;
|
||||
};
|
||||
return keyBase64;
|
||||
};
|
||||
|
||||
return self;
|
||||
return self;
|
||||
});
|
@ -58,6 +58,7 @@ define(function(require) {
|
||||
self._crypto.init({
|
||||
emailAddress: emailAddress,
|
||||
password: password,
|
||||
salt: self._account.salt,
|
||||
keySize: self._account.symKeySize,
|
||||
rsaKeySize: self._account.asymKeySize,
|
||||
storedKeypair: storedKeypair
|
||||
|
@ -21,15 +21,20 @@ define(function(require) {
|
||||
afterEach(function() {});
|
||||
|
||||
describe('login', function() {
|
||||
it('should work', function(done) {
|
||||
appController.fetchOAuthToken(test.passphrase, function(err, userId) {
|
||||
expect(err).to.not.exist;
|
||||
expect(userId).to.exist;
|
||||
emailDao = appController._emailDao;
|
||||
this.timeout(20000);
|
||||
|
||||
emailDao.imapLogin(function(err) {
|
||||
it('should work', function(done) {
|
||||
appController.start(function(err) {
|
||||
expect(err).to.not.exist;
|
||||
|
||||
appController.fetchOAuthToken(test.passphrase, function(err) {
|
||||
expect(err).to.not.exist;
|
||||
done();
|
||||
emailDao = appController._emailDao;
|
||||
|
||||
emailDao.imapLogin(function(err) {
|
||||
expect(err).to.not.exist;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -15,7 +15,7 @@ define(function(require) {
|
||||
describe('App Controller unit tests', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
sinon.stub(controller, 'login', function(userId, password, token, callback) {
|
||||
sinon.stub(controller, 'login', function(userId, password, salt, token, callback) {
|
||||
controller._emailDao = sinon.createStubInstance(EmailDAO);
|
||||
callback();
|
||||
});
|
||||
@ -50,29 +50,31 @@ define(function(require) {
|
||||
});
|
||||
});
|
||||
|
||||
describe('login', function() {
|
||||
it('should work the first time', function(done) {
|
||||
// clear db
|
||||
var deviceStorage = new DeviceStorageDAO();
|
||||
deviceStorage.init('app-config', function() {
|
||||
deviceStorage.clear(function() {
|
||||
describe('fetchOAuthToken', function() {
|
||||
beforeEach(function() {
|
||||
controller._appConfigStore = sinon.createStubInstance(DeviceStorageDAO);
|
||||
});
|
||||
|
||||
// do test with fresh db
|
||||
controller.fetchOAuthToken(appControllerTest.passphrase, function(err, userId) {
|
||||
expect(err).to.not.exist;
|
||||
expect(userId).to.equal(appControllerTest.user);
|
||||
expect(window.chrome.identity.getAuthToken.calledOnce).to.be.true;
|
||||
expect($.ajax.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('should work the first time', function(done) {
|
||||
controller._appConfigStore.listItems.yields(null, []);
|
||||
controller._appConfigStore.storeList.yields();
|
||||
|
||||
controller.fetchOAuthToken(appControllerTest.passphrase, function(err) {
|
||||
expect(err).to.not.exist;
|
||||
expect(controller._appConfigStore.listItems.calledTwice).to.be.true;
|
||||
expect(controller._appConfigStore.storeList.calledTwice).to.be.true;
|
||||
expect(window.chrome.identity.getAuthToken.calledOnce).to.be.true;
|
||||
expect($.ajax.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work when the email address is cached', function(done) {
|
||||
controller.fetchOAuthToken(appControllerTest.passphrase, function(err, userId) {
|
||||
controller._appConfigStore.listItems.yields(null, ['asdf']);
|
||||
|
||||
controller.fetchOAuthToken(appControllerTest.passphrase, function(err) {
|
||||
expect(err).to.not.exist;
|
||||
expect(userId).to.equal(appControllerTest.user);
|
||||
expect(controller._appConfigStore.listItems.calledTwice).to.be.true;
|
||||
expect(window.chrome.identity.getAuthToken.calledOnce).to.be.true;
|
||||
expect($.ajax.called).to.be.false;
|
||||
done();
|
||||
|
@ -8,7 +8,8 @@ define(['js/crypto/crypto', 'cryptoLib/util', 'test/test-data'], function(Crypto
|
||||
password: 'Password',
|
||||
keySize: 128,
|
||||
ivSize: 128,
|
||||
rsaKeySize: 1024
|
||||
rsaKeySize: 1024,
|
||||
salt: util.random(128)
|
||||
};
|
||||
|
||||
var crypto;
|
||||
@ -22,6 +23,7 @@ define(['js/crypto/crypto', 'cryptoLib/util', 'test/test-data'], function(Crypto
|
||||
crypto.init({
|
||||
emailAddress: cryptoTest.user,
|
||||
password: cryptoTest.password,
|
||||
salt: cryptoTest.salt,
|
||||
keySize: cryptoTest.keySize,
|
||||
rsaKeySize: cryptoTest.rsaKeySize
|
||||
}, function(err, generatedKeypair) {
|
||||
@ -40,6 +42,7 @@ define(['js/crypto/crypto', 'cryptoLib/util', 'test/test-data'], function(Crypto
|
||||
crypto.init({
|
||||
emailAddress: cryptoTest.user,
|
||||
password: cryptoTest.password,
|
||||
salt: cryptoTest.salt,
|
||||
keySize: cryptoTest.keySize,
|
||||
rsaKeySize: cryptoTest.rsaKeySize,
|
||||
storedKeypair: cryptoTest.generatedKeypair
|
||||
@ -51,7 +54,7 @@ define(['js/crypto/crypto', 'cryptoLib/util', 'test/test-data'], function(Crypto
|
||||
});
|
||||
|
||||
asyncTest("PBKDF2 (Async/Worker)", 2, function() {
|
||||
crypto.deriveKey(cryptoTest.password, cryptoTest.keySize, function(err, key) {
|
||||
crypto.deriveKey(cryptoTest.password, cryptoTest.salt, cryptoTest.keySize, function(err, key) {
|
||||
ok(!err);
|
||||
equal(util.base642Str(key).length * 8, cryptoTest.keySize, 'Keysize ' + cryptoTest.keySize);
|
||||
|
||||
|
@ -27,6 +27,7 @@ define(['underscore', 'cryptoLib/util', 'js/crypto/crypto', 'js/dao/devicestorag
|
||||
crypto.init({
|
||||
emailAddress: devicestorageTest.user,
|
||||
password: devicestorageTest.password,
|
||||
salt: util.random(devicestorageTest.keySize),
|
||||
keySize: devicestorageTest.keySize,
|
||||
rsaKeySize: devicestorageTest.rsaKeySize
|
||||
}, function(err, generatedKeypair) {
|
||||
|
Loading…
Reference in New Issue
Block a user