1
0
mirror of https://github.com/moparisthebest/mail synced 2024-11-26 02:42:17 -05:00

Implement client side key sync protocol and ui

This commit is contained in:
Tankred Hase 2014-06-13 12:33:30 +02:00
parent c890cbe71d
commit b5fda88b8a
38 changed files with 1296 additions and 212 deletions

View File

@ -25,6 +25,8 @@ define(function(require) {
*/ */
app.config = { app.config = {
cloudUrl: cloudUrl || 'https://keys.whiteout.io', cloudUrl: cloudUrl || 'https://keys.whiteout.io',
privkeyServerUrl: 'https://keychain-test.whiteout.io',
serverPrivateKeyId: 'EE342F0DDBB0F3BE',
symKeySize: 256, symKeySize: 256,
symIvSize: 96, symIvSize: 96,
asymKeySize: 2048, asymKeySize: 2048,

View File

@ -12,6 +12,7 @@ define(function(require) {
OutboxBO = require('js/bo/outbox'), OutboxBO = require('js/bo/outbox'),
mailreader = require('mailreader'), mailreader = require('mailreader'),
ImapClient = require('imap-client'), ImapClient = require('imap-client'),
Crypto = require('js/crypto/crypto'),
RestDAO = require('js/dao/rest-dao'), RestDAO = require('js/dao/rest-dao'),
EmailDAO = require('js/dao/email-dao'), EmailDAO = require('js/dao/email-dao'),
appConfig = require('js/app-config'), appConfig = require('js/app-config'),
@ -20,6 +21,7 @@ define(function(require) {
KeychainDAO = require('js/dao/keychain-dao'), KeychainDAO = require('js/dao/keychain-dao'),
PublicKeyDAO = require('js/dao/publickey-dao'), PublicKeyDAO = require('js/dao/publickey-dao'),
LawnchairDAO = require('js/dao/lawnchair-dao'), LawnchairDAO = require('js/dao/lawnchair-dao'),
PrivateKeyDAO = require('js/dao/privatekey-dao'),
InvitationDAO = require('js/dao/invitation-dao'), InvitationDAO = require('js/dao/invitation-dao'),
DeviceStorageDAO = require('js/dao/devicestorage-dao'), DeviceStorageDAO = require('js/dao/devicestorage-dao'),
UpdateHandler = require('js/util/update/update-handler'); UpdateHandler = require('js/util/update/update-handler');
@ -55,7 +57,7 @@ define(function(require) {
}; };
self.buildModules = function(options) { self.buildModules = function(options) {
var lawnchairDao, restDao, pubkeyDao, emailDao, keychain, pgp, userStorage, pgpbuilder, oauth, appConfigStore; var lawnchairDao, restDao, pubkeyDao, privkeyDao, crypto, emailDao, keychain, pgp, userStorage, pgpbuilder, oauth, appConfigStore;
// start the mailreader's worker thread // start the mailreader's worker thread
mailreader.startWorker(config.workerPath + '/../lib/mailreader-parser-worker.js'); mailreader.startWorker(config.workerPath + '/../lib/mailreader-parser-worker.js');
@ -64,9 +66,12 @@ define(function(require) {
restDao = new RestDAO(); restDao = new RestDAO();
lawnchairDao = new LawnchairDAO(); lawnchairDao = new LawnchairDAO();
pubkeyDao = new PublicKeyDAO(restDao); pubkeyDao = new PublicKeyDAO(restDao);
privkeyDao = new PrivateKeyDAO(new RestDAO(config.privkeyServerUrl));
oauth = new OAuth(new RestDAO('https://www.googleapis.com')); oauth = new OAuth(new RestDAO('https://www.googleapis.com'));
self._keychain = keychain = new KeychainDAO(lawnchairDao, pubkeyDao); crypto = new Crypto();
self._pgp = pgp = new PGP();
self._keychain = keychain = new KeychainDAO(lawnchairDao, pubkeyDao, privkeyDao, crypto, pgp);
keychain.requestPermissionForKeyUpdate = function(params, callback) { keychain.requestPermissionForKeyUpdate = function(params, callback) {
var message = params.newKey ? str.updatePublicKeyMsgNewKey : str.updatePublicKeyMsgRemovedKey; var message = params.newKey ? str.updatePublicKeyMsgNewKey : str.updatePublicKeyMsgRemovedKey;
message = message.replace('{0}', params.userId); message = message.replace('{0}', params.userId);
@ -85,7 +90,6 @@ define(function(require) {
self._auth = new Auth(appConfigStore, oauth, new RestDAO('/ca')); self._auth = new Auth(appConfigStore, oauth, new RestDAO('/ca'));
self._userStorage = userStorage = new DeviceStorageDAO(lawnchairDao); self._userStorage = userStorage = new DeviceStorageDAO(lawnchairDao);
self._invitationDao = new InvitationDAO(restDao); self._invitationDao = new InvitationDAO(restDao);
self._crypto = pgp = new PGP();
self._pgpbuilder = pgpbuilder = new PgpBuilder(); self._pgpbuilder = pgpbuilder = new PgpBuilder();
self._emailDao = emailDao = new EmailDAO(keychain, pgp, userStorage, pgpbuilder, mailreader); self._emailDao = emailDao = new EmailDAO(keychain, pgp, userStorage, pgpbuilder, mailreader);
self._outboxBo = new OutboxBO(emailDao, keychain, userStorage); self._outboxBo = new OutboxBO(emailDao, keychain, userStorage);

View File

@ -8,12 +8,14 @@ requirejs([
'js/controller/add-account', 'js/controller/add-account',
'js/controller/account', 'js/controller/account',
'js/controller/set-passphrase', 'js/controller/set-passphrase',
'js/controller/privatekey-upload',
'js/controller/contacts', 'js/controller/contacts',
'js/controller/about', 'js/controller/about',
'js/controller/login', 'js/controller/login',
'js/controller/login-initial', 'js/controller/login-initial',
'js/controller/login-new-device', 'js/controller/login-new-device',
'js/controller/login-existing', 'js/controller/login-existing',
'js/controller/login-privatekey-download',
'js/controller/mail-list', 'js/controller/mail-list',
'js/controller/read', 'js/controller/read',
'js/controller/write', 'js/controller/write',
@ -31,12 +33,14 @@ requirejs([
AddAccountCtrl, AddAccountCtrl,
AccountCtrl, AccountCtrl,
SetPassphraseCtrl, SetPassphraseCtrl,
PrivateKeyUploadCtrl,
ContactsCtrl, ContactsCtrl,
AboutCtrl, AboutCtrl,
LoginCtrl, LoginCtrl,
LoginInitialCtrl, LoginInitialCtrl,
LoginNewDeviceCtrl, LoginNewDeviceCtrl,
LoginExistingCtrl, LoginExistingCtrl,
LoginPrivateKeyDownloadCtrl,
MailListCtrl, MailListCtrl,
ReadCtrl, ReadCtrl,
WriteCtrl, WriteCtrl,
@ -89,6 +93,10 @@ requirejs([
templateUrl: 'tpl/login-new-device.html', templateUrl: 'tpl/login-new-device.html',
controller: LoginNewDeviceCtrl controller: LoginNewDeviceCtrl
}); });
$routeProvider.when('/login-privatekey-download', {
templateUrl: 'tpl/login-privatekey-download.html',
controller: LoginPrivateKeyDownloadCtrl
});
$routeProvider.when('/desktop', { $routeProvider.when('/desktop', {
templateUrl: 'tpl/desktop.html', templateUrl: 'tpl/desktop.html',
controller: NavigationCtrl controller: NavigationCtrl
@ -113,6 +121,7 @@ requirejs([
app.controller('MailListCtrl', MailListCtrl); app.controller('MailListCtrl', MailListCtrl);
app.controller('AccountCtrl', AccountCtrl); app.controller('AccountCtrl', AccountCtrl);
app.controller('SetPassphraseCtrl', SetPassphraseCtrl); app.controller('SetPassphraseCtrl', SetPassphraseCtrl);
app.controller('PrivateKeyUploadCtrl', PrivateKeyUploadCtrl);
app.controller('ContactsCtrl', ContactsCtrl); app.controller('ContactsCtrl', ContactsCtrl);
app.controller('AboutCtrl', AboutCtrl); app.controller('AboutCtrl', AboutCtrl);
app.controller('DialogCtrl', DialogCtrl); app.controller('DialogCtrl', DialogCtrl);

View File

@ -12,7 +12,7 @@ define(function(require) {
var AccountCtrl = function($scope) { var AccountCtrl = function($scope) {
userId = appController._emailDao._account.emailAddress; userId = appController._emailDao._account.emailAddress;
keychain = appController._keychain; keychain = appController._keychain;
pgp = appController._crypto; pgp = appController._pgp;
$scope.state.account = { $scope.state.account = {
toggle: function(to) { toggle: function(to) {

View File

@ -12,7 +12,7 @@ define(function(require) {
var ContactsCtrl = function($scope) { var ContactsCtrl = function($scope) {
keychain = appController._keychain, keychain = appController._keychain,
pgp = appController._crypto; pgp = appController._pgp;
$scope.state.contacts = { $scope.state.contacts = {
toggle: function(to) { toggle: function(to) {

View File

@ -6,7 +6,7 @@ define(function(require) {
var LoginExistingCtrl = function($scope, $location) { var LoginExistingCtrl = function($scope, $location) {
var emailDao = appController._emailDao, var emailDao = appController._emailDao,
pgp = appController._crypto; pgp = appController._pgp;
$scope.incorrect = false; $scope.incorrect = false;

View File

@ -0,0 +1,103 @@
define(function(require) {
'use strict';
var appController = require('js/app-controller');
var LoginPrivateKeyDownloadCtrl = function($scope, $location) {
var keychain = appController._keychain,
emailDao = appController._emailDao,
userId = emailDao._account.emailAddress;
$scope.step = 1;
$scope.verifyRecoveryToken = function(callback) {
if (!$scope.recoveryToken) {
$scope.onError(new Error('Please set the recovery token!'));
return;
}
keychain.getUserKeyPair(userId, function(err, keypair) {
if (err) {
$scope.onError(err);
return;
}
// remember for storage later
$scope.cachedKeypair = keypair;
keychain.downloadPrivateKey({
userId: userId,
keyId: keypair.publicKey._id,
recoveryToken: $scope.recoveryToken
}, function(err, encryptedPrivateKey) {
if (err) {
$scope.onError(err);
return;
}
$scope.encryptedPrivateKey = encryptedPrivateKey;
callback();
});
});
};
$scope.decryptAndStorePrivateKeyLocally = function() {
var inputCode = '' + $scope.code0 + $scope.code1 + $scope.code2 + $scope.code3 + $scope.code4 + $scope.code5;
if (!inputCode) {
$scope.onError(new Error('Please enter the keychain code!'));
return;
}
var options = $scope.encryptedPrivateKey;
options.code = inputCode.toUpperCase();
keychain.decryptAndStorePrivateKeyLocally(options, function(err, privateKey) {
if (err) {
$scope.onError(err);
return;
}
// add private key to cached keypair object
$scope.cachedKeypair.privateKey = privateKey;
// try empty passphrase
emailDao.unlock({
keypair: $scope.cachedKeypair,
passphrase: undefined
}, function(err) {
if (err) {
// go to passphrase login screen
$scope.goTo('/login-existing');
return;
}
// passphrase is corrent ... go to main app
$scope.goTo('/desktop');
});
});
};
$scope.goForward = function() {
if ($scope.step === 1) {
$scope.verifyRecoveryToken(function() {
$scope.step++;
$scope.$apply();
});
return;
}
if ($scope.step === 2) {
$scope.decryptAndStorePrivateKeyLocally();
return;
}
};
$scope.goTo = function(location) {
$location.path(location);
$scope.$apply();
};
};
return LoginPrivateKeyDownloadCtrl;
});

View File

@ -52,9 +52,28 @@ define(function(require) {
if (typeof availableKeys === 'undefined') { if (typeof availableKeys === 'undefined') {
// no public key available, start onboarding process // no public key available, start onboarding process
goTo('/login-initial'); goTo('/login-initial');
} else if (!availableKeys.privateKey) {
// no private key, import key } else if (availableKeys && !availableKeys.privateKey) {
goTo('/login-new-device'); // check if private key is synced
appController._keychain.requestPrivateKeyDownload({
userId: availableKeys.publicKey.userId,
keyId: availableKeys.publicKey._id,
}, function(err, privateKeySynced) {
if (err) {
$scope.onError(err);
return;
}
if (privateKeySynced) {
// private key is synced, proceed to download
goTo('/login-privatekey-download');
return;
}
// no private key, import key file
goTo('/login-new-device');
});
} else { } else {
// public and private key available, try empty passphrase // public and private key available, try empty passphrase
appController._emailDao.unlock({ appController._emailDao.unlock({

View File

@ -0,0 +1,170 @@
define(function(require) {
'use strict';
var appController = require('js/app-controller'),
keychain, pgp;
var PrivateKeyUploadCtrl = function($scope) {
keychain = appController._keychain;
pgp = keychain._pgp;
$scope.state.privateKeyUpload = {
toggle: function(to) {
// open lightbox
$scope.state.lightbox = (to) ? 'privatekey-upload' : undefined;
if (!to) {
return;
}
// show syncing status
$scope.step = 4;
// check if key is already synced
$scope.checkServerForKey(function(privateKeySynced) {
if (privateKeySynced) {
// close lightbox
$scope.state.lightbox = undefined;
// show message
$scope.onError({
title: 'Info',
message: 'Your PGP key has already been synced.'
});
return;
}
// show sync ui if key is not synced
$scope.displayUploadUi();
});
}
};
$scope.checkServerForKey = function(callback) {
var keyParams = pgp.getKeyParams();
keychain.requestPrivateKeyDownload({
userId: keyParams.userId,
keyId: keyParams._id,
}, function(err, privateKeySynced) {
if (err) {
$scope.onError(err);
return;
}
if (privateKeySynced) {
callback(privateKeySynced);
return;
}
callback();
});
};
$scope.displayUploadUi = function() {
// go to step 1
$scope.step = 1;
// generate new code for the user
$scope.code = $scope.generateCode();
$scope.displayedCode = $scope.code.slice(0, 4) + '-' + $scope.code.slice(4, 8) + '-' + $scope.code.slice(8, 12) + '-' + $scope.code.slice(12, 16) + '-' + $scope.code.slice(16, 20) + '-' + $scope.code.slice(20, 24);
};
$scope.generateCode = function() {
function randomString(length, chars) {
var result = '';
var randomValues = new Uint8Array(length); // get random length number of bytes
window.crypto.getRandomValues(randomValues);
for (var i = 0; i < length; i++) {
result += chars[Math.round(randomValues[i] / 255 * (chars.length - 1))];
}
return result;
}
return randomString(24, '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ');
};
$scope.verifyCode = function() {
var inputCode = '' + $scope.code0 + $scope.code1 + $scope.code2 + $scope.code3 + $scope.code4 + $scope.code5;
if (inputCode.toUpperCase() !== $scope.code) {
var err = new Error('The code does not match. Please go back and check the generated code.');
err.sync = true;
$scope.onError(err);
return false;
}
return true;
};
$scope.setDeviceName = function(callback) {
keychain.setDeviceName($scope.deviceName, callback);
};
$scope.encryptAndUploadKey = function(callback) {
var userId = appController._emailDao._account.emailAddress;
var code = $scope.code;
// register device to keychain service
keychain.registerDevice({
userId: userId
}, function(err) {
if (err) {
$scope.onError(err);
return;
}
// encrypt private PGP key using code and upload
keychain.uploadPrivateKey({
userId: userId,
code: code
}, callback);
});
};
$scope.goBack = function() {
if ($scope.step > 1) {
$scope.step--;
}
};
$scope.goForward = function() {
if ($scope.step < 2) {
$scope.step++;
return;
}
if ($scope.step === 2 && $scope.verifyCode()) {
$scope.step++;
return;
}
if ($scope.step === 3) {
// set device name to local storage
$scope.setDeviceName(function(err) {
if (err) {
$scope.onError(err);
return;
}
// show spinner
$scope.step++;
$scope.$apply();
// init key sync
$scope.encryptAndUploadKey(function(err) {
if (err) {
$scope.onError(err);
return;
}
// close sync dialog
$scope.state.privateKeyUpload.toggle(false);
// show success message
$scope.onError({
title: 'Success',
message: 'Whiteout Keychain setup successful!'
});
});
});
}
};
};
return PrivateKeyUploadCtrl;
});

View File

@ -5,7 +5,7 @@ define(function(require) {
download = require('js/util/download'), download = require('js/util/download'),
angular = require('angular'), angular = require('angular'),
str = require('js/app-config').string, str = require('js/app-config').string,
emailDao, invitationDao, outbox, crypto, keychain; emailDao, invitationDao, outbox, pgp, keychain;
// //
// Controller // Controller
@ -16,7 +16,7 @@ define(function(require) {
emailDao = appController._emailDao; emailDao = appController._emailDao;
invitationDao = appController._invitationDao; invitationDao = appController._invitationDao;
outbox = appController._outboxBo; outbox = appController._outboxBo;
crypto = appController._crypto; pgp = appController._pgp;
keychain = appController._keychain; keychain = appController._keychain;
// set default value so that the popover height is correct on init // set default value so that the popover height is correct on init
@ -47,7 +47,7 @@ define(function(require) {
return; return;
} }
var fpr = crypto.getFingerprint(pubkey.publicKey); var fpr = pgp.getFingerprint(pubkey.publicKey);
var formatted = fpr.slice(32); var formatted = fpr.slice(32);
$scope.keyId = 'PGP key: ' + formatted; $scope.keyId = 'PGP key: ' + formatted;

View File

@ -10,7 +10,7 @@ define(function(require) {
var SetPassphraseCtrl = function($scope) { var SetPassphraseCtrl = function($scope) {
keychain = appController._keychain; keychain = appController._keychain;
pgp = appController._crypto; pgp = appController._pgp;
$scope.state.setPassphrase = { $scope.state.setPassphrase = {
toggle: function(to) { toggle: function(to) {

View File

@ -7,14 +7,14 @@ define(function(require) {
aes = require('js/crypto/aes-gcm'), aes = require('js/crypto/aes-gcm'),
util = require('js/crypto/util'), util = require('js/crypto/util'),
str = require('js/app-config').string, str = require('js/app-config').string,
crypto, emailDao, outbox, keychainDao; pgp, emailDao, outbox, keychainDao;
// //
// Controller // Controller
// //
var WriteCtrl = function($scope, $filter) { var WriteCtrl = function($scope, $filter) {
crypto = appController._crypto; pgp = appController._pgp;
emailDao = appController._emailDao, emailDao = appController._emailDao,
outbox = appController._outboxBo; outbox = appController._outboxBo;
keychainDao = appController._keychain; keychainDao = appController._keychain;
@ -218,7 +218,7 @@ define(function(require) {
return; return;
} }
var fpr = crypto.getFingerprint(recipient.key.publicKey); var fpr = pgp.getFingerprint(recipient.key.publicKey);
var formatted = fpr.slice(32); var formatted = fpr.slice(32);
$scope.keyId = formatted; $scope.keyId = formatted;

View File

@ -7,7 +7,7 @@ define(['forge'], function(forge) {
var self = {}; var self = {};
/** /**
* PBKDF2-HMAC-SHA1 key derivation with a random salt and 1000 iterations * PBKDF2-HMAC-SHA256 key derivation with a random salt and 10000 iterations
* @param {String} password The password in UTF8 * @param {String} password The password in UTF8
* @param {String} salt The base64 encoded salt * @param {String} salt The base64 encoded salt
* @param {String} keySize The key size in bits * @param {String} keySize The key size in bits

View File

@ -20,9 +20,7 @@ define(function(require) {
var userId, passphrase; var userId, passphrase;
if (!util.emailRegEx.test(options.emailAddress) || !options.keySize) { if (!util.emailRegEx.test(options.emailAddress) || !options.keySize) {
callback({ callback(new Error('Crypto init failed. Not all options set!'));
errMsg: 'Crypto init failed. Not all options set!'
});
return; return;
} }
@ -38,10 +36,7 @@ define(function(require) {
function onGenerated(err, keys) { function onGenerated(err, keys) {
if (err) { if (err) {
callback({ callback(new Error('Keygeneration failed!'));
errMsg: 'Keygeneration failed!',
err: err
});
return; return;
} }
@ -146,9 +141,7 @@ define(function(require) {
// check options // check options
if (!options.privateKeyArmored || !options.publicKeyArmored) { if (!options.privateKeyArmored || !options.publicKeyArmored) {
callback({ callback(new Error('Importing keys failed. Not all options set!'));
errMsg: 'Importing keys failed. Not all options set!'
});
return; return;
} }
@ -163,18 +156,14 @@ define(function(require) {
this._privateKey = openpgp.key.readArmored(options.privateKeyArmored).keys[0]; this._privateKey = openpgp.key.readArmored(options.privateKeyArmored).keys[0];
} catch (e) { } catch (e) {
resetKeys(); resetKeys();
callback({ callback(new Error('Importing keys failed. Parsing error!'));
errMsg: 'Importing keys failed. Parsing error!'
});
return; return;
} }
// decrypt private key with passphrase // decrypt private key with passphrase
if (!this._privateKey.decrypt(options.passphrase)) { if (!this._privateKey.decrypt(options.passphrase)) {
resetKeys(); resetKeys();
callback({ callback(new Error('Incorrect passphrase!'));
errMsg: 'Incorrect passphrase!'
});
return; return;
} }
@ -183,9 +172,7 @@ define(function(require) {
privKeyId = this._privateKey.getKeyPacket().getKeyId().toHex(); privKeyId = this._privateKey.getKeyPacket().getKeyId().toHex();
if (!pubKeyId || !privKeyId || pubKeyId !== privKeyId) { if (!pubKeyId || !privKeyId || pubKeyId !== privKeyId) {
resetKeys(); resetKeys();
callback({ callback(new Error('Key IDs dont match!'));
errMsg: 'Key IDs dont match!'
});
return; return;
} }
@ -197,9 +184,7 @@ define(function(require) {
*/ */
PGP.prototype.exportKeys = function(callback) { PGP.prototype.exportKeys = function(callback) {
if (!this._publicKey || !this._privateKey) { if (!this._publicKey || !this._privateKey) {
callback({ callback(new Error('Could not export keys!'));
errMsg: 'Could not export keys!'
});
return; return;
} }
@ -220,9 +205,7 @@ define(function(require) {
newPassphrase = (options.newPassphrase) ? options.newPassphrase : undefined; newPassphrase = (options.newPassphrase) ? options.newPassphrase : undefined;
if (!options.privateKeyArmored) { if (!options.privateKeyArmored) {
callback({ callback(new Error('Private key must be specified to change passphrase!'));
errMsg: 'Private key must be specified to change passphrase!'
});
return; return;
} }
@ -236,17 +219,13 @@ define(function(require) {
try { try {
privKey = openpgp.key.readArmored(options.privateKeyArmored).keys[0]; privKey = openpgp.key.readArmored(options.privateKeyArmored).keys[0];
} catch (e) { } catch (e) {
callback({ callback(new Error('Importing key failed. Parsing error!'));
errMsg: 'Importing key failed. Parsing error!'
});
return; return;
} }
// decrypt private key with passphrase // decrypt private key with passphrase
if (!privKey.decrypt(options.oldPassphrase)) { if (!privKey.decrypt(options.oldPassphrase)) {
callback({ callback(new Error('Old passphrase incorrect!'));
errMsg: 'Old passphrase incorrect!'
});
return; return;
} }
@ -258,17 +237,13 @@ define(function(require) {
} }
newKeyArmored = privKey.armor(); newKeyArmored = privKey.armor();
} catch (e) { } catch (e) {
callback({ callback(new Error('Setting new passphrase failed!'));
errMsg: 'Setting new passphrase failed!'
});
return; return;
} }
// check if new passphrase really works // check if new passphrase really works
if (!privKey.decrypt(newPassphrase)) { if (!privKey.decrypt(newPassphrase)) {
callback({ callback(new Error('Decrypting key with new passphrase failed!'));
errMsg: 'Decrypting key with new passphrase failed!'
});
return; return;
} }
@ -283,9 +258,7 @@ define(function(require) {
// check keys // check keys
if (!this._privateKey || publicKeysArmored.length < 1) { if (!this._privateKey || publicKeysArmored.length < 1) {
callback({ callback(new Error('Error encrypting. Keys must be set!'));
errMsg: 'Error encrypting. Keys must be set!'
});
return; return;
} }
@ -295,10 +268,7 @@ define(function(require) {
publicKeys = publicKeys.concat(openpgp.key.readArmored(pubkeyArmored).keys); publicKeys = publicKeys.concat(openpgp.key.readArmored(pubkeyArmored).keys);
}); });
} catch (err) { } catch (err) {
callback({ callback(new Error('Error encrypting plaintext!'));
errMsg: 'Error encrypting plaintext!',
err: err
});
return; return;
} }
@ -314,9 +284,7 @@ define(function(require) {
// check keys // check keys
if (!this._privateKey || !publicKeyArmored) { if (!this._privateKey || !publicKeyArmored) {
callback({ callback(new Error('Error decrypting. Keys must be set!'));
errMsg: 'Error decrypting. Keys must be set!'
});
return; return;
} }
@ -325,10 +293,7 @@ define(function(require) {
publicKeys = openpgp.key.readArmored(publicKeyArmored).keys; publicKeys = openpgp.key.readArmored(publicKeyArmored).keys;
message = openpgp.message.readArmored(ciphertext); message = openpgp.message.readArmored(ciphertext);
} catch (err) { } catch (err) {
callback({ callback(new Error('Error decrypting PGP message!'));
errMsg: 'Error decrypting PGP message!',
err: err
});
return; return;
} }
@ -337,10 +302,7 @@ define(function(require) {
function onDecrypted(err, decrypted) { function onDecrypted(err, decrypted) {
if (err) { if (err) {
callback({ callback(new Error('Error decrypting PGP message!'));
errMsg: 'Error decrypting PGP message!',
err: err
});
return; return;
} }
@ -352,9 +314,7 @@ define(function(require) {
} }
}); });
if (!signaturesValid) { if (!signaturesValid) {
callback({ callback(new Error('Verifying PGP signature failed!'));
errMsg: 'Verifying PGP signature failed!'
});
return; return;
} }

View File

@ -11,14 +11,14 @@ define(function(require) {
* PGP de-/encryption, receiving via IMAP, sending via SMTP, MIME parsing, local db persistence * PGP de-/encryption, receiving via IMAP, sending via SMTP, MIME parsing, local db persistence
* *
* @param {Object} keychain The keychain DAO handles keys transparently * @param {Object} keychain The keychain DAO handles keys transparently
* @param {Object} crypto Orchestrates decryption * @param {Object} pgp Orchestrates decryption
* @param {Object} devicestorage Handles persistence to the local indexed db * @param {Object} devicestorage Handles persistence to the local indexed db
* @param {Object} pgpbuilder Generates and encrypts MIME and SMTP messages * @param {Object} pgpbuilder Generates and encrypts MIME and SMTP messages
* @param {Object} mailreader Parses MIME messages received from IMAP * @param {Object} mailreader Parses MIME messages received from IMAP
*/ */
var EmailDAO = function(keychain, crypto, devicestorage, pgpbuilder, mailreader) { var EmailDAO = function(keychain, pgp, devicestorage, pgpbuilder, mailreader) {
this._keychain = keychain; this._keychain = keychain;
this._crypto = crypto; this._pgp = pgp;
this._devicestorage = devicestorage; this._devicestorage = devicestorage;
this._pgpbuilder = pgpbuilder; this._pgpbuilder = pgpbuilder;
this._mailreader = mailreader; this._mailreader = mailreader;
@ -105,7 +105,7 @@ define(function(require) {
} }
// no keypair for is stored for the user... generate a new one // no keypair for is stored for the user... generate a new one
self._crypto.generateKeys({ self._pgp.generateKeys({
emailAddress: self._account.emailAddress, emailAddress: self._account.emailAddress,
keySize: self._account.asymKeySize, keySize: self._account.asymKeySize,
passphrase: options.passphrase passphrase: options.passphrase
@ -121,8 +121,8 @@ define(function(require) {
function handleExistingKeypair(keypair) { function handleExistingKeypair(keypair) {
var privKeyParams, pubKeyParams; var privKeyParams, pubKeyParams;
try { try {
privKeyParams = self._crypto.getKeyParams(keypair.privateKey.encryptedKey); privKeyParams = self._pgp.getKeyParams(keypair.privateKey.encryptedKey);
pubKeyParams = self._crypto.getKeyParams(keypair.publicKey.publicKey); pubKeyParams = self._pgp.getKeyParams(keypair.publicKey.publicKey);
} catch (e) { } catch (e) {
callback(new Error('Error reading key params!')); callback(new Error('Error reading key params!'));
return; return;
@ -148,7 +148,7 @@ define(function(require) {
} }
// import existing key pair into crypto module // import existing key pair into crypto module
self._crypto.importKeys({ self._pgp.importKeys({
passphrase: options.passphrase, passphrase: options.passphrase,
privateKeyArmored: keypair.privateKey.encryptedKey, privateKeyArmored: keypair.privateKey.encryptedKey,
publicKeyArmored: keypair.publicKey.publicKey publicKeyArmored: keypair.publicKey.publicKey
@ -159,14 +159,14 @@ define(function(require) {
} }
// set decrypted privateKey to pgpMailer // set decrypted privateKey to pgpMailer
self._pgpbuilder._privateKey = self._crypto._privateKey; self._pgpbuilder._privateKey = self._pgp._privateKey;
callback(); callback();
}); });
} }
function handleGenerated(generatedKeypair) { function handleGenerated(generatedKeypair) {
// import the new key pair into crypto module // import the new key pair into crypto module
self._crypto.importKeys({ self._pgp.importKeys({
passphrase: options.passphrase, passphrase: options.passphrase,
privateKeyArmored: generatedKeypair.privateKeyArmored, privateKeyArmored: generatedKeypair.privateKeyArmored,
publicKeyArmored: generatedKeypair.publicKeyArmored publicKeyArmored: generatedKeypair.publicKeyArmored
@ -196,7 +196,7 @@ define(function(require) {
} }
// set decrypted privateKey to pgpMailer // set decrypted privateKey to pgpMailer
self._pgpbuilder._privateKey = self._crypto._privateKey; self._pgpbuilder._privateKey = self._pgp._privateKey;
callback(); callback();
}); });
}); });
@ -816,7 +816,7 @@ define(function(require) {
// get the receiver's public key to check the message signature // get the receiver's public key to check the message signature
var encryptedNode = filterBodyParts(message.bodyParts, 'encrypted')[0]; var encryptedNode = filterBodyParts(message.bodyParts, 'encrypted')[0];
self._crypto.decrypt(encryptedNode.content, senderPublicKey.publicKey, function(err, decrypted) { self._pgp.decrypt(encryptedNode.content, senderPublicKey.publicKey, function(err, decrypted) {
if (err || !decrypted) { if (err || !decrypted) {
showError(err.errMsg || err.message || 'An error occurred during the decryption.'); showError(err.errMsg || err.message || 'An error occurred during the decryption.');
return; return;

View File

@ -258,6 +258,11 @@ define(function(require) {
* @param {Function} callback(error) * @param {Function} callback(error)
*/ */
KeychainDAO.prototype.setDeviceName = function(deviceName, callback) { KeychainDAO.prototype.setDeviceName = function(deviceName, callback) {
if (!deviceName) {
callback(new Error('Please set a device name!'));
return;
}
this._localDbDao.persist(DB_DEVICENAME, deviceName, callback); this._localDbDao.persist(DB_DEVICENAME, deviceName, callback);
}; };
@ -323,7 +328,8 @@ define(function(require) {
* @param {Function} callback(error) * @param {Function} callback(error)
*/ */
KeychainDAO.prototype.registerDevice = function(options, callback) { KeychainDAO.prototype.registerDevice = function(options, callback) {
var self = this; var self = this,
devName;
// check if deviceName is already persisted in storage // check if deviceName is already persisted in storage
self.getDeviceName(function(err, deviceName) { self.getDeviceName(function(err, deviceName) {
@ -336,6 +342,8 @@ define(function(require) {
}); });
function requestDeviceRegistration(deviceName) { function requestDeviceRegistration(deviceName) {
devName = deviceName;
// request device registration session key // request device registration session key
self._privateKeyDao.requestDeviceRegistration({ self._privateKeyDao.requestDeviceRegistration({
userId: options.userId, userId: options.userId,
@ -357,15 +365,20 @@ define(function(require) {
function decryptSessionKey(regSessionKey) { function decryptSessionKey(regSessionKey) {
// TODO: fetch public key for service to verify response // TODO: fetch public key for service to verify response
self.lookupPublicKey('WELL_KNOWN_SERVER_KEY_ID', function(err, serverPubkey) { self.lookupPublicKey(config.serverPrivateKeyId, function(err, serverPubkey) {
if (err) { if (err) {
callback(err); callback(err);
return; return;
} }
if (!serverPubkey || !serverPubkey.publicKey) {
callback(new Error('Server public key for device registration not found!'));
return;
}
// decrypt the session key // decrypt the session key
var ct = regSessionKey.encryptedRegSessionKey; var ct = regSessionKey.encryptedRegSessionKey;
self._pgp.decrypt(ct, serverPubkey, function(err, decrypedSessionKey) { self._pgp.decrypt(ct, serverPubkey.publicKey, function(err, decrypedSessionKey) {
if (err) { if (err) {
callback(err); callback(err);
return; return;
@ -384,10 +397,10 @@ define(function(require) {
return; return;
} }
// generate deviceSecretIv // generate iv
var deviceSecretIv = util.random(config.symIvSize); var iv = util.random(config.symIvSize);
// encrypt deviceSecret // encrypt deviceSecret
self._crypto.encrypt(deviceSecret, regSessionKey, deviceSecretIv, function(err, encryptedDeviceSecret) { self._crypto.encrypt(deviceSecret, regSessionKey, iv, function(err, encryptedDeviceSecret) {
if (err) { if (err) {
callback(err); callback(err);
return; return;
@ -396,9 +409,9 @@ define(function(require) {
// upload encryptedDeviceSecret // upload encryptedDeviceSecret
self._privateKeyDao.uploadDeviceSecret({ self._privateKeyDao.uploadDeviceSecret({
userId: options.userId, userId: options.userId,
deviceName: options.deviceName, deviceName: devName,
encryptedDeviceSecret: encryptedDeviceSecret, encryptedDeviceSecret: encryptedDeviceSecret,
deviceSecretIv: deviceSecretIv iv: iv
}, callback); }, callback);
}); });
}); });
@ -420,7 +433,7 @@ define(function(require) {
sessionId; sessionId;
// request auth session key required for upload // request auth session key required for upload
self._privateKeyDao.requestAuthSessionKeys({ self._privateKeyDao.requestAuthSessionKey({
userId: userId userId: userId
}, function(err, authSessionKey) { }, function(err, authSessionKey) {
if (err) { if (err) {
@ -441,12 +454,17 @@ define(function(require) {
function decryptSessionKey(authSessionKey) { function decryptSessionKey(authSessionKey) {
// TODO: fetch public key for service to verify response // TODO: fetch public key for service to verify response
self.lookupPublicKey('WELL_KNOWN_SERVER_KEY_ID', function(err, serverPubkey) { self.lookupPublicKey(config.serverPrivateKeyId, function(err, serverPubkey) {
if (err) { if (err) {
callback(err); callback(err);
return; return;
} }
if (!serverPubkey || !serverPubkey.publicKey) {
callback(new Error('Server public key for authentication not found!'));
return;
}
// decrypt the session key // decrypt the session key
var ct1 = authSessionKey.encryptedAuthSessionKey; var ct1 = authSessionKey.encryptedAuthSessionKey;
self._pgp.decrypt(ct1, serverPubkey.publicKey, function(err, decryptedSessionKey) { self._pgp.decrypt(ct1, serverPubkey.publicKey, function(err, decryptedSessionKey) {
@ -456,7 +474,7 @@ define(function(require) {
} }
// decrypt the challenge // decrypt the challenge
var ct2 = authSessionKey.encryptedAuthSessionKey; var ct2 = authSessionKey.encryptedChallenge;
self._pgp.decrypt(ct2, serverPubkey.publicKey, function(err, decryptedChallenge) { self._pgp.decrypt(ct2, serverPubkey.publicKey, function(err, decryptedChallenge) {
if (err) { if (err) {
callback(err); callback(err);
@ -502,12 +520,14 @@ define(function(require) {
}); });
} }
function replyChallenge(encryptedChallenge, sessionKey) { function replyChallenge(response, sessionKey) {
// respond to challenge by uploading the with the session key encrypted challenge // respond to challenge by uploading the with the session key encrypted challenge
self._privateKeyDao.verifyAuthentication({ self._privateKeyDao.verifyAuthentication({
userId: userId, userId: userId,
sessionId: sessionId, sessionId: sessionId,
encryptedChallenge: encryptedChallenge encryptedChallenge: response.encryptedChallenge,
encryptedDeviceSecret: response.encryptedDeviceSecret,
iv: response.iv
}, function(err) { }, function(err) {
if (err) { if (err) {
callback(err); callback(err);
@ -565,7 +585,7 @@ define(function(require) {
var privkeyId = keypair.privateKey._id, var privkeyId = keypair.privateKey._id,
pgpBlock = keypair.privateKey.encryptedKey; pgpBlock = keypair.privateKey.encryptedKey;
// encrypt the private key with the derived key (AES-GCM authenticated encryption) // encrypt the private key with the derived key
var iv = util.random(config.symIvSize); var iv = util.random(config.symIvSize);
self._crypto.encrypt(pgpBlock, encryptionKey, iv, function(err, ct) { self._crypto.encrypt(pgpBlock, encryptionKey, iv, function(err, ct) {
if (err) { if (err) {
@ -575,6 +595,7 @@ define(function(require) {
var payload = { var payload = {
_id: privkeyId, _id: privkeyId,
userId: options.userId,
encryptedPrivateKey: ct, encryptedPrivateKey: ct,
salt: salt, salt: salt,
iv: iv iv: iv
@ -617,13 +638,12 @@ define(function(require) {
/** /**
* Request downloading the user's encrypted private key. This will initiate the server to send the recovery token via email/sms to the user. * Request downloading the user's encrypted private key. This will initiate the server to send the recovery token via email/sms to the user.
* @param {String} userId The user's email address * @param {String} options.userId The user's email address
* @param {String} options.keyId The private PGP key id
* @param {Function} callback(error) * @param {Function} callback(error)
*/ */
KeychainDAO.prototype.requestPrivateKeyDownload = function(userId, callback) { KeychainDAO.prototype.requestPrivateKeyDownload = function(options, callback) {
this._privateKeyDao.requestDownload({ this._privateKeyDao.requestDownload(options, callback);
userId: userId
}, callback);
}; };
/** /**
@ -639,21 +659,21 @@ define(function(require) {
/** /**
* This is called after the encrypted private key has successfully been downloaded and it's ready to be decrypted and stored in localstorage. * This is called after the encrypted private key has successfully been downloaded and it's ready to be decrypted and stored in localstorage.
* @param {String} options._id The private PGP key id
* @param {String} options.userId The user's email address * @param {String} options.userId The user's email address
* @param {String} options.keyId The user's email address
* @param {String} options.code The randomly generated or self selected code used to derive the key for the decryption of the private PGP key * @param {String} options.code The randomly generated or self selected code used to derive the key for the decryption of the private PGP key
* @param {String} options.encryptedPrivkey The encrypted private PGP key * @param {String} options.encryptedPrivateKey The encrypted private PGP key
* @param {String} options.salt The salt required to derive the code derived key * @param {String} options.salt The salt required to derive the code derived key
* @param {String} options.iv The iv used to encrypt the private PGP key * @param {String} options.iv The iv used to encrypt the private PGP key
* @param {Function} callback(error) * @param {Function} callback(error, keyObject)
*/ */
KeychainDAO.prototype.decryptAndStorePrivateKeyLocally = function(options, callback) { KeychainDAO.prototype.decryptAndStorePrivateKeyLocally = function(options, callback) {
var self = this, var self = this,
code = options.code, code = options.code,
salt = options.salt, salt = options.salt,
keySize = config.keySize; keySize = config.symKeySize;
if (!options.keyId || !options.userId) { if (!options._id || !options.userId || !options.code || !options.salt || !options.encryptedPrivateKey || !options.iv) {
callback(new Error('Incomplete arguments!')); callback(new Error('Incomplete arguments!'));
return; return;
} }
@ -670,21 +690,44 @@ define(function(require) {
function decryptAndStore(derivedKey) { function decryptAndStore(derivedKey) {
// decrypt the private key with the derived key // decrypt the private key with the derived key
var pt = options.encryptedPrivkey, var ct = options.encryptedPrivateKey,
iv = options.iv; iv = options.iv;
self._crypto.decrypt(pt, derivedKey, iv, function(err, pgpBlock) { self._crypto.decrypt(ct, derivedKey, iv, function(err, privateKeyArmored) {
if (err) { if (err) {
callback(err); callback(new Error('Invalid keychain code!'));
return; return;
} }
// store private key locally // validate pgp key
self.saveLocalPrivateKey({ var keyParams;
_id: options.keyId, try {
keyParams = self._pgp.getKeyParams(privateKeyArmored);
} catch (e) {
callback(new Error('Error parsing private PGP key!'));
return;
}
if (keyParams._id !== options._id || keyParams.userId !== options.userId) {
callback(new Error('Private key parameters don\'t match with public key\'s!'));
return;
}
var keyObject = {
_id: options._id,
userId: options.userId, userId: options.userId,
encryptedKey: pgpBlock encryptedKey: privateKeyArmored
}, callback); };
// store private key locally
self.saveLocalPrivateKey(keyObject, function(err) {
if (err) {
callback(err);
return;
}
callback(null, keyObject);
});
}); });
} }
}; };

View File

@ -30,21 +30,22 @@ define(function() {
/** /**
* Authenticate device registration by uploading the deviceSecret encrypted with the regSessionKeys. * Authenticate device registration by uploading the deviceSecret encrypted with the regSessionKeys.
* @param {String} options.userId The user's email address * @param {String} options.userId The user's email address
* @param {String} options.deviceName The device's memorable name * @param {String} options.deviceName The device's memorable name
* @param {Object} options.encryptedDeviceSecret {encryptedDeviceSecret:[base64 encoded]} * @param {String} options.encryptedDeviceSecret The base64 encoded encrypted device secret
* @param {String} options.iv The iv used for encryption
* @param {Function} callback(error) * @param {Function} callback(error)
*/ */
PrivateKeyDAO.prototype.uploadDeviceSecret = function(options, callback) { PrivateKeyDAO.prototype.uploadDeviceSecret = function(options, callback) {
var uri; var uri;
if (!options.userId || !options.deviceName || !options.encryptedDeviceSecret) { if (!options.userId || !options.deviceName || !options.encryptedDeviceSecret || !options.iv) {
callback(new Error('Incomplete arguments!')); callback(new Error('Incomplete arguments!'));
return; return;
} }
uri = '/device/user/' + options.userId + '/devicename/' + options.deviceName; uri = '/device/user/' + options.userId + '/devicename/' + options.deviceName;
this._restDao.put(options.encryptedDeviceSecret, uri, callback); this._restDao.put(options, uri, callback);
}; };
// //
@ -55,9 +56,9 @@ define(function() {
* Request authSessionKeys required for upload the encrypted private PGP key. * Request authSessionKeys required for upload the encrypted private PGP key.
* @param {String} options.userId The user's email address * @param {String} options.userId The user's email address
* @param {Function} callback(error, authSessionKey) * @param {Function} callback(error, authSessionKey)
* @return {Object} {sessionId, encryptedAuthSessionKeys:[base64 encoded], encryptedChallenge:[base64 encoded]} * @return {Object} {sessionId, encryptedAuthSessionKey:[base64 encoded], encryptedChallenge:[base64 encoded]}
*/ */
PrivateKeyDAO.prototype.requestAuthSessionKeys = function(options, callback) { PrivateKeyDAO.prototype.requestAuthSessionKey = function(options, callback) {
var uri; var uri;
if (!options.userId) { if (!options.userId) {
@ -71,44 +72,50 @@ define(function() {
/** /**
* Verifiy authentication by uploading the challenge and deviceSecret encrypted with the authSessionKeys as a response. * Verifiy authentication by uploading the challenge and deviceSecret encrypted with the authSessionKeys as a response.
* @param {String} options.userId The user's email address * @param {String} options.userId The user's email address
* @param {Object} options.encryptedChallenge The server's challenge encrypted using the authSessionKey {encryptedChallenge:[base64 encoded], encryptedDeviceSecret:[base64 encoded], iv} * @param {String} options.encryptedChallenge The server's base64 encoded challenge encrypted using the authSessionKey
* @param {String} options.encryptedDeviceSecret The server's base64 encoded deviceSecret encrypted using the authSessionKey
* @param {String} options.iv The iv used for encryption
* @param {Function} callback(error) * @param {Function} callback(error)
*/ */
PrivateKeyDAO.prototype.verifyAuthentication = function(options, callback) { PrivateKeyDAO.prototype.verifyAuthentication = function(options, callback) {
var uri; var uri;
if (!options.userId || !options.sessionId || !options.encryptedChallenge) { if (!options.userId || !options.sessionId || !options.encryptedChallenge || !options.encryptedDeviceSecret || !options.iv) {
callback(new Error('Incomplete arguments!')); callback(new Error('Incomplete arguments!'));
return; return;
} }
uri = '/auth/user/' + options.userId + '/session/' + options.sessionId; uri = '/auth/user/' + options.userId + '/session/' + options.sessionId;
this._restDao.put(options.encryptedChallenge, uri, callback); this._restDao.put(options, uri, callback);
}; };
/** /**
* Upload the encrypted private PGP key. * Upload the encrypted private PGP key.
* @param {String} options.encryptedPrivateKey {_id:[hex encoded capital 16 char key id], encryptedPrivateKey:[base64 encoded], sessionId: [base64 encoded]} * @param {String} options._id The hex encoded capital 16 char key id
* @param {String} options.userId The user's email address
* @param {String} options.encryptedPrivateKey The base64 encoded encrypted private PGP key
* @param {String} options.sessionId The session id
* @param {Function} callback(error) * @param {Function} callback(error)
*/ */
PrivateKeyDAO.prototype.upload = function(options, callback) { PrivateKeyDAO.prototype.upload = function(options, callback) {
var uri, var uri;
key = options.encryptedPrivateKey;
if (!options.userId || !key || !key._id) { if (!options._id || !options.userId || !options.encryptedPrivateKey || !options.sessionId || !options.salt || !options.iv) {
callback(new Error('Incomplete arguments!')); callback(new Error('Incomplete arguments!'));
return; return;
} }
uri = '/privatekey/user/' + options.userId + '/key/' + key._id; uri = '/privatekey/user/' + options.userId + '/session/' + options.sessionId;
this._restDao.post(key, uri, callback); this._restDao.post(options, uri, callback);
}; };
/** /**
* Request download for the encrypted private PGP key. * Request download for the encrypted private PGP key.
* @param {[type]} options.userId The user's email address * @param {String} options.userId The user's email address
* @param {Function} callback(error) * @param {String} options.keyId The private PGP key id
* @param {Function} callback(error, found)
* @return {Boolean} weather the key was found on the server or not.
*/ */
PrivateKeyDAO.prototype.requestDownload = function(options, callback) { PrivateKeyDAO.prototype.requestDownload = function(options, callback) {
var uri; var uri;
@ -121,7 +128,20 @@ define(function() {
uri = '/privatekey/user/' + options.userId + '/key/' + options.keyId; uri = '/privatekey/user/' + options.userId + '/key/' + options.keyId;
this._restDao.get({ this._restDao.get({
uri: uri uri: uri
}, callback); }, function(err) {
// 404: there is no encrypted private key on the server
if (err && err.code !== 200) {
callback(null, false);
return;
}
if (err) {
callback(err);
return;
}
callback(null, true);
});
}; };
/** /**

View File

@ -32,6 +32,7 @@
@import "views/shared"; @import "views/shared";
@import "views/add-account"; @import "views/add-account";
@import "views/account"; @import "views/account";
@import "views/privatekey-upload";
@import "views/set-passphrase"; @import "views/set-passphrase";
@import "views/contacts"; @import "views/contacts";
@import "views/about"; @import "views/about";

View File

@ -1,8 +1,12 @@
.view-account { .view-account {
a {
color: $color-blue;
}
table { table {
margin: 50px auto 60px auto; margin: 50px auto 60px auto;
td { td {
padding-top: 15px; padding-top: 15px;

View File

@ -119,4 +119,15 @@
margin-right: 10px; margin-right: 10px;
} }
} }
}
.view-login-privatekey-download {
.content {
max-width: 500px;
input.code {
margin-right: 0;
width: auto;
}
}
} }

View File

@ -0,0 +1,17 @@
.view-privatekey-upload {
a {
color: $color-blue;
}
.step {
margin: 20px 0;
.working {
text-align: center;
font-size: 30px;
margin: 70px;
}
}
}

View File

@ -15,7 +15,7 @@
</tr> </tr>
<tr> <tr>
<td>PGP Key ID</td> <td>PGP Key ID</td>
<td>{{keyId}}</td> <td>{{keyId}} (<a href="https://whiteout.io/revocation.html" title="Click here to reset your account." target="_blank">Revoke key</a>)</td>
</tr> </tr>
<tr> <tr>
<td>PGP Fingerprint</td> <td>PGP Fingerprint</td>

View File

@ -31,6 +31,10 @@
<div class="lightbox" ng-include="'tpl/set-passphrase.html'"></div> <div class="lightbox" ng-include="'tpl/set-passphrase.html'"></div>
</div> </div>
<div class="lightbox-overlay" ng-class="{'show': state.lightbox === 'privatekey-upload'}">
<div class="lightbox" ng-include="'tpl/privatekey-upload.html'"></div>
</div>
<div class="lightbox-overlay" ng-class="{'show': state.lightbox === 'contacts'}"> <div class="lightbox-overlay" ng-class="{'show': state.lightbox === 'contacts'}">
<div class="lightbox" ng-include="'tpl/contacts.html'"></div> <div class="lightbox" ng-include="'tpl/contacts.html'"></div>
</div> </div>

View File

@ -0,0 +1,41 @@
<div class="view-login view-login-privatekey-download">
<div class="logo">
<img src="img/whiteout_logo.svg" alt="whiteout.io">
</div><!--/logo-->
<div class="content">
<div class="step" ng-show="step === 1">
<p><b>Key sync.</b> We have sent you an email containing a recovery token. Please copy and paste the identifier below.</p>
<input type="text" class="input-text" size="42" ng-model="recoveryToken" placeholder="Recovery token" focus-me="step === 1">
</div>
<div class="step" ng-show="step === 2">
<p><b>Key sync.</b> Please enter the keychain code you wrote down during sync setup.</p>
<input type="text" class="input-text code" size="4" maxlength="4" ng-model="code0" focus-me="step === 2"> -
<input type="text" class="input-text code" size="4" maxlength="4" ng-model="code1"> -
<input type="text" class="input-text code" size="4" maxlength="4" ng-model="code2"> -
<input type="text" class="input-text code" size="4" maxlength="4" ng-model="code3"> -
<input type="text" class="input-text code" size="4" maxlength="4" ng-model="code4"> -
<input type="text" class="input-text code" size="4" maxlength="4" ng-model="code5">
<!--<a href="https://whiteout.io/revocation.html" title="Click here to reset your account." target="_blank">Lost your keychain code?</a>-->
</div>
<div>
<button class="btn" ng-click="goForward()">Continue</button>
</div>
</div><!--/content-->
</div>
<!-- popovers -->
<div id="passphrase-info" class="popover right" ng-controller="PopoverCtrl">
<div class="arrow"></div>
<div class="popover-title"><b>What is this?</b></div>
<div class="popover-content">
<p>A passphrase is like a password that protects your PGP key.</p>
<p>There is no way to access your messages without your passphrase.</p>
<p>If you have forgotten your passphrase, please request an account reset by sending an email to <b>support@whiteout.io</b>. You will not be able to read previous messages after a reset.</p>
</div>
</div><!--/.popover-->

View File

@ -17,6 +17,7 @@
<ul class="nav-secondary"> <ul class="nav-secondary">
<li><a href="#" ng-click="state.account.toggle(true); $event.preventDefault()">Account</a></li> <li><a href="#" ng-click="state.account.toggle(true); $event.preventDefault()">Account</a></li>
<li><a href="#" ng-click="state.contacts.toggle(true); $event.preventDefault()">Contacts</a></li> <li><a href="#" ng-click="state.contacts.toggle(true); $event.preventDefault()">Contacts</a></li>
<li><a href="#" ng-click="state.privateKeyUpload.toggle(true); $event.preventDefault()">Key sync (experimental)</a></li>
<li><a href="#" ng-click="state.about.toggle(true); $event.preventDefault()">About</a></li> <li><a href="#" ng-click="state.about.toggle(true); $event.preventDefault()">About</a></li>
</ul> </ul>

View File

@ -0,0 +1,46 @@
<div class="lightbox-body" ng-controller="PrivateKeyUploadCtrl">
<header>
<h2>Setup Key Sync</h2>
<button class="close" ng-click="state.privateKeyUpload.toggle(false)" data-action="lightbox-close">&#xe007;</button>
</header>
<div class="content">
<div class="dialog view-privatekey-upload">
<div class="step" ng-show="step === 1">
<p>Your keychain code can be used to securely backup and sync your PGP key between devices. This feature is experimental and not recommended for production use. <a href="" target="_blank">Learn more</a>.</p>
<h2>{{displayedCode}}</h2>
<p>Please write down your keychain code and keep it in a safe place. Whiteout Networks cannot recover a lost code.</p>
</div>
<div class="step" ng-show="step === 2">
<p>Please confirm the keychain code you have written down.</p>
<input type="text" class="input-text" size="4" maxlength="4" ng-model="code0" focus-me="step === 2"> -
<input type="text" class="input-text" size="4" maxlength="4" ng-model="code1"> -
<input type="text" class="input-text" size="4" maxlength="4" ng-model="code2"> -
<input type="text" class="input-text" size="4" maxlength="4" ng-model="code3"> -
<input type="text" class="input-text" size="4" maxlength="4" ng-model="code4"> -
<input type="text" class="input-text" size="4" maxlength="4" ng-model="code5">
</div>
<div class="step" ng-show="step === 3">
<p>Please enter a memorable name for this device e.g. "MacBook Work".</p>
<input type="text" class="input-text" ng-model="deviceName" placeholder="Device name" focus-me="step === 3">
</div>
<div class="step" ng-show="step === 4">
<div class="working">
<span class="spinner"></span>
</div><!--/.working-->
</div>
<div class="control">
<button ng-show="step > 1 && step < 4" class="btn btn-alt" ng-click="goBack()">Go back</button>
<button ng-show="step < 4" class="btn" ng-click="goForward()">Continue</button>
</div>
</div><!-- /.view-privatekey-upload -->
</div><!-- /.content -->
</div><!-- /.lightbox-body -->

View File

@ -14,18 +14,18 @@ define(function(require) {
var scope, accountCtrl, var scope, accountCtrl,
dummyFingerprint, expectedFingerprint, dummyFingerprint, expectedFingerprint,
dummyKeyId, expectedKeyId, dummyKeyId, expectedKeyId,
emailAddress, keySize, cryptoMock, keychainMock; emailAddress, keySize, pgpMock, keychainMock;
beforeEach(function() { beforeEach(function() {
appController._crypto = cryptoMock = sinon.createStubInstance(PGP); appController._pgp = pgpMock = sinon.createStubInstance(PGP);
appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO); appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO);
dummyFingerprint = '3A2D39B4E1404190B8B949DE7D7E99036E712926'; dummyFingerprint = '3A2D39B4E1404190B8B949DE7D7E99036E712926';
expectedFingerprint = '3A2D 39B4 E140 4190 B8B9 49DE 7D7E 9903 6E71 2926'; expectedFingerprint = '3A2D 39B4 E140 4190 B8B9 49DE 7D7E 9903 6E71 2926';
dummyKeyId = '9FEB47936E712926'; dummyKeyId = '9FEB47936E712926';
expectedKeyId = '6E712926'; expectedKeyId = '6E712926';
cryptoMock.getFingerprint.returns(dummyFingerprint); pgpMock.getFingerprint.returns(dummyFingerprint);
cryptoMock.getKeyId.returns(dummyKeyId); pgpMock.getKeyId.returns(dummyKeyId);
emailAddress = 'fred@foo.com'; emailAddress = 'fred@foo.com';
keySize = 1234; keySize = 1234;
appController._emailDao = { appController._emailDao = {
@ -34,7 +34,7 @@ define(function(require) {
asymKeySize: keySize asymKeySize: keySize
} }
}; };
cryptoMock.getKeyParams.returns({ pgpMock.getKeyParams.returns({
_id: dummyKeyId, _id: dummyKeyId,
fingerprint: dummyFingerprint, fingerprint: dummyFingerprint,
userId: emailAddress, userId: emailAddress,

View File

@ -37,7 +37,7 @@ define(function(require) {
expect(controller._userStorage).to.exist; expect(controller._userStorage).to.exist;
expect(controller._invitationDao).to.exist; expect(controller._invitationDao).to.exist;
expect(controller._keychain).to.exist; expect(controller._keychain).to.exist;
expect(controller._crypto).to.exist; expect(controller._pgp).to.exist;
expect(controller._pgpbuilder).to.exist; expect(controller._pgpbuilder).to.exist;
expect(controller._emailDao).to.exist; expect(controller._emailDao).to.exist;
expect(controller._outboxBo).to.exist; expect(controller._outboxBo).to.exist;

View File

@ -12,11 +12,11 @@ define(function(require) {
describe('Contacts Controller unit test', function() { describe('Contacts Controller unit test', function() {
var scope, contactsCtrl, var scope, contactsCtrl,
origKeychain, keychainMock, origKeychain, keychainMock,
origCrypto, cryptoMock; origPgp, pgpMock;
beforeEach(function() { beforeEach(function() {
origCrypto = appController._crypto; origPgp = appController._pgp;
appController._crypto = cryptoMock = sinon.createStubInstance(PGP); appController._pgp = pgpMock = sinon.createStubInstance(PGP);
origKeychain = appController._keychain; origKeychain = appController._keychain;
appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO); appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO);
@ -33,7 +33,7 @@ define(function(require) {
afterEach(function() { afterEach(function() {
// restore the module // restore the module
appController._crypto = origCrypto; appController._pgp = origPgp;
appController._keychain = origKeychain; appController._keychain = origKeychain;
}); });
@ -60,7 +60,7 @@ define(function(require) {
keychainMock.listLocalPublicKeys.yields(null, [{ keychainMock.listLocalPublicKeys.yields(null, [{
_id: '12345' _id: '12345'
}]); }]);
cryptoMock.getKeyParams.returns({ pgpMock.getKeyParams.returns({
fingerprint: 'asdf' fingerprint: 'asdf'
}); });
@ -92,7 +92,7 @@ define(function(require) {
it('should work', function(done) { it('should work', function(done) {
var keyArmored = '-----BEGIN PGP PUBLIC KEY BLOCK-----'; var keyArmored = '-----BEGIN PGP PUBLIC KEY BLOCK-----';
cryptoMock.getKeyParams.returns({ pgpMock.getKeyParams.returns({
_id: '12345', _id: '12345',
userId: 'max@example.com', userId: 'max@example.com',
userIds: [] userIds: []
@ -127,7 +127,7 @@ define(function(require) {
it('should fail due to error in pgp.getKeyParams', function(done) { it('should fail due to error in pgp.getKeyParams', function(done) {
var keyArmored = '-----BEGIN PGP PUBLIC KEY BLOCK-----'; var keyArmored = '-----BEGIN PGP PUBLIC KEY BLOCK-----';
cryptoMock.getKeyParams.throws(new Error('WAT')); pgpMock.getKeyParams.throws(new Error('WAT'));
scope.onError = function(err) { scope.onError = function(err) {
expect(err).to.exist; expect(err).to.exist;
@ -140,7 +140,7 @@ define(function(require) {
it('should fail due to error in keychain.saveLocalPublicKey', function(done) { it('should fail due to error in keychain.saveLocalPublicKey', function(done) {
var keyArmored = '-----BEGIN PGP PUBLIC KEY BLOCK-----'; var keyArmored = '-----BEGIN PGP PUBLIC KEY BLOCK-----';
cryptoMock.getKeyParams.returns({ pgpMock.getKeyParams.returns({
_id: '12345', _id: '12345',
userId: 'max@example.com' userId: 'max@example.com'
}); });

View File

@ -114,7 +114,7 @@ define(function(require) {
// check configuration // check configuration
// //
expect(dao._keychain).to.equal(keychainStub); expect(dao._keychain).to.equal(keychainStub);
expect(dao._crypto).to.equal(pgpStub); expect(dao._pgp).to.equal(pgpStub);
expect(dao._devicestorage).to.equal(devicestorageStub); expect(dao._devicestorage).to.equal(devicestorageStub);
expect(dao._mailreader).to.equal(mailreader); expect(dao._mailreader).to.equal(mailreader);
expect(dao._pgpbuilder).to.equal(pgpBuilderStub); expect(dao._pgpbuilder).to.equal(pgpBuilderStub);

View File

@ -743,6 +743,26 @@ define(function(require) {
}); });
}); });
it('should fail when server public key not found', function(done) {
getDeviceNameStub.yields(null, 'iPhone');
privkeyDaoStub.requestDeviceRegistration.withArgs({
userId: testUser,
deviceName: 'iPhone'
}).yields(null, {
encryptedRegSessionKey: 'asdf'
});
lookupPublicKeyStub.yields();
keychainDao.registerDevice({
userId: testUser
}, function(err) {
expect(err).to.exist;
done();
});
});
it('should fail in decrypt', function(done) { it('should fail in decrypt', function(done) {
getDeviceNameStub.yields(null, 'iPhone'); getDeviceNameStub.yields(null, 'iPhone');
@ -753,7 +773,9 @@ define(function(require) {
encryptedRegSessionKey: 'asdf' encryptedRegSessionKey: 'asdf'
}); });
lookupPublicKeyStub.yields(null, 'pubkey'); lookupPublicKeyStub.yields(null, {
publicKey: 'pubkey'
});
pgpStub.decrypt.withArgs('asdf', 'pubkey').yields(42); pgpStub.decrypt.withArgs('asdf', 'pubkey').yields(42);
keychainDao.registerDevice({ keychainDao.registerDevice({
@ -774,7 +796,9 @@ define(function(require) {
encryptedRegSessionKey: 'asdf' encryptedRegSessionKey: 'asdf'
}); });
lookupPublicKeyStub.yields(null, 'pubkey'); lookupPublicKeyStub.yields(null, {
publicKey: 'pubkey'
});
pgpStub.decrypt.withArgs('asdf', 'pubkey').yields(null, 'decrypted'); pgpStub.decrypt.withArgs('asdf', 'pubkey').yields(null, 'decrypted');
getDeviceSecretStub.yields(42); getDeviceSecretStub.yields(42);
@ -796,7 +820,9 @@ define(function(require) {
encryptedRegSessionKey: 'asdf' encryptedRegSessionKey: 'asdf'
}); });
lookupPublicKeyStub.yields(null, 'pubkey'); lookupPublicKeyStub.yields(null, {
publicKey: 'pubkey'
});
pgpStub.decrypt.withArgs('asdf', 'pubkey').yields(null, 'decrypted'); pgpStub.decrypt.withArgs('asdf', 'pubkey').yields(null, 'decrypted');
getDeviceSecretStub.yields(null, 'secret'); getDeviceSecretStub.yields(null, 'secret');
cryptoStub.encrypt.withArgs('secret', 'decrypted').yields(42); cryptoStub.encrypt.withArgs('secret', 'decrypted').yields(42);
@ -819,7 +845,9 @@ define(function(require) {
encryptedRegSessionKey: 'asdf' encryptedRegSessionKey: 'asdf'
}); });
lookupPublicKeyStub.yields(null, 'pubkey'); lookupPublicKeyStub.yields(null, {
publicKey: 'pubkey'
});
pgpStub.decrypt.withArgs('asdf', 'pubkey').yields(null, 'decrypted'); pgpStub.decrypt.withArgs('asdf', 'pubkey').yields(null, 'decrypted');
getDeviceSecretStub.yields(null, 'secret'); getDeviceSecretStub.yields(null, 'secret');
cryptoStub.encrypt.withArgs('secret', 'decrypted').yields(null, 'encryptedDeviceSecret'); cryptoStub.encrypt.withArgs('secret', 'decrypted').yields(null, 'encryptedDeviceSecret');
@ -847,8 +875,8 @@ define(function(require) {
getDeviceSecretStub.restore(); getDeviceSecretStub.restore();
}); });
it('should fail due to privkeyDao.requestAuthSessionKeys', function(done) { it('should fail due to privkeyDao.requestAuthSessionKey', function(done) {
privkeyDaoStub.requestAuthSessionKeys.withArgs({ privkeyDaoStub.requestAuthSessionKey.withArgs({
userId: testUser userId: testUser
}).yields(42); }).yields(42);
@ -859,8 +887,8 @@ define(function(require) {
}); });
}); });
it('should fail due to privkeyDao.requestAuthSessionKeys response', function(done) { it('should fail due to privkeyDao.requestAuthSessionKey response', function(done) {
privkeyDaoStub.requestAuthSessionKeys.yields(null, {}); privkeyDaoStub.requestAuthSessionKey.yields(null, {});
keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) { keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) {
expect(err).to.exist; expect(err).to.exist;
@ -870,7 +898,7 @@ define(function(require) {
}); });
it('should fail due to lookupPublicKey', function(done) { it('should fail due to lookupPublicKey', function(done) {
privkeyDaoStub.requestAuthSessionKeys.yields(null, { privkeyDaoStub.requestAuthSessionKey.yields(null, {
encryptedAuthSessionKey: 'encryptedAuthSessionKey', encryptedAuthSessionKey: 'encryptedAuthSessionKey',
encryptedChallenge: 'encryptedChallenge', encryptedChallenge: 'encryptedChallenge',
sessionId: 'sessionId' sessionId: 'sessionId'
@ -886,7 +914,7 @@ define(function(require) {
}); });
it('should fail due to pgp.decrypt', function(done) { it('should fail due to pgp.decrypt', function(done) {
privkeyDaoStub.requestAuthSessionKeys.yields(null, { privkeyDaoStub.requestAuthSessionKey.yields(null, {
encryptedAuthSessionKey: 'encryptedAuthSessionKey', encryptedAuthSessionKey: 'encryptedAuthSessionKey',
encryptedChallenge: 'encryptedChallenge', encryptedChallenge: 'encryptedChallenge',
sessionId: 'sessionId' sessionId: 'sessionId'
@ -906,7 +934,7 @@ define(function(require) {
}); });
it('should fail due to getDeviceSecret', function(done) { it('should fail due to getDeviceSecret', function(done) {
privkeyDaoStub.requestAuthSessionKeys.yields(null, { privkeyDaoStub.requestAuthSessionKey.yields(null, {
encryptedAuthSessionKey: 'encryptedAuthSessionKey', encryptedAuthSessionKey: 'encryptedAuthSessionKey',
encryptedChallenge: 'encryptedChallenge', encryptedChallenge: 'encryptedChallenge',
sessionId: 'sessionId' sessionId: 'sessionId'
@ -927,7 +955,7 @@ define(function(require) {
}); });
it('should fail due to crypto.encrypt', function(done) { it('should fail due to crypto.encrypt', function(done) {
privkeyDaoStub.requestAuthSessionKeys.yields(null, { privkeyDaoStub.requestAuthSessionKey.yields(null, {
encryptedAuthSessionKey: 'encryptedAuthSessionKey', encryptedAuthSessionKey: 'encryptedAuthSessionKey',
encryptedChallenge: 'encryptedChallenge', encryptedChallenge: 'encryptedChallenge',
sessionId: 'sessionId' sessionId: 'sessionId'
@ -949,7 +977,7 @@ define(function(require) {
}); });
it('should fail due to privkeyDao.verifyAuthentication', function(done) { it('should fail due to privkeyDao.verifyAuthentication', function(done) {
privkeyDaoStub.requestAuthSessionKeys.yields(null, { privkeyDaoStub.requestAuthSessionKey.yields(null, {
encryptedAuthSessionKey: 'encryptedAuthSessionKey', encryptedAuthSessionKey: 'encryptedAuthSessionKey',
encryptedChallenge: 'encryptedChallenge', encryptedChallenge: 'encryptedChallenge',
sessionId: 'sessionId' sessionId: 'sessionId'
@ -971,15 +999,36 @@ define(function(require) {
}); });
}); });
it('should fail due to server public key nto found', function(done) {
privkeyDaoStub.requestAuthSessionKey.yields(null, {
encryptedAuthSessionKey: 'encryptedAuthSessionKey',
encryptedChallenge: 'encryptedChallenge',
sessionId: 'sessionId'
});
lookupPublicKeyStub.yields();
pgpStub.decrypt.yields(null, 'decryptedStuff');
getDeviceSecretStub.yields(null, 'deviceSecret');
cryptoStub.encrypt.yields(null, 'encryptedStuff');
privkeyDaoStub.verifyAuthentication.yields();
keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) {
expect(err).to.exist;
expect(authSessionKey).to.not.exist;
done();
});
});
it('should work', function(done) { it('should work', function(done) {
privkeyDaoStub.requestAuthSessionKeys.yields(null, { privkeyDaoStub.requestAuthSessionKey.yields(null, {
encryptedAuthSessionKey: 'encryptedAuthSessionKey', encryptedAuthSessionKey: 'encryptedAuthSessionKey',
encryptedChallenge: 'encryptedChallenge', encryptedChallenge: 'encryptedChallenge',
sessionId: 'sessionId' sessionId: 'sessionId'
}); });
lookupPublicKeyStub.yields(null, { lookupPublicKeyStub.yields(null, {
publickKey: 'publicKey' publicKey: 'publicKey'
}); });
pgpStub.decrypt.yields(null, 'decryptedStuff'); pgpStub.decrypt.yields(null, 'decryptedStuff');
@ -1152,10 +1201,12 @@ define(function(require) {
describe('requestPrivateKeyDownload', function() { describe('requestPrivateKeyDownload', function() {
it('should work', function(done) { it('should work', function(done) {
privkeyDaoStub.requestDownload.withArgs({ var options = {
userId: testUser userId: testUser
}).yields(); };
keychainDao.requestPrivateKeyDownload(testUser, done);
privkeyDaoStub.requestDownload.withArgs(options).yields();
keychainDao.requestPrivateKeyDownload(options, done);
}); });
}); });
@ -1171,9 +1222,18 @@ define(function(require) {
}); });
describe('decryptAndStorePrivateKeyLocally', function() { describe('decryptAndStorePrivateKeyLocally', function() {
var saveLocalPrivateKeyStub; var saveLocalPrivateKeyStub, testData;
beforeEach(function() { beforeEach(function() {
testData = {
_id: 'keyId',
userId: testUser,
encryptedPrivateKey: 'encryptedPrivateKey',
code: 'code',
salt: 'salt',
iv: 'iv'
};
saveLocalPrivateKeyStub = sinon.stub(keychainDao, 'saveLocalPrivateKey'); saveLocalPrivateKeyStub = sinon.stub(keychainDao, 'saveLocalPrivateKey');
}); });
afterEach(function() { afterEach(function() {
@ -1190,10 +1250,7 @@ define(function(require) {
it('should fail due to crypto.deriveKey', function(done) { it('should fail due to crypto.deriveKey', function(done) {
cryptoStub.deriveKey.yields(42); cryptoStub.deriveKey.yields(42);
keychainDao.decryptAndStorePrivateKeyLocally({ keychainDao.decryptAndStorePrivateKeyLocally(testData, function(err) {
userId: testUser,
keyId: 'keyId'
}, function(err) {
expect(err).to.exist; expect(err).to.exist;
expect(cryptoStub.deriveKey.calledOnce).to.be.true; expect(cryptoStub.deriveKey.calledOnce).to.be.true;
done(); done();
@ -1204,10 +1261,7 @@ define(function(require) {
cryptoStub.deriveKey.yields(null, 'derivedKey'); cryptoStub.deriveKey.yields(null, 'derivedKey');
cryptoStub.decrypt.yields(42); cryptoStub.decrypt.yields(42);
keychainDao.decryptAndStorePrivateKeyLocally({ keychainDao.decryptAndStorePrivateKeyLocally(testData, function(err) {
userId: testUser,
keyId: 'keyId'
}, function(err) {
expect(err).to.exist; expect(err).to.exist;
expect(cryptoStub.deriveKey.calledOnce).to.be.true; expect(cryptoStub.deriveKey.calledOnce).to.be.true;
expect(cryptoStub.decrypt.calledOnce).to.be.true; expect(cryptoStub.decrypt.calledOnce).to.be.true;
@ -1215,18 +1269,52 @@ define(function(require) {
}); });
}); });
it('should work', function(done) { it('should fail due to pgp.getKeyParams', function(done) {
cryptoStub.deriveKey.yields(null, 'derivedKey'); cryptoStub.deriveKey.yields(null, 'derivedKey');
cryptoStub.decrypt.yields(null, 'pgpBlock'); cryptoStub.decrypt.yields(null, 'privateKeyArmored');
saveLocalPrivateKeyStub.yields(); pgpStub.getKeyParams.throws(new Error());
keychainDao.decryptAndStorePrivateKeyLocally({ keychainDao.decryptAndStorePrivateKeyLocally(testData, function(err) {
userId: testUser, expect(err).to.exist;
keyId: 'keyId'
}, function(err) {
expect(err).to.not.exist;
expect(cryptoStub.deriveKey.calledOnce).to.be.true; expect(cryptoStub.deriveKey.calledOnce).to.be.true;
expect(cryptoStub.decrypt.calledOnce).to.be.true; expect(cryptoStub.decrypt.calledOnce).to.be.true;
expect(pgpStub.getKeyParams.calledOnce).to.be.true;
done();
});
});
it('should fail due to saveLocalPrivateKey', function(done) {
cryptoStub.deriveKey.yields(null, 'derivedKey');
cryptoStub.decrypt.yields(null, 'privateKeyArmored');
pgpStub.getKeyParams.returns(testData);
saveLocalPrivateKeyStub.yields(42);
keychainDao.decryptAndStorePrivateKeyLocally(testData, function(err) {
expect(err).to.exist;
expect(cryptoStub.deriveKey.calledOnce).to.be.true;
expect(cryptoStub.decrypt.calledOnce).to.be.true;
expect(pgpStub.getKeyParams.calledOnce).to.be.true;
expect(saveLocalPrivateKeyStub.calledOnce).to.be.true;
done();
});
});
it('should work', function(done) {
cryptoStub.deriveKey.yields(null, 'derivedKey');
cryptoStub.decrypt.yields(null, 'privateKeyArmored');
pgpStub.getKeyParams.returns(testData);
saveLocalPrivateKeyStub.yields();
keychainDao.decryptAndStorePrivateKeyLocally(testData, function(err, keyObject) {
expect(err).to.not.exist;
expect(keyObject).to.deep.equal({
_id: 'keyId',
userId: testUser,
encryptedKey: 'privateKeyArmored'
});
expect(cryptoStub.deriveKey.calledOnce).to.be.true;
expect(cryptoStub.decrypt.calledOnce).to.be.true;
expect(pgpStub.getKeyParams.calledOnce).to.be.true;
expect(saveLocalPrivateKeyStub.calledOnce).to.be.true; expect(saveLocalPrivateKeyStub.calledOnce).to.be.true;
done(); done();

View File

@ -7,10 +7,13 @@ define(function(require) {
LoginCtrl = require('js/controller/login'), LoginCtrl = require('js/controller/login'),
EmailDAO = require('js/dao/email-dao'), EmailDAO = require('js/dao/email-dao'),
Auth = require('js/bo/auth'), Auth = require('js/bo/auth'),
appController = require('js/app-controller'); appController = require('js/app-controller'),
KeychainDAO = require('js/dao/keychain-dao');
describe('Login Controller unit test', function() { describe('Login Controller unit test', function() {
var scope, location, ctrl, origEmailDao, emailDaoMock, var scope, location, ctrl,
origEmailDao, emailDaoMock,
origKeychain, keychainMock,
emailAddress = 'fred@foo.com', emailAddress = 'fred@foo.com',
startAppStub, startAppStub,
checkForUpdateStub, checkForUpdateStub,
@ -21,14 +24,16 @@ define(function(require) {
var hasChrome, hasIdentity; var hasChrome, hasIdentity;
beforeEach(function() { beforeEach(function() {
hasChrome = !! window.chrome; hasChrome = !!window.chrome;
hasIdentity = !! window.chrome.identity; hasIdentity = !!window.chrome.identity;
window.chrome = window.chrome || {}; window.chrome = window.chrome || {};
window.chrome.identity = window.chrome.identity || {}; window.chrome.identity = window.chrome.identity || {};
// remember original module to restore later, then replace it // remember original module to restore later, then replace it
origEmailDao = appController._emailDao; origEmailDao = appController._emailDao;
origKeychain = appController._keychain;
appController._emailDao = emailDaoMock = sinon.createStubInstance(EmailDAO); appController._emailDao = emailDaoMock = sinon.createStubInstance(EmailDAO);
appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO);
appController._auth = authStub = sinon.createStubInstance(Auth); appController._auth = authStub = sinon.createStubInstance(Auth);
startAppStub = sinon.stub(appController, 'start'); startAppStub = sinon.stub(appController, 'start');
@ -48,6 +53,7 @@ define(function(require) {
// restore the app controller module // restore the app controller module
appController._emailDao = origEmailDao; appController._emailDao = origEmailDao;
appController._keychain = origKeychain;
appController.start.restore && appController.start.restore(); appController.start.restore && appController.start.restore();
appController.checkForUpdate.restore && appController.checkForUpdate.restore(); appController.checkForUpdate.restore && appController.checkForUpdate.restore();
appController.init.restore && appController.init.restore(); appController.init.restore && appController.init.restore();
@ -128,12 +134,42 @@ define(function(require) {
}); });
}); });
it('should forward to privatekey download login', function(done) {
startAppStub.yields();
authStub.getEmailAddress.yields(null, emailAddress);
initStub.yields(null, {
publicKey: 'b'
});
keychainMock.requestPrivateKeyDownload.yields(null, {});
angular.module('logintest', []);
mocks.module('logintest');
mocks.inject(function($controller, $rootScope, $location) {
location = $location;
sinon.stub(location, 'path', function(path) {
expect(path).to.equal('/login-privatekey-download');
expect(startAppStub.calledOnce).to.be.true;
expect(checkForUpdateStub.calledOnce).to.be.true;
expect(authStub.getEmailAddress.calledOnce).to.be.true;
expect(keychainMock.requestPrivateKeyDownload.calledOnce).to.be.true;
done();
});
scope = $rootScope.$new();
scope.state = {};
ctrl = $controller(LoginCtrl, {
$location: location,
$scope: scope
});
});
});
it('should forward to new device login', function(done) { it('should forward to new device login', function(done) {
startAppStub.yields(); startAppStub.yields();
authStub.getEmailAddress.yields(null, emailAddress); authStub.getEmailAddress.yields(null, emailAddress);
initStub.yields(null, { initStub.yields(null, {
publicKey: 'b' publicKey: 'b'
}); });
keychainMock.requestPrivateKeyDownload.yields();
angular.module('logintest', []); angular.module('logintest', []);
mocks.module('logintest'); mocks.module('logintest');
@ -144,6 +180,7 @@ define(function(require) {
expect(startAppStub.calledOnce).to.be.true; expect(startAppStub.calledOnce).to.be.true;
expect(checkForUpdateStub.calledOnce).to.be.true; expect(checkForUpdateStub.calledOnce).to.be.true;
expect(authStub.getEmailAddress.calledOnce).to.be.true; expect(authStub.getEmailAddress.calledOnce).to.be.true;
expect(keychainMock.requestPrivateKeyDownload.calledOnce).to.be.true;
done(); done();
}); });
scope = $rootScope.$new(); scope = $rootScope.$new();

View File

@ -0,0 +1,235 @@
define(function(require) {
'use strict';
var expect = chai.expect,
angular = require('angular'),
mocks = require('angularMocks'),
LoginPrivateKeyDownloadCtrl = require('js/controller/login-privatekey-download'),
EmailDAO = require('js/dao/email-dao'),
appController = require('js/app-controller'),
KeychainDAO = require('js/dao/keychain-dao');
describe('Login Private Key Download Controller unit test', function() {
var scope, location, ctrl,
origEmailDao, emailDaoMock,
origKeychain, keychainMock,
emailAddress = 'fred@foo.com';
beforeEach(function(done) {
// remember original module to restore later, then replace it
origEmailDao = appController._emailDao;
origKeychain = appController._keychain;
appController._emailDao = emailDaoMock = sinon.createStubInstance(EmailDAO);
appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO);
emailDaoMock._account = {
emailAddress: emailAddress
};
angular.module('login-privatekey-download-test', []);
mocks.module('login-privatekey-download-test');
mocks.inject(function($controller, $rootScope) {
scope = $rootScope.$new();
scope.state = {};
ctrl = $controller(LoginPrivateKeyDownloadCtrl, {
$location: location,
$scope: scope
});
done();
});
});
afterEach(function() {
// restore the app controller module
appController._emailDao = origEmailDao;
appController._keychain = origKeychain;
});
describe('initialization', function() {
it('should work', function() {
expect(scope.step).to.equal(1);
});
});
describe('verifyRecoveryToken', function() {
var testKeypair = {
publicKey: {
_id: 'id'
}
};
it('should fail for empty recovery token', function(done) {
scope.onError = function(err) {
expect(err).to.exist;
done();
};
scope.recoveryToken = undefined;
scope.verifyRecoveryToken();
});
it('should fail in keychain.getUserKeyPair', function(done) {
keychainMock.getUserKeyPair.yields(42);
scope.onError = function(err) {
expect(err).to.exist;
expect(keychainMock.getUserKeyPair.calledOnce).to.be.true;
done();
};
scope.recoveryToken = 'token';
scope.verifyRecoveryToken();
});
it('should fail in keychain.downloadPrivateKey', function(done) {
keychainMock.getUserKeyPair.yields(null, testKeypair);
keychainMock.downloadPrivateKey.yields(42);
scope.onError = function(err) {
expect(err).to.exist;
expect(keychainMock.getUserKeyPair.calledOnce).to.be.true;
expect(keychainMock.downloadPrivateKey.calledOnce).to.be.true;
done();
};
scope.recoveryToken = 'token';
scope.verifyRecoveryToken();
});
it('should work', function(done) {
keychainMock.getUserKeyPair.yields(null, testKeypair);
keychainMock.downloadPrivateKey.yields(null, 'encryptedPrivateKey');
scope.recoveryToken = 'token';
scope.verifyRecoveryToken(function() {
expect(scope.encryptedPrivateKey).to.equal('encryptedPrivateKey');
done();
});
});
});
describe('decryptAndStorePrivateKeyLocally', function() {
beforeEach(function() {
scope.code0 = '0';
scope.code1 = '1';
scope.code2 = '2';
scope.code3 = '3';
scope.code4 = '4';
scope.code5 = '5';
scope.encryptedPrivateKey = {
encryptedPrivateKey: 'encryptedPrivateKey'
};
scope.cachedKeypair = {
publicKey: {
_id: 'keyId'
}
};
});
it('should fail on empty code', function(done) {
scope.code0 = '';
scope.code1 = '';
scope.code2 = '';
scope.code3 = '';
scope.code4 = '';
scope.code5 = '';
scope.onError = function(err) {
expect(err).to.exist;
done();
};
scope.decryptAndStorePrivateKeyLocally();
});
it('should fail on decryptAndStorePrivateKeyLocally', function(done) {
keychainMock.decryptAndStorePrivateKeyLocally.yields(42);
scope.onError = function(err) {
expect(err).to.exist;
expect(keychainMock.decryptAndStorePrivateKeyLocally.calledOnce).to.be.true;
done();
};
scope.decryptAndStorePrivateKeyLocally();
});
it('should goto /login-existing on emailDao.unlock fail', function(done) {
keychainMock.decryptAndStorePrivateKeyLocally.yields(null, {
encryptedKey: 'keyArmored'
});
emailDaoMock.unlock.yields(42);
scope.goTo = function(location) {
expect(location).to.equal('/login-existing');
expect(keychainMock.decryptAndStorePrivateKeyLocally.calledOnce).to.be.true;
expect(emailDaoMock.unlock.calledOnce).to.be.true;
done();
};
scope.decryptAndStorePrivateKeyLocally();
});
it('should goto /desktop on emailDao.unlock success', function(done) {
keychainMock.decryptAndStorePrivateKeyLocally.yields(null, {
encryptedKey: 'keyArmored'
});
emailDaoMock.unlock.yields();
scope.goTo = function(location) {
expect(location).to.equal('/desktop');
expect(keychainMock.decryptAndStorePrivateKeyLocally.calledOnce).to.be.true;
expect(emailDaoMock.unlock.calledOnce).to.be.true;
done();
};
scope.decryptAndStorePrivateKeyLocally();
});
});
describe('goForward', function() {
it('should work in step 1', function() {
var verifyRecoveryTokenStub = sinon.stub(scope, 'verifyRecoveryToken');
verifyRecoveryTokenStub.yields();
scope.step = 1;
scope.goForward();
expect(verifyRecoveryTokenStub.calledOnce).to.be.true;
expect(scope.step).to.equal(2);
verifyRecoveryTokenStub.restore();
});
it('should work in step 2', function() {
var decryptAndStorePrivateKeyLocallyStub = sinon.stub(scope, 'decryptAndStorePrivateKeyLocally');
decryptAndStorePrivateKeyLocallyStub.returns();
scope.step = 2;
scope.goForward();
expect(decryptAndStorePrivateKeyLocallyStub.calledOnce).to.be.true;
decryptAndStorePrivateKeyLocallyStub.restore();
});
});
describe('goTo', function() {
it('should work', function(done) {
mocks.inject(function($controller, $rootScope, $location) {
location = $location;
sinon.stub(location, 'path', function(path) {
expect(path).to.equal('/desktop');
done();
});
scope = $rootScope.$new();
scope.state = {};
ctrl = $controller(LoginPrivateKeyDownloadCtrl, {
$location: location,
$scope: scope
});
});
scope.goTo('/desktop');
});
});
});
});

View File

@ -48,6 +48,8 @@ function startTests() {
'test/unit/login-existing-ctrl-test', 'test/unit/login-existing-ctrl-test',
'test/unit/login-initial-ctrl-test', 'test/unit/login-initial-ctrl-test',
'test/unit/login-new-device-ctrl-test', 'test/unit/login-new-device-ctrl-test',
'test/unit/login-privatekey-download-ctrl-test',
'test/unit/privatekey-upload-ctrl-test',
'test/unit/login-ctrl-test', 'test/unit/login-ctrl-test',
'test/unit/read-ctrl-test', 'test/unit/read-ctrl-test',
'test/unit/navigation-ctrl-test', 'test/unit/navigation-ctrl-test',

View File

@ -92,7 +92,7 @@ define(function(require) {
publicKeyArmored: pubkey publicKeyArmored: pubkey
}, function(err) { }, function(err) {
expect(err).to.exist; expect(err).to.exist;
expect(err.errMsg).to.equal('Incorrect passphrase!'); expect(err.message).to.equal('Incorrect passphrase!');
pgp.exportKeys(function(err, keys) { pgp.exportKeys(function(err, keys) {
expect(err).to.exist; expect(err).to.exist;

View File

@ -57,7 +57,8 @@ define(function(require) {
privkeyDao.uploadDeviceSecret({ privkeyDao.uploadDeviceSecret({
userId: emailAddress, userId: emailAddress,
deviceName: deviceName, deviceName: deviceName,
encryptedDeviceSecret: 'asdf' encryptedDeviceSecret: 'asdf',
iv: 'iv'
}, function(err) { }, function(err) {
expect(err).to.not.exist; expect(err).to.not.exist;
done(); done();
@ -65,9 +66,9 @@ define(function(require) {
}); });
}); });
describe('requestAuthSessionKeys', function() { describe('requestAuthSessionKey', function() {
it('should fail due to invalid args', function(done) { it('should fail due to invalid args', function(done) {
privkeyDao.requestAuthSessionKeys({}, function(err) { privkeyDao.requestAuthSessionKey({}, function(err) {
expect(err).to.exist; expect(err).to.exist;
done(); done();
}); });
@ -76,7 +77,7 @@ define(function(require) {
it('should work', function(done) { it('should work', function(done) {
restDaoStub.post.withArgs(undefined, '/auth/user/' + emailAddress).yields(); restDaoStub.post.withArgs(undefined, '/auth/user/' + emailAddress).yields();
privkeyDao.requestAuthSessionKeys({ privkeyDao.requestAuthSessionKey({
userId: emailAddress userId: emailAddress
}, function(err) { }, function(err) {
expect(err).to.not.exist; expect(err).to.not.exist;
@ -96,13 +97,17 @@ define(function(require) {
it('should work', function(done) { it('should work', function(done) {
var sessionId = '1'; var sessionId = '1';
restDaoStub.put.withArgs('asdf', '/auth/user/' + emailAddress + '/session/' + sessionId).yields(); var options = {
privkeyDao.verifyAuthentication({
userId: emailAddress, userId: emailAddress,
sessionId: sessionId, sessionId: sessionId,
encryptedChallenge: 'asdf' encryptedChallenge: 'asdf',
}, function(err) { encryptedDeviceSecret: 'qwer',
iv: ' iv'
};
restDaoStub.put.withArgs(options, '/auth/user/' + emailAddress + '/session/' + sessionId).yields();
privkeyDao.verifyAuthentication(options, function(err) {
expect(err).to.not.exist; expect(err).to.not.exist;
done(); done();
}); });
@ -118,16 +123,18 @@ define(function(require) {
}); });
it('should work', function(done) { it('should work', function(done) {
var key = { var options = {
_id: '12345' _id: '12345',
userId: emailAddress,
encryptedPrivateKey: 'asdf',
sessionId: '1',
salt: 'salt',
iv: 'iv'
}; };
restDaoStub.post.withArgs(key, '/privatekey/user/' + emailAddress + '/key/' + key._id).yields(); restDaoStub.post.withArgs(options, '/privatekey/user/' + emailAddress + '/session/' + options.sessionId).yields();
privkeyDao.upload({ privkeyDao.upload(options, function(err) {
userId: emailAddress,
encryptedPrivateKey: key
}, function(err) {
expect(err).to.not.exist; expect(err).to.not.exist;
done(); done();
}); });

View File

@ -0,0 +1,260 @@
define(function(require) {
'use strict';
var expect = chai.expect,
angular = require('angular'),
mocks = require('angularMocks'),
PrivateKeyUploadCtrl = require('js/controller/privatekey-upload'),
appController = require('js/app-controller'),
KeychainDAO = require('js/dao/keychain-dao'),
PGP = require('js/crypto/pgp');
describe('Private Key Upload Controller unit test', function() {
var scope, location, ctrl,
origEmailDao, emailDaoMock,
origKeychain, keychainMock,
pgpStub,
emailAddress = 'fred@foo.com';
beforeEach(function(done) {
// remember original module to restore later, then replace it
origEmailDao = appController._emailDao;
appController._emailDao = emailDaoMock = {
_account: {
emailAddress: emailAddress
}
};
origKeychain = appController._keychain;
appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO);
keychainMock._pgp = pgpStub = sinon.createStubInstance(PGP);
angular.module('login-privatekey-download-test', []);
mocks.module('login-privatekey-download-test');
mocks.inject(function($controller, $rootScope) {
scope = $rootScope.$new();
scope.state = {};
ctrl = $controller(PrivateKeyUploadCtrl, {
$location: location,
$scope: scope
});
done();
});
});
afterEach(function() {
// restore the app controller module
appController._keychain = origKeychain;
appController._emailDao = origEmailDao;
});
describe('checkServerForKey', function() {
var keyParams = {
userId: emailAddress,
_id: 'keyId'
};
it('should fail', function(done) {
pgpStub.getKeyParams.returns(keyParams);
keychainMock.requestPrivateKeyDownload.yields(42);
scope.onError = function(err) {
expect(err).to.exist;
expect(keychainMock.requestPrivateKeyDownload.calledOnce).to.be.true;
done();
};
scope.checkServerForKey();
});
it('should return true', function(done) {
pgpStub.getKeyParams.returns(keyParams);
keychainMock.requestPrivateKeyDownload.yields(null, true);
scope.checkServerForKey(function(privateKeySynced) {
expect(privateKeySynced).to.be.true;
done();
});
});
it('should return undefined', function(done) {
pgpStub.getKeyParams.returns(keyParams);
keychainMock.requestPrivateKeyDownload.yields(null, false);
scope.checkServerForKey(function(privateKeySynced) {
expect(privateKeySynced).to.be.undefined;
done();
});
});
});
describe('displayUploadUi', function() {
it('should work', function() {
var generateCodeStub = sinon.stub(scope, 'generateCode');
generateCodeStub.returns('asdf');
scope.displayUploadUi();
expect(scope.step).to.equal(1);
expect(scope.code).to.equal('asdf');
generateCodeStub.restore();
});
});
describe('generateCode', function() {
it('should work', function() {
expect(scope.generateCode().length).to.equal(24);
});
});
describe('verifyCode', function() {
it('should fail for wrong code', function() {
scope.code0 = 'b';
scope.code1 = 'b';
scope.code2 = 'b';
scope.code3 = 'b';
scope.code4 = 'b';
scope.code5 = 'b';
scope.code = 'aaaaaa';
scope.onError = function() {};
expect(scope.verifyCode()).to.be.false;
});
it('should work', function() {
scope.code0 = 'a';
scope.code1 = 'a';
scope.code2 = 'a';
scope.code3 = 'a';
scope.code4 = 'a';
scope.code5 = 'a';
scope.code = 'aaaaaa';
scope.onError = function() {};
expect(scope.verifyCode()).to.be.false;
});
});
describe('setDeviceName', function() {
it('should work', function(done) {
keychainMock.setDeviceName.yields();
scope.setDeviceName(done);
});
});
describe('encryptAndUploadKey', function() {
it('should fail due to keychain.registerDevice', function(done) {
keychainMock.registerDevice.yields(42);
scope.onError = function(err) {
expect(err).to.exist;
expect(keychainMock.registerDevice.calledOnce).to.be.true;
done();
};
scope.encryptAndUploadKey();
});
it('should work', function(done) {
keychainMock.registerDevice.yields();
keychainMock.uploadPrivateKey.yields();
scope.encryptAndUploadKey(function(err) {
expect(err).to.not.exist;
expect(keychainMock.registerDevice.calledOnce).to.be.true;
expect(keychainMock.uploadPrivateKey.calledOnce).to.be.true;
done();
});
});
});
describe('goBack', function() {
it('should work', function() {
scope.step = 2;
scope.goBack();
expect(scope.step).to.equal(1);
});
it('should not work for < 2', function() {
scope.step = 1;
scope.goBack();
expect(scope.step).to.equal(1);
});
});
describe('goForward', function() {
var verifyCodeStub, setDeviceNameStub, encryptAndUploadKeyStub;
beforeEach(function() {
verifyCodeStub = sinon.stub(scope, 'verifyCode');
setDeviceNameStub = sinon.stub(scope, 'setDeviceName');
encryptAndUploadKeyStub = sinon.stub(scope, 'encryptAndUploadKey');
});
afterEach(function() {
verifyCodeStub.restore();
setDeviceNameStub.restore();
encryptAndUploadKeyStub.restore();
});
it('should work for < 2', function() {
scope.step = 1;
scope.goForward();
expect(scope.step).to.equal(2);
});
it('should work for 2', function() {
verifyCodeStub.returns(true);
scope.step = 2;
scope.goForward();
expect(scope.step).to.equal(3);
});
it('should not work for 2 when code invalid', function() {
verifyCodeStub.returns(false);
scope.step = 2;
scope.goForward();
expect(scope.step).to.equal(2);
});
it('should fail for 3 due to error in setDeviceName', function(done) {
scope.step = 3;
setDeviceNameStub.yields(42);
scope.onError = function(err) {
expect(err).to.exist;
expect(scope.step).to.equal(3);
done();
};
scope.goForward();
});
it('should fail for 3 due to error in encryptAndUploadKey', function(done) {
scope.step = 3;
setDeviceNameStub.yields();
encryptAndUploadKeyStub.yields(42);
scope.onError = function(err) {
expect(err).to.exist;
expect(scope.step).to.equal(4);
done();
};
scope.goForward();
});
it('should work for 3', function(done) {
scope.step = 3;
setDeviceNameStub.yields();
encryptAndUploadKeyStub.yields();
scope.onError = function(err) {
expect(err.title).to.equal('Success');
expect(scope.step).to.equal(4);
done();
};
scope.goForward();
});
});
});
});

View File

@ -16,7 +16,7 @@ define(function(require) {
emailAddress, keySize, cryptoMock, keychainMock; emailAddress, keySize, cryptoMock, keychainMock;
beforeEach(function() { beforeEach(function() {
appController._crypto = cryptoMock = sinon.createStubInstance(PGP); appController._pgp = cryptoMock = sinon.createStubInstance(PGP);
appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO); appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO);
dummyFingerprint = '3A2D39B4E1404190B8B949DE7D7E99036E712926'; dummyFingerprint = '3A2D39B4E1404190B8B949DE7D7E99036E712926';