From b5fda88b8a696f9f55edde851d5259741fdf0190 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Fri, 13 Jun 2014 12:33:30 +0200 Subject: [PATCH] Implement client side key sync protocol and ui --- src/js/app-config.js | 2 + src/js/app-controller.js | 10 +- src/js/app.js | 9 + src/js/controller/account.js | 2 +- src/js/controller/contacts.js | 2 +- src/js/controller/login-new-device.js | 2 +- .../controller/login-privatekey-download.js | 103 +++++++ src/js/controller/login.js | 25 +- src/js/controller/privatekey-upload.js | 170 ++++++++++++ src/js/controller/read.js | 6 +- src/js/controller/set-passphrase.js | 2 +- src/js/controller/write.js | 6 +- src/js/crypto/pbkdf2.js | 2 +- src/js/crypto/pgp.js | 76 ++--- src/js/dao/email-dao.js | 22 +- src/js/dao/keychain-dao.js | 107 ++++--- src/js/dao/privatekey-dao.js | 60 ++-- src/sass/all.scss | 1 + src/sass/views/_account.scss | 6 +- src/sass/views/_login.scss | 11 + src/sass/views/_privatekey-upload.scss | 17 ++ src/tpl/account.html | 2 +- src/tpl/desktop.html | 4 + src/tpl/login-privatekey-download.html | 41 +++ src/tpl/navigation.html | 1 + src/tpl/privatekey-upload.html | 46 ++++ test/unit/account-ctrl-test.js | 10 +- test/unit/app-controller-test.js | 2 +- test/unit/contacts-ctrl-test.js | 16 +- test/unit/email-dao-test.js | 2 +- test/unit/keychain-dao-test.js | 158 ++++++++--- test/unit/login-ctrl-test.js | 45 ++- .../login-privatekey-download-ctrl-test.js | 235 ++++++++++++++++ test/unit/main.js | 2 + test/unit/pgp-test.js | 2 +- test/unit/privatekey-dao-test.js | 39 +-- test/unit/privatekey-upload-ctrl-test.js | 260 ++++++++++++++++++ test/unit/set-passphrase-ctrl-test.js | 2 +- 38 files changed, 1296 insertions(+), 212 deletions(-) create mode 100644 src/js/controller/login-privatekey-download.js create mode 100644 src/js/controller/privatekey-upload.js create mode 100644 src/sass/views/_privatekey-upload.scss create mode 100644 src/tpl/login-privatekey-download.html create mode 100644 src/tpl/privatekey-upload.html create mode 100644 test/unit/login-privatekey-download-ctrl-test.js create mode 100644 test/unit/privatekey-upload-ctrl-test.js diff --git a/src/js/app-config.js b/src/js/app-config.js index a19688f..b841d80 100644 --- a/src/js/app-config.js +++ b/src/js/app-config.js @@ -25,6 +25,8 @@ define(function(require) { */ app.config = { cloudUrl: cloudUrl || 'https://keys.whiteout.io', + privkeyServerUrl: 'https://keychain-test.whiteout.io', + serverPrivateKeyId: 'EE342F0DDBB0F3BE', symKeySize: 256, symIvSize: 96, asymKeySize: 2048, diff --git a/src/js/app-controller.js b/src/js/app-controller.js index b799ee4..15f5ef5 100644 --- a/src/js/app-controller.js +++ b/src/js/app-controller.js @@ -12,6 +12,7 @@ define(function(require) { OutboxBO = require('js/bo/outbox'), mailreader = require('mailreader'), ImapClient = require('imap-client'), + Crypto = require('js/crypto/crypto'), RestDAO = require('js/dao/rest-dao'), EmailDAO = require('js/dao/email-dao'), appConfig = require('js/app-config'), @@ -20,6 +21,7 @@ define(function(require) { KeychainDAO = require('js/dao/keychain-dao'), PublicKeyDAO = require('js/dao/publickey-dao'), LawnchairDAO = require('js/dao/lawnchair-dao'), + PrivateKeyDAO = require('js/dao/privatekey-dao'), InvitationDAO = require('js/dao/invitation-dao'), DeviceStorageDAO = require('js/dao/devicestorage-dao'), UpdateHandler = require('js/util/update/update-handler'); @@ -55,7 +57,7 @@ define(function(require) { }; 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 mailreader.startWorker(config.workerPath + '/../lib/mailreader-parser-worker.js'); @@ -64,9 +66,12 @@ define(function(require) { restDao = new RestDAO(); lawnchairDao = new LawnchairDAO(); pubkeyDao = new PublicKeyDAO(restDao); + privkeyDao = new PrivateKeyDAO(new RestDAO(config.privkeyServerUrl)); 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) { var message = params.newKey ? str.updatePublicKeyMsgNewKey : str.updatePublicKeyMsgRemovedKey; message = message.replace('{0}', params.userId); @@ -85,7 +90,6 @@ define(function(require) { self._auth = new Auth(appConfigStore, oauth, new RestDAO('/ca')); self._userStorage = userStorage = new DeviceStorageDAO(lawnchairDao); self._invitationDao = new InvitationDAO(restDao); - self._crypto = pgp = new PGP(); self._pgpbuilder = pgpbuilder = new PgpBuilder(); self._emailDao = emailDao = new EmailDAO(keychain, pgp, userStorage, pgpbuilder, mailreader); self._outboxBo = new OutboxBO(emailDao, keychain, userStorage); diff --git a/src/js/app.js b/src/js/app.js index af72bca..37a70ed 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -8,12 +8,14 @@ requirejs([ 'js/controller/add-account', 'js/controller/account', 'js/controller/set-passphrase', + 'js/controller/privatekey-upload', 'js/controller/contacts', 'js/controller/about', 'js/controller/login', 'js/controller/login-initial', 'js/controller/login-new-device', 'js/controller/login-existing', + 'js/controller/login-privatekey-download', 'js/controller/mail-list', 'js/controller/read', 'js/controller/write', @@ -31,12 +33,14 @@ requirejs([ AddAccountCtrl, AccountCtrl, SetPassphraseCtrl, + PrivateKeyUploadCtrl, ContactsCtrl, AboutCtrl, LoginCtrl, LoginInitialCtrl, LoginNewDeviceCtrl, LoginExistingCtrl, + LoginPrivateKeyDownloadCtrl, MailListCtrl, ReadCtrl, WriteCtrl, @@ -89,6 +93,10 @@ requirejs([ templateUrl: 'tpl/login-new-device.html', controller: LoginNewDeviceCtrl }); + $routeProvider.when('/login-privatekey-download', { + templateUrl: 'tpl/login-privatekey-download.html', + controller: LoginPrivateKeyDownloadCtrl + }); $routeProvider.when('/desktop', { templateUrl: 'tpl/desktop.html', controller: NavigationCtrl @@ -113,6 +121,7 @@ requirejs([ app.controller('MailListCtrl', MailListCtrl); app.controller('AccountCtrl', AccountCtrl); app.controller('SetPassphraseCtrl', SetPassphraseCtrl); + app.controller('PrivateKeyUploadCtrl', PrivateKeyUploadCtrl); app.controller('ContactsCtrl', ContactsCtrl); app.controller('AboutCtrl', AboutCtrl); app.controller('DialogCtrl', DialogCtrl); diff --git a/src/js/controller/account.js b/src/js/controller/account.js index 3eceaaf..a811175 100644 --- a/src/js/controller/account.js +++ b/src/js/controller/account.js @@ -12,7 +12,7 @@ define(function(require) { var AccountCtrl = function($scope) { userId = appController._emailDao._account.emailAddress; keychain = appController._keychain; - pgp = appController._crypto; + pgp = appController._pgp; $scope.state.account = { toggle: function(to) { diff --git a/src/js/controller/contacts.js b/src/js/controller/contacts.js index 9fb6791..f66f01e 100644 --- a/src/js/controller/contacts.js +++ b/src/js/controller/contacts.js @@ -12,7 +12,7 @@ define(function(require) { var ContactsCtrl = function($scope) { keychain = appController._keychain, - pgp = appController._crypto; + pgp = appController._pgp; $scope.state.contacts = { toggle: function(to) { diff --git a/src/js/controller/login-new-device.js b/src/js/controller/login-new-device.js index 3918ff8..bac997a 100644 --- a/src/js/controller/login-new-device.js +++ b/src/js/controller/login-new-device.js @@ -6,7 +6,7 @@ define(function(require) { var LoginExistingCtrl = function($scope, $location) { var emailDao = appController._emailDao, - pgp = appController._crypto; + pgp = appController._pgp; $scope.incorrect = false; diff --git a/src/js/controller/login-privatekey-download.js b/src/js/controller/login-privatekey-download.js new file mode 100644 index 0000000..4ea5532 --- /dev/null +++ b/src/js/controller/login-privatekey-download.js @@ -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; +}); \ No newline at end of file diff --git a/src/js/controller/login.js b/src/js/controller/login.js index ac96b47..37c7caf 100644 --- a/src/js/controller/login.js +++ b/src/js/controller/login.js @@ -52,9 +52,28 @@ define(function(require) { if (typeof availableKeys === 'undefined') { // no public key available, start onboarding process goTo('/login-initial'); - } else if (!availableKeys.privateKey) { - // no private key, import key - goTo('/login-new-device'); + + } else if (availableKeys && !availableKeys.privateKey) { + // 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 { // public and private key available, try empty passphrase appController._emailDao.unlock({ diff --git a/src/js/controller/privatekey-upload.js b/src/js/controller/privatekey-upload.js new file mode 100644 index 0000000..020a578 --- /dev/null +++ b/src/js/controller/privatekey-upload.js @@ -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; +}); \ No newline at end of file diff --git a/src/js/controller/read.js b/src/js/controller/read.js index 8b3245b..5a803d8 100644 --- a/src/js/controller/read.js +++ b/src/js/controller/read.js @@ -5,7 +5,7 @@ define(function(require) { download = require('js/util/download'), angular = require('angular'), str = require('js/app-config').string, - emailDao, invitationDao, outbox, crypto, keychain; + emailDao, invitationDao, outbox, pgp, keychain; // // Controller @@ -16,7 +16,7 @@ define(function(require) { emailDao = appController._emailDao; invitationDao = appController._invitationDao; outbox = appController._outboxBo; - crypto = appController._crypto; + pgp = appController._pgp; keychain = appController._keychain; // set default value so that the popover height is correct on init @@ -47,7 +47,7 @@ define(function(require) { return; } - var fpr = crypto.getFingerprint(pubkey.publicKey); + var fpr = pgp.getFingerprint(pubkey.publicKey); var formatted = fpr.slice(32); $scope.keyId = 'PGP key: ' + formatted; diff --git a/src/js/controller/set-passphrase.js b/src/js/controller/set-passphrase.js index 811a55b..13fc7ad 100644 --- a/src/js/controller/set-passphrase.js +++ b/src/js/controller/set-passphrase.js @@ -10,7 +10,7 @@ define(function(require) { var SetPassphraseCtrl = function($scope) { keychain = appController._keychain; - pgp = appController._crypto; + pgp = appController._pgp; $scope.state.setPassphrase = { toggle: function(to) { diff --git a/src/js/controller/write.js b/src/js/controller/write.js index 4325da7..4b84054 100644 --- a/src/js/controller/write.js +++ b/src/js/controller/write.js @@ -7,14 +7,14 @@ define(function(require) { aes = require('js/crypto/aes-gcm'), util = require('js/crypto/util'), str = require('js/app-config').string, - crypto, emailDao, outbox, keychainDao; + pgp, emailDao, outbox, keychainDao; // // Controller // var WriteCtrl = function($scope, $filter) { - crypto = appController._crypto; + pgp = appController._pgp; emailDao = appController._emailDao, outbox = appController._outboxBo; keychainDao = appController._keychain; @@ -218,7 +218,7 @@ define(function(require) { return; } - var fpr = crypto.getFingerprint(recipient.key.publicKey); + var fpr = pgp.getFingerprint(recipient.key.publicKey); var formatted = fpr.slice(32); $scope.keyId = formatted; diff --git a/src/js/crypto/pbkdf2.js b/src/js/crypto/pbkdf2.js index a6ece33..3a0b12a 100644 --- a/src/js/crypto/pbkdf2.js +++ b/src/js/crypto/pbkdf2.js @@ -7,7 +7,7 @@ define(['forge'], function(forge) { 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} salt The base64 encoded salt * @param {String} keySize The key size in bits diff --git a/src/js/crypto/pgp.js b/src/js/crypto/pgp.js index 2fcf08c..2719925 100644 --- a/src/js/crypto/pgp.js +++ b/src/js/crypto/pgp.js @@ -20,9 +20,7 @@ define(function(require) { var userId, passphrase; if (!util.emailRegEx.test(options.emailAddress) || !options.keySize) { - callback({ - errMsg: 'Crypto init failed. Not all options set!' - }); + callback(new Error('Crypto init failed. Not all options set!')); return; } @@ -38,10 +36,7 @@ define(function(require) { function onGenerated(err, keys) { if (err) { - callback({ - errMsg: 'Keygeneration failed!', - err: err - }); + callback(new Error('Keygeneration failed!')); return; } @@ -146,9 +141,7 @@ define(function(require) { // check options if (!options.privateKeyArmored || !options.publicKeyArmored) { - callback({ - errMsg: 'Importing keys failed. Not all options set!' - }); + callback(new Error('Importing keys failed. Not all options set!')); return; } @@ -163,18 +156,14 @@ define(function(require) { this._privateKey = openpgp.key.readArmored(options.privateKeyArmored).keys[0]; } catch (e) { resetKeys(); - callback({ - errMsg: 'Importing keys failed. Parsing error!' - }); + callback(new Error('Importing keys failed. Parsing error!')); return; } // decrypt private key with passphrase if (!this._privateKey.decrypt(options.passphrase)) { resetKeys(); - callback({ - errMsg: 'Incorrect passphrase!' - }); + callback(new Error('Incorrect passphrase!')); return; } @@ -183,9 +172,7 @@ define(function(require) { privKeyId = this._privateKey.getKeyPacket().getKeyId().toHex(); if (!pubKeyId || !privKeyId || pubKeyId !== privKeyId) { resetKeys(); - callback({ - errMsg: 'Key IDs dont match!' - }); + callback(new Error('Key IDs dont match!')); return; } @@ -197,9 +184,7 @@ define(function(require) { */ PGP.prototype.exportKeys = function(callback) { if (!this._publicKey || !this._privateKey) { - callback({ - errMsg: 'Could not export keys!' - }); + callback(new Error('Could not export keys!')); return; } @@ -220,9 +205,7 @@ define(function(require) { newPassphrase = (options.newPassphrase) ? options.newPassphrase : undefined; if (!options.privateKeyArmored) { - callback({ - errMsg: 'Private key must be specified to change passphrase!' - }); + callback(new Error('Private key must be specified to change passphrase!')); return; } @@ -236,17 +219,13 @@ define(function(require) { try { privKey = openpgp.key.readArmored(options.privateKeyArmored).keys[0]; } catch (e) { - callback({ - errMsg: 'Importing key failed. Parsing error!' - }); + callback(new Error('Importing key failed. Parsing error!')); return; } // decrypt private key with passphrase if (!privKey.decrypt(options.oldPassphrase)) { - callback({ - errMsg: 'Old passphrase incorrect!' - }); + callback(new Error('Old passphrase incorrect!')); return; } @@ -258,17 +237,13 @@ define(function(require) { } newKeyArmored = privKey.armor(); } catch (e) { - callback({ - errMsg: 'Setting new passphrase failed!' - }); + callback(new Error('Setting new passphrase failed!')); return; } // check if new passphrase really works if (!privKey.decrypt(newPassphrase)) { - callback({ - errMsg: 'Decrypting key with new passphrase failed!' - }); + callback(new Error('Decrypting key with new passphrase failed!')); return; } @@ -283,9 +258,7 @@ define(function(require) { // check keys if (!this._privateKey || publicKeysArmored.length < 1) { - callback({ - errMsg: 'Error encrypting. Keys must be set!' - }); + callback(new Error('Error encrypting. Keys must be set!')); return; } @@ -295,10 +268,7 @@ define(function(require) { publicKeys = publicKeys.concat(openpgp.key.readArmored(pubkeyArmored).keys); }); } catch (err) { - callback({ - errMsg: 'Error encrypting plaintext!', - err: err - }); + callback(new Error('Error encrypting plaintext!')); return; } @@ -314,9 +284,7 @@ define(function(require) { // check keys if (!this._privateKey || !publicKeyArmored) { - callback({ - errMsg: 'Error decrypting. Keys must be set!' - }); + callback(new Error('Error decrypting. Keys must be set!')); return; } @@ -325,10 +293,7 @@ define(function(require) { publicKeys = openpgp.key.readArmored(publicKeyArmored).keys; message = openpgp.message.readArmored(ciphertext); } catch (err) { - callback({ - errMsg: 'Error decrypting PGP message!', - err: err - }); + callback(new Error('Error decrypting PGP message!')); return; } @@ -337,10 +302,7 @@ define(function(require) { function onDecrypted(err, decrypted) { if (err) { - callback({ - errMsg: 'Error decrypting PGP message!', - err: err - }); + callback(new Error('Error decrypting PGP message!')); return; } @@ -352,9 +314,7 @@ define(function(require) { } }); if (!signaturesValid) { - callback({ - errMsg: 'Verifying PGP signature failed!' - }); + callback(new Error('Verifying PGP signature failed!')); return; } diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index 57d2eb7..2d8efa8 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -11,14 +11,14 @@ define(function(require) { * 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} crypto Orchestrates decryption + * @param {Object} pgp Orchestrates decryption * @param {Object} devicestorage Handles persistence to the local indexed db * @param {Object} pgpbuilder Generates and encrypts MIME and SMTP messages * @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._crypto = crypto; + this._pgp = pgp; this._devicestorage = devicestorage; this._pgpbuilder = pgpbuilder; this._mailreader = mailreader; @@ -105,7 +105,7 @@ define(function(require) { } // no keypair for is stored for the user... generate a new one - self._crypto.generateKeys({ + self._pgp.generateKeys({ emailAddress: self._account.emailAddress, keySize: self._account.asymKeySize, passphrase: options.passphrase @@ -121,8 +121,8 @@ define(function(require) { function handleExistingKeypair(keypair) { var privKeyParams, pubKeyParams; try { - privKeyParams = self._crypto.getKeyParams(keypair.privateKey.encryptedKey); - pubKeyParams = self._crypto.getKeyParams(keypair.publicKey.publicKey); + privKeyParams = self._pgp.getKeyParams(keypair.privateKey.encryptedKey); + pubKeyParams = self._pgp.getKeyParams(keypair.publicKey.publicKey); } catch (e) { callback(new Error('Error reading key params!')); return; @@ -148,7 +148,7 @@ define(function(require) { } // import existing key pair into crypto module - self._crypto.importKeys({ + self._pgp.importKeys({ passphrase: options.passphrase, privateKeyArmored: keypair.privateKey.encryptedKey, publicKeyArmored: keypair.publicKey.publicKey @@ -159,14 +159,14 @@ define(function(require) { } // set decrypted privateKey to pgpMailer - self._pgpbuilder._privateKey = self._crypto._privateKey; + self._pgpbuilder._privateKey = self._pgp._privateKey; callback(); }); } function handleGenerated(generatedKeypair) { // import the new key pair into crypto module - self._crypto.importKeys({ + self._pgp.importKeys({ passphrase: options.passphrase, privateKeyArmored: generatedKeypair.privateKeyArmored, publicKeyArmored: generatedKeypair.publicKeyArmored @@ -196,7 +196,7 @@ define(function(require) { } // set decrypted privateKey to pgpMailer - self._pgpbuilder._privateKey = self._crypto._privateKey; + self._pgpbuilder._privateKey = self._pgp._privateKey; callback(); }); }); @@ -816,7 +816,7 @@ define(function(require) { // get the receiver's public key to check the message signature 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) { showError(err.errMsg || err.message || 'An error occurred during the decryption.'); return; diff --git a/src/js/dao/keychain-dao.js b/src/js/dao/keychain-dao.js index c0d7127..0de689d 100644 --- a/src/js/dao/keychain-dao.js +++ b/src/js/dao/keychain-dao.js @@ -258,6 +258,11 @@ define(function(require) { * @param {Function} callback(error) */ KeychainDAO.prototype.setDeviceName = function(deviceName, callback) { + if (!deviceName) { + callback(new Error('Please set a device name!')); + return; + } + this._localDbDao.persist(DB_DEVICENAME, deviceName, callback); }; @@ -323,7 +328,8 @@ define(function(require) { * @param {Function} callback(error) */ KeychainDAO.prototype.registerDevice = function(options, callback) { - var self = this; + var self = this, + devName; // check if deviceName is already persisted in storage self.getDeviceName(function(err, deviceName) { @@ -336,6 +342,8 @@ define(function(require) { }); function requestDeviceRegistration(deviceName) { + devName = deviceName; + // request device registration session key self._privateKeyDao.requestDeviceRegistration({ userId: options.userId, @@ -357,15 +365,20 @@ define(function(require) { function decryptSessionKey(regSessionKey) { // 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) { callback(err); return; } + if (!serverPubkey || !serverPubkey.publicKey) { + callback(new Error('Server public key for device registration not found!')); + return; + } + // decrypt the session key var ct = regSessionKey.encryptedRegSessionKey; - self._pgp.decrypt(ct, serverPubkey, function(err, decrypedSessionKey) { + self._pgp.decrypt(ct, serverPubkey.publicKey, function(err, decrypedSessionKey) { if (err) { callback(err); return; @@ -384,10 +397,10 @@ define(function(require) { return; } - // generate deviceSecretIv - var deviceSecretIv = util.random(config.symIvSize); + // generate iv + var iv = util.random(config.symIvSize); // encrypt deviceSecret - self._crypto.encrypt(deviceSecret, regSessionKey, deviceSecretIv, function(err, encryptedDeviceSecret) { + self._crypto.encrypt(deviceSecret, regSessionKey, iv, function(err, encryptedDeviceSecret) { if (err) { callback(err); return; @@ -396,9 +409,9 @@ define(function(require) { // upload encryptedDeviceSecret self._privateKeyDao.uploadDeviceSecret({ userId: options.userId, - deviceName: options.deviceName, + deviceName: devName, encryptedDeviceSecret: encryptedDeviceSecret, - deviceSecretIv: deviceSecretIv + iv: iv }, callback); }); }); @@ -420,7 +433,7 @@ define(function(require) { sessionId; // request auth session key required for upload - self._privateKeyDao.requestAuthSessionKeys({ + self._privateKeyDao.requestAuthSessionKey({ userId: userId }, function(err, authSessionKey) { if (err) { @@ -441,12 +454,17 @@ define(function(require) { function decryptSessionKey(authSessionKey) { // 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) { callback(err); return; } + if (!serverPubkey || !serverPubkey.publicKey) { + callback(new Error('Server public key for authentication not found!')); + return; + } + // decrypt the session key var ct1 = authSessionKey.encryptedAuthSessionKey; self._pgp.decrypt(ct1, serverPubkey.publicKey, function(err, decryptedSessionKey) { @@ -456,7 +474,7 @@ define(function(require) { } // decrypt the challenge - var ct2 = authSessionKey.encryptedAuthSessionKey; + var ct2 = authSessionKey.encryptedChallenge; self._pgp.decrypt(ct2, serverPubkey.publicKey, function(err, decryptedChallenge) { if (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 self._privateKeyDao.verifyAuthentication({ userId: userId, sessionId: sessionId, - encryptedChallenge: encryptedChallenge + encryptedChallenge: response.encryptedChallenge, + encryptedDeviceSecret: response.encryptedDeviceSecret, + iv: response.iv }, function(err) { if (err) { callback(err); @@ -565,7 +585,7 @@ define(function(require) { var privkeyId = keypair.privateKey._id, 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); self._crypto.encrypt(pgpBlock, encryptionKey, iv, function(err, ct) { if (err) { @@ -575,6 +595,7 @@ define(function(require) { var payload = { _id: privkeyId, + userId: options.userId, encryptedPrivateKey: ct, salt: salt, 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. - * @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) */ - KeychainDAO.prototype.requestPrivateKeyDownload = function(userId, callback) { - this._privateKeyDao.requestDownload({ - userId: userId - }, callback); + KeychainDAO.prototype.requestPrivateKeyDownload = function(options, callback) { + this._privateKeyDao.requestDownload(options, 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. + * @param {String} options._id The private PGP key id * @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.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.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) { var self = this, code = options.code, 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!')); return; } @@ -670,21 +690,44 @@ define(function(require) { function decryptAndStore(derivedKey) { // decrypt the private key with the derived key - var pt = options.encryptedPrivkey, + var ct = options.encryptedPrivateKey, iv = options.iv; - self._crypto.decrypt(pt, derivedKey, iv, function(err, pgpBlock) { + self._crypto.decrypt(ct, derivedKey, iv, function(err, privateKeyArmored) { if (err) { - callback(err); + callback(new Error('Invalid keychain code!')); return; } - // store private key locally - self.saveLocalPrivateKey({ - _id: options.keyId, + // validate pgp key + var keyParams; + 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, - encryptedKey: pgpBlock - }, callback); + encryptedKey: privateKeyArmored + }; + + // store private key locally + self.saveLocalPrivateKey(keyObject, function(err) { + if (err) { + callback(err); + return; + } + + callback(null, keyObject); + }); }); } }; diff --git a/src/js/dao/privatekey-dao.js b/src/js/dao/privatekey-dao.js index f3cf24d..782965c 100644 --- a/src/js/dao/privatekey-dao.js +++ b/src/js/dao/privatekey-dao.js @@ -30,21 +30,22 @@ define(function() { /** * Authenticate device registration by uploading the deviceSecret encrypted with the regSessionKeys. - * @param {String} options.userId The user's email address - * @param {String} options.deviceName The device's memorable name - * @param {Object} options.encryptedDeviceSecret {encryptedDeviceSecret:[base64 encoded]} + * @param {String} options.userId The user's email address + * @param {String} options.deviceName The device's memorable name + * @param {String} options.encryptedDeviceSecret The base64 encoded encrypted device secret + * @param {String} options.iv The iv used for encryption * @param {Function} callback(error) */ PrivateKeyDAO.prototype.uploadDeviceSecret = function(options, callback) { var uri; - if (!options.userId || !options.deviceName || !options.encryptedDeviceSecret) { + if (!options.userId || !options.deviceName || !options.encryptedDeviceSecret || !options.iv) { callback(new Error('Incomplete arguments!')); return; } 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. * @param {String} options.userId The user's email address * @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; if (!options.userId) { @@ -71,44 +72,50 @@ define(function() { /** * 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 {Object} options.encryptedChallenge The server's challenge encrypted using the authSessionKey {encryptedChallenge:[base64 encoded], encryptedDeviceSecret:[base64 encoded], iv} + * @param {String} options.userId The user's email address + * @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) */ PrivateKeyDAO.prototype.verifyAuthentication = function(options, callback) { 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!')); return; } 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. - * @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) */ PrivateKeyDAO.prototype.upload = function(options, callback) { - var uri, - key = options.encryptedPrivateKey; + var uri; - if (!options.userId || !key || !key._id) { + if (!options._id || !options.userId || !options.encryptedPrivateKey || !options.sessionId || !options.salt || !options.iv) { callback(new Error('Incomplete arguments!')); return; } - uri = '/privatekey/user/' + options.userId + '/key/' + key._id; - this._restDao.post(key, uri, callback); + uri = '/privatekey/user/' + options.userId + '/session/' + options.sessionId; + this._restDao.post(options, uri, callback); }; /** * Request download for the encrypted private PGP key. - * @param {[type]} options.userId The user's email address - * @param {Function} callback(error) + * @param {String} options.userId The user's email address + * @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) { var uri; @@ -121,7 +128,20 @@ define(function() { uri = '/privatekey/user/' + options.userId + '/key/' + options.keyId; this._restDao.get({ 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); + }); }; /** diff --git a/src/sass/all.scss b/src/sass/all.scss index 49a9811..b650788 100755 --- a/src/sass/all.scss +++ b/src/sass/all.scss @@ -32,6 +32,7 @@ @import "views/shared"; @import "views/add-account"; @import "views/account"; +@import "views/privatekey-upload"; @import "views/set-passphrase"; @import "views/contacts"; @import "views/about"; diff --git a/src/sass/views/_account.scss b/src/sass/views/_account.scss index 5466502..4dd4140 100644 --- a/src/sass/views/_account.scss +++ b/src/sass/views/_account.scss @@ -1,8 +1,12 @@ .view-account { + a { + color: $color-blue; + } + table { margin: 50px auto 60px auto; - + td { padding-top: 15px; diff --git a/src/sass/views/_login.scss b/src/sass/views/_login.scss index 090b123..c5b7c64 100644 --- a/src/sass/views/_login.scss +++ b/src/sass/views/_login.scss @@ -119,4 +119,15 @@ margin-right: 10px; } } +} + +.view-login-privatekey-download { + .content { + max-width: 500px; + + input.code { + margin-right: 0; + width: auto; + } + } } \ No newline at end of file diff --git a/src/sass/views/_privatekey-upload.scss b/src/sass/views/_privatekey-upload.scss new file mode 100644 index 0000000..8f0c09b --- /dev/null +++ b/src/sass/views/_privatekey-upload.scss @@ -0,0 +1,17 @@ +.view-privatekey-upload { + + a { + color: $color-blue; + } + + .step { + margin: 20px 0; + + .working { + text-align: center; + font-size: 30px; + margin: 70px; + } + } + +} \ No newline at end of file diff --git a/src/tpl/account.html b/src/tpl/account.html index 90c5491..b535022 100644 --- a/src/tpl/account.html +++ b/src/tpl/account.html @@ -15,7 +15,7 @@ PGP Key ID - {{keyId}} + {{keyId}} (Revoke key) PGP Fingerprint diff --git a/src/tpl/desktop.html b/src/tpl/desktop.html index 88ec5d9..c85aa4d 100644 --- a/src/tpl/desktop.html +++ b/src/tpl/desktop.html @@ -31,6 +31,10 @@ + + diff --git a/src/tpl/login-privatekey-download.html b/src/tpl/login-privatekey-download.html new file mode 100644 index 0000000..55b08f5 --- /dev/null +++ b/src/tpl/login-privatekey-download.html @@ -0,0 +1,41 @@ +
+ + +
+ +
+

Key sync. We have sent you an email containing a recovery token. Please copy and paste the identifier below.

+ + +
+ +
+

Key sync. Please enter the keychain code you wrote down during sync setup.

+ - + - + - + - + - + + +
+ +
+ +
+ +
+
+ + +
+
+
What is this?
+
+

A passphrase is like a password that protects your PGP key.

+

There is no way to access your messages without your passphrase.

+

If you have forgotten your passphrase, please request an account reset by sending an email to support@whiteout.io. You will not be able to read previous messages after a reset.

+
+
\ No newline at end of file diff --git a/src/tpl/navigation.html b/src/tpl/navigation.html index a054a3a..637474c 100644 --- a/src/tpl/navigation.html +++ b/src/tpl/navigation.html @@ -17,6 +17,7 @@ diff --git a/src/tpl/privatekey-upload.html b/src/tpl/privatekey-upload.html new file mode 100644 index 0000000..c0366de --- /dev/null +++ b/src/tpl/privatekey-upload.html @@ -0,0 +1,46 @@ + \ No newline at end of file diff --git a/test/unit/account-ctrl-test.js b/test/unit/account-ctrl-test.js index f9b74f0..f5c58e0 100644 --- a/test/unit/account-ctrl-test.js +++ b/test/unit/account-ctrl-test.js @@ -14,18 +14,18 @@ define(function(require) { var scope, accountCtrl, dummyFingerprint, expectedFingerprint, dummyKeyId, expectedKeyId, - emailAddress, keySize, cryptoMock, keychainMock; + emailAddress, keySize, pgpMock, keychainMock; beforeEach(function() { - appController._crypto = cryptoMock = sinon.createStubInstance(PGP); + appController._pgp = pgpMock = sinon.createStubInstance(PGP); appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO); dummyFingerprint = '3A2D39B4E1404190B8B949DE7D7E99036E712926'; expectedFingerprint = '3A2D 39B4 E140 4190 B8B9 49DE 7D7E 9903 6E71 2926'; dummyKeyId = '9FEB47936E712926'; expectedKeyId = '6E712926'; - cryptoMock.getFingerprint.returns(dummyFingerprint); - cryptoMock.getKeyId.returns(dummyKeyId); + pgpMock.getFingerprint.returns(dummyFingerprint); + pgpMock.getKeyId.returns(dummyKeyId); emailAddress = 'fred@foo.com'; keySize = 1234; appController._emailDao = { @@ -34,7 +34,7 @@ define(function(require) { asymKeySize: keySize } }; - cryptoMock.getKeyParams.returns({ + pgpMock.getKeyParams.returns({ _id: dummyKeyId, fingerprint: dummyFingerprint, userId: emailAddress, diff --git a/test/unit/app-controller-test.js b/test/unit/app-controller-test.js index d2ed543..b76caf4 100644 --- a/test/unit/app-controller-test.js +++ b/test/unit/app-controller-test.js @@ -37,7 +37,7 @@ define(function(require) { expect(controller._userStorage).to.exist; expect(controller._invitationDao).to.exist; expect(controller._keychain).to.exist; - expect(controller._crypto).to.exist; + expect(controller._pgp).to.exist; expect(controller._pgpbuilder).to.exist; expect(controller._emailDao).to.exist; expect(controller._outboxBo).to.exist; diff --git a/test/unit/contacts-ctrl-test.js b/test/unit/contacts-ctrl-test.js index 2e52d51..15085ef 100644 --- a/test/unit/contacts-ctrl-test.js +++ b/test/unit/contacts-ctrl-test.js @@ -12,11 +12,11 @@ define(function(require) { describe('Contacts Controller unit test', function() { var scope, contactsCtrl, origKeychain, keychainMock, - origCrypto, cryptoMock; + origPgp, pgpMock; beforeEach(function() { - origCrypto = appController._crypto; - appController._crypto = cryptoMock = sinon.createStubInstance(PGP); + origPgp = appController._pgp; + appController._pgp = pgpMock = sinon.createStubInstance(PGP); origKeychain = appController._keychain; appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO); @@ -33,7 +33,7 @@ define(function(require) { afterEach(function() { // restore the module - appController._crypto = origCrypto; + appController._pgp = origPgp; appController._keychain = origKeychain; }); @@ -60,7 +60,7 @@ define(function(require) { keychainMock.listLocalPublicKeys.yields(null, [{ _id: '12345' }]); - cryptoMock.getKeyParams.returns({ + pgpMock.getKeyParams.returns({ fingerprint: 'asdf' }); @@ -92,7 +92,7 @@ define(function(require) { it('should work', function(done) { var keyArmored = '-----BEGIN PGP PUBLIC KEY BLOCK-----'; - cryptoMock.getKeyParams.returns({ + pgpMock.getKeyParams.returns({ _id: '12345', userId: 'max@example.com', userIds: [] @@ -127,7 +127,7 @@ define(function(require) { it('should fail due to error in pgp.getKeyParams', function(done) { var keyArmored = '-----BEGIN PGP PUBLIC KEY BLOCK-----'; - cryptoMock.getKeyParams.throws(new Error('WAT')); + pgpMock.getKeyParams.throws(new Error('WAT')); scope.onError = function(err) { expect(err).to.exist; @@ -140,7 +140,7 @@ define(function(require) { it('should fail due to error in keychain.saveLocalPublicKey', function(done) { var keyArmored = '-----BEGIN PGP PUBLIC KEY BLOCK-----'; - cryptoMock.getKeyParams.returns({ + pgpMock.getKeyParams.returns({ _id: '12345', userId: 'max@example.com' }); diff --git a/test/unit/email-dao-test.js b/test/unit/email-dao-test.js index 900523c..5215772 100644 --- a/test/unit/email-dao-test.js +++ b/test/unit/email-dao-test.js @@ -114,7 +114,7 @@ define(function(require) { // check configuration // 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._mailreader).to.equal(mailreader); expect(dao._pgpbuilder).to.equal(pgpBuilderStub); diff --git a/test/unit/keychain-dao-test.js b/test/unit/keychain-dao-test.js index 3472de8..be19cef 100644 --- a/test/unit/keychain-dao-test.js +++ b/test/unit/keychain-dao-test.js @@ -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) { getDeviceNameStub.yields(null, 'iPhone'); @@ -753,7 +773,9 @@ define(function(require) { encryptedRegSessionKey: 'asdf' }); - lookupPublicKeyStub.yields(null, 'pubkey'); + lookupPublicKeyStub.yields(null, { + publicKey: 'pubkey' + }); pgpStub.decrypt.withArgs('asdf', 'pubkey').yields(42); keychainDao.registerDevice({ @@ -774,7 +796,9 @@ define(function(require) { encryptedRegSessionKey: 'asdf' }); - lookupPublicKeyStub.yields(null, 'pubkey'); + lookupPublicKeyStub.yields(null, { + publicKey: 'pubkey' + }); pgpStub.decrypt.withArgs('asdf', 'pubkey').yields(null, 'decrypted'); getDeviceSecretStub.yields(42); @@ -796,7 +820,9 @@ define(function(require) { encryptedRegSessionKey: 'asdf' }); - lookupPublicKeyStub.yields(null, 'pubkey'); + lookupPublicKeyStub.yields(null, { + publicKey: 'pubkey' + }); pgpStub.decrypt.withArgs('asdf', 'pubkey').yields(null, 'decrypted'); getDeviceSecretStub.yields(null, 'secret'); cryptoStub.encrypt.withArgs('secret', 'decrypted').yields(42); @@ -819,7 +845,9 @@ define(function(require) { encryptedRegSessionKey: 'asdf' }); - lookupPublicKeyStub.yields(null, 'pubkey'); + lookupPublicKeyStub.yields(null, { + publicKey: 'pubkey' + }); pgpStub.decrypt.withArgs('asdf', 'pubkey').yields(null, 'decrypted'); getDeviceSecretStub.yields(null, 'secret'); cryptoStub.encrypt.withArgs('secret', 'decrypted').yields(null, 'encryptedDeviceSecret'); @@ -847,8 +875,8 @@ define(function(require) { getDeviceSecretStub.restore(); }); - it('should fail due to privkeyDao.requestAuthSessionKeys', function(done) { - privkeyDaoStub.requestAuthSessionKeys.withArgs({ + it('should fail due to privkeyDao.requestAuthSessionKey', function(done) { + privkeyDaoStub.requestAuthSessionKey.withArgs({ userId: testUser }).yields(42); @@ -859,8 +887,8 @@ define(function(require) { }); }); - it('should fail due to privkeyDao.requestAuthSessionKeys response', function(done) { - privkeyDaoStub.requestAuthSessionKeys.yields(null, {}); + it('should fail due to privkeyDao.requestAuthSessionKey response', function(done) { + privkeyDaoStub.requestAuthSessionKey.yields(null, {}); keychainDao._authenticateToPrivateKeyServer(testUser, function(err, authSessionKey) { expect(err).to.exist; @@ -870,7 +898,7 @@ define(function(require) { }); it('should fail due to lookupPublicKey', function(done) { - privkeyDaoStub.requestAuthSessionKeys.yields(null, { + privkeyDaoStub.requestAuthSessionKey.yields(null, { encryptedAuthSessionKey: 'encryptedAuthSessionKey', encryptedChallenge: 'encryptedChallenge', sessionId: 'sessionId' @@ -886,7 +914,7 @@ define(function(require) { }); it('should fail due to pgp.decrypt', function(done) { - privkeyDaoStub.requestAuthSessionKeys.yields(null, { + privkeyDaoStub.requestAuthSessionKey.yields(null, { encryptedAuthSessionKey: 'encryptedAuthSessionKey', encryptedChallenge: 'encryptedChallenge', sessionId: 'sessionId' @@ -906,7 +934,7 @@ define(function(require) { }); it('should fail due to getDeviceSecret', function(done) { - privkeyDaoStub.requestAuthSessionKeys.yields(null, { + privkeyDaoStub.requestAuthSessionKey.yields(null, { encryptedAuthSessionKey: 'encryptedAuthSessionKey', encryptedChallenge: 'encryptedChallenge', sessionId: 'sessionId' @@ -927,7 +955,7 @@ define(function(require) { }); it('should fail due to crypto.encrypt', function(done) { - privkeyDaoStub.requestAuthSessionKeys.yields(null, { + privkeyDaoStub.requestAuthSessionKey.yields(null, { encryptedAuthSessionKey: 'encryptedAuthSessionKey', encryptedChallenge: 'encryptedChallenge', sessionId: 'sessionId' @@ -949,7 +977,7 @@ define(function(require) { }); it('should fail due to privkeyDao.verifyAuthentication', function(done) { - privkeyDaoStub.requestAuthSessionKeys.yields(null, { + privkeyDaoStub.requestAuthSessionKey.yields(null, { encryptedAuthSessionKey: 'encryptedAuthSessionKey', encryptedChallenge: 'encryptedChallenge', 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) { - privkeyDaoStub.requestAuthSessionKeys.yields(null, { + privkeyDaoStub.requestAuthSessionKey.yields(null, { encryptedAuthSessionKey: 'encryptedAuthSessionKey', encryptedChallenge: 'encryptedChallenge', sessionId: 'sessionId' }); lookupPublicKeyStub.yields(null, { - publickKey: 'publicKey' + publicKey: 'publicKey' }); pgpStub.decrypt.yields(null, 'decryptedStuff'); @@ -1152,10 +1201,12 @@ define(function(require) { describe('requestPrivateKeyDownload', function() { it('should work', function(done) { - privkeyDaoStub.requestDownload.withArgs({ + var options = { 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() { - var saveLocalPrivateKeyStub; + var saveLocalPrivateKeyStub, testData; beforeEach(function() { + testData = { + _id: 'keyId', + userId: testUser, + encryptedPrivateKey: 'encryptedPrivateKey', + code: 'code', + salt: 'salt', + iv: 'iv' + }; + saveLocalPrivateKeyStub = sinon.stub(keychainDao, 'saveLocalPrivateKey'); }); afterEach(function() { @@ -1190,10 +1250,7 @@ define(function(require) { it('should fail due to crypto.deriveKey', function(done) { cryptoStub.deriveKey.yields(42); - keychainDao.decryptAndStorePrivateKeyLocally({ - userId: testUser, - keyId: 'keyId' - }, function(err) { + keychainDao.decryptAndStorePrivateKeyLocally(testData, function(err) { expect(err).to.exist; expect(cryptoStub.deriveKey.calledOnce).to.be.true; done(); @@ -1204,10 +1261,7 @@ define(function(require) { cryptoStub.deriveKey.yields(null, 'derivedKey'); cryptoStub.decrypt.yields(42); - keychainDao.decryptAndStorePrivateKeyLocally({ - userId: testUser, - keyId: 'keyId' - }, function(err) { + keychainDao.decryptAndStorePrivateKeyLocally(testData, function(err) { expect(err).to.exist; expect(cryptoStub.deriveKey.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.decrypt.yields(null, 'pgpBlock'); - saveLocalPrivateKeyStub.yields(); + cryptoStub.decrypt.yields(null, 'privateKeyArmored'); + pgpStub.getKeyParams.throws(new Error()); - keychainDao.decryptAndStorePrivateKeyLocally({ - userId: testUser, - keyId: 'keyId' - }, function(err) { - expect(err).to.not.exist; + 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; + 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; done(); diff --git a/test/unit/login-ctrl-test.js b/test/unit/login-ctrl-test.js index 9ca206b..52176f6 100644 --- a/test/unit/login-ctrl-test.js +++ b/test/unit/login-ctrl-test.js @@ -7,10 +7,13 @@ define(function(require) { LoginCtrl = require('js/controller/login'), EmailDAO = require('js/dao/email-dao'), 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() { - var scope, location, ctrl, origEmailDao, emailDaoMock, + var scope, location, ctrl, + origEmailDao, emailDaoMock, + origKeychain, keychainMock, emailAddress = 'fred@foo.com', startAppStub, checkForUpdateStub, @@ -21,14 +24,16 @@ define(function(require) { var hasChrome, hasIdentity; beforeEach(function() { - hasChrome = !! window.chrome; - hasIdentity = !! window.chrome.identity; + hasChrome = !!window.chrome; + hasIdentity = !!window.chrome.identity; window.chrome = window.chrome || {}; window.chrome.identity = window.chrome.identity || {}; // 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); appController._auth = authStub = sinon.createStubInstance(Auth); startAppStub = sinon.stub(appController, 'start'); @@ -48,6 +53,7 @@ define(function(require) { // restore the app controller module appController._emailDao = origEmailDao; + appController._keychain = origKeychain; appController.start.restore && appController.start.restore(); appController.checkForUpdate.restore && appController.checkForUpdate.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) { startAppStub.yields(); authStub.getEmailAddress.yields(null, emailAddress); initStub.yields(null, { publicKey: 'b' }); + keychainMock.requestPrivateKeyDownload.yields(); angular.module('logintest', []); mocks.module('logintest'); @@ -144,6 +180,7 @@ define(function(require) { 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(); diff --git a/test/unit/login-privatekey-download-ctrl-test.js b/test/unit/login-privatekey-download-ctrl-test.js new file mode 100644 index 0000000..bb9c32a --- /dev/null +++ b/test/unit/login-privatekey-download-ctrl-test.js @@ -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'); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/unit/main.js b/test/unit/main.js index 411d10a..44b541e 100644 --- a/test/unit/main.js +++ b/test/unit/main.js @@ -48,6 +48,8 @@ function startTests() { 'test/unit/login-existing-ctrl-test', 'test/unit/login-initial-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/read-ctrl-test', 'test/unit/navigation-ctrl-test', diff --git a/test/unit/pgp-test.js b/test/unit/pgp-test.js index 0e4affa..b0c64ae 100644 --- a/test/unit/pgp-test.js +++ b/test/unit/pgp-test.js @@ -92,7 +92,7 @@ define(function(require) { publicKeyArmored: pubkey }, function(err) { expect(err).to.exist; - expect(err.errMsg).to.equal('Incorrect passphrase!'); + expect(err.message).to.equal('Incorrect passphrase!'); pgp.exportKeys(function(err, keys) { expect(err).to.exist; diff --git a/test/unit/privatekey-dao-test.js b/test/unit/privatekey-dao-test.js index 01d5eb4..8234a03 100644 --- a/test/unit/privatekey-dao-test.js +++ b/test/unit/privatekey-dao-test.js @@ -57,7 +57,8 @@ define(function(require) { privkeyDao.uploadDeviceSecret({ userId: emailAddress, deviceName: deviceName, - encryptedDeviceSecret: 'asdf' + encryptedDeviceSecret: 'asdf', + iv: 'iv' }, function(err) { expect(err).to.not.exist; done(); @@ -65,9 +66,9 @@ define(function(require) { }); }); - describe('requestAuthSessionKeys', function() { + describe('requestAuthSessionKey', function() { it('should fail due to invalid args', function(done) { - privkeyDao.requestAuthSessionKeys({}, function(err) { + privkeyDao.requestAuthSessionKey({}, function(err) { expect(err).to.exist; done(); }); @@ -76,7 +77,7 @@ define(function(require) { it('should work', function(done) { restDaoStub.post.withArgs(undefined, '/auth/user/' + emailAddress).yields(); - privkeyDao.requestAuthSessionKeys({ + privkeyDao.requestAuthSessionKey({ userId: emailAddress }, function(err) { expect(err).to.not.exist; @@ -96,13 +97,17 @@ define(function(require) { it('should work', function(done) { var sessionId = '1'; - restDaoStub.put.withArgs('asdf', '/auth/user/' + emailAddress + '/session/' + sessionId).yields(); - - privkeyDao.verifyAuthentication({ + var options = { userId: emailAddress, sessionId: sessionId, - encryptedChallenge: 'asdf' - }, function(err) { + encryptedChallenge: 'asdf', + encryptedDeviceSecret: 'qwer', + iv: ' iv' + }; + + restDaoStub.put.withArgs(options, '/auth/user/' + emailAddress + '/session/' + sessionId).yields(); + + privkeyDao.verifyAuthentication(options, function(err) { expect(err).to.not.exist; done(); }); @@ -118,16 +123,18 @@ define(function(require) { }); it('should work', function(done) { - var key = { - _id: '12345' + var options = { + _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({ - userId: emailAddress, - encryptedPrivateKey: key - }, function(err) { + privkeyDao.upload(options, function(err) { expect(err).to.not.exist; done(); }); diff --git a/test/unit/privatekey-upload-ctrl-test.js b/test/unit/privatekey-upload-ctrl-test.js new file mode 100644 index 0000000..5ee8328 --- /dev/null +++ b/test/unit/privatekey-upload-ctrl-test.js @@ -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(); + }); + }); + + }); +}); \ No newline at end of file diff --git a/test/unit/set-passphrase-ctrl-test.js b/test/unit/set-passphrase-ctrl-test.js index 8709556..5c4062b 100644 --- a/test/unit/set-passphrase-ctrl-test.js +++ b/test/unit/set-passphrase-ctrl-test.js @@ -16,7 +16,7 @@ define(function(require) { emailAddress, keySize, cryptoMock, keychainMock; beforeEach(function() { - appController._crypto = cryptoMock = sinon.createStubInstance(PGP); + appController._pgp = cryptoMock = sinon.createStubInstance(PGP); appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO); dummyFingerprint = '3A2D39B4E1404190B8B949DE7D7E99036E712926';