refactor to generate and store random salt for PBKDF2

This commit is contained in:
Tankred Hase 2013-10-09 16:40:36 +02:00
parent cb0e974fea
commit 1eb14d1e11
11 changed files with 195 additions and 114 deletions

View File

@ -5,6 +5,7 @@ define(function(require) {
'use strict'; 'use strict';
var $ = require('jquery'), var $ = require('jquery'),
util = require('cryptoLib/util'),
ImapClient = require('imap-client'), ImapClient = require('imap-client'),
SmtpClient = require('smtp-client'), SmtpClient = require('smtp-client'),
EmailDAO = require('js/dao/email-dao'), EmailDAO = require('js/dao/email-dao'),
@ -18,7 +19,7 @@ define(function(require) {
var self = {}; var self = {};
/** /**
* Start the application by loading the view templates * Start the application
*/ */
self.start = function(callback) { self.start = function(callback) {
// are we running in native app or in browser? // are we running in native app or in browser?
@ -32,7 +33,9 @@ define(function(require) {
function onDeviceReady() { function onDeviceReady() {
console.log('Starting app.'); 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; return;
} }
// login using the received email address self.getSalt(function(err, salt) {
self.login(emailAddress, password, token, function(err) { if (err || !salt) {
// send email address to sandbox callback({
callback(err, emailAddress); 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 * 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) { self.queryEmailAddress = function(token, callback) {
var deviceStorage, key = 'emailaddress'; var itemKey = 'emailaddress';
// check device storage self._appConfigStore.listItems(itemKey, 0, null, function(err, cachedItems) {
deviceStorage = new DeviceStorageDAO(); if (err) {
deviceStorage.init('app-config', function() { callback(err);
deviceStorage.listItems(key, 0, null, function(err, cachedItems) { return;
if (err) { }
callback(err);
return;
}
// do roundtrip to google api if no email address is cached yet // do roundtrip to google api if no email address is cached yet
if (!cachedItems || cachedItems.length < 1) { if (!cachedItems || cachedItems.length < 1) {
queryGoogleApi(); queryGoogleApi();
return; return;
} }
callback(null, cachedItems[0]); callback(null, cachedItems[0]);
});
}); });
function queryGoogleApi() { function queryGoogleApi() {
@ -112,7 +118,7 @@ define(function(require) {
} }
// cache the email address on the device // 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); 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. * 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, var auth, imapOptions, smtpOptions,
keychain, imapClient, smtpClient, crypto, deviceStorage; keychain, imapClient, smtpClient, crypto, userStorage;
// create mail credentials objects for imap/smtp // create mail credentials objects for imap/smtp
auth = { auth = {
@ -159,15 +198,16 @@ define(function(require) {
imapClient = new ImapClient(imapOptions); imapClient = new ImapClient(imapOptions);
smtpClient = new SmtpClient(smtpOptions); smtpClient = new SmtpClient(smtpOptions);
crypto = new Crypto(); crypto = new Crypto();
deviceStorage = new DeviceStorageDAO(); userStorage = new DeviceStorageDAO();
self._emailDao = new EmailDAO(keychain, imapClient, smtpClient, crypto, deviceStorage); self._emailDao = new EmailDAO(keychain, imapClient, smtpClient, crypto, userStorage);
// init email dao // init email dao
var account = { var account = {
emailAddress: userId, emailAddress: userId,
symKeySize: config.symKeySize, symKeySize: config.symKeySize,
symIvSize: config.symIvSize, symIvSize: config.symIvSize,
asymKeySize: config.asymKeySize asymKeySize: config.asymKeySize,
salt: salt
}; };
self._emailDao.init(account, password, callback); self._emailDao.init(account, password, callback);
}; };

View File

@ -4,23 +4,37 @@ define(function(require) {
var appController = require('js/app-controller'); var appController = require('js/app-controller');
var LoginCtrl = function($scope, $location) { var LoginCtrl = function($scope, $location) {
var nextPath = '/desktop';
if (window.chrome && chrome.identity) { // start the main app controller
// start the main app controller appController.start(function(err) {
appController.fetchOAuthToken('passphrase', 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) { if (err) {
console.error(err); console.error(err);
return; return;
} }
$location.path(nextPath); callback();
$scope.$apply();
}); });
return;
} }
$location.path(nextPath); function onLogin() {
$location.path('/desktop');
$scope.$apply();
}
}; };
return LoginCtrl; return LoginCtrl;

View File

@ -15,15 +15,29 @@ define(function(require) {
var WriteCtrl = function($scope) { var WriteCtrl = function($scope) {
$scope.signature = str.signature; $scope.signature = str.signature;
if (window.chrome && chrome.identity) { // start the main app controller
// start the main app controller appController.start(function(err) {
appController.fetchOAuthToken('passphrase', 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) { if (err) {
console.log(err); console.error(err);
return; return;
} }
emailDao = appController._emailDao; callback();
}); });
} }

View File

@ -28,7 +28,7 @@ define(function(require) {
var self = this; var self = this;
// valdiate input // valdiate input
if (!args.emailAddress || !args.keySize || !args.rsaKeySize) { if (!args.emailAddress || !args.keySize || !args.rsaKeySize || typeof args.password !== 'string' || !args.salt) {
callback({ callback({
errMsg: 'Crypto init failed. Not all args set!' errMsg: 'Crypto init failed. Not all args set!'
}); });
@ -41,7 +41,7 @@ define(function(require) {
self.rsaKeySize = args.rsaKeySize; self.rsaKeySize = args.rsaKeySize;
// derive PBKDF2 from password in web worker thread // 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) { if (err) {
callback(err); callback(err);
return; return;
@ -124,16 +124,17 @@ define(function(require) {
/** /**
* Do PBKDF2 key derivation in a WebWorker thread * Do PBKDF2 key derivation in a WebWorker thread
*/ */
Crypto.prototype.deriveKey = function(password, keySize, callback) { Crypto.prototype.deriveKey = function(password, salt, keySize, callback) {
startWorker({ startWorker({
script: PBKDF2_WORKER, script: PBKDF2_WORKER,
args: { args: {
password: password, password: password,
salt: salt,
keySize: keySize keySize: keySize
}, },
callback: callback, callback: callback,
noWorker: function() { noWorker: function() {
return pbkdf2.getKey(password, keySize); return pbkdf2.getKey(password, salt, keySize);
} }
}); });
}; };

View File

@ -1,38 +1,38 @@
(function() { (function() {
'use strict'; 'use strict';
// import web worker dependencies // import web worker dependencies
importScripts('../../lib/require.js'); importScripts('../../lib/require.js');
/** /**
* In the web worker thread context, 'this' and 'self' can be used as a global * 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 * variable namespace similar to the 'window' object in the main thread
*/ */
self.onmessage = function(e) { self.onmessage = function(e) {
// fetch dependencies via require.js // fetch dependencies via require.js
require(['../../require-config'], function() { require(['../../require-config'], function() {
require.config({ require.config({
baseUrl: '../../lib' baseUrl: '../../lib'
}); });
require(['js/crypto/pbkdf2'], function(pbkdf2) { require(['js/crypto/pbkdf2'], function(pbkdf2) {
var i = e.data, var i = e.data,
key = null; key = null;
if (i.password && i.keySize) { if (i.password && i.salt && i.keySize) {
// start deriving key // start deriving key
key = pbkdf2.getKey(i.password, i.keySize); key = pbkdf2.getKey(i.password, i.salt, i.keySize);
} else { } else {
throw 'Not all arguments for web worker crypto are defined!'; throw 'Not all arguments for web worker crypto are defined!';
} }
// pass output back to main thread // pass output back to main thread
self.postMessage(key); self.postMessage(key);
}); });
}); });
}; };
}()); }());

View File

@ -2,23 +2,23 @@
* A Wrapper for Forge's PBKDF2 function * A Wrapper for Forge's PBKDF2 function
*/ */
define(['node-forge'], function(forge) { 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 * PBKDF2-HMAC-SHA1 key derivation with a constant salt and 1000 iterations
* @param password [String] The password in UTF8 * @param password [String] The password in UTF8
* @param keySize [Number] The key size in bits * @param salt [String] The base64 encoded salt
* @return [String] The base64 encoded key * @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="); self.getKey = function(password, salt, keySize) {
var key = forge.pkcs5.pbkdf2(password, salt, 1000, keySize / 8); var key = forge.pkcs5.pbkdf2(password, forge.util.decode64(salt), 1000, keySize / 8);
var keyBase64 = forge.util.encode64(key); var keyBase64 = forge.util.encode64(key);
return keyBase64; return keyBase64;
}; };
return self; return self;
}); });

View File

@ -58,6 +58,7 @@ define(function(require) {
self._crypto.init({ self._crypto.init({
emailAddress: emailAddress, emailAddress: emailAddress,
password: password, password: password,
salt: self._account.salt,
keySize: self._account.symKeySize, keySize: self._account.symKeySize,
rsaKeySize: self._account.asymKeySize, rsaKeySize: self._account.asymKeySize,
storedKeypair: storedKeypair storedKeypair: storedKeypair

View File

@ -21,15 +21,20 @@ define(function(require) {
afterEach(function() {}); afterEach(function() {});
describe('login', function() { describe('login', function() {
it('should work', function(done) { this.timeout(20000);
appController.fetchOAuthToken(test.passphrase, function(err, userId) {
expect(err).to.not.exist;
expect(userId).to.exist;
emailDao = appController._emailDao;
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; expect(err).to.not.exist;
done(); emailDao = appController._emailDao;
emailDao.imapLogin(function(err) {
expect(err).to.not.exist;
done();
});
}); });
}); });
}); });

View File

@ -15,7 +15,7 @@ define(function(require) {
describe('App Controller unit tests', function() { describe('App Controller unit tests', function() {
beforeEach(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); controller._emailDao = sinon.createStubInstance(EmailDAO);
callback(); callback();
}); });
@ -50,29 +50,31 @@ define(function(require) {
}); });
}); });
describe('login', function() { describe('fetchOAuthToken', function() {
it('should work the first time', function(done) { beforeEach(function() {
// clear db controller._appConfigStore = sinon.createStubInstance(DeviceStorageDAO);
var deviceStorage = new DeviceStorageDAO(); });
deviceStorage.init('app-config', function() {
deviceStorage.clear(function() {
// do test with fresh db it('should work the first time', function(done) {
controller.fetchOAuthToken(appControllerTest.passphrase, function(err, userId) { controller._appConfigStore.listItems.yields(null, []);
expect(err).to.not.exist; controller._appConfigStore.storeList.yields();
expect(userId).to.equal(appControllerTest.user);
expect(window.chrome.identity.getAuthToken.calledOnce).to.be.true; controller.fetchOAuthToken(appControllerTest.passphrase, function(err) {
expect($.ajax.calledOnce).to.be.true; expect(err).to.not.exist;
done(); 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) { 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(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(window.chrome.identity.getAuthToken.calledOnce).to.be.true;
expect($.ajax.called).to.be.false; expect($.ajax.called).to.be.false;
done(); done();

View File

@ -8,7 +8,8 @@ define(['js/crypto/crypto', 'cryptoLib/util', 'test/test-data'], function(Crypto
password: 'Password', password: 'Password',
keySize: 128, keySize: 128,
ivSize: 128, ivSize: 128,
rsaKeySize: 1024 rsaKeySize: 1024,
salt: util.random(128)
}; };
var crypto; var crypto;
@ -22,6 +23,7 @@ define(['js/crypto/crypto', 'cryptoLib/util', 'test/test-data'], function(Crypto
crypto.init({ crypto.init({
emailAddress: cryptoTest.user, emailAddress: cryptoTest.user,
password: cryptoTest.password, password: cryptoTest.password,
salt: cryptoTest.salt,
keySize: cryptoTest.keySize, keySize: cryptoTest.keySize,
rsaKeySize: cryptoTest.rsaKeySize rsaKeySize: cryptoTest.rsaKeySize
}, function(err, generatedKeypair) { }, function(err, generatedKeypair) {
@ -40,6 +42,7 @@ define(['js/crypto/crypto', 'cryptoLib/util', 'test/test-data'], function(Crypto
crypto.init({ crypto.init({
emailAddress: cryptoTest.user, emailAddress: cryptoTest.user,
password: cryptoTest.password, password: cryptoTest.password,
salt: cryptoTest.salt,
keySize: cryptoTest.keySize, keySize: cryptoTest.keySize,
rsaKeySize: cryptoTest.rsaKeySize, rsaKeySize: cryptoTest.rsaKeySize,
storedKeypair: cryptoTest.generatedKeypair storedKeypair: cryptoTest.generatedKeypair
@ -51,7 +54,7 @@ define(['js/crypto/crypto', 'cryptoLib/util', 'test/test-data'], function(Crypto
}); });
asyncTest("PBKDF2 (Async/Worker)", 2, function() { 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); ok(!err);
equal(util.base642Str(key).length * 8, cryptoTest.keySize, 'Keysize ' + cryptoTest.keySize); equal(util.base642Str(key).length * 8, cryptoTest.keySize, 'Keysize ' + cryptoTest.keySize);

View File

@ -27,6 +27,7 @@ define(['underscore', 'cryptoLib/util', 'js/crypto/crypto', 'js/dao/devicestorag
crypto.init({ crypto.init({
emailAddress: devicestorageTest.user, emailAddress: devicestorageTest.user,
password: devicestorageTest.password, password: devicestorageTest.password,
salt: util.random(devicestorageTest.keySize),
keySize: devicestorageTest.keySize, keySize: devicestorageTest.keySize,
rsaKeySize: devicestorageTest.rsaKeySize rsaKeySize: devicestorageTest.rsaKeySize
}, function(err, generatedKeypair) { }, function(err, generatedKeypair) {