[WO-895] Implement encrypted private key imap sync

* Add copy and paste private key import during setup
* Simplify key setup screen (login-initial) import option
* Make checkbox background color white
This commit is contained in:
Tankred Hase 2015-02-27 20:04:44 +01:00
parent 220b8af509
commit 809de91354
40 changed files with 1028 additions and 2208 deletions

View File

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

View File

@ -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": "https://github.com/whiteout-io/imap-client/tarball/dev/WO-885",
"jquery": "~2.1.1",
"mailbuild": "^0.3.7",
"mailreader": "~0.4.0",
"mocha": "^1.21.4",
"ng-infinite-scroll": "~1.1.2",

View File

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

View File

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

View File

@ -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,42 @@ 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) {
// send to key upload screen
$timeout(function() {
$location.path('/login-privatekey-upload');
});
}
}
});
}
// logout of imap
return privateKey.destroy();
}).catch(axe.error);
};
};
module.exports = NavigationCtrl;

View File

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

View File

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

View File

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

View File

@ -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,65 +21,52 @@ 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) {
}).catch(function() {
// passphrase incorrct ... go to passphrase login screen
$scope.goTo('/login-existing');
throw err;
});
}).then(function() {
// passphrase is corrent ...
return auth.storeCredentials();
}).then(function() {
// logout of imap
return privateKey.destroy();
}).then(function() {
// continue to main app
$scope.goTo('/account');

View File

@ -0,0 +1,84 @@
'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() {
if ($scope.inputCode.toUpperCase() !== $scope.code) {
$scope.errMsg = 'The code does not match. Please go back and check the generated code.';
return;
}
// register device to keychain service
return $q(function(resolve) {
$scope.busy = true;
$scope.errMsg = undefined;
$scope.incorrect = false;
resolve();
}).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;

View File

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

View File

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

View File

@ -4,6 +4,7 @@ angular.module('woEmail', ['woAppConfig', 'woUtil', 'woServices', 'woCrypto']);
require('./mailreader');
require('./pgpbuilder');
require('./mailbuild');
require('./email');
require('./outbox');
require('./account');

View File

@ -0,0 +1,8 @@
'use strict';
var Mailbuild = require('mailbuild');
var ngModule = angular.module('woEmail');
ngModule.factory('mailbuild', function() {
return Mailbuild;
});

View File

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

View File

@ -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.login();
// 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,244 @@ 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() {
self._axe.debug('Creating imap folder ' + IMAP_KEYS_FOLDER + ' failed. Probably already available.');
});
}).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);
}
});
});
};
};
/**
* 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;
}

View File

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

View File

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

View File

@ -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/",

View File

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

View File

@ -27,4 +27,5 @@
.typo-code {
font-family: monospace;
font-weight: bold;
user-select: text;
}

View File

@ -32,6 +32,14 @@
}
}
.toolbar {
.toolbar__label {
@include respond-to(xs-only) {
padding-left: 0;
}
}
}
&__main {
flex-grow: 1;
margin: 0 auto 20px;

View File

@ -24,9 +24,6 @@
<div class="lightbox lightbox--dialog" ng-class="{'lightbox--show': state.lightbox === 'set-passphrase'}"
ng-include="'tpl/set-passphrase.html'"></div>
<div class="lightbox" ng-class="{'lightbox--show': state.lightbox === 'privatekey-upload'}"
ng-include="'tpl/privatekey-upload.html'"></div>
<div class="lightbox" ng-class="{'lightbox--show': state.lightbox === 'publickey-import'}"
ng-include="'tpl/publickey-import.html'"></div>

View File

@ -5,10 +5,9 @@
</header>
<main class="page__main">
<div ng-show="state.ui === 1">
<h2 class="typo-title">PGP key</h2>
<h2 class="typo-title">Setup encryption key</h2>
<p class="typo-paragraph">
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.
</p>
<form class="form" name="form">
@ -31,13 +30,17 @@
Stay up to date on Whiteout Networks products and important announcements.
</label>
</div>
<div class="form__row">
<button type="submit" ng-click="generateKey()" class="btn" tabindex="3">Generate new key</button>
</div>
<div class="form__row">
<button type="button" ng-click="importKey()" class="btn btn--secondary">Import existing key</button>
</div>
</form>
<p class="typo-paragraph">
<a href="#" wo-touch="$event.preventDefault(); importKey()">
Or import an existing PGP key
</a>
</p>
</div>
<div ng-show="state.ui === 2">

View File

@ -5,33 +5,28 @@
</header>
<main class="page__main">
<h2 class="typo-title">Import PGP key</h2>
<p class="typo-paragraph">Please import an existing key from the file system.</p>
<p class="typo-paragraph">Please import an existing key. You can import a key via copy/paste or from the filesystem.</p>
<form class="form" name="form">
<fieldset class="form-fieldset form-fieldset--standalone">
<legend>On a mobile device?</legend>
<p class="typo-paragraph">
If you cannot import your key via a USB stick, you can setup <em>Key sync</em>
on a desktop PC to securely transfer your PGP key over the Whiteout cloud.
<a href="https://blog.whiteout.io/2014/07/07/secure-pgp-key-sync-a-proposal/" target="_blank">Learn more</a>.
</p>
</fieldset>
<p class="form__error-message" ng-show="errMsg">{{errMsg}}</p>
<div class="form__row">
<input class="input-file" type="file" file-reader tabindex="1" required>
<textarea class="textarea" placeholder="Paste PRIVATE PGP KEY BLOCK here..." ng-model="pastedKey" ng-change="pasteKey(pastedKey)" tabindex="1"></textarea>
</div>
<div class="form__row">
<input class="input-file" type="file" file-reader tabindex="2">
</div>
<div class="form__row">
<input class="input-text" type="password" ng-model="passphrase"
ng-class="{'input-text--error':incorrect}" placeholder="Passphrase" tabindex="2">
ng-class="{'input-text--error':incorrect}" placeholder="Passphrase" tabindex="3">
</div>
<div class="spinner-block" ng-show="busy">
<span class="spinner spinner--big"></span>
</div>
<div class="form__row">
<button type="submit" ng-click="confirmPassphrase()" class="btn" tabindex="3">Import</button>
<button type="submit" ng-click="confirmPassphrase()" class="btn" tabindex="4">Import</button>
</div>
</form>
<p class="typo-paragraph">
<a href="https://whiteout.io/revocation.html" title="Click here to reset your account." target="_blank">
Lost your keyfile or passphrase?

View File

@ -5,59 +5,29 @@
</header>
<main class="page__main">
<div ng-show="step === 1">
<h2 class="typo-title">Key sync</h2>
<p class="typo-paragraph">We have sent you an email containing a recovery token. Please copy and paste the token below to download your key.</p>
<form class="form" name="tokenForm">
<p class="form__error-message" ng-show="errMsg">{{errMsg}}</p>
<h2 class="typo-title">Enter backup code</h2>
<p class="typo-paragraph">Please enter the backup code you wrote down during setup to read encrypted messages on this device.</p>
<form class="form" name="form">
<p class="form__error-message" ng-show="errMsg">{{errMsg}}</p>
<div class="form__row">
<input type="text" class="input-text input-text--big" ng-class="{'input-text--error':incorrect}"
size="6" maxlength="6" ng-model="recoveryToken" placeholder="Token" wo-focus-me="step === 1" pattern="([a-zA-Z0-9]*)" required>
</div>
<div class="spinner-block" ng-show="busy">
<span class="spinner spinner--big"></span>
</div>
<div class="form__row">
<button class="btn btn--big" type="submit" ng-click="checkToken()">Confirm token</button>
</div>
</form>
<div class="form__row">
<input type="text" class="input-text" ng-model="code" wo-input-code
required pattern="([a-zA-Z0-9\-]*)" placeholder="0000-0000-0000-0000-0000-0000"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
</div>
<div class="spinner-block" ng-show="busy">
<span class="spinner spinner--big"></span>
</div>
<div class="form__row">
<button class="btn btn--big" type="submit" ng-click="checkCode()">Confirm code</button>
</div>
</form>
<form class="form">
<fieldset class="form-fieldset">
<legend>Got USB?</legend>
<p class="typo-paragraph">
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.
</p>
</fieldset>
<div class="form__row">
<a class="btn btn--secondary" href="#login-new-device">Import key file</a>
</div>
</form>
</div>
<div ng-show="step === 2">
<h2 class="typo-title">Key sync</h2>
<p class="typo-paragraph">Please enter the keychain code you wrote down during sync setup.</p>
<form class="form" name="codeForm">
<p class="form__error-message" ng-show="errMsg">{{errMsg}}</p>
<div class="form__row">
<input type="text" class="input-text" ng-model="code" wo-input-code wo-focus-me="step === 2"
required pattern="([a-zA-Z0-9\-]*)" placeholder="0000-0000-0000-0000-0000-0000"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
</div>
<div class="spinner-block" ng-show="busy">
<span class="spinner spinner--big"></span>
</div>
<div class="form__row">
<button class="btn" type="submit" ng-click="checkCode()">Complete sync</button>
</div>
</form>
<p class="typo-paragraph">
<a href="https://whiteout.io/revocation.html" title="Click here to reset your account." target="_blank">Lost your keychain code?</a>
</p>
</div>
<p class="typo-paragraph">
<a href="#login-new-device">
Or import PGP key as file
</a>
</p>
</main>
<div ng-include="'tpl/page-footer.html'"></div>

View File

@ -0,0 +1,51 @@
<section class="page" ng-class="{'u-waiting-cursor': busy}">
<div class="page__canvas">
<div class="toolbar" ng-show="step > 1">
<a class="toolbar__label" href="#" wo-touch="$event.preventDefault(); goBack()"><svg><use xlink:href="#icon-back" /></svg> Back</a>
</div>
<header class="page__header">
<img src="img/whiteout_logo.svg" alt="whiteout.io">
</header>
<main class="page__main">
<h2 class="typo-title">Backup code</h2>
<div ng-show="step === 1">
<p class="typo-paragraph">
Your backup code can be used to securely backup and synchronize your encryption key between devices.
</p>
<p class="typo-paragraph">
<code class="typo-code">{{displayedCode}}</code>
</p>
<p class="typo-paragraph">
Please write down your backup code and keep it in a safe place. Whiteout Networks cannot recover a lost code.
</p>
<form class="form">
<div class="form__row">
<button class="btn btn--big" type="submit" ng-click="goForward()">Continue</button>
</div>
</form>
</div>
<div ng-show="step === 2">
<p class="typo-paragraph">Please confirm the backup code you have written down.</p>
<form class="form">
<div class="form__row">
<input type="text" class="input-text" ng-model="inputCode" wo-input-code wo-focus-me="step === 2"
required pattern="([a-zA-Z0-9\-]*)" placeholder="0000-0000-0000-0000-0000-0000"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
</div>
<div class="spinner-block" ng-show="busy">
<span class="spinner spinner--big"></span>
</div>
<div class="form__row">
<button class="btn btn--big" type="submit" ng-click="encryptAndUploadKey()">Confirm code</button>
</div>
</form>
</div>
</main>
<div ng-include="'tpl/page-footer.html'"></div>
</div>
</section>

View File

@ -4,15 +4,15 @@
<img src="img/whiteout_logo.svg" alt="whiteout.io">
</header>
<main class="page__main">
<h2 class="typo-title">Email address verification</h2>
<p class="typo-paragraph">
We will now automatically verify your email address with a confirmation message we've sent you.
</p>
<h2 class="typo-title">Email address verification</h2>
<p class="typo-paragraph">
We will now automatically verify your email address with a confirmation message we've sent you.
</p>
<div ng-show="!busy">
<p class="typo-paragraph">
Verifying your email address in {{countdown}} seconds.
</p>
<form class="form" name="form">
<p class="form__error-message" ng-show="errMsg">{{errMsg}}</p>
<div class="form__row">

View File

@ -57,11 +57,6 @@
<svg role="presentation"><use xlink:href="#icon-contact" /></svg> Contacts
</a>
</li>
<li>
<a href="#" wo-touch="$event.preventDefault(); state.privateKeyUpload.toggle(true)">
<svg role="presentation"><use xlink:href="#icon-key" /></svg> Key sync (experimental)
</a>
</li>
<li>
<a href="#" wo-touch="$event.preventDefault(); state.writer.reportBug()">
<svg role="presentation"><use xlink:href="#icon-bug" /></svg> Report a bug

View File

@ -1,53 +0,0 @@
<div class="lightbox__body" ng-controller="PrivateKeyUploadCtrl">
<header class="lightbox__header">
<h2>Setup Key Sync</h2>
<button class="lightbox__close" wo-touch="state.privateKeyUpload.toggle(false)" data-action="lightbox-close">
<svg><use xlink:href="#icon-close" /><title>Close</title></svg>
</button>
</header>
<div class="lightbox__content">
<div ng-show="step === 1">
<p class="typo-paragraph">
Your keychain code can be used to securely backup and sync your PGP key between devices.
This feature is experimental and not recommended for production use.
<a href="https://blog.whiteout.io/2014/07/07/secure-pgp-key-sync-a-proposal/" target="_blank">Learn more</a>.
</p>
<p class="typo-paragraph">
<code class="typo-code">{{displayedCode}}</code>
</p>
<p class="typo-paragraph">
Please write down your keychain code and keep it in a safe place. Whiteout Networks cannot recover a lost code.
</p>
</div>
<div ng-show="step === 2">
<p class="typo-paragraph">Please confirm the keychain code you have written down.</p>
<form class="form">
<input type="text" class="input-text" ng-model="inputCode" wo-input-code wo-focus-me="step === 2"
required pattern="([a-zA-Z0-9\-]*)" placeholder="0000-0000-0000-0000-0000-0000"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
</form>
</div>
<div ng-show="step === 3">
<p class="typo-paragraph">Please enter a memorable name for this device e.g. “MacBook Work”.</p>
<form class="form">
<input type="text" class="input-text" ng-model="deviceName" placeholder="Device name" wo-focus-me="step === 3">
</form>
</div>
<div ng-show="step === 4">
<div class="spinner-block spinner-block--standalone">
<span class="spinner spinner--big"></span>
</div>
</div>
</div>
<footer class="lightbox__controls">
<button ng-show="step > 1 && step < 4" class="btn btn--secondary" wo-touch="goBack()">Go back</button>
<button ng-show="step < 4" class="btn" wo-touch="goForward()">Continue</button>
</footer>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,74 @@ 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')));
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'
}));
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;
expect(keychainMock.decryptAndStorePrivateKeyLocally.calledOnce).to.be.true;
expect(emailDaoMock.unlock.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());
authMock.storeCredentials.returns(resolves());
privateKeyStub.destroy.returns(resolves());
scope.checkCode().then(function() {
expect(scope.errMsg).to.not.exist;
expect(scope.goTo.withArgs('/login-existing').calledOnce).to.be.true;
done();
});
});

View File

@ -0,0 +1,77 @@
'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();
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();
});
});
});
});

View File

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

View File

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

View File

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