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';
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);
};

View File

@ -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;

View File

@ -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();
});
}

View File

@ -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);
}
});
};

View File

@ -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);
});
});
};
});
});
};
}());

View File

@ -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;
});

View File

@ -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

View File

@ -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();
});
});
});
});

View File

@ -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();

View File

@ -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);

View File

@ -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) {