diff --git a/Gruntfile.js b/Gruntfile.js index ff51b07..619b7ae 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -190,11 +190,11 @@ module.exports = function(grunt) { 'test/unit/controller/login/login-initial-ctrl-test.js', 'test/unit/controller/login/login-new-device-ctrl-test.js', 'test/unit/controller/login/login-privatekey-download-ctrl-test.js', + 'test/unit/controller/login/login-privatekey-upload-ctrl-test.js', 'test/unit/controller/login/login-verify-public-key-ctrl-test.js', 'test/unit/controller/login/login-set-credentials-ctrl-test.js', 'test/unit/controller/login/login-ctrl-test.js', 'test/unit/controller/app/dialog-ctrl-test.js', - 'test/unit/controller/app/privatekey-upload-ctrl-test.js', 'test/unit/controller/app/publickey-import-ctrl-test.js', 'test/unit/controller/app/account-ctrl-test.js', 'test/unit/controller/app/set-passphrase-ctrl-test.js', @@ -677,8 +677,7 @@ module.exports = function(grunt) { patchManifest({ version: version, deleteKey: true, - keyServer: 'https://keys.whiteout.io/', - keychainServer: 'https://keychain.whiteout.io/' + keyServer: 'https://keys.whiteout.io/' }); }); @@ -700,10 +699,6 @@ module.exports = function(grunt) { var ksIndex = manifest.permissions.indexOf('https://keys-test.whiteout.io/'); manifest.permissions[ksIndex] = options.keyServer; } - if (options.keychainServer) { - var kcsIndex = manifest.permissions.indexOf('https://keychain-test.whiteout.io/'); - manifest.permissions[kcsIndex] = options.keychainServer; - } if (options.deleteKey) { delete manifest.key; } diff --git a/package.json b/package.json index b9bdb48..cb4d271 100644 --- a/package.json +++ b/package.json @@ -62,8 +62,9 @@ "grunt-svgmin": "~1.0.0", "grunt-svgstore": "~0.3.4", "iframe-resizer": "^2.8.3", - "imap-client": "~0.11.0", + "imap-client": "~0.12.0", "jquery": "~2.1.1", + "mailbuild": "^0.3.7", "mailreader": "~0.4.0", "mocha": "^1.21.4", "ng-infinite-scroll": "~1.1.2", @@ -75,4 +76,4 @@ "time-grunt": "^1.0.0", "wo-smtpclient": "~0.6.0" } -} \ No newline at end of file +} diff --git a/src/js/app-config.js b/src/js/app-config.js index 64ec102..2a1ff2d 100644 --- a/src/js/app-config.js +++ b/src/js/app-config.js @@ -15,7 +15,6 @@ appCfg.config = { pgpComment: 'Whiteout Mail - https://whiteout.io', keyServerUrl: 'https://keys.whiteout.io', hkpUrl: 'http://keyserver.ubuntu.com', - privkeyServerUrl: 'https://keychain.whiteout.io', adminUrl: 'https://admin-node.whiteout.io', settingsUrl: 'https://settings.whiteout.io/autodiscovery/', mailServer: { @@ -70,8 +69,6 @@ function setConfigParams(manifest) { // get key server base url cfg.keyServerUrl = getUrl('https://keys'); - // get keychain server base url - cfg.privkeyServerUrl = getUrl('https://keychain'); // get the app version cfg.appVersion = manifest.version; } diff --git a/src/js/app.js b/src/js/app.js index a935a83..3663b73 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -68,6 +68,10 @@ app.config(function($routeProvider, $animateProvider) { templateUrl: 'tpl/login-set-credentials.html', controller: require('./controller/login/login-set-credentials') }); + $routeProvider.when('/login-privatekey-upload', { + templateUrl: 'tpl/login-privatekey-upload.html', + controller: require('./controller/login/login-privatekey-upload') + }); $routeProvider.when('/login-verify-public-key', { templateUrl: 'tpl/login-verify-public-key.html', controller: require('./controller/login/login-verify-public-key') @@ -114,7 +118,6 @@ app.controller('WriteCtrl', require('./controller/app/write')); app.controller('MailListCtrl', require('./controller/app/mail-list')); app.controller('AccountCtrl', require('./controller/app/account')); app.controller('SetPassphraseCtrl', require('./controller/app/set-passphrase')); -app.controller('PrivateKeyUploadCtrl', require('./controller/app/privatekey-upload')); app.controller('PublicKeyImportCtrl', require('./controller/app/publickey-import')); app.controller('ContactsCtrl', require('./controller/app/contacts')); app.controller('AboutCtrl', require('./controller/app/about')); diff --git a/src/js/controller/app/navigation.js b/src/js/controller/app/navigation.js index 4089ad4..45dfcb8 100644 --- a/src/js/controller/app/navigation.js +++ b/src/js/controller/app/navigation.js @@ -11,7 +11,7 @@ var NOTIFICATION_SENT_TIMEOUT = 2000; // Controller // -var NavigationCtrl = function($scope, $location, $q, account, email, outbox, notification, appConfig, dialog, dummy) { +var NavigationCtrl = function($scope, $location, $q, $timeout, account, email, outbox, notification, appConfig, dialog, dummy, privateKey, axe) { if (!$location.search().dev && !account.isLoggedIn()) { $location.path('/'); // init app return; @@ -149,6 +149,9 @@ var NavigationCtrl = function($scope, $location, $q, account, email, outbox, not if (!$scope.state.nav.currentFolder) { $scope.navigate(0); } + + // check if the private PGP key is synced + $scope.checkKeySyncStatus(); }); // @@ -178,6 +181,45 @@ var NavigationCtrl = function($scope, $location, $q, account, email, outbox, not // start checking outbox periodically outbox.startChecking($scope.onOutboxUpdate); } + + $scope.checkKeySyncStatus = function() { + return $q(function(resolve) { + resolve(); + + }).then(function() { + // login to imap + return privateKey.init(); + + }).then(function() { + // check key sync status + return privateKey.isSynced(); + + }).then(function(synced) { + if (!synced) { + dialog.confirm({ + title: 'Key backup', + message: 'Your private key is not backed up. Back up now?', + positiveBtnStr: 'Backup', + negativeBtnStr: 'Not now', + showNegativeBtn: true, + callback: function(granted) { + if (granted) { + // logout of the current session + email.onDisconnect().then(function() { + // send to key upload screen + $timeout(function() { + $location.path('/login-privatekey-upload'); + }); + }); + } + } + }); + } + // logout of imap + return privateKey.destroy(); + + }).catch(axe.error); + }; }; module.exports = NavigationCtrl; \ No newline at end of file diff --git a/src/js/controller/app/privatekey-upload.js b/src/js/controller/app/privatekey-upload.js deleted file mode 100644 index 6669f42..0000000 --- a/src/js/controller/app/privatekey-upload.js +++ /dev/null @@ -1,155 +0,0 @@ -'use strict'; - -var util = require('crypto-lib').util; - -var PrivateKeyUploadCtrl = function($scope, $q, keychain, pgp, dialog, auth) { - - // - // scope state - // - - $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 - return $scope.checkServerForKey().then(function(privateKeySynced) { - if (privateKeySynced) { - // close lightbox - $scope.state.lightbox = undefined; - // show message - return dialog.info({ - title: 'Info', - message: 'Your PGP key has already been synced.' - }); - } - - // show sync ui if key is not synced - $scope.displayUploadUi(); - }); - } - }; - - // - // scope functions - // - - $scope.checkServerForKey = function() { - var keyParams = pgp.getKeyParams(); - - return $q(function(resolve) { - resolve(); - - }).then(function() { - return keychain.hasPrivateKey({ - userId: keyParams.userId, - keyId: keyParams._id - }); - - }).then(function(privateKeySynced) { - return privateKeySynced ? privateKeySynced : undefined; - - }).catch(dialog.error); - }; - - $scope.displayUploadUi = function() { - // go to step 1 - $scope.step = 1; - // generate new code for the user - $scope.code = util.randomString(24); - $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); - - // clear input field of any previous artifacts - $scope.inputCode = ''; - }; - - $scope.verifyCode = function() { - if ($scope.inputCode.toUpperCase() !== $scope.code) { - var err = new Error('The code does not match. Please go back and check the generated code.'); - dialog.error(err); - return false; - } - - return true; - }; - - $scope.setDeviceName = function() { - return $q(function(resolve) { - resolve(); - - }).then(function() { - return keychain.setDeviceName($scope.deviceName); - }); - }; - - $scope.encryptAndUploadKey = function() { - var userId = auth.emailAddress; - var code = $scope.code; - - // register device to keychain service - return $q(function(resolve) { - resolve(); - - }).then(function() { - // register the device - return keychain.registerDevice({ - userId: userId - }); - - }).then(function() { - // encrypt private PGP key using code and upload - return keychain.uploadPrivateKey({ - userId: userId, - code: code - }); - - }).catch(dialog.error); - }; - - $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 - return $scope.setDeviceName().then(function() { - // show spinner - $scope.step++; - // init key sync - return $scope.encryptAndUploadKey(); - - }).then(function() { - // close sync dialog - $scope.state.privateKeyUpload.toggle(false); - // show success message - dialog.info({ - title: 'Success', - message: 'Whiteout Keychain setup successful!' - }); - - }).catch(dialog.error); - } - }; - -}; - -module.exports = PrivateKeyUploadCtrl; \ No newline at end of file diff --git a/src/js/controller/login/login-initial.js b/src/js/controller/login/login-initial.js index 656fb53..80684e2 100644 --- a/src/js/controller/login/login-initial.js +++ b/src/js/controller/login/login-initial.js @@ -61,9 +61,9 @@ var LoginInitialCtrl = function($scope, $location, $routeParams, $q, newsletter, }); }).then(function(keypair) { - // go to public key verification + // remember keypair for storing after public key verification publickeyVerifier.keypair = keypair; - $location.path('/login-verify-public-key'); + $location.path('/login-privatekey-upload'); }).catch(displayError); }; diff --git a/src/js/controller/login/login-new-device.js b/src/js/controller/login/login-new-device.js index 98bbf4f..4300882 100644 --- a/src/js/controller/login/login-new-device.js +++ b/src/js/controller/login/login-new-device.js @@ -5,6 +5,13 @@ var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, aut $scope.incorrect = false; + $scope.pasteKey = function(pasted) { + var index = pasted.indexOf('-----BEGIN PGP PRIVATE KEY BLOCK-----'); + $scope.key = { + privateKeyArmored: pasted.substring(index, pasted.length).trim() + }; + }; + $scope.confirmPassphrase = function() { if ($scope.form.$invalid || !$scope.key) { $scope.errMsg = 'Please fill out all required fields!'; @@ -84,11 +91,10 @@ var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, aut }); } - // go to public key verification + // remember keypair for public key verification publickeyVerifier.keypair = keypair; - return keychain.uploadPublicKey(keypair.publicKey).then(function() { - $location.path('/login-verify-public-key'); - }); + // upload private key and then go to public key verification + $location.path('/login-privatekey-upload'); }).catch(displayError); }; diff --git a/src/js/controller/login/login-privatekey-download.js b/src/js/controller/login/login-privatekey-download.js index 929ff1e..e7db1e4 100644 --- a/src/js/controller/login/login-privatekey-download.js +++ b/src/js/controller/login/login-privatekey-download.js @@ -1,20 +1,19 @@ 'use strict'; -var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams, $q, auth, email, keychain) { +var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams, $q, auth, email, privateKey, keychain) { !$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app - $scope.step = 1; - // - // Token + // scope functions // - $scope.checkToken = function() { - if ($scope.tokenForm.$invalid) { - $scope.errMsg = 'Please enter a valid recovery token!'; + $scope.checkCode = function() { + if ($scope.form.$invalid) { + $scope.errMsg = 'Please fill out all required fields!'; return; } + var cachedKeypair; var userId = auth.emailAddress; return $q(function(resolve) { @@ -22,54 +21,38 @@ var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams, $q, $scope.errMsg = undefined; resolve(); + }).then(function() { + // login to imap + return privateKey.init(); + }).then(function() { // get public key id for reference return keychain.getUserKeyPair(userId); }).then(function(keypair) { // remember for storage later - $scope.cachedKeypair = keypair; - return keychain.downloadPrivateKey({ + cachedKeypair = keypair; + return privateKey.download({ userId: userId, - keyId: keypair.publicKey._id, - recoveryToken: $scope.recoveryToken.toUpperCase() + keyId: keypair.publicKey._id }); - }).then(function(encryptedPrivateKey) { - $scope.encryptedPrivateKey = encryptedPrivateKey; - $scope.busy = false; - $scope.step++; + }).then(function(encryptedKey) { + // set decryption code + encryptedKey.code = $scope.code.toUpperCase(); + // decrypt the downloaded encrypted private key + return privateKey.decrypt(encryptedKey); - }).catch(displayError); - }; - - // - // Keychain code - // - - $scope.checkCode = function() { - if ($scope.codeForm.$invalid) { - $scope.errMsg = 'Please fill out all required fields!'; - return; - } - - var options = $scope.encryptedPrivateKey; - options.code = $scope.code.toUpperCase(); - - return $q(function(resolve) { - $scope.busy = true; - $scope.errMsg = undefined; - resolve(); + }).then(function(privkey) { + // add private key to cached keypair object + cachedKeypair.privateKey = privkey; + // store the decrypted private key locally + return keychain.putUserKeyPair(cachedKeypair); }).then(function() { - return keychain.decryptAndStorePrivateKeyLocally(options); - - }).then(function(privateKey) { - // add private key to cached keypair object - $scope.cachedKeypair.privateKey = privateKey; // try empty passphrase return email.unlock({ - keypair: $scope.cachedKeypair, + keypair: cachedKeypair, passphrase: undefined }).catch(function(err) { // passphrase incorrct ... go to passphrase login screen @@ -81,6 +64,10 @@ var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams, $q, // passphrase is corrent ... return auth.storeCredentials(); + }).then(function() { + // logout of imap + return privateKey.destroy(); + }).then(function() { // continue to main app $scope.goTo('/account'); diff --git a/src/js/controller/login/login-privatekey-upload.js b/src/js/controller/login/login-privatekey-upload.js new file mode 100644 index 0000000..59c522a --- /dev/null +++ b/src/js/controller/login/login-privatekey-upload.js @@ -0,0 +1,83 @@ +'use strict'; + +var util = require('crypto-lib').util; + +var LoginPrivateKeyUploadCtrl = function($scope, $location, $routeParams, $q, auth, privateKey) { + !$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app + + // + // scope state + // + + // go to step 1 + $scope.step = 1; + // generate new code for the user + $scope.code = util.randomString(24); + $scope.displayedCode = $scope.code.replace(/.{4}/g, "$&-").replace(/-$/, ''); + // clear input field of any previous artifacts + $scope.inputCode = ''; + + // + // scope functions + // + + $scope.encryptAndUploadKey = function() { + return $q(function(resolve) { + $scope.busy = true; + $scope.errMsg = undefined; + $scope.incorrect = false; + resolve(); + + }).then(function() { + if ($scope.inputCode.toUpperCase() !== $scope.code) { + throw new Error('The code does not match. Please go back and check the generated code.'); + } + + }).then(function() { + // login to imap + return privateKey.init(); + + }).then(function() { + // encrypt the private key + return privateKey.encrypt($scope.code); + + }).then(function(encryptedPayload) { + // set user id to encrypted payload + encryptedPayload.userId = auth.emailAddress; + + // encrypt private PGP key using code and upload + return privateKey.upload(encryptedPayload); + + }).then(function() { + // logout of imap + return privateKey.destroy(); + + }).then(function() { + // continue to public key verification + $location.path('/login-verify-public-key'); + + }).catch(displayError); + }; + + $scope.goForward = function() { + $scope.step++; + }; + + $scope.goBack = function() { + if ($scope.step > 1) { + $scope.step--; + } + }; + + // + // helper functions + // + + function displayError(err) { + $scope.busy = false; + $scope.incorrect = true; + $scope.errMsg = err.errMsg || err.message; + } +}; + +module.exports = LoginPrivateKeyUploadCtrl; \ No newline at end of file diff --git a/src/js/controller/login/login-verify-public-key.js b/src/js/controller/login/login-verify-public-key.js index 8c75396..f0579f0 100644 --- a/src/js/controller/login/login-verify-public-key.js +++ b/src/js/controller/login/login-verify-public-key.js @@ -2,7 +2,7 @@ var RETRY_INTERVAL = 10000; -var PublicKeyVerifierCtrl = function($scope, $location, $q, $timeout, $interval, auth, publickeyVerifier, keychain) { +var PublicKeyVerifierCtrl = function($scope, $location, $q, $timeout, $interval, auth, publickeyVerifier, publicKey) { $scope.retries = 0; /** @@ -22,10 +22,10 @@ var PublicKeyVerifierCtrl = function($scope, $location, $q, $timeout, $interval, }).then(function() { // pre-flight check: is there already a public key for the user? - return keychain.getUserKeyPair(auth.emailAddress); + return publicKey.getByUserId(auth.emailAddress); - }).then(function(keypair) { - if (!keypair || !keypair.publicKey) { + }).then(function(cloudPubkey) { + if (!cloudPubkey || (cloudPubkey && cloudPubkey.source)) { // no pubkey, need to do the roundtrip return verifyImap(); } @@ -94,7 +94,8 @@ var PublicKeyVerifierCtrl = function($scope, $location, $q, $timeout, $interval, clearInterval($scope.countdownDecrement); } - scheduleVerification(); + // upload public key and then schedule verifcation + publickeyVerifier.uploadPublicKey().then(scheduleVerification); }; module.exports = PublicKeyVerifierCtrl; \ No newline at end of file diff --git a/src/js/controller/login/login.js b/src/js/controller/login/login.js index 6026643..29beb6e 100644 --- a/src/js/controller/login/login.js +++ b/src/js/controller/login/login.js @@ -52,19 +52,8 @@ var LoginCtrl = function($scope, $timeout, $location, updateHandler, account, au }); } else if (availableKeys && availableKeys.publicKey && !availableKeys.privateKey) { - // check if private key is synced - return keychain.requestPrivateKeyDownload({ - userId: availableKeys.publicKey.userId, - keyId: availableKeys.publicKey._id, - }).then(function(privateKeySynced) { - if (privateKeySynced) { - // private key is synced, proceed to download - return $scope.goTo('/login-privatekey-download'); - } else { - // no private key, import key file - return $scope.goTo('/login-new-device'); - } - }); + // proceed to private key download + return $scope.goTo('/login-privatekey-download'); } else { // no public key available, start onboarding process diff --git a/src/js/email/index.js b/src/js/email/index.js index 4eaece7..f14b173 100644 --- a/src/js/email/index.js +++ b/src/js/email/index.js @@ -4,6 +4,7 @@ angular.module('woEmail', ['woAppConfig', 'woUtil', 'woServices', 'woCrypto']); require('./mailreader'); require('./pgpbuilder'); +require('./mailbuild'); require('./email'); require('./outbox'); require('./account'); diff --git a/src/js/email/mailbuild.js b/src/js/email/mailbuild.js new file mode 100644 index 0000000..cf850ba --- /dev/null +++ b/src/js/email/mailbuild.js @@ -0,0 +1,8 @@ +'use strict'; + +var Mailbuild = require('mailbuild'); + +var ngModule = angular.module('woEmail'); +ngModule.factory('mailbuild', function() { + return Mailbuild; +}); \ No newline at end of file diff --git a/src/js/service/keychain.js b/src/js/service/keychain.js index 1f1a4c0..819d3d4 100644 --- a/src/js/service/keychain.js +++ b/src/js/service/keychain.js @@ -4,12 +4,8 @@ var ngModule = angular.module('woServices'); ngModule.service('keychain', Keychain); module.exports = Keychain; -var util = require('crypto-lib').util; - var DB_PUBLICKEY = 'publickey', - DB_PRIVATEKEY = 'privatekey', - DB_DEVICENAME = 'devicename', - DB_DEVICE_SECRET = 'devicesecret'; + DB_PRIVATEKEY = 'privatekey'; /** * A high-level Data-Access Api for handling Keypair synchronization @@ -199,390 +195,6 @@ Keychain.prototype.getReceiverPublicKey = function(userId) { } }; -// -// Device registration functions -// - -/** - * Set the device's memorable name e.g 'iPhone Work' - * @param {String} deviceName The device name - */ -Keychain.prototype.setDeviceName = function(deviceName) { - if (!deviceName) { - return new Promise(function() { - throw new Error('Please set a device name!'); - }); - } - return this._lawnchairDAO.persist(DB_DEVICENAME, deviceName); -}; - -/** - * Get the device' memorable name from local storage. Throws an error if not set - * @return {String} The device name - */ -Keychain.prototype.getDeviceName = function() { - // check if deviceName is already persisted in storage - return this._lawnchairDAO.read(DB_DEVICENAME).then(function(deviceName) { - if (!deviceName) { - throw new Error('Device name not set!'); - } - return deviceName; - }); -}; - -/** - * Geneate a device specific key and secret to authenticate to the private key service. - */ -Keychain.prototype.getDeviceSecret = function() { - var self = this, - config = self._appConfig.config; - - // generate random deviceSecret or get from storage - return self._lawnchairDAO.read(DB_DEVICE_SECRET).then(function(storedDevSecret) { - if (storedDevSecret) { - // a device key is already available locally - return storedDevSecret; - } - - // generate random deviceSecret - var deviceSecret = util.random(config.symKeySize); - // persist deviceSecret to local storage (in plaintext) - return self._lawnchairDAO.persist(DB_DEVICE_SECRET, deviceSecret).then(function() { - return deviceSecret; - }); - }); -}; - -/** - * Register the device on the private key server. This will give the device access to upload an encrypted private key. - * @param {String} options.userId The user's email address - */ -Keychain.prototype.registerDevice = function(options) { - var self = this, - devName, - config = self._appConfig.config; - - // check if deviceName is already persisted in storage - return self.getDeviceName().then(function(deviceName) { - return requestDeviceRegistration(deviceName); - }); - - function requestDeviceRegistration(deviceName) { - devName = deviceName; - - // request device registration session key - return self._privateKeyDao.requestDeviceRegistration({ - userId: options.userId, - deviceName: deviceName - }).then(function(regSessionKey) { - if (!regSessionKey.encryptedRegSessionKey) { - throw new Error('Invalid format for session key!'); - } - - return decryptSessionKey(regSessionKey); - }); - } - - function decryptSessionKey(regSessionKey) { - return self.lookupPublicKey(config.serverPrivateKeyId).then(function(serverPubkey) { - if (!serverPubkey || !serverPubkey.publicKey) { - throw new Error('Server public key for device registration not found!'); - } - - // decrypt the session key - var ct = regSessionKey.encryptedRegSessionKey; - return self._pgp.decrypt(ct, serverPubkey.publicKey).then(function(pt) { - if (!pt.signaturesValid) { - throw new Error('Verifying PGP signature failed!'); - } - - return uploadDeviceSecret(pt.decrypted); - }); - }); - } - - function uploadDeviceSecret(regSessionKey) { - // generate iv - var iv = util.random(config.symIvSize); - // read device secret from local storage - return self.getDeviceSecret().then(function(deviceSecret) { - // encrypt deviceSecret - return self._crypto.encrypt(deviceSecret, regSessionKey, iv); - - }).then(function(encryptedDeviceSecret) { - // upload encryptedDeviceSecret - return self._privateKeyDao.uploadDeviceSecret({ - userId: options.userId, - deviceName: devName, - encryptedDeviceSecret: encryptedDeviceSecret, - iv: iv - }); - }); - } -}; - -// -// Private key functions -// - -/** - * Authenticate to the private key server (required before private PGP key upload). - * @param {String} userId The user's email address - * @return {Object} {sessionId:String, sessionKey:[base64 encoded]} - */ -Keychain.prototype._authenticateToPrivateKeyServer = function(userId) { - var self = this, - sessionId, - config = self._appConfig.config; - - // request auth session key required for upload - return self._privateKeyDao.requestAuthSessionKey({ - userId: userId - }).then(function(authSessionKey) { - if (!authSessionKey.encryptedAuthSessionKey || !authSessionKey.encryptedChallenge || !authSessionKey.sessionId) { - throw new Error('Invalid format for session key!'); - } - - // remember session id for verification - sessionId = authSessionKey.sessionId; - - return decryptSessionKey(authSessionKey); - }); - - function decryptSessionKey(authSessionKey) { - var ptSessionKey, ptChallenge, serverPubkey; - return self.lookupPublicKey(config.serverPrivateKeyId).then(function(pubkey) { - if (!pubkey || !pubkey.publicKey) { - throw new Error('Server public key for authentication not found!'); - } - - serverPubkey = pubkey; - // decrypt the session key - var ct1 = authSessionKey.encryptedAuthSessionKey; - return self._pgp.decrypt(ct1, serverPubkey.publicKey); - - }).then(function(pt) { - if (!pt.signaturesValid) { - throw new Error('Verifying PGP signature failed!'); - } - - ptSessionKey = pt.decrypted; - // decrypt the challenge - var ct2 = authSessionKey.encryptedChallenge; - return self._pgp.decrypt(ct2, serverPubkey.publicKey); - - }).then(function(pt) { - if (!pt.signaturesValid) { - throw new Error('Verifying PGP signature failed!'); - } - - ptChallenge = pt.decrypted; - return encryptChallenge(ptSessionKey, ptChallenge); - }); - } - - function encryptChallenge(sessionKey, challenge) { - var deviceSecret, encryptedChallenge; - var iv = util.random(config.symIvSize); - // get device secret - return self.getDeviceSecret().then(function(secret) { - deviceSecret = secret; - // encrypt the challenge - return self._crypto.encrypt(challenge, sessionKey, iv); - - }).then(function(ct) { - encryptedChallenge = ct; - // encrypt the device secret - return self._crypto.encrypt(deviceSecret, sessionKey, iv); - - }).then(function(encryptedDeviceSecret) { - return replyChallenge({ - encryptedChallenge: encryptedChallenge, - encryptedDeviceSecret: encryptedDeviceSecret, - iv: iv - }, sessionKey); - }); - } - - function replyChallenge(response, sessionKey) { - // respond to challenge by uploading the with the session key encrypted challenge - return self._privateKeyDao.verifyAuthentication({ - userId: userId, - sessionId: sessionId, - encryptedChallenge: response.encryptedChallenge, - encryptedDeviceSecret: response.encryptedDeviceSecret, - iv: response.iv - }).then(function() { - return { - sessionId: sessionId, - sessionKey: sessionKey - }; - }); - } -}; - -/** - * Encrypt and upload the private PGP key to the server. - * @param {String} options.userId The user's email address - * @param {String} options.code The randomly generated or self selected code used to derive the key for the encryption of the private PGP key - */ -Keychain.prototype.uploadPrivateKey = function(options) { - var self = this, - config = self._appConfig.config, - keySize = config.symKeySize, - salt; - - if (!options.userId || !options.code) { - return new Promise(function() { - throw new Error('Incomplete arguments!'); - }); - } - - return deriveKey(options.code); - - function deriveKey(code) { - // generate random salt - salt = util.random(keySize); - // derive key from the code using PBKDF2 - return self._crypto.deriveKey(code, salt, keySize).then(function(key) { - return encryptPrivateKey(key); - }); - } - - function encryptPrivateKey(encryptionKey) { - var privkeyId, pgpBlock, - iv = util.random(config.symIvSize); - - // get private key from local storage - return self.getUserKeyPair(options.userId).then(function(keypair) { - privkeyId = keypair.privateKey._id; - pgpBlock = keypair.privateKey.encryptedKey; - - // encrypt the private key with the derived key - return self._crypto.encrypt(pgpBlock, encryptionKey, iv); - - }).then(function(ct) { - return uploadPrivateKey({ - _id: privkeyId, - userId: options.userId, - encryptedPrivateKey: ct, - salt: salt, - iv: iv - }); - }); - } - - function uploadPrivateKey(payload) { - var pt = payload.encryptedPrivateKey, - iv = payload.iv; - - // authenticate to server for upload - return self._authenticateToPrivateKeyServer(options.userId).then(function(authSessionKey) { - // set sessionId - payload.sessionId = authSessionKey.sessionId; - // encrypt encryptedPrivateKey again using authSessionKey - var key = authSessionKey.sessionKey; - return self._crypto.encrypt(pt, key, iv); - - }).then(function(ct) { - // replace the encryptedPrivateKey with the double wrapped ciphertext - payload.encryptedPrivateKey = ct; - // upload the encrypted priavet key - return self._privateKeyDao.upload(payload); - }); - } -}; - -/** - * 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} options.userId The user's email address - * @param {String} options.keyId The private PGP key id - */ -Keychain.prototype.requestPrivateKeyDownload = function(options) { - return this._privateKeyDao.requestDownload(options); -}; - -/** - * Query if an encrypted private PGP key exists on the server without initializing the recovery procedure - * @param {String} options.userId The user's email address - * @param {String} options.keyId The private PGP key id - */ -Keychain.prototype.hasPrivateKey = function(options) { - return this._privateKeyDao.hasPrivateKey(options); -}; - -/** - * Download the encrypted private PGP key from the server using the recovery token. - * @param {String} options.userId The user's email address - * @param {String} options.keyId The user's email address - * @param {String} options.recoveryToken The recovery token acquired via email/sms from the key server - */ -Keychain.prototype.downloadPrivateKey = function(options) { - return this._privateKeyDao.download(options); -}; - -/** - * 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.code The randomly generated or self selected code used to derive the key for the decryption of the 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 - */ -Keychain.prototype.decryptAndStorePrivateKeyLocally = function(options) { - var self = this, - code = options.code, - salt = options.salt, - config = self._appConfig.config, - keySize = config.symKeySize; - - if (!options._id || !options.userId || !options.code || !options.salt || !options.encryptedPrivateKey || !options.iv) { - return new Promise(function() { - throw new Error('Incomplete arguments!'); - }); - } - - // derive key from the code and the salt using PBKDF2 - return self._crypto.deriveKey(code, salt, keySize).then(function(key) { - return decryptAndStore(key); - }); - - function decryptAndStore(derivedKey) { - // decrypt the private key with the derived key - var ct = options.encryptedPrivateKey, - iv = options.iv; - - return self._crypto.decrypt(ct, derivedKey, iv).then(function(privateKeyArmored) { - // validate pgp key - var keyParams; - try { - keyParams = self._pgp.getKeyParams(privateKeyArmored); - } catch (e) { - throw new Error('Error parsing private PGP key!'); - } - - if (keyParams._id !== options._id || keyParams.userId !== options.userId) { - throw new Error('Private key parameters don\'t match with public key\'s!'); - } - - var keyObject = { - _id: options._id, - userId: options.userId, - encryptedKey: privateKeyArmored - }; - - // store private key locally - return self.saveLocalPrivateKey(keyObject).then(function() { - return keyObject; - }); - - }).catch(function() { - throw new Error('Invalid keychain code!'); - }); - } -}; - // // Keypair functions // diff --git a/src/js/service/privatekey.js b/src/js/service/privatekey.js index 27d3135..3470aaa 100644 --- a/src/js/service/privatekey.js +++ b/src/js/service/privatekey.js @@ -4,96 +4,89 @@ var ngModule = angular.module('woServices'); ngModule.service('privateKey', PrivateKey); module.exports = PrivateKey; -function PrivateKey(privateKeyRestDao) { - this._restDao = privateKeyRestDao; +var ImapClient = require('imap-client'); +var util = require('crypto-lib').util; + +var IMAP_KEYS_FOLDER = 'openpgp_keys'; +var MIME_TYPE = 'application/x.encrypted-pgp-key'; +var MSG_PART_TYPE_ATTACHMENT = 'attachment'; + +function PrivateKey(auth, mailbuild, mailreader, appConfig, pgp, crypto, axe) { + this._auth = auth; + this._Mailbuild = mailbuild; + this._mailreader = mailreader; + this._appConfig = appConfig; + this._pgp = pgp; + this._crypto = crypto; + this._axe = axe; } -// -// Device registration functions -// - /** - * Request registration of a new device by fetching registration session key. - * @param {String} options.userId The user's email address - * @param {String} options.deviceName The device's memorable name - * @return {Object} {encryptedRegSessionKey:[base64]} + * Configure the local imap client used for key-sync with credentials from the auth module. */ -PrivateKey.prototype.requestDeviceRegistration = function(options) { +PrivateKey.prototype.init = function() { var self = this; - return new Promise(function(resolve) { - if (!options.userId || !options.deviceName) { - throw new Error('Incomplete arguments!'); - } - resolve(); - }).then(function() { - var uri = '/device/user/' + options.userId + '/devicename/' + options.deviceName; - return self._restDao.post(undefined, uri); + return self._auth.getCredentials().then(function(credentials) { + // tls socket worker path for multithreaded tls in non-native tls environments + credentials.imap.tlsWorkerPath = self._appConfig.config.workerPath + '/tcp-socket-tls-worker.min.js'; + self._imap = new ImapClient(credentials.imap); + self._imap.onError = self._axe.error; + // login to the imap server + return self._imap.login(); }); }; /** - * 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 {String} options.encryptedDeviceSecret The base64 encoded encrypted device secret - * @param {String} options.iv The iv used for encryption + * Cleanup by logging out of the imap client. */ -PrivateKey.prototype.uploadDeviceSecret = function(options) { - var self = this; +PrivateKey.prototype.destroy = function() { + this._imap.logout(); + // don't wait for logout to complete return new Promise(function(resolve) { - if (!options.userId || !options.deviceName || !options.encryptedDeviceSecret || !options.iv) { - throw new Error('Incomplete arguments!'); - } resolve(); - - }).then(function() { - var uri = '/device/user/' + options.userId + '/devicename/' + options.deviceName; - return self._restDao.put(options, uri); - }); -}; - -// -// Private key functions -// - -/** - * Request authSessionKeys required for upload the encrypted private PGP key. - * @param {String} options.userId The user's email address - * @return {Object} {sessionId, encryptedAuthSessionKey:[base64 encoded], encryptedChallenge:[base64 encoded]} - */ -PrivateKey.prototype.requestAuthSessionKey = function(options) { - var self = this; - return new Promise(function(resolve) { - if (!options.userId) { - throw new Error('Incomplete arguments!'); - } - resolve(); - - }).then(function() { - var uri = '/auth/user/' + options.userId; - return self._restDao.post(undefined, uri); }); }; /** - * 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.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 + * Encrypt and upload the private PGP key to the server. + * @param {String} code The randomly generated or self selected code used to derive the key for the encryption of the private PGP key */ -PrivateKey.prototype.verifyAuthentication = function(options) { - var self = this; - return new Promise(function(resolve) { - if (!options.userId || !options.sessionId || !options.encryptedChallenge || !options.encryptedDeviceSecret || !options.iv) { - throw new Error('Incomplete arguments!'); - } - resolve(); +PrivateKey.prototype.encrypt = function(code) { + var self = this, + config = self._appConfig.config, + keySize = config.symKeySize, + encryptionKey, salt, iv, privkeyId; - }).then(function() { - var uri = '/auth/user/' + options.userId + '/session/' + options.sessionId; - return self._restDao.put(options, uri); + if (!code) { + return new Promise(function() { + throw new Error('Incomplete arguments!'); + }); + } + + // generate random salt and iv + salt = util.random(keySize); + iv = util.random(config.symIvSize); + + // derive key from the code using PBKDF2 + return self._crypto.deriveKey(code, salt, keySize).then(function(key) { + encryptionKey = key; + + // get private key from local storage + return self._pgp.exportKeys(); + }).then(function(keypair) { + privkeyId = keypair.keyId; + + // encrypt the private key with the derived key + return self._crypto.encrypt(keypair.privateKeyArmored, encryptionKey, iv); + + }).then(function(ct) { + return { + _id: privkeyId, + encryptedPrivateKey: ct, + salt: salt, + iv: iv + }; }); }; @@ -102,104 +95,246 @@ PrivateKey.prototype.verifyAuthentication = function(options) { * @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 */ PrivateKey.prototype.upload = function(options) { var self = this; + return new Promise(function(resolve) { - if (!options._id || !options.userId || !options.encryptedPrivateKey || !options.sessionId || !options.salt || !options.iv) { + if (!options._id || !options.userId || !options.encryptedPrivateKey || !options.salt || !options.iv) { throw new Error('Incomplete arguments!'); } resolve(); }).then(function() { - var uri = '/privatekey/user/' + options.userId + '/session/' + options.sessionId; - return self._restDao.post(options, uri); - }); -}; - -/** - * Query if an encrypted private PGP key exists on the server without initializing the recovery procedure. - * @param {String} options.userId The user's email address - * @param {String} options.keyId The private PGP key id - * @return {Boolean} whether the key was found on the server or not. - */ -PrivateKey.prototype.hasPrivateKey = function(options) { - var self = this; - return new Promise(function(resolve) { - if (!options.userId || !options.keyId) { - throw new Error('Incomplete arguments!'); - } - resolve(); - - }).then(function() { - return self._restDao.get({ - uri: '/privatekey/user/' + options.userId + '/key/' + options.keyId + '?ignoreRecovery=true', + // create imap folder + return self._imap.createFolder({ + path: IMAP_KEYS_FOLDER + }).then(function() { + self._axe.debug('Successfully created imap folder ' + IMAP_KEYS_FOLDER); + }).catch(function(err) { + var prettyErr = new Error('Creating imap folder ' + IMAP_KEYS_FOLDER + ' failed: ' + err.message); + self._axe.error(prettyErr); + throw prettyErr; }); - - }).then(function() { - return true; - - }).catch(function(err) { - // 404: there is no encrypted private key on the server - if (err.code && err.code !== 200) { - return false; - } - - throw err; - }); -}; - -/** - * Request download for the encrypted private PGP key. - * @param {String} options.userId The user's email address - * @param {String} options.keyId The private PGP key id - * @return {Boolean} whether the key was found on the server or not. - */ -PrivateKey.prototype.requestDownload = function(options) { - var self = this; - return new Promise(function(resolve) { - if (!options.userId || !options.keyId) { - throw new Error('Incomplete arguments!'); - } - resolve(); - - }).then(function() { - return self._restDao.get({ - uri: '/privatekey/user/' + options.userId + '/key/' + options.keyId + }).then(createMessage).then(function(message) { + // upload to imap folder + return self._imap.uploadMessage({ + path: IMAP_KEYS_FOLDER, + message: message }); + }); - }).then(function() { - return true; + function createMessage() { + var encryptedKeyBuf = util.binStr2Uint8Arr(util.base642Str(options.encryptedPrivateKey)); + var saltBuf = util.binStr2Uint8Arr(util.base642Str(options.salt)); + var ivBuf = util.binStr2Uint8Arr(util.base642Str(options.iv)); - }).catch(function(err) { - // 404: there is no encrypted private key on the server - if (err.code && err.code !== 200) { - return false; - } + // allocate payload buffer for sync + var payloadBuf = new Uint8Array(1 + saltBuf.length + ivBuf.length + encryptedKeyBuf.length); + var offset = 0; + // set version byte + payloadBuf[offset] = 0x01; // version 1 of the key-sync protocol + offset++; + // copy salt bytes + payloadBuf.set(saltBuf, offset); + offset += saltBuf.length; + // copy iv bytes + payloadBuf.set(ivBuf, offset); + offset += ivBuf.length; + // copy encrypted key bytes + payloadBuf.set(encryptedKeyBuf, offset); - throw err; + // create MIME tree + var rootNode = options.rootNode || new self._Mailbuild(); + rootNode.setHeader({ + subject: options._id, + from: options.userId, + to: options.userId, + 'content-type': MIME_TYPE + '; charset=us-ascii', + 'content-transfer-encoding': 'base64' + }); + rootNode.setContent(payloadBuf); + + return rootNode.build(); + } +}; + +/** + * Check if matching private key is stored in IMAP. + */ +PrivateKey.prototype.isSynced = function() { + return this._fetchMessage({ + userId: this._auth.emailAddress, + keyId: this._pgp.getKeyId() + }).then(function(msg) { + return !!msg; + }).catch(function() { + return false; }); }; /** - * Verify the download request for the private PGP key using the recovery token sent via email. This downloads the actual encrypted private key. + * Verify the download request for the private PGP key. * @param {String} options.userId The user's email address * @param {String} options.keyId The private key id - * @param {String} options.recoveryToken The token proving the user own the email account * @return {Object} {_id:[hex encoded capital 16 char key id], encryptedPrivateKey:[base64 encoded], encryptedUserId: [base64 encoded]} */ PrivateKey.prototype.download = function(options) { - var self = this; - return new Promise(function(resolve) { - if (!options.userId || !options.keyId || !options.recoveryToken) { - throw new Error('Incomplete arguments!'); + var self = this, + message; + + return self._fetchMessage(options).then(function(msg) { + if (!msg) { + throw new Error('Private key not synced!'); } - resolve(); + + message = msg; + }).then(function() { + // get the body for the message + return self._imap.getBodyParts({ + path: IMAP_KEYS_FOLDER, + uid: message.uid, + bodyParts: message.bodyParts + }); }).then(function() { - return self._restDao.get({ - uri: '/privatekey/user/' + options.userId + '/key/' + options.keyId + '/recovery/' + options.recoveryToken + // parse the message + return self._parse(message); + + }).then(function(root) { + var payloadBuf = filterBodyParts(root, MSG_PART_TYPE_ATTACHMENT)[0].content; + var offset = 0; + var SALT_LEN = 32; + var IV_LEN = 12; + + // check version + var version = payloadBuf[offset]; + offset++; + if (version !== 1) { + throw new Error('Unsupported key sync protocol version!'); + } + // salt + var saltBuf = payloadBuf.subarray(offset, offset + SALT_LEN); + offset += SALT_LEN; + // iv + var ivBuf = payloadBuf.subarray(offset, offset + IV_LEN); + offset += IV_LEN; + // encrypted private key + var encryptedKeyBuf = payloadBuf.subarray(offset, payloadBuf.length); + + return { + _id: options.keyId, + userId: options.userId, + encryptedPrivateKey: util.str2Base64(util.uint8Arr2BinStr(encryptedKeyBuf)), + salt: util.str2Base64(util.uint8Arr2BinStr(saltBuf)), + iv: util.str2Base64(util.uint8Arr2BinStr(ivBuf)) + }; + }); +}; + +/** + * 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.code The randomly generated or self selected code used to derive the key for the decryption of the 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 + */ +PrivateKey.prototype.decrypt = function(options) { + var self = this, + config = self._appConfig.config, + keySize = config.symKeySize; + + if (!options._id || !options.userId || !options.code || !options.salt || !options.encryptedPrivateKey || !options.iv) { + return new Promise(function() { + throw new Error('Incomplete arguments!'); + }); + } + + // derive key from the code and the salt using PBKDF2 + return self._crypto.deriveKey(options.code, options.salt, keySize).then(function(derivedKey) { + // decrypt the private key with the derived key + return self._crypto.decrypt(options.encryptedPrivateKey, derivedKey, options.iv).catch(function() { + throw new Error('Invalid backup code!'); + }); + + }).then(function(privateKeyArmored) { + // validate pgp key + var keyParams; + try { + keyParams = self._pgp.getKeyParams(privateKeyArmored); + } catch (e) { + throw new Error('Error parsing private PGP key!'); + } + + if (keyParams._id !== options._id || keyParams.userId !== options.userId) { + throw new Error('Private key parameters don\'t match with public key\'s!'); + } + + return { + _id: options._id, + userId: options.userId, + encryptedKey: privateKeyArmored + }; + }); +}; + +PrivateKey.prototype._fetchMessage = function(options) { + var self = this; + + if (!options.userId || !options.keyId) { + return new Promise(function() { + throw new Error('Incomplete arguments!'); + }); + } + + // get the metadata for the message + return self._imap.listMessages({ + path: IMAP_KEYS_FOLDER, + }).then(function(messages) { + if (!messages.length) { + // message has been deleted in the meantime + return; + } + + // get matching private key if multiple keys uloaded + return _.findWhere(messages, { + subject: options.keyId + }); + }).catch(function() { + throw new Error('Imap folder ' + IMAP_KEYS_FOLDER + ' does not exist for key sync!'); + }); +}; + +PrivateKey.prototype._parse = function(options) { + var self = this; + return new Promise(function(resolve, reject) { + self._mailreader.parse(options, function(err, root) { + if (err) { + reject(err); + } else { + resolve(root); + } }); }); -}; \ No newline at end of file +}; + +/** + * Helper function that recursively traverses the body parts tree. Looks for bodyParts that match the provided type and aggregates them + * + * @param {Array} bodyParts The bodyParts array + * @param {String} type The type to look up + * @param {undefined} result Leave undefined, only used for recursion + */ +function filterBodyParts(bodyParts, type, result) { + result = result || []; + bodyParts.forEach(function(part) { + if (part.type === type) { + result.push(part); + } else if (Array.isArray(part.content)) { + filterBodyParts(part.content, type, result); + } + }); + return result; +} \ No newline at end of file diff --git a/src/js/service/publickey-verifier.js b/src/js/service/publickey-verifier.js index 3e0ac99..323a304 100644 --- a/src/js/service/publickey-verifier.js +++ b/src/js/service/publickey-verifier.js @@ -32,8 +32,22 @@ PublickeyVerifier.prototype.configure = function() { }); }; +PublickeyVerifier.prototype.uploadPublicKey = function() { + if (this.keypair) { + return this._keychain.uploadPublicKey(this.keypair.publicKey); + } + return new Promise(function(resolve) { + resolve(); + }); +}; + PublickeyVerifier.prototype.persistKeypair = function() { - return this._keychain.putUserKeyPair(this.keypair); + if (this.keypair) { + return this._keychain.putUserKeyPair(this.keypair); + } + return new Promise(function(resolve) { + resolve(); + }); }; PublickeyVerifier.prototype.verify = function() { diff --git a/src/js/service/rest.js b/src/js/service/rest.js index 1807a66..0c4b082 100644 --- a/src/js/service/rest.js +++ b/src/js/service/rest.js @@ -9,13 +9,6 @@ ngModule.factory('publicKeyRestDao', function(appConfig) { return dao; }); -// rest dao for use in the private key service -ngModule.factory('privateKeyRestDao', function(appConfig) { - var dao = new RestDAO(); - dao.setBaseUri(appConfig.config.privkeyServerUrl); - return dao; -}); - // rest dao for use in the invitation service ngModule.factory('invitationRestDao', function(appConfig) { var dao = new RestDAO(); diff --git a/src/manifest.json b/src/manifest.json index 9a043a1..24022cb 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -12,7 +12,6 @@ "unlimitedStorage", "notifications", "https://keys-test.whiteout.io/", - "https://keychain-test.whiteout.io/", "https://settings.whiteout.io/", "https://admin-node.whiteout.io/", "https://www.googleapis.com/", diff --git a/src/sass/blocks/basics/_form.scss b/src/sass/blocks/basics/_form.scss index b2a56e1..a8a3de8 100644 --- a/src/sass/blocks/basics/_form.scss +++ b/src/sass/blocks/basics/_form.scss @@ -243,6 +243,7 @@ line-height: 1em; border: 1px solid $color-text-light; text-align: center; + background-color: $color-bg; svg { display: inline-block; fill: $color-main; diff --git a/src/sass/blocks/basics/_typo.scss b/src/sass/blocks/basics/_typo.scss index 6821405..8ca5478 100644 --- a/src/sass/blocks/basics/_typo.scss +++ b/src/sass/blocks/basics/_typo.scss @@ -27,4 +27,5 @@ .typo-code { font-family: monospace; font-weight: bold; + user-select: text; } \ No newline at end of file diff --git a/src/sass/blocks/layout/_page.scss b/src/sass/blocks/layout/_page.scss index bd641f3..9c7169a 100644 --- a/src/sass/blocks/layout/_page.scss +++ b/src/sass/blocks/layout/_page.scss @@ -32,6 +32,14 @@ } } + .toolbar { + .toolbar__label { + @include respond-to(xs-only) { + padding-left: 0; + } + } + } + &__main { flex-grow: 1; margin: 0 auto 20px; diff --git a/src/tpl/desktop.html b/src/tpl/desktop.html index 285c100..bf86928 100644 --- a/src/tpl/desktop.html +++ b/src/tpl/desktop.html @@ -24,9 +24,6 @@ - - diff --git a/src/tpl/login-initial.html b/src/tpl/login-initial.html index b2bc63c..0f59c75 100644 --- a/src/tpl/login-initial.html +++ b/src/tpl/login-initial.html @@ -5,10 +5,9 @@
-

PGP key

+

Setup encryption key

- You can either import an existing PGP key or generate a new one. - Your private key remains on your device and is not sent to our servers. + Generate a new encryption key. Your key belongs to you and only you can read encrypted messages.

@@ -31,13 +30,17 @@ Stay up to date on Whiteout Networks products and important announcements.
+
-
- -
+ +

+ + Or import an existing PGP key + +

diff --git a/src/tpl/login-new-device.html b/src/tpl/login-new-device.html index 960a428..75342ae 100644 --- a/src/tpl/login-new-device.html +++ b/src/tpl/login-new-device.html @@ -5,33 +5,28 @@

Import PGP key

-

Please import an existing key from the file system.

+

Please import an existing key. You can import a key via copy/paste or from the filesystem.

-
- On a mobile device? -

- If you cannot import your key via a USB stick, you can setup Key sync - on a desktop PC to securely transfer your PGP key over the Whiteout cloud. - Learn more. -

-
-

{{errMsg}}

- + +
+
+
+ ng-class="{'input-text--error':incorrect}" placeholder="Passphrase" tabindex="3">
- +
+

Lost your keyfile or passphrase? diff --git a/src/tpl/login-privatekey-download.html b/src/tpl/login-privatekey-download.html index 3e38e09..a96b1b2 100644 --- a/src/tpl/login-privatekey-download.html +++ b/src/tpl/login-privatekey-download.html @@ -5,59 +5,29 @@

-
-

Key sync

-

We have sent you an email containing a recovery token. Please copy and paste the token below to download your key.

-
-

{{errMsg}}

+

Enter backup code

+

Please enter the backup code you wrote down during setup to read encrypted messages on this device.

+ +

{{errMsg}}

-
- -
-
- -
-
- -
-
+
+ +
+
+ +
+
+ +
+ -
-
- Got USB? -

- You can also import the key file manually if you're on a device with USB access and your key is on a flash drive. -

-
-
-
-
- -
-

Key sync

-

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

-
-

{{errMsg}}

- -
- -
-
- -
-
- -
-
-

- Lost your keychain code? -

-
+

+ + Or import PGP key as file + +

diff --git a/src/tpl/login-privatekey-upload.html b/src/tpl/login-privatekey-upload.html new file mode 100644 index 0000000..864610b --- /dev/null +++ b/src/tpl/login-privatekey-upload.html @@ -0,0 +1,54 @@ +
+
+
+ Back +
+ + +
+

Backup code

+ +
+

+ Your backup code can be used to securely backup and synchronize your encryption key between devices. +

+

+ {{displayedCode}} +

+

+ Please write down your backup code and keep it in a safe place. Whiteout Networks cannot recover a lost code. +

+ +
+
+ +
+
+
+ +
+

Please confirm the backup code you have written down.

+
+

{{errMsg}}

+ +
+ +
+
+ +
+
+ +
+
+
+ +
+
+
+
\ No newline at end of file diff --git a/src/tpl/login-verify-public-key.html b/src/tpl/login-verify-public-key.html index 5ee4e74..e5af695 100644 --- a/src/tpl/login-verify-public-key.html +++ b/src/tpl/login-verify-public-key.html @@ -4,15 +4,15 @@ whiteout.io
-

Email address verification

-

- We will now automatically verify your email address with a confirmation message we've sent you. -

+

Email address verification

+

+ We will now automatically verify your email address with a confirmation message we've sent you. +

Verifying your email address in {{countdown}} seconds.

- +

{{errMsg}}

diff --git a/src/tpl/nav.html b/src/tpl/nav.html index 5cc44bf..2973555 100644 --- a/src/tpl/nav.html +++ b/src/tpl/nav.html @@ -57,11 +57,6 @@ Contacts -
  • - - Key sync (experimental) - -
  • Report a bug diff --git a/src/tpl/privatekey-upload.html b/src/tpl/privatekey-upload.html deleted file mode 100644 index cb8aaed..0000000 --- a/src/tpl/privatekey-upload.html +++ /dev/null @@ -1,53 +0,0 @@ - \ No newline at end of file diff --git a/test/unit/controller/app/navigation-ctrl-test.js b/test/unit/controller/app/navigation-ctrl-test.js index 2ce131e..936b34b 100644 --- a/test/unit/controller/app/navigation-ctrl-test.js +++ b/test/unit/controller/app/navigation-ctrl-test.js @@ -5,10 +5,11 @@ var NavigationCtrl = require('../../../../src/js/controller/app/navigation'), Account = require('../../../../src/js/email/account'), Outbox = require('../../../../src/js/email/outbox'), Dialog = require('../../../../src/js/util/dialog'), - Notif = require('../../../../src/js/util/notification'); + Notif = require('../../../../src/js/util/notification'), + PrivateKey = require('../../../../src/js/service/privatekey'); describe('Navigation Controller unit test', function() { - var scope, ctrl, emailDaoMock, accountMock, notificationStub, dialogStub, outboxBoMock, outboxFolder; + var scope, ctrl, emailDaoMock, accountMock, notificationStub, privateKeyStub, dialogStub, outboxBoMock, outboxFolder; beforeEach(function() { var account = { @@ -29,6 +30,7 @@ describe('Navigation Controller unit test', function() { outboxBoMock.startChecking.returns(); dialogStub = sinon.createStubInstance(Dialog); notificationStub = sinon.createStubInstance(Notif); + privateKeyStub = sinon.createStubInstance(PrivateKey); accountMock = sinon.createStubInstance(Account); accountMock.list.returns([account]); accountMock.isLoggedIn.returns(true); @@ -46,7 +48,8 @@ describe('Navigation Controller unit test', function() { email: emailDaoMock, outbox: outboxBoMock, notification: notificationStub, - dialog: dialogStub + dialog: dialogStub, + privateKey: privateKeyStub }); }); }); @@ -85,4 +88,20 @@ describe('Navigation Controller unit test', function() { expect(outboxFolder.count).to.equal(5); }); }); + + describe('checkKeySyncStatus', function() { + it('should work', function(done) { + privateKeyStub.init.returns(resolves()); + privateKeyStub.isSynced.returns(resolves()); + privateKeyStub.destroy.returns(resolves()); + + scope.checkKeySyncStatus().then(done); + }); + + it('should fail silently', function(done) { + privateKeyStub.init.returns(rejects()); + + scope.checkKeySyncStatus().then(done); + }); + }); }); \ No newline at end of file diff --git a/test/unit/controller/app/privatekey-upload-ctrl-test.js b/test/unit/controller/app/privatekey-upload-ctrl-test.js deleted file mode 100644 index a88538d..0000000 --- a/test/unit/controller/app/privatekey-upload-ctrl-test.js +++ /dev/null @@ -1,225 +0,0 @@ -'use strict'; - -var PrivateKeyUploadCtrl = require('../../../../src/js/controller/app/privatekey-upload'), - KeychainDAO = require('../../../../src/js/service/keychain'), - PGP = require('../../../../src/js/crypto/pgp'), - Dialog = require('../../../../src/js/util/dialog'); - -describe('Private Key Upload Controller unit test', function() { - var scope, location, ctrl, - keychainMock, pgpStub, dialogStub, - emailAddress = 'fred@foo.com'; - - beforeEach(function() { - keychainMock = sinon.createStubInstance(KeychainDAO); - pgpStub = sinon.createStubInstance(PGP); - dialogStub = sinon.createStubInstance(Dialog); - - angular.module('login-privatekey-download-test', ['woServices']); - angular.mock.module('login-privatekey-download-test'); - angular.mock.inject(function($controller, $rootScope) { - scope = $rootScope.$new(); - scope.state = {}; - ctrl = $controller(PrivateKeyUploadCtrl, { - $location: location, - $scope: scope, - $q: window.qMock, - keychain: keychainMock, - pgp: pgpStub, - dialog: dialogStub, - auth: { - emailAddress: emailAddress - } - }); - }); - }); - - afterEach(function() {}); - - describe('checkServerForKey', function() { - var keyParams = { - userId: emailAddress, - _id: 'keyId', - }; - - it('should fail', function(done) { - pgpStub.getKeyParams.returns(keyParams); - keychainMock.hasPrivateKey.returns(rejects(42)); - - scope.checkServerForKey().then(function() { - expect(dialogStub.error.calledOnce).to.be.true; - expect(keychainMock.hasPrivateKey.calledOnce).to.be.true; - done(); - }); - }); - - it('should return true', function(done) { - pgpStub.getKeyParams.returns(keyParams); - keychainMock.hasPrivateKey.withArgs({ - userId: keyParams.userId, - keyId: keyParams._id - }).returns(resolves(true)); - - scope.checkServerForKey().then(function(privateKeySynced) { - expect(privateKeySynced).to.be.true; - done(); - }); - }); - - it('should return undefined', function(done) { - pgpStub.getKeyParams.returns(keyParams); - keychainMock.hasPrivateKey.withArgs({ - userId: keyParams.userId, - keyId: keyParams._id - }).returns(resolves(false)); - - scope.checkServerForKey().then(function(privateKeySynced) { - expect(privateKeySynced).to.be.undefined; - done(); - }); - }); - }); - - describe('displayUploadUi', function() { - it('should work', function() { - // add some artifacts from a previous key input - scope.inputCode = 'asdasd'; - - scope.displayUploadUi(); - expect(scope.step).to.equal(1); - expect(scope.code.length).to.equal(24); - - // artifacts should be cleared - expect(scope.inputCode).to.be.empty; - }); - }); - - describe('verifyCode', function() { - it('should fail for wrong code', function() { - scope.inputCode = 'bbbbbb'; - scope.code = 'AAAAAA'; - - expect(scope.verifyCode()).to.be.false; - }); - - it('should work', function() { - scope.inputCode = 'aaAaaa'; - scope.code = 'AAAAAA'; - - expect(scope.verifyCode()).to.be.true; - }); - }); - - describe('setDeviceName', function() { - it('should work', function(done) { - keychainMock.setDeviceName.returns(resolves()); - scope.setDeviceName().then(done); - }); - }); - - describe('encryptAndUploadKey', function() { - it('should fail due to keychain.registerDevice', function(done) { - keychainMock.registerDevice.returns(rejects(42)); - - scope.encryptAndUploadKey().then(function() { - expect(dialogStub.error.calledOnce).to.be.true; - expect(keychainMock.registerDevice.calledOnce).to.be.true; - done(); - }); - }); - - it('should work', function(done) { - keychainMock.registerDevice.returns(resolves()); - keychainMock.uploadPrivateKey.returns(resolves()); - - scope.encryptAndUploadKey().then(function() { - 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.returns(rejects(42)); - - scope.goForward().then(function() { - expect(dialogStub.error.calledOnce).to.be.true; - expect(scope.step).to.equal(3); - done(); - }); - }); - - it('should fail for 3 due to error in encryptAndUploadKey', function(done) { - scope.step = 3; - setDeviceNameStub.returns(resolves()); - encryptAndUploadKeyStub.returns(rejects(42)); - - scope.goForward().then(function() { - expect(dialogStub.error.calledOnce).to.be.true; - expect(scope.step).to.equal(4); - done(); - }); - }); - - it('should work for 3', function(done) { - scope.step = 3; - setDeviceNameStub.returns(resolves()); - encryptAndUploadKeyStub.returns(resolves()); - - scope.goForward().then(function() { - expect(dialogStub.info.calledOnce).to.be.true; - expect(scope.step).to.equal(4); - done(); - }); - }); - }); -}); \ No newline at end of file diff --git a/test/unit/controller/login/login-ctrl-test.js b/test/unit/controller/login/login-ctrl-test.js index 839cfa1..06af002 100644 --- a/test/unit/controller/login/login-ctrl-test.js +++ b/test/unit/controller/login/login-ctrl-test.js @@ -148,22 +148,6 @@ describe('Login Controller unit test', function() { }); }); - it('should fail for keychain.requestPrivateKeyDownload', function(done) { - authMock.init.returns(resolves()); - authMock.getEmailAddress.returns(resolves({ - emailAddress: emailAddress - })); - accountMock.init.returns(resolves({ - publicKey: 'publicKey' - })); - keychainMock.requestPrivateKeyDownload.returns(rejects(new Error())); - - scope.init().then(function() { - expect(dialogMock.error.calledOnce).to.be.true; - done(); - }); - }); - it('should redirect to /login-privatekey-download', function(done) { authMock.init.returns(resolves()); authMock.getEmailAddress.returns(resolves({ @@ -172,7 +156,6 @@ describe('Login Controller unit test', function() { accountMock.init.returns(resolves({ publicKey: 'publicKey' })); - keychainMock.requestPrivateKeyDownload.returns(resolves(true)); scope.init().then(function() { expect(goToStub.withArgs('/login-privatekey-download').called).to.be.true; @@ -181,23 +164,6 @@ describe('Login Controller unit test', function() { }); }); - it('should redirect to /login-new-device', function(done) { - authMock.init.returns(resolves()); - authMock.getEmailAddress.returns(resolves({ - emailAddress: emailAddress - })); - accountMock.init.returns(resolves({ - publicKey: 'publicKey' - })); - keychainMock.requestPrivateKeyDownload.returns(resolves()); - - scope.init().then(function() { - expect(goToStub.withArgs('/login-new-device').called).to.be.true; - expect(goToStub.calledOnce).to.be.true; - done(); - }); - }); - it('should redirect to /login-initial', function(done) { authMock.init.returns(resolves()); authMock.getEmailAddress.returns(resolves({ diff --git a/test/unit/controller/login/login-initial-ctrl-test.js b/test/unit/controller/login/login-initial-ctrl-test.js index 7e0296f..cd47726 100644 --- a/test/unit/controller/login/login-initial-ctrl-test.js +++ b/test/unit/controller/login/login-initial-ctrl-test.js @@ -112,7 +112,7 @@ describe('Login (initial user) Controller unit test', function() { expect(scope.errMsg).to.not.exist; expect(scope.state.ui).to.equal(2); expect(newsletterStub.called).to.be.true; - expect(location.$$path).to.equal('/login-verify-public-key'); + expect(location.$$path).to.equal('/login-privatekey-upload'); expect(emailMock.unlock.calledOnce).to.be.true; done(); }); diff --git a/test/unit/controller/login/login-new-device-ctrl-test.js b/test/unit/controller/login/login-new-device-ctrl-test.js index 3962730..c7b322b 100644 --- a/test/unit/controller/login/login-new-device-ctrl-test.js +++ b/test/unit/controller/login/login-new-device-ctrl-test.js @@ -60,6 +60,15 @@ describe('Login (new device) Controller unit test', function() { }); }); + describe('pasteKey', function() { + it('should work', function() { + var keyStr = '-----BEGIN PGP PRIVATE KEY BLOCK----- asdf -----END PGP PRIVATE KEY BLOCK-----'; + scope.pasteKey(keyStr); + + expect(scope.key.privateKeyArmored).to.equal(keyStr); + }); + }); + describe('confirm passphrase', function() { it('should unlock crypto with a public key on the server', function(done) { scope.passphrase = passphrase; @@ -107,7 +116,7 @@ describe('Login (new device) Controller unit test', function() { scope.confirmPassphrase().then(function() { expect(emailMock.unlock.calledOnce).to.be.true; expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; - expect(location.$$path).to.equal('/login-verify-public-key'); + expect(location.$$path).to.equal('/login-privatekey-upload'); done(); }); }); diff --git a/test/unit/controller/login/login-privatekey-download-ctrl-test.js b/test/unit/controller/login/login-privatekey-download-ctrl-test.js index 3a8eb94..b99e9bf 100644 --- a/test/unit/controller/login/login-privatekey-download-ctrl-test.js +++ b/test/unit/controller/login/login-privatekey-download-ctrl-test.js @@ -3,16 +3,18 @@ var Auth = require('../../../../src/js/service/auth'), LoginPrivateKeyDownloadCtrl = require('../../../../src/js/controller/login/login-privatekey-download'), Email = require('../../../../src/js/email/email'), - Keychain = require('../../../../src/js/service/keychain'); + Keychain = require('../../../../src/js/service/keychain'), + PrivateKey = require('../../../../src/js/service/privatekey'); describe('Login Private Key Download Controller unit test', function() { var scope, location, ctrl, - emailDaoMock, authMock, keychainMock, + emailDaoMock, authMock, keychainMock, privateKeyStub, emailAddress = 'fred@foo.com'; beforeEach(function(done) { emailDaoMock = sinon.createStubInstance(Email); keychainMock = sinon.createStubInstance(Keychain); + privateKeyStub = sinon.createStubInstance(PrivateKey); authMock = sinon.createStubInstance(Auth); authMock.emailAddress = emailAddress; @@ -22,8 +24,7 @@ describe('Login Private Key Download Controller unit test', function() { angular.mock.inject(function($controller, $rootScope, $location) { scope = $rootScope.$new(); scope.state = {}; - scope.tokenForm = {}; - scope.codeForm = {}; + scope.form = {}; location = $location; ctrl = $controller(LoginPrivateKeyDownloadCtrl, { $location: location, @@ -32,7 +33,8 @@ describe('Login Private Key Download Controller unit test', function() { $q: window.qMock, auth: authMock, email: emailDaoMock, - keychain: keychainMock + keychain: keychainMock, + privateKey: privateKeyStub }); done(); }); @@ -40,117 +42,73 @@ describe('Login Private Key Download Controller unit test', function() { afterEach(function() {}); - describe('initialization', function() { - it('should work', function() { - expect(scope.step).to.equal(1); - }); - }); - - describe('checkToken', function() { - var testKeypair = { + describe('checkCode', function() { + var encryptedPrivateKey = { + encryptedPrivateKey: 'encryptedPrivateKey' + }; + var cachedKeypair = { publicKey: { - _id: 'id' + _id: 'keyId' } }; + var privkey = { + _id: cachedKeypair.publicKey._id, + userId: emailAddress, + encryptedKey: 'PRIVATE PGP BLOCK' + }; - it('should fail for empty recovery token', function() { - scope.tokenForm.$invalid = true; - - scope.checkToken(); - - expect(keychainMock.getUserKeyPair.calledOnce).to.be.false; - expect(scope.errMsg).to.exist; - }); - - it('should fail in keychain.getUserKeyPair', function(done) { - keychainMock.getUserKeyPair.returns(rejects(new Error('asdf'))); - - scope.checkToken().then(function() { - expect(scope.errMsg).to.exist; - expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; - done(); - }); - }); - - it('should fail in keychain.downloadPrivateKey', function(done) { - keychainMock.getUserKeyPair.returns(resolves(testKeypair)); - keychainMock.downloadPrivateKey.returns(rejects(new Error('asdf'))); - scope.recoveryToken = 'token'; - - scope.checkToken().then(function() { - expect(scope.errMsg).to.exist; - expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; - expect(keychainMock.downloadPrivateKey.calledOnce).to.be.true; - done(); - }); - }); - - it('should work', function(done) { - keychainMock.getUserKeyPair.returns(resolves(testKeypair)); - keychainMock.downloadPrivateKey.returns(resolves('encryptedPrivateKey')); - scope.recoveryToken = 'token'; - - scope.checkToken().then(function() { - expect(scope.encryptedPrivateKey).to.equal('encryptedPrivateKey'); - done(); - }); - }); - }); - - describe('checkCode', function() { beforeEach(function() { scope.code = '012345'; - scope.encryptedPrivateKey = { - encryptedPrivateKey: 'encryptedPrivateKey' - }; - scope.cachedKeypair = { - publicKey: { - _id: 'keyId' - } - }; - sinon.stub(scope, 'goTo'); }); afterEach(function() { scope.goTo.restore(); }); - it('should fail on decryptAndStorePrivateKeyLocally', function(done) { - keychainMock.decryptAndStorePrivateKeyLocally.returns(rejects(new Error('asdf'))); + it('should fail on privateKey.init', function(done) { + privateKeyStub.init.returns(rejects(new Error('asdf'))); scope.checkCode().then(function() { - expect(scope.errMsg).to.exist; - expect(keychainMock.decryptAndStorePrivateKeyLocally.calledOnce).to.be.true; + expect(scope.errMsg).to.match(/asdf/); + expect(privateKeyStub.init.calledOnce).to.be.true; done(); }); }); - it('should goto /login-existing on emailDao.unlock fail', function(done) { - keychainMock.decryptAndStorePrivateKeyLocally.returns(resolves({ - encryptedKey: 'keyArmored' - })); - emailDaoMock.unlock.returns(rejects(new Error('asdf'))); + it('should work with empty passphrase', function(done) { + privateKeyStub.init.returns(resolves()); + keychainMock.getUserKeyPair.withArgs(emailAddress).returns(resolves(cachedKeypair)); + privateKeyStub.download.withArgs({ + userId: emailAddress, + keyId: cachedKeypair.publicKey._id + }).returns(resolves(encryptedPrivateKey)); + privateKeyStub.decrypt.returns(resolves(privkey)); + emailDaoMock.unlock.returns(resolves()); + authMock.storeCredentials.returns(resolves()); + privateKeyStub.destroy.returns(resolves()); + + scope.checkCode().then(function() { + expect(scope.errMsg).to.not.exist; + expect(scope.goTo.withArgs('/account').calledOnce).to.be.true; + done(); + }); + }); + + it('should work with passphrase', function(done) { + privateKeyStub.init.returns(resolves()); + keychainMock.getUserKeyPair.withArgs(emailAddress).returns(resolves(cachedKeypair)); + privateKeyStub.download.withArgs({ + userId: emailAddress, + keyId: cachedKeypair.publicKey._id + }).returns(resolves(encryptedPrivateKey)); + privateKeyStub.decrypt.returns(resolves(privkey)); + emailDaoMock.unlock.returns(rejects(new Error())); + authMock.storeCredentials.returns(resolves()); + privateKeyStub.destroy.returns(resolves()); scope.checkCode().then(function() { expect(scope.goTo.withArgs('/login-existing').calledOnce).to.be.true; - expect(keychainMock.decryptAndStorePrivateKeyLocally.calledOnce).to.be.true; - expect(emailDaoMock.unlock.calledOnce).to.be.true; - done(); - }); - }); - - it('should goto /account on emailDao.unlock success', function(done) { - keychainMock.decryptAndStorePrivateKeyLocally.returns(resolves({ - encryptedKey: 'keyArmored' - })); - emailDaoMock.unlock.returns(resolves()); - authMock.storeCredentials.returns(resolves()); - - scope.checkCode().then(function() { - expect(scope.goTo.withArgs('/account').calledOnce).to.be.true; - expect(keychainMock.decryptAndStorePrivateKeyLocally.calledOnce).to.be.true; - expect(emailDaoMock.unlock.calledOnce).to.be.true; done(); }); }); diff --git a/test/unit/controller/login/login-privatekey-upload-ctrl-test.js b/test/unit/controller/login/login-privatekey-upload-ctrl-test.js new file mode 100644 index 0000000..9183841 --- /dev/null +++ b/test/unit/controller/login/login-privatekey-upload-ctrl-test.js @@ -0,0 +1,78 @@ +'use strict'; + +var Auth = require('../../../../src/js/service/auth'), + LoginPrivateKeyUploadCtrl = require('../../../../src/js/controller/login/login-privatekey-upload'), + PrivateKey = require('../../../../src/js/service/privatekey'); + +describe('Login Private Key Upload Controller unit test', function() { + var scope, location, ctrl, + authMock, privateKeyStub, + emailAddress = 'fred@foo.com'; + + beforeEach(function(done) { + privateKeyStub = sinon.createStubInstance(PrivateKey); + authMock = sinon.createStubInstance(Auth); + + authMock.emailAddress = emailAddress; + + angular.module('login-privatekey-download-test', ['woServices']); + angular.mock.module('login-privatekey-download-test'); + angular.mock.inject(function($controller, $rootScope, $location) { + scope = $rootScope.$new(); + scope.state = {}; + scope.form = {}; + location = $location; + ctrl = $controller(LoginPrivateKeyUploadCtrl, { + $location: location, + $scope: scope, + $routeParams: {}, + $q: window.qMock, + auth: authMock, + privateKey: privateKeyStub + }); + done(); + }); + }); + + afterEach(function() {}); + + describe('init', function() { + it('should work', function() { + expect(scope.step).to.equal(1); + expect(scope.code).to.exist; + expect(scope.displayedCode).to.exist; + expect(scope.inputCode).to.equal(''); + }); + }); + + describe('encryptAndUploadKey', function() { + var encryptedPrivateKey = { + encryptedPrivateKey: 'encryptedPrivateKey' + }; + + beforeEach(function() { + scope.inputCode = scope.code; + sinon.spy(location, 'path'); + }); + + it('should fail for invalid code', function() { + scope.inputCode = 'asdf'; + scope.encryptAndUploadKey().then(function() { + expect(scope.errMsg).to.match(/go back and check/); + }); + }); + + it('should work', function(done) { + privateKeyStub.init.returns(resolves()); + privateKeyStub.encrypt.withArgs(scope.code).returns(resolves(encryptedPrivateKey)); + privateKeyStub.upload.returns(resolves()); + privateKeyStub.destroy.returns(resolves()); + + scope.encryptAndUploadKey().then(function() { + expect(scope.errMsg).to.not.exist; + location.path.calledWith('/login-verify-public-key'); + done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/unit/controller/login/login-verify-public-key-ctrl-test.js b/test/unit/controller/login/login-verify-public-key-ctrl-test.js index f3281e6..e6fe7c0 100644 --- a/test/unit/controller/login/login-verify-public-key-ctrl-test.js +++ b/test/unit/controller/login/login-verify-public-key-ctrl-test.js @@ -3,7 +3,7 @@ var Auth = require('../../../../src/js/service/auth'), Dialog = require('../../../../src/js/util/dialog'), PublicKeyVerifier = require('../../../../src/js/service/publickey-verifier'), - KeychainDAO = require('../../../../src/js/service/keychain'), + PublicKey = require('../../../../src/js/service/publickey'), PublicKeyVerifierCtrl = require('../../../../src/js/controller/login/login-verify-public-key'); describe('Public Key Verification Controller unit test', function() { @@ -11,7 +11,7 @@ describe('Public Key Verification Controller unit test', function() { var scope, location; // Stubs & Fixture - var auth, verifier, dialogStub, keychain; + var auth, verifier, dialogStub, publicKeyStub; var emailAddress = 'foo@foo.com'; // SUT @@ -22,8 +22,9 @@ describe('Public Key Verification Controller unit test', function() { auth = sinon.createStubInstance(Auth); verifier = sinon.createStubInstance(PublicKeyVerifier); dialogStub = sinon.createStubInstance(Dialog); - keychain = sinon.createStubInstance(KeychainDAO); + publicKeyStub = sinon.createStubInstance(PublicKey); + verifier.uploadPublicKey.returns(resolves()); auth.emailAddress = emailAddress; // setup the controller @@ -39,7 +40,7 @@ describe('Public Key Verification Controller unit test', function() { auth: auth, publickeyVerifier: verifier, dialog: dialogStub, - keychain: keychain, + publicKey: publicKeyStub, appConfig: { string: { publickeyVerificationSkipTitle: 'foo', @@ -56,14 +57,14 @@ describe('Public Key Verification Controller unit test', function() { it('should verify', function(done) { var credentials = {}; - keychain.getUserKeyPair.withArgs(emailAddress).returns(resolves({})); + publicKeyStub.getByUserId.withArgs(emailAddress).returns(resolves()); auth.getCredentials.returns(resolves(credentials)); verifier.configure.withArgs(credentials).returns(resolves()); verifier.verify.withArgs().returns(resolves()); verifier.persistKeypair.returns(resolves()); scope.verify().then(function() { - expect(keychain.getUserKeyPair.calledOnce).to.be.true; + expect(publicKeyStub.getByUserId.calledOnce).to.be.true; expect(auth.getCredentials.calledOnce).to.be.true; expect(verifier.configure.calledOnce).to.be.true; expect(verifier.verify.calledOnce).to.be.true; @@ -75,12 +76,12 @@ describe('Public Key Verification Controller unit test', function() { }); it('should skip verification when key is already verified', function(done) { - keychain.getUserKeyPair.withArgs(emailAddress).returns(resolves({ + publicKeyStub.getByUserId.withArgs(emailAddress).returns(resolves({ publicKey: {} })); scope.verify().then(function() { - expect(keychain.getUserKeyPair.calledOnce).to.be.true; + expect(publicKeyStub.getByUserId.calledOnce).to.be.true; expect(auth.getCredentials.called).to.be.false; expect(verifier.configure.called).to.be.false; expect(verifier.verify.called).to.be.false; diff --git a/test/unit/service/keychain-dao-test.js b/test/unit/service/keychain-dao-test.js index 1e7bcf8..a7739ae 100644 --- a/test/unit/service/keychain-dao-test.js +++ b/test/unit/service/keychain-dao-test.js @@ -598,764 +598,6 @@ describe('Keychain DAO unit tests', function() { }); }); - describe('setDeviceName', function() { - it('should work', function(done) { - lawnchairDaoStub.persist.returns(resolves()); - - keychainDao.setDeviceName('iPhone').then(done); - }); - }); - - describe('getDeviceName', function() { - it('should fail when device name is not set', function(done) { - lawnchairDaoStub.read.withArgs('devicename').returns(resolves()); - - keychainDao.getDeviceName().catch(function(err) { - expect(err.message).to.equal('Device name not set!'); - done(); - }); - }); - - it('should fail due to error when reading device name', function(done) { - lawnchairDaoStub.read.withArgs('devicename').returns(rejects(42)); - - keychainDao.getDeviceName().catch(function(err) { - expect(err).to.equal(42); - done(); - }); - }); - - it('should work', function(done) { - lawnchairDaoStub.read.withArgs('devicename').returns(resolves('iPhone')); - - keychainDao.getDeviceName().then(function(deviceName) { - expect(deviceName).to.equal('iPhone'); - done(); - }); - }); - }); - - describe('getDeviceSecret', function() { - it('should fail due to error when reading device secret', function(done) { - lawnchairDaoStub.read.withArgs('devicename').returns(resolves('iPhone')); - lawnchairDaoStub.read.withArgs('devicesecret').returns(rejects(42)); - - keychainDao.getDeviceSecret().catch(function(err) { - expect(err).to.equal(42); - done(); - }); - }); - - it('should fail due to error when storing device secret', function(done) { - lawnchairDaoStub.read.withArgs('devicename').returns(resolves('iPhone')); - lawnchairDaoStub.read.withArgs('devicesecret').returns(resolves()); - lawnchairDaoStub.persist.withArgs('devicesecret').returns(rejects(42)); - - keychainDao.getDeviceSecret().catch(function(err) { - expect(err).to.equal(42); - done(); - }); - }); - - it('should work when device secret is not set', function(done) { - lawnchairDaoStub.read.withArgs('devicename').returns(resolves('iPhone')); - lawnchairDaoStub.read.withArgs('devicesecret').returns(resolves()); - lawnchairDaoStub.persist.withArgs('devicesecret').returns(resolves()); - - keychainDao.getDeviceSecret().then(function(deviceSecret) { - expect(deviceSecret).to.exist; - done(); - }); - }); - - it('should work when device secret is set', function(done) { - lawnchairDaoStub.read.withArgs('devicename').returns(resolves('iPhone')); - lawnchairDaoStub.read.withArgs('devicesecret').returns(resolves('secret')); - - keychainDao.getDeviceSecret().then(function(deviceSecret) { - expect(deviceSecret).to.equal('secret'); - done(); - }); - }); - }); - - describe('registerDevice', function() { - var getDeviceNameStub, lookupPublicKeyStub, getDeviceSecretStub; - - beforeEach(function() { - getDeviceNameStub = sinon.stub(keychainDao, 'getDeviceName'); - lookupPublicKeyStub = sinon.stub(keychainDao, 'lookupPublicKey'); - getDeviceSecretStub = sinon.stub(keychainDao, 'getDeviceSecret'); - }); - afterEach(function() { - getDeviceNameStub.restore(); - lookupPublicKeyStub.restore(); - getDeviceSecretStub.restore(); - }); - - it('should fail when reading devicename', function(done) { - getDeviceNameStub.returns(rejects(42)); - - keychainDao.registerDevice({}).catch(function(err) { - expect(err).to.equal(42); - done(); - }); - }); - - it('should fail in requestDeviceRegistration', function(done) { - getDeviceNameStub.returns(resolves('iPhone')); - - privkeyDaoStub.requestDeviceRegistration.withArgs({ - userId: testUser, - deviceName: 'iPhone' - }).returns(rejects(42)); - - keychainDao.registerDevice({ - userId: testUser - }).catch(function(err) { - expect(err).to.equal(42); - done(); - }); - }); - - it('should fail due to invalid requestDeviceRegistration return value', function(done) { - getDeviceNameStub.returns(resolves('iPhone')); - - privkeyDaoStub.requestDeviceRegistration.withArgs({ - userId: testUser, - deviceName: 'iPhone' - }).returns(resolves({})); - - keychainDao.registerDevice({ - userId: testUser - }).catch(function(err) { - expect(err.message).to.equal('Invalid format for session key!'); - done(); - }); - }); - - it('should fail in lookupPublicKey', function(done) { - getDeviceNameStub.returns(resolves('iPhone')); - - privkeyDaoStub.requestDeviceRegistration.withArgs({ - userId: testUser, - deviceName: 'iPhone' - }).returns(resolves({ - encryptedRegSessionKey: 'asdf' - })); - - lookupPublicKeyStub.returns(rejects(42)); - - keychainDao.registerDevice({ - userId: testUser - }).catch(function(err) { - expect(err).to.equal(42); - done(); - }); - }); - - it('should fail when server public key not found', function(done) { - getDeviceNameStub.returns(resolves('iPhone')); - - privkeyDaoStub.requestDeviceRegistration.withArgs({ - userId: testUser, - deviceName: 'iPhone' - }).returns(resolves({ - encryptedRegSessionKey: 'asdf' - })); - - lookupPublicKeyStub.returns(resolves()); - - keychainDao.registerDevice({ - userId: testUser - }).catch(function(err) { - expect(err).to.exist; - done(); - }); - }); - - it('should fail in decrypt', function(done) { - getDeviceNameStub.returns(resolves('iPhone')); - - privkeyDaoStub.requestDeviceRegistration.withArgs({ - userId: testUser, - deviceName: 'iPhone' - }).returns(resolves({ - encryptedRegSessionKey: 'asdf' - })); - - lookupPublicKeyStub.returns(resolves({ - publicKey: 'pubkey' - })); - pgpStub.decrypt.withArgs('asdf', 'pubkey').returns(rejects(42)); - - keychainDao.registerDevice({ - userId: testUser - }).catch(function(err) { - expect(err).to.equal(42); - done(); - }); - }); - - it('should fail in getDeviceSecret', function(done) { - getDeviceNameStub.returns(resolves('iPhone')); - - privkeyDaoStub.requestDeviceRegistration.withArgs({ - userId: testUser, - deviceName: 'iPhone' - }).returns(resolves({ - encryptedRegSessionKey: 'asdf' - })); - - lookupPublicKeyStub.returns(resolves({ - publicKey: 'pubkey' - })); - pgpStub.decrypt.withArgs('asdf', 'pubkey').returns(resolves({ - decrypted: 'decrypted', - signaturesValid: true - })); - getDeviceSecretStub.returns(rejects(42)); - - keychainDao.registerDevice({ - userId: testUser - }).catch(function(err) { - expect(err).to.equal(42); - done(); - }); - }); - - it('should fail in encrypt', function(done) { - getDeviceNameStub.returns(resolves('iPhone')); - - privkeyDaoStub.requestDeviceRegistration.withArgs({ - userId: testUser, - deviceName: 'iPhone' - }).returns(resolves({ - encryptedRegSessionKey: 'asdf' - })); - - lookupPublicKeyStub.returns(resolves({ - publicKey: 'pubkey' - })); - pgpStub.decrypt.withArgs('asdf', 'pubkey').returns(resolves({ - decrypted: 'decrypted', - signaturesValid: true - })); - getDeviceSecretStub.returns(resolves('secret')); - cryptoStub.encrypt.withArgs('secret', 'decrypted').returns(rejects(42)); - - keychainDao.registerDevice({ - userId: testUser - }).catch(function(err) { - expect(err).to.equal(42); - done(); - }); - }); - - it('should work', function(done) { - getDeviceNameStub.returns(resolves('iPhone')); - - privkeyDaoStub.requestDeviceRegistration.withArgs({ - userId: testUser, - deviceName: 'iPhone' - }).returns(resolves({ - encryptedRegSessionKey: 'asdf' - })); - - lookupPublicKeyStub.returns(resolves({ - publicKey: 'pubkey' - })); - pgpStub.decrypt.withArgs('asdf', 'pubkey').returns(resolves({ - decrypted: 'decrypted', - signaturesValid: true - })); - getDeviceSecretStub.returns(resolves('secret')); - cryptoStub.encrypt.withArgs('secret', 'decrypted').returns(resolves('encryptedDeviceSecret')); - privkeyDaoStub.uploadDeviceSecret.returns(resolves()); - - keychainDao.registerDevice({ - userId: testUser - }).then(function() { - expect(privkeyDaoStub.uploadDeviceSecret.calledOnce).to.be.true; - done(); - }); - }); - }); - - describe('_authenticateToPrivateKeyServer', function() { - var lookupPublicKeyStub, getDeviceSecretStub; - - beforeEach(function() { - lookupPublicKeyStub = sinon.stub(keychainDao, 'lookupPublicKey'); - getDeviceSecretStub = sinon.stub(keychainDao, 'getDeviceSecret'); - }); - afterEach(function() { - lookupPublicKeyStub.restore(); - getDeviceSecretStub.restore(); - }); - - it('should fail due to privkeyDao.requestAuthSessionKey', function(done) { - privkeyDaoStub.requestAuthSessionKey.withArgs({ - userId: testUser - }).returns(rejects(42)); - - keychainDao._authenticateToPrivateKeyServer(testUser).catch(function(err) { - expect(err).to.equal(42); - done(); - }); - }); - - it('should fail due to privkeyDao.requestAuthSessionKey response', function(done) { - privkeyDaoStub.requestAuthSessionKey.returns(resolves({})); - - keychainDao._authenticateToPrivateKeyServer(testUser).catch(function(err) { - expect(err).to.exist; - done(); - }); - }); - - it('should fail due to lookupPublicKey', function(done) { - privkeyDaoStub.requestAuthSessionKey.returns(resolves({ - encryptedAuthSessionKey: 'encryptedAuthSessionKey', - encryptedChallenge: 'encryptedChallenge', - sessionId: 'sessionId' - })); - - lookupPublicKeyStub.returns(rejects(42)); - - keychainDao._authenticateToPrivateKeyServer(testUser).catch(function(err) { - expect(err).to.exist; - done(); - }); - }); - - it('should fail due to pgp.decrypt', function(done) { - privkeyDaoStub.requestAuthSessionKey.returns(resolves({ - encryptedAuthSessionKey: 'encryptedAuthSessionKey', - encryptedChallenge: 'encryptedChallenge', - sessionId: 'sessionId' - })); - - lookupPublicKeyStub.returns(resolves({ - publickKey: 'publicKey' - })); - - pgpStub.decrypt.returns(rejects(42)); - - keychainDao._authenticateToPrivateKeyServer(testUser).catch(function(err) { - expect(err).to.exist; - done(); - }); - }); - - it('should fail due to getDeviceSecret', function(done) { - privkeyDaoStub.requestAuthSessionKey.returns(resolves({ - encryptedAuthSessionKey: 'encryptedAuthSessionKey', - encryptedChallenge: 'encryptedChallenge', - sessionId: 'sessionId' - })); - - lookupPublicKeyStub.returns(resolves({ - publickKey: 'publicKey' - })); - - pgpStub.decrypt.returns(resolves({ - decrypted: 'decryptedStuff' - })); - getDeviceSecretStub.returns(rejects(42)); - - keychainDao._authenticateToPrivateKeyServer(testUser).catch(function(err) { - expect(err).to.exist; - done(); - }); - }); - - it('should fail due to crypto.encrypt', function(done) { - privkeyDaoStub.requestAuthSessionKey.returns(resolves({ - encryptedAuthSessionKey: 'encryptedAuthSessionKey', - encryptedChallenge: 'encryptedChallenge', - sessionId: 'sessionId' - })); - - lookupPublicKeyStub.returns(resolves({ - publickKey: 'publicKey' - })); - - pgpStub.decrypt.returns(resolves({ - decrypted: 'decryptedStuff' - })); - getDeviceSecretStub.returns(resolves('deviceSecret')); - cryptoStub.encrypt.returns(rejects(42)); - - keychainDao._authenticateToPrivateKeyServer(testUser).catch(function(err) { - expect(err).to.exist; - done(); - }); - }); - - it('should fail due to privkeyDao.verifyAuthentication', function(done) { - privkeyDaoStub.requestAuthSessionKey.returns(resolves({ - encryptedAuthSessionKey: 'encryptedAuthSessionKey', - encryptedChallenge: 'encryptedChallenge', - sessionId: 'sessionId' - })); - - lookupPublicKeyStub.returns(resolves({ - publickKey: 'publicKey' - })); - - pgpStub.decrypt.returns(resolves({ - decrypted: 'decryptedStuff', - signaturesValid: true - })); - getDeviceSecretStub.returns(resolves('deviceSecret')); - cryptoStub.encrypt.returns(resolves('encryptedStuff')); - privkeyDaoStub.verifyAuthentication.returns(rejects(42)); - - keychainDao._authenticateToPrivateKeyServer(testUser).catch(function(err) { - expect(err).to.exist; - done(); - }); - }); - - it('should fail due to server public key nto found', function(done) { - privkeyDaoStub.requestAuthSessionKey.returns(resolves({ - encryptedAuthSessionKey: 'encryptedAuthSessionKey', - encryptedChallenge: 'encryptedChallenge', - sessionId: 'sessionId' - })); - - lookupPublicKeyStub.returns(resolves()); - - pgpStub.decrypt.returns(resolves({ - decrypted: 'decryptedStuff', - signaturesValid: true - })); - getDeviceSecretStub.returns(resolves('deviceSecret')); - cryptoStub.encrypt.returns(resolves('encryptedStuff')); - privkeyDaoStub.verifyAuthentication.returns(resolves()); - - keychainDao._authenticateToPrivateKeyServer(testUser).catch(function(err) { - expect(err).to.exist; - done(); - }); - }); - - it('should work', function(done) { - privkeyDaoStub.requestAuthSessionKey.returns(resolves({ - encryptedAuthSessionKey: 'encryptedAuthSessionKey', - encryptedChallenge: 'encryptedChallenge', - sessionId: 'sessionId' - })); - - lookupPublicKeyStub.returns(resolves({ - publicKey: 'publicKey' - })); - - pgpStub.decrypt.returns(resolves({ - decrypted: 'decryptedStuff', - signaturesValid: true - })); - getDeviceSecretStub.returns(resolves('deviceSecret')); - cryptoStub.encrypt.returns(resolves('encryptedStuff')); - privkeyDaoStub.verifyAuthentication.returns(resolves()); - - keychainDao._authenticateToPrivateKeyServer(testUser).then(function(authSessionKey) { - expect(authSessionKey).to.deep.equal({ - sessionKey: 'decryptedStuff', - sessionId: 'sessionId' - }); - done(); - }); - }); - }); - - describe('uploadPrivateKey', function() { - var getUserKeyPairStub, _authenticateToPrivateKeyServerStub; - - beforeEach(function() { - getUserKeyPairStub = sinon.stub(keychainDao, 'getUserKeyPair'); - _authenticateToPrivateKeyServerStub = sinon.stub(keychainDao, '_authenticateToPrivateKeyServer'); - }); - afterEach(function() { - getUserKeyPairStub.restore(); - _authenticateToPrivateKeyServerStub.restore(); - }); - - it('should fail due to missing args', function(done) { - keychainDao.uploadPrivateKey({}).catch(function(err) { - expect(err).to.exist; - done(); - }); - }); - - it('should fail due to error in derive key', function(done) { - cryptoStub.deriveKey.returns(rejects(42)); - - keychainDao.uploadPrivateKey({ - code: 'code', - userId: testUser - }).catch(function(err) { - expect(err).to.exist; - expect(cryptoStub.deriveKey.calledOnce).to.be.true; - done(); - }); - }); - - it('should fail due to error in getUserKeyPair', function(done) { - cryptoStub.deriveKey.returns(resolves('derivedKey')); - getUserKeyPairStub.returns(rejects(42)); - - keychainDao.uploadPrivateKey({ - code: 'code', - userId: testUser - }).catch(function(err) { - expect(err).to.exist; - expect(cryptoStub.deriveKey.calledOnce).to.be.true; - expect(getUserKeyPairStub.calledOnce).to.be.true; - done(); - }); - }); - - it('should fail due to error in crypto.encrypt', function(done) { - cryptoStub.deriveKey.returns(resolves('derivedKey')); - getUserKeyPairStub.returns(resolves({ - privateKey: { - _id: 'pgpKeyId', - encryptedKey: 'pgpKey' - } - })); - cryptoStub.encrypt.returns(rejects(42)); - - keychainDao.uploadPrivateKey({ - code: 'code', - userId: testUser - }).catch(function(err) { - expect(err).to.exist; - expect(cryptoStub.deriveKey.calledOnce).to.be.true; - expect(getUserKeyPairStub.calledOnce).to.be.true; - expect(cryptoStub.encrypt.calledOnce).to.be.true; - done(); - }); - }); - - it('should fail due to error in _authenticateToPrivateKeyServer', function(done) { - cryptoStub.deriveKey.returns(resolves('derivedKey')); - getUserKeyPairStub.returns(resolves({ - privateKey: { - _id: 'pgpKeyId', - encryptedKey: 'pgpKey' - } - })); - cryptoStub.encrypt.returns(resolves('encryptedPgpKey')); - _authenticateToPrivateKeyServerStub.returns(rejects(42)); - - keychainDao.uploadPrivateKey({ - code: 'code', - userId: testUser - }).catch(function(err) { - expect(err).to.exist; - expect(cryptoStub.deriveKey.calledOnce).to.be.true; - expect(getUserKeyPairStub.calledOnce).to.be.true; - expect(cryptoStub.encrypt.calledOnce).to.be.true; - expect(_authenticateToPrivateKeyServerStub.calledOnce).to.be.true; - done(); - }); - }); - - it('should fail due to error in cryptoStub.encrypt', function(done) { - cryptoStub.deriveKey.returns(resolves('derivedKey')); - getUserKeyPairStub.returns(resolves({ - privateKey: { - _id: 'pgpKeyId', - encryptedKey: 'pgpKey' - } - })); - cryptoStub.encrypt.withArgs('pgpKey').returns(resolves('encryptedPgpKey')); - _authenticateToPrivateKeyServerStub.returns(resolves({ - sessionId: 'sessionId', - sessionKey: 'sessionKey' - })); - cryptoStub.encrypt.withArgs('encryptedPgpKey').returns(rejects(42)); - - keychainDao.uploadPrivateKey({ - code: 'code', - userId: testUser - }).catch(function(err) { - expect(err).to.exist; - expect(cryptoStub.deriveKey.calledOnce).to.be.true; - expect(getUserKeyPairStub.calledOnce).to.be.true; - expect(cryptoStub.encrypt.calledTwice).to.be.true; - expect(_authenticateToPrivateKeyServerStub.calledOnce).to.be.true; - done(); - }); - }); - - it('should work', function(done) { - cryptoStub.deriveKey.returns(resolves('derivedKey')); - getUserKeyPairStub.returns(resolves({ - privateKey: { - _id: 'pgpKeyId', - encryptedKey: 'pgpKey' - } - })); - cryptoStub.encrypt.withArgs('pgpKey').returns(resolves('encryptedPgpKey')); - _authenticateToPrivateKeyServerStub.returns(resolves({ - sessionId: 'sessionId', - sessionKey: 'sessionKey' - })); - cryptoStub.encrypt.withArgs('encryptedPgpKey').returns(resolves('doubleEncryptedPgpKey')); - privkeyDaoStub.upload.returns(resolves()); - - keychainDao.uploadPrivateKey({ - code: 'code', - userId: testUser - }).then(function() { - expect(cryptoStub.deriveKey.calledOnce).to.be.true; - expect(getUserKeyPairStub.calledOnce).to.be.true; - expect(cryptoStub.encrypt.calledTwice).to.be.true; - expect(_authenticateToPrivateKeyServerStub.calledOnce).to.be.true; - expect(privkeyDaoStub.upload.calledOnce).to.be.true; - done(); - }); - }); - }); - - describe('requestPrivateKeyDownload', function() { - it('should work', function(done) { - var options = { - userId: testUser, - keyId: 'someId' - }; - - privkeyDaoStub.requestDownload.withArgs(options).returns(resolves()); - keychainDao.requestPrivateKeyDownload(options).then(done); - }); - }); - - describe('hasPrivateKey', function() { - it('should work', function(done) { - var options = { - userId: testUser, - keyId: 'someId' - }; - - privkeyDaoStub.hasPrivateKey.withArgs(options).returns(resolves()); - keychainDao.hasPrivateKey(options).then(done); - }); - }); - - describe('downloadPrivateKey', function() { - it('should work', function(done) { - var options = { - recoveryToken: 'token' - }; - - privkeyDaoStub.download.withArgs(options).returns(resolves()); - keychainDao.downloadPrivateKey(options).then(done); - }); - }); - - describe('decryptAndStorePrivateKeyLocally', function() { - 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() { - saveLocalPrivateKeyStub.restore(); - }); - - it('should fail due to invlaid args', function(done) { - keychainDao.decryptAndStorePrivateKeyLocally({}).catch(function(err) { - expect(err).to.exist; - done(); - }); - }); - - it('should fail due to crypto.deriveKey', function(done) { - cryptoStub.deriveKey.returns(rejects(42)); - - keychainDao.decryptAndStorePrivateKeyLocally(testData).catch(function(err) { - expect(err).to.exist; - expect(cryptoStub.deriveKey.calledOnce).to.be.true; - done(); - }); - }); - - it('should fail due to crypto.decrypt', function(done) { - cryptoStub.deriveKey.returns(resolves('derivedKey')); - cryptoStub.decrypt.returns(rejects(42)); - - keychainDao.decryptAndStorePrivateKeyLocally(testData).catch(function(err) { - expect(err).to.exist; - expect(cryptoStub.deriveKey.calledOnce).to.be.true; - expect(cryptoStub.decrypt.calledOnce).to.be.true; - done(); - }); - }); - - it('should fail due to pgp.getKeyParams', function(done) { - cryptoStub.deriveKey.returns(resolves('derivedKey')); - cryptoStub.decrypt.returns(resolves('privateKeyArmored')); - pgpStub.getKeyParams.returns(rejects(new Error())); - - keychainDao.decryptAndStorePrivateKeyLocally(testData).catch(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.returns(resolves('derivedKey')); - cryptoStub.decrypt.returns(resolves('privateKeyArmored')); - pgpStub.getKeyParams.returns(testData); - saveLocalPrivateKeyStub.returns(rejects(42)); - - keychainDao.decryptAndStorePrivateKeyLocally(testData).catch(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.returns(resolves('derivedKey')); - cryptoStub.decrypt.returns(resolves('privateKeyArmored')); - pgpStub.getKeyParams.returns(testData); - saveLocalPrivateKeyStub.returns(resolves()); - - keychainDao.decryptAndStorePrivateKeyLocally(testData).then(function(keyObject) { - 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(); - }); - }); - }); - describe('upload public key', function() { it('should upload key', function(done) { var keypair = { diff --git a/test/unit/service/privatekey-dao-test.js b/test/unit/service/privatekey-dao-test.js index e3de859..3ddaff6 100644 --- a/test/unit/service/privatekey-dao-test.js +++ b/test/unit/service/privatekey-dao-test.js @@ -1,239 +1,337 @@ 'use strict'; -var RestDAO = require('../../../src/js/service/rest'), - PrivateKeyDAO = require('../../../src/js/service/privatekey'), - appConfig = require('../../../src/js/app-config'); +var Auth = require('../../../src/js/service/auth'), + PrivateKey = require('../../../src/js/service/privatekey'), + PGP = require('../../../src/js/crypto/pgp'), + Crypto = require('../../../src/js/crypto/crypto'), + axe = require('axe-logger'), + appConfig = require('../../../src/js/app-config'), + util = require('crypto-lib').util, + Mailbuild = require('mailbuild'), + mailreader = require('mailreader'), + ImapClient = require('imap-client'); describe('Private Key DAO unit tests', function() { - var privkeyDao, restDaoStub, + var privkeyDao, authStub, pgpStub, cryptoStub, imapClientStub, emailAddress = 'test@example.com', - deviceName = 'iPhone Work'; + keyId = '12345', + salt = util.random(appConfig.config.symKeySize), + iv = util.random(appConfig.config.symIvSize), + encryptedPrivateKey = util.random(1024 * 8); beforeEach(function() { - restDaoStub = sinon.createStubInstance(RestDAO); - privkeyDao = new PrivateKeyDAO(restDaoStub, appConfig); + authStub = sinon.createStubInstance(Auth); + authStub.emailAddress = emailAddress; + pgpStub = sinon.createStubInstance(PGP); + cryptoStub = sinon.createStubInstance(Crypto); + privkeyDao = new PrivateKey(authStub, Mailbuild, mailreader, appConfig, pgpStub, cryptoStub, axe); + imapClientStub = sinon.createStubInstance(ImapClient); + privkeyDao._imap = imapClientStub; }); afterEach(function() {}); - describe('requestDeviceRegistration', function() { + describe('destroy', function() { + it('should work', function(done) { + privkeyDao.destroy().then(function() { + expect(imapClientStub.logout.calledOnce).to.be.true; + done(); + }); + }); + }); + + describe('encrypt', function() { it('should fail due to invalid args', function(done) { - privkeyDao.requestDeviceRegistration({}).catch(function(err) { - expect(err).to.exist; + privkeyDao.encrypt().catch(function(err) { + expect(err.message).to.match(/Incomplete/); done(); }); }); it('should work', function(done) { - restDaoStub.post.returns(resolves({ - encryptedRegSessionKey: 'asdf' + cryptoStub.deriveKey.returns(resolves('derivedKey')); + pgpStub.exportKeys.returns(resolves({ + keyId: keyId, + privateKeyArmored: 'PGP BLOCK' })); + cryptoStub.encrypt.returns(resolves(encryptedPrivateKey)); - privkeyDao.requestDeviceRegistration({ - userId: emailAddress, - deviceName: deviceName - }).then(function(sessionKey) { - expect(sessionKey).to.exist; + privkeyDao.encrypt('asdf').then(function(encryptedKey) { + expect(encryptedKey._id).to.equal(keyId); + expect(encryptedKey.encryptedPrivateKey).to.equal(encryptedPrivateKey); + expect(encryptedKey.salt).to.exist; + expect(encryptedKey.iv).to.exist; done(); }); }); }); - describe('uploadDeviceSecret', function() { - it('should fail due to invalid args', function(done) { - privkeyDao.uploadDeviceSecret({}).catch(function(err) { - expect(err).to.exist; - done(); - }); - }); - - it('should work', function(done) { - restDaoStub.put.returns(resolves()); - - privkeyDao.uploadDeviceSecret({ - userId: emailAddress, - deviceName: deviceName, - encryptedDeviceSecret: 'asdf', - iv: 'iv' - }).then(done); - }); - }); - - describe('requestAuthSessionKey', function() { - it('should fail due to invalid args', function(done) { - privkeyDao.requestAuthSessionKey({}).catch(function(err) { - expect(err).to.exist; - done(); - }); - }); - - it('should work', function(done) { - restDaoStub.post.withArgs(undefined, '/auth/user/' + emailAddress).returns(resolves()); - - privkeyDao.requestAuthSessionKey({ - userId: emailAddress - }).then(done); - }); - }); - - describe('verifyAuthentication', function() { - it('should fail due to invalid args', function(done) { - privkeyDao.verifyAuthentication({}).catch(function(err) { - expect(err).to.exist; - done(); - }); - }); - - it('should work', function(done) { - var sessionId = '1'; - - var options = { - userId: emailAddress, - sessionId: sessionId, - encryptedChallenge: 'asdf', - encryptedDeviceSecret: 'qwer', - iv: ' iv' - }; - - restDaoStub.put.withArgs(options, '/auth/user/' + emailAddress + '/session/' + sessionId).returns(resolves()); - - privkeyDao.verifyAuthentication(options).then(done); - }); - }); - describe('upload', function() { it('should fail due to invalid args', function(done) { privkeyDao.upload({}).catch(function(err) { - expect(err).to.exist; + expect(err.message).to.match(/Incomplete/); done(); }); }); it('should work', function(done) { - var options = { - _id: '12345', + imapClientStub.createFolder.returns(resolves()); + imapClientStub.uploadMessage.returns(resolves()); + + privkeyDao.upload({ + _id: keyId, userId: emailAddress, - encryptedPrivateKey: 'asdf', - sessionId: '1', - salt: 'salt', - iv: 'iv' - }; - - restDaoStub.post.withArgs(options, '/privatekey/user/' + emailAddress + '/session/' + options.sessionId).returns(resolves()); - - privkeyDao.upload(options).then(done); - }); - }); - - describe('requestDownload', function() { - it('should fail due to invalid args', function(done) { - privkeyDao.requestDownload({}).catch(function(err) { - expect(err).to.exist; - done(); - }); - }); - - it('should not find a key', function(done) { - var keyId = '12345'; - - restDaoStub.get.withArgs({ - uri: '/privatekey/user/' + emailAddress + '/key/' + keyId - }).returns(rejects({ - code: 404 - })); - - privkeyDao.requestDownload({ - userId: emailAddress, - keyId: keyId - }).then(function(found) { - expect(found).to.be.false; - done(); - }); - }); - - it('should work', function(done) { - var keyId = '12345'; - - restDaoStub.get.withArgs({ - uri: '/privatekey/user/' + emailAddress + '/key/' + keyId - }).returns(resolves()); - - privkeyDao.requestDownload({ - userId: emailAddress, - keyId: keyId - }).then(function(found) { - expect(found).to.be.true; + encryptedPrivateKey: encryptedPrivateKey, + salt: salt, + iv: iv + }).then(function() { + expect(imapClientStub.uploadMessage.calledOnce).to.be.true; done(); }); }); }); - describe('hasPrivateKey', function() { - it('should fail due to invalid args', function(done) { - privkeyDao.hasPrivateKey({}).catch(function(err) { - expect(err).to.exist; + describe('isSynced', function() { + beforeEach(function() { + sinon.stub(privkeyDao, '_fetchMessage'); + }); + afterEach(function() { + privkeyDao._fetchMessage.restore(); + }); + + it('should be synced', function(done) { + privkeyDao._fetchMessage.returns(resolves({})); + + privkeyDao.isSynced().then(function(synced) { + expect(synced).to.be.true; done(); }); }); - it('should not find a key', function(done) { - var keyId = '12345'; + it('should not be synced', function(done) { + privkeyDao._fetchMessage.returns(resolves()); - restDaoStub.get.withArgs({ - uri: '/privatekey/user/' + emailAddress + '/key/' + keyId + '?ignoreRecovery=true' - }).returns(rejects({ - code: 404 - })); - - privkeyDao.hasPrivateKey({ - userId: emailAddress, - keyId: keyId - }).then(function(found) { - expect(found).to.be.false; + privkeyDao.isSynced().then(function(synced) { + expect(synced).to.be.false; done(); }); }); - it('should work', function(done) { - var keyId = '12345'; + it('should not be synced in case of error', function(done) { + privkeyDao._fetchMessage.returns(rejects(new Error())); - restDaoStub.get.withArgs({ - uri: '/privatekey/user/' + emailAddress + '/key/' + keyId + '?ignoreRecovery=true' - }).returns(resolves()); - - privkeyDao.hasPrivateKey({ - userId: emailAddress, - keyId: keyId - }).then(function(found) { - expect(found).to.be.true; + privkeyDao.isSynced().then(function(synced) { + expect(synced).to.be.false; done(); }); }); }); describe('download', function() { - it('should fail due to invalid args', function(done) { - privkeyDao.download({}).catch(function(err) { - expect(err).to.exist; + var base64Content = 'AYzsvV+hGMMT4BIl/XFjbl60BaM5DpDYVNyKPnoZ4ZyW1qy1udkQR7VUeNKJw5v2gWOqc3y6KHkZIqybOVro6e8tzhK1Fvpz+rgmME0tbrrh/Dd6QMBXb9c6ZAzgbLdq0sxftqXO9GoxINAVcfGN/MkcOIhonEjIsLSaYY2WLuGOLp8ZNdgO0tPxfcdd/f1hVXH2JRYmkOwStH3y2uYDmUhEWWeLfP2vF57F4NgtK2Ln4Ypn4VDx1SWtI6E1IMpwchpwXssBwzY2uWKUPNbWEwEYDU6pleWCKphc2YBp0ohJg1HfE+Et9/8wsZtQAjTiigZuovRd5ABd6LkCCuPNenmzKvR5os8fbe9HDsAiDYl5OrA1iGTWVcAKec1OWxRWKn3Ktt/v+W39gxvmA6OOSuPkA3PF+1rY2lU05busVlNVmNmv6vY3LTJz4J/jVPP7Bn6+Wl/BwdGC7OagZCORmDUujk4AaIz5y+x/hgS6g9yY8oaY5EGdFCxRpS7aptqiBNIXIpuxGtKZpP3bmjI4pIcVb4xTA57SFTE7czfvlvTjvBSCQP7MGYCNC+SbDRgt1beyM8uUrKiuLTWK+YJ6rvcIvOIEqvUBDR7ak+9S6+fyxw033vNHfQSAagIUC1eq+c8yoUzvtSRISOMEbu7MnjI5i4AQrD5yfJDJdp5NTpZ0Dz3fW3RVmMhghTGN3ch+6vVwkzO2ik11EGTqwaLfOgZuwunEonXLT4v4fJjIFvsl+hMab0keksuW1G8AQCdkNcgDfxMTIz6S/k51yVIGE2DZo1e1LTc7pu8gOCNHtuNMuwzDTZuutWdd0P93ZL7W6j1eq33DShX2zeuxk5S28crn6DdlK5QBYMSpECU1JDKRu1QMBNtiEgGlJaVOi1AQ+cDdZKthMYfJ0MPHCeRyQFMpEYkYUBfhBMnGaiDTmDFMvEy0WGhabKChhtdBF/rlyug8Kx9M60lx1t9dYVbxSmWkqWgUZ36vRwQXPVEWWmRROHtG/V9+CSPCCa9heDDqqj8nKzL0vK9kBG8nh2XlPAVg7ICicTLw0u93pz4US3pRKwfMys1mQNV0z0k0uXB1zJZqDrIsCihcUMC2vFOXg+dNlanPXeP8AMp3ojuMPAClIt+bxTyrjZ7MV0mkDuaWUaeEq2xuaU5cKlG1Aam6vSb3jmURgEzOk1onlkGrCfVUTne18W8V6KL7iG+lX+331baiZVGoMUXT++0T05KYBdTRYL0OZ0P3OPRqilPpxaZCY0NG5rJxC5ij0vnu9ECAvN3xSdiRF7SobVSVFIdc32aY24nLKv8/gSnROgmQbAqeCMOz0bULRyVTe0lzSXBcCgu5gK+KEo8p38trTSJ/S95sKQnyNbrnz2QOIXvzxLrL6/nnC/4pwxXKZ6XqB/2zLVfiJRjUQ1NUC0xDXA=='; + var root = [{ + type: 'attachment', + content: util.binStr2Uint8Arr(util.base642Str(base64Content)) + }]; + + beforeEach(function() { + sinon.stub(privkeyDao, '_fetchMessage'); + sinon.stub(privkeyDao, '_parse'); + }); + afterEach(function() { + privkeyDao._fetchMessage.restore(); + privkeyDao._parse.restore(); + }); + + it('should fail if key not synced', function(done) { + privkeyDao._fetchMessage.returns(resolves()); + + privkeyDao.download({ + userId: emailAddress, + keyId: keyId + }).catch(function(err) { + expect(err.message).to.match(/not synced/); done(); }); }); it('should work', function(done) { - var key = { - _id: '12345' - }; - - restDaoStub.get.withArgs({ - uri: '/privatekey/user/' + emailAddress + '/key/' + key._id + '/recovery/token' - }).returns(resolves()); + privkeyDao._fetchMessage.returns(resolves({})); + imapClientStub.getBodyParts.returns(resolves()); + privkeyDao._parse.returns(resolves(root)); privkeyDao.download({ userId: emailAddress, - keyId: key._id, - recoveryToken: 'token' - }).then(done); + keyId: keyId + }).then(function(privkey) { + expect(privkey._id).to.equal(keyId); + expect(privkey.userId).to.equal(emailAddress); + expect(privkey.encryptedPrivateKey).to.exist; + done(); + }); + }); + }); + + describe('decrypt', function() { + it('should fail due to invalid args', function(done) { + privkeyDao.decrypt({}).catch(function(err) { + expect(err.message).to.match(/Incomplete/); + done(); + }); + }); + + it('should fail for invalid code', function(done) { + cryptoStub.deriveKey.returns(resolves('derivedKey')); + cryptoStub.decrypt.returns(rejects(new Error())); + + privkeyDao.decrypt({ + _id: keyId, + userId: emailAddress, + code: 'asdf', + encryptedPrivateKey: encryptedPrivateKey, + salt: salt, + iv: iv + }).catch(function(err) { + expect(err.message).to.match(/Invalid/); + done(); + }); + }); + + it('should fail for invalid key params', function(done) { + cryptoStub.deriveKey.returns(resolves('derivedKey')); + cryptoStub.decrypt.returns(resolves('PGP BLOCK')); + pgpStub.getKeyParams.returns({ + _id: '7890', + userId: emailAddress + }); + + privkeyDao.decrypt({ + _id: keyId, + userId: emailAddress, + code: 'asdf', + encryptedPrivateKey: encryptedPrivateKey, + salt: salt, + iv: iv + }).catch(function(err) { + expect(err.message).to.match(/key parameters/); + done(); + }); + }); + + it('should work', function(done) { + cryptoStub.deriveKey.returns(resolves('derivedKey')); + cryptoStub.decrypt.returns(resolves('PGP BLOCK')); + pgpStub.getKeyParams.returns({ + _id: keyId, + userId: emailAddress + }); + + privkeyDao.decrypt({ + _id: keyId, + userId: emailAddress, + code: 'asdf', + encryptedPrivateKey: encryptedPrivateKey, + salt: salt, + iv: iv + }).then(function(privkey) { + expect(privkey._id).to.equal(keyId); + expect(privkey.userId).to.equal(emailAddress); + expect(privkey.encryptedKey).to.equal('PGP BLOCK'); + done(); + }); + }); + }); + + describe('_fetchMessage', function() { + it('should fail due to invalid args', function(done) { + privkeyDao._fetchMessage({}).catch(function(err) { + expect(err.message).to.match(/Incomplete/); + done(); + }); + }); + + it('should fail if imap folder does not exist', function(done) { + imapClientStub.listMessages.returns(rejects(new Error())); + + privkeyDao._fetchMessage({ + userId: emailAddress, + keyId: keyId + }).catch(function(err) { + expect(err.message).to.match(/Imap folder/); + done(); + }); + }); + + it('should work', function(done) { + imapClientStub.listMessages.returns(resolves([{ + subject: keyId + }])); + + privkeyDao._fetchMessage({ + userId: emailAddress, + keyId: keyId + }).then(function(msg) { + expect(msg.subject).to.equal(keyId); + done(); + }); + }); + + it('should work for not matching message', function(done) { + imapClientStub.listMessages.returns(resolves([{ + subject: '7890' + }])); + + privkeyDao._fetchMessage({ + userId: emailAddress, + keyId: keyId + }).then(function(msg) { + expect(msg).to.not.exist; + done(); + }); + }); + + it('should work for no messages', function(done) { + imapClientStub.listMessages.returns(resolves([])); + + privkeyDao._fetchMessage({ + userId: emailAddress, + keyId: keyId + }).then(function(msg) { + expect(msg).to.not.exist; + done(); + }); + }); + }); + + describe('_parse', function() { + var root = { + foo: 'bar' + }; + + beforeEach(function() { + sinon.stub(mailreader, 'parse'); + }); + afterEach(function() { + mailreader.parse.restore(); + }); + + it('should fail', function(done) { + mailreader.parse.yields(new Error('asdf')); + + privkeyDao._parse().catch(function(err) { + expect(err.message).to.match(/asdf/); + done(); + }); + }); + + it('should work', function(done) { + mailreader.parse.yields(null, root); + + privkeyDao._parse().then(function(res) { + expect(res).to.equal(root); + done(); + }); }); });