1
0
mirror of https://github.com/moparisthebest/mail synced 2024-11-28 20:02:16 -05:00

Merge pull request #317 from whiteout-io/dev/WO-885

Implement encrypted private key imap sync
This commit is contained in:
Tankred Hase 2015-04-01 14:39:50 +02:00
commit c9981239c8
40 changed files with 1037 additions and 2209 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-initial-ctrl-test.js',
'test/unit/controller/login/login-new-device-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-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-verify-public-key-ctrl-test.js',
'test/unit/controller/login/login-set-credentials-ctrl-test.js', 'test/unit/controller/login/login-set-credentials-ctrl-test.js',
'test/unit/controller/login/login-ctrl-test.js', 'test/unit/controller/login/login-ctrl-test.js',
'test/unit/controller/app/dialog-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/publickey-import-ctrl-test.js',
'test/unit/controller/app/account-ctrl-test.js', 'test/unit/controller/app/account-ctrl-test.js',
'test/unit/controller/app/set-passphrase-ctrl-test.js', 'test/unit/controller/app/set-passphrase-ctrl-test.js',
@ -677,8 +677,7 @@ module.exports = function(grunt) {
patchManifest({ patchManifest({
version: version, version: version,
deleteKey: true, deleteKey: true,
keyServer: 'https://keys.whiteout.io/', keyServer: 'https://keys.whiteout.io/'
keychainServer: 'https://keychain.whiteout.io/'
}); });
}); });
@ -700,10 +699,6 @@ module.exports = function(grunt) {
var ksIndex = manifest.permissions.indexOf('https://keys-test.whiteout.io/'); var ksIndex = manifest.permissions.indexOf('https://keys-test.whiteout.io/');
manifest.permissions[ksIndex] = options.keyServer; 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) { if (options.deleteKey) {
delete manifest.key; delete manifest.key;
} }

View File

@ -62,8 +62,9 @@
"grunt-svgmin": "~1.0.0", "grunt-svgmin": "~1.0.0",
"grunt-svgstore": "~0.3.4", "grunt-svgstore": "~0.3.4",
"iframe-resizer": "^2.8.3", "iframe-resizer": "^2.8.3",
"imap-client": "~0.11.0", "imap-client": "~0.12.0",
"jquery": "~2.1.1", "jquery": "~2.1.1",
"mailbuild": "^0.3.7",
"mailreader": "~0.4.0", "mailreader": "~0.4.0",
"mocha": "^1.21.4", "mocha": "^1.21.4",
"ng-infinite-scroll": "~1.1.2", "ng-infinite-scroll": "~1.1.2",

View File

@ -15,7 +15,6 @@ appCfg.config = {
pgpComment: 'Whiteout Mail - https://whiteout.io', pgpComment: 'Whiteout Mail - https://whiteout.io',
keyServerUrl: 'https://keys.whiteout.io', keyServerUrl: 'https://keys.whiteout.io',
hkpUrl: 'http://keyserver.ubuntu.com', hkpUrl: 'http://keyserver.ubuntu.com',
privkeyServerUrl: 'https://keychain.whiteout.io',
adminUrl: 'https://admin-node.whiteout.io', adminUrl: 'https://admin-node.whiteout.io',
settingsUrl: 'https://settings.whiteout.io/autodiscovery/', settingsUrl: 'https://settings.whiteout.io/autodiscovery/',
mailServer: { mailServer: {
@ -70,8 +69,6 @@ function setConfigParams(manifest) {
// get key server base url // get key server base url
cfg.keyServerUrl = getUrl('https://keys'); cfg.keyServerUrl = getUrl('https://keys');
// get keychain server base url
cfg.privkeyServerUrl = getUrl('https://keychain');
// get the app version // get the app version
cfg.appVersion = manifest.version; cfg.appVersion = manifest.version;
} }

View File

@ -68,6 +68,10 @@ app.config(function($routeProvider, $animateProvider) {
templateUrl: 'tpl/login-set-credentials.html', templateUrl: 'tpl/login-set-credentials.html',
controller: require('./controller/login/login-set-credentials') 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', { $routeProvider.when('/login-verify-public-key', {
templateUrl: 'tpl/login-verify-public-key.html', templateUrl: 'tpl/login-verify-public-key.html',
controller: require('./controller/login/login-verify-public-key') 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('MailListCtrl', require('./controller/app/mail-list'));
app.controller('AccountCtrl', require('./controller/app/account')); app.controller('AccountCtrl', require('./controller/app/account'));
app.controller('SetPassphraseCtrl', require('./controller/app/set-passphrase')); 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('PublicKeyImportCtrl', require('./controller/app/publickey-import'));
app.controller('ContactsCtrl', require('./controller/app/contacts')); app.controller('ContactsCtrl', require('./controller/app/contacts'));
app.controller('AboutCtrl', require('./controller/app/about')); app.controller('AboutCtrl', require('./controller/app/about'));

View File

@ -11,7 +11,7 @@ var NOTIFICATION_SENT_TIMEOUT = 2000;
// Controller // 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()) { if (!$location.search().dev && !account.isLoggedIn()) {
$location.path('/'); // init app $location.path('/'); // init app
return; return;
@ -149,6 +149,9 @@ var NavigationCtrl = function($scope, $location, $q, account, email, outbox, not
if (!$scope.state.nav.currentFolder) { if (!$scope.state.nav.currentFolder) {
$scope.navigate(0); $scope.navigate(0);
} }
// check if the private PGP key is synced
$scope.checkKeySyncStatus();
}); });
// //
@ -178,6 +181,45 @@ var NavigationCtrl = function($scope, $location, $q, account, email, outbox, not
// start checking outbox periodically // start checking outbox periodically
outbox.startChecking($scope.onOutboxUpdate); outbox.startChecking($scope.onOutboxUpdate);
} }
$scope.checkKeySyncStatus = function() {
return $q(function(resolve) {
resolve();
}).then(function() {
// login to imap
return privateKey.init();
}).then(function() {
// check key sync status
return privateKey.isSynced();
}).then(function(synced) {
if (!synced) {
dialog.confirm({
title: 'Key backup',
message: 'Your private key is not backed up. Back up now?',
positiveBtnStr: 'Backup',
negativeBtnStr: 'Not now',
showNegativeBtn: true,
callback: function(granted) {
if (granted) {
// logout of the current session
email.onDisconnect().then(function() {
// send to key upload screen
$timeout(function() {
$location.path('/login-privatekey-upload');
});
});
}
}
});
}
// logout of imap
return privateKey.destroy();
}).catch(axe.error);
};
}; };
module.exports = NavigationCtrl; 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) { }).then(function(keypair) {
// go to public key verification // remember keypair for storing after public key verification
publickeyVerifier.keypair = keypair; publickeyVerifier.keypair = keypair;
$location.path('/login-verify-public-key'); $location.path('/login-privatekey-upload');
}).catch(displayError); }).catch(displayError);
}; };

View File

@ -5,6 +5,13 @@ var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, aut
$scope.incorrect = false; $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() { $scope.confirmPassphrase = function() {
if ($scope.form.$invalid || !$scope.key) { if ($scope.form.$invalid || !$scope.key) {
$scope.errMsg = 'Please fill out all required fields!'; $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; publickeyVerifier.keypair = keypair;
return keychain.uploadPublicKey(keypair.publicKey).then(function() { // upload private key and then go to public key verification
$location.path('/login-verify-public-key'); $location.path('/login-privatekey-upload');
});
}).catch(displayError); }).catch(displayError);
}; };

View File

@ -1,20 +1,19 @@
'use strict'; '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 !$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
$scope.step = 1;
// //
// Token // scope functions
// //
$scope.checkToken = function() { $scope.checkCode = function() {
if ($scope.tokenForm.$invalid) { if ($scope.form.$invalid) {
$scope.errMsg = 'Please enter a valid recovery token!'; $scope.errMsg = 'Please fill out all required fields!';
return; return;
} }
var cachedKeypair;
var userId = auth.emailAddress; var userId = auth.emailAddress;
return $q(function(resolve) { return $q(function(resolve) {
@ -22,54 +21,38 @@ var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams, $q,
$scope.errMsg = undefined; $scope.errMsg = undefined;
resolve(); resolve();
}).then(function() {
// login to imap
return privateKey.init();
}).then(function() { }).then(function() {
// get public key id for reference // get public key id for reference
return keychain.getUserKeyPair(userId); return keychain.getUserKeyPair(userId);
}).then(function(keypair) { }).then(function(keypair) {
// remember for storage later // remember for storage later
$scope.cachedKeypair = keypair; cachedKeypair = keypair;
return keychain.downloadPrivateKey({ return privateKey.download({
userId: userId, userId: userId,
keyId: keypair.publicKey._id, keyId: keypair.publicKey._id
recoveryToken: $scope.recoveryToken.toUpperCase()
}); });
}).then(function(encryptedPrivateKey) { }).then(function(encryptedKey) {
$scope.encryptedPrivateKey = encryptedPrivateKey; // set decryption code
$scope.busy = false; encryptedKey.code = $scope.code.toUpperCase();
$scope.step++; // decrypt the downloaded encrypted private key
return privateKey.decrypt(encryptedKey);
}).catch(displayError); }).then(function(privkey) {
}; // add private key to cached keypair object
cachedKeypair.privateKey = privkey;
// // store the decrypted private key locally
// Keychain code return keychain.putUserKeyPair(cachedKeypair);
//
$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() { }).then(function() {
return keychain.decryptAndStorePrivateKeyLocally(options);
}).then(function(privateKey) {
// add private key to cached keypair object
$scope.cachedKeypair.privateKey = privateKey;
// try empty passphrase // try empty passphrase
return email.unlock({ return email.unlock({
keypair: $scope.cachedKeypair, keypair: cachedKeypair,
passphrase: undefined passphrase: undefined
}).catch(function(err) { }).catch(function(err) {
// passphrase incorrct ... go to passphrase login screen // passphrase incorrct ... go to passphrase login screen
@ -81,6 +64,10 @@ var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams, $q,
// passphrase is corrent ... // passphrase is corrent ...
return auth.storeCredentials(); return auth.storeCredentials();
}).then(function() {
// logout of imap
return privateKey.destroy();
}).then(function() { }).then(function() {
// continue to main app // continue to main app
$scope.goTo('/account'); $scope.goTo('/account');

View File

@ -0,0 +1,83 @@
'use strict';
var util = require('crypto-lib').util;
var LoginPrivateKeyUploadCtrl = function($scope, $location, $routeParams, $q, auth, privateKey) {
!$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
//
// scope state
//
// go to step 1
$scope.step = 1;
// generate new code for the user
$scope.code = util.randomString(24);
$scope.displayedCode = $scope.code.replace(/.{4}/g, "$&-").replace(/-$/, '');
// clear input field of any previous artifacts
$scope.inputCode = '';
//
// scope functions
//
$scope.encryptAndUploadKey = function() {
return $q(function(resolve) {
$scope.busy = true;
$scope.errMsg = undefined;
$scope.incorrect = false;
resolve();
}).then(function() {
if ($scope.inputCode.toUpperCase() !== $scope.code) {
throw new Error('The code does not match. Please go back and check the generated code.');
}
}).then(function() {
// login to imap
return privateKey.init();
}).then(function() {
// encrypt the private key
return privateKey.encrypt($scope.code);
}).then(function(encryptedPayload) {
// set user id to encrypted payload
encryptedPayload.userId = auth.emailAddress;
// encrypt private PGP key using code and upload
return privateKey.upload(encryptedPayload);
}).then(function() {
// logout of imap
return privateKey.destroy();
}).then(function() {
// continue to public key verification
$location.path('/login-verify-public-key');
}).catch(displayError);
};
$scope.goForward = function() {
$scope.step++;
};
$scope.goBack = function() {
if ($scope.step > 1) {
$scope.step--;
}
};
//
// helper functions
//
function displayError(err) {
$scope.busy = false;
$scope.incorrect = true;
$scope.errMsg = err.errMsg || err.message;
}
};
module.exports = LoginPrivateKeyUploadCtrl;

View File

@ -2,7 +2,7 @@
var RETRY_INTERVAL = 10000; 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; $scope.retries = 0;
/** /**
@ -22,10 +22,10 @@ var PublicKeyVerifierCtrl = function($scope, $location, $q, $timeout, $interval,
}).then(function() { }).then(function() {
// pre-flight check: is there already a public key for the user? // 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) { }).then(function(cloudPubkey) {
if (!keypair || !keypair.publicKey) { if (!cloudPubkey || (cloudPubkey && cloudPubkey.source)) {
// no pubkey, need to do the roundtrip // no pubkey, need to do the roundtrip
return verifyImap(); return verifyImap();
} }
@ -94,7 +94,8 @@ var PublicKeyVerifierCtrl = function($scope, $location, $q, $timeout, $interval,
clearInterval($scope.countdownDecrement); clearInterval($scope.countdownDecrement);
} }
scheduleVerification(); // upload public key and then schedule verifcation
publickeyVerifier.uploadPublicKey().then(scheduleVerification);
}; };
module.exports = PublicKeyVerifierCtrl; 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) { } else if (availableKeys && availableKeys.publicKey && !availableKeys.privateKey) {
// check if private key is synced // proceed to private key download
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'); return $scope.goTo('/login-privatekey-download');
} else {
// no private key, import key file
return $scope.goTo('/login-new-device');
}
});
} else { } else {
// no public key available, start onboarding process // no public key available, start onboarding process

View File

@ -4,6 +4,7 @@ angular.module('woEmail', ['woAppConfig', 'woUtil', 'woServices', 'woCrypto']);
require('./mailreader'); require('./mailreader');
require('./pgpbuilder'); require('./pgpbuilder');
require('./mailbuild');
require('./email'); require('./email');
require('./outbox'); require('./outbox');
require('./account'); 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); ngModule.service('keychain', Keychain);
module.exports = Keychain; module.exports = Keychain;
var util = require('crypto-lib').util;
var DB_PUBLICKEY = 'publickey', var DB_PUBLICKEY = 'publickey',
DB_PRIVATEKEY = 'privatekey', DB_PRIVATEKEY = 'privatekey';
DB_DEVICENAME = 'devicename',
DB_DEVICE_SECRET = 'devicesecret';
/** /**
* A high-level Data-Access Api for handling Keypair synchronization * 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 // Keypair functions
// //

View File

@ -4,96 +4,89 @@ var ngModule = angular.module('woServices');
ngModule.service('privateKey', PrivateKey); ngModule.service('privateKey', PrivateKey);
module.exports = PrivateKey; module.exports = PrivateKey;
function PrivateKey(privateKeyRestDao) { var ImapClient = require('imap-client');
this._restDao = privateKeyRestDao; 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. * Configure the local imap client used for key-sync with credentials from the auth module.
* @param {String} options.userId The user's email address
* @param {String} options.deviceName The device's memorable name
* @return {Object} {encryptedRegSessionKey:[base64]}
*/ */
PrivateKey.prototype.requestDeviceRegistration = function(options) { PrivateKey.prototype.init = function() {
var self = this; var self = this;
return new Promise(function(resolve) {
if (!options.userId || !options.deviceName) {
throw new Error('Incomplete arguments!');
}
resolve();
}).then(function() { return self._auth.getCredentials().then(function(credentials) {
var uri = '/device/user/' + options.userId + '/devicename/' + options.deviceName; // tls socket worker path for multithreaded tls in non-native tls environments
return self._restDao.post(undefined, uri); 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. * Cleanup by logging out of the imap client.
* @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
*/ */
PrivateKey.prototype.uploadDeviceSecret = function(options) { PrivateKey.prototype.destroy = function() {
var self = this; this._imap.logout();
// don't wait for logout to complete
return new Promise(function(resolve) { return new Promise(function(resolve) {
if (!options.userId || !options.deviceName || !options.encryptedDeviceSecret || !options.iv) {
throw new Error('Incomplete arguments!');
}
resolve(); 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. * Encrypt and upload the private PGP key to the server.
* @param {String} options.userId The user's email address * @param {String} code The randomly generated or self selected code used to derive the key for the encryption of the private PGP key
* @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
*/ */
PrivateKey.prototype.verifyAuthentication = function(options) { PrivateKey.prototype.encrypt = function(code) {
var self = this; var self = this,
return new Promise(function(resolve) { config = self._appConfig.config,
if (!options.userId || !options.sessionId || !options.encryptedChallenge || !options.encryptedDeviceSecret || !options.iv) { keySize = config.symKeySize,
throw new Error('Incomplete arguments!'); encryptionKey, salt, iv, privkeyId;
}
resolve();
}).then(function() { if (!code) {
var uri = '/auth/user/' + options.userId + '/session/' + options.sessionId; return new Promise(function() {
return self._restDao.put(options, uri); throw new Error('Incomplete arguments!');
});
}
// generate random salt and iv
salt = util.random(keySize);
iv = util.random(config.symIvSize);
// derive key from the code using PBKDF2
return self._crypto.deriveKey(code, salt, keySize).then(function(key) {
encryptionKey = key;
// get private key from local storage
return self._pgp.exportKeys();
}).then(function(keypair) {
privkeyId = keypair.keyId;
// encrypt the private key with the derived key
return self._crypto.encrypt(keypair.privateKeyArmored, encryptionKey, iv);
}).then(function(ct) {
return {
_id: privkeyId,
encryptedPrivateKey: ct,
salt: salt,
iv: iv
};
}); });
}; };
@ -102,104 +95,246 @@ PrivateKey.prototype.verifyAuthentication = function(options) {
* @param {String} options._id The hex encoded capital 16 char key id * @param {String} options._id The hex encoded capital 16 char key id
* @param {String} options.userId The user's email address * @param {String} options.userId The user's email address
* @param {String} options.encryptedPrivateKey The base64 encoded encrypted private PGP key * @param {String} options.encryptedPrivateKey The base64 encoded encrypted private PGP key
* @param {String} options.sessionId The session id
*/ */
PrivateKey.prototype.upload = function(options) { PrivateKey.prototype.upload = function(options) {
var self = this; var self = this;
return new Promise(function(resolve) { 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!'); throw new Error('Incomplete arguments!');
} }
resolve(); resolve();
}).then(function() { }).then(function() {
var uri = '/privatekey/user/' + options.userId + '/session/' + options.sessionId; // create imap folder
return self._restDao.post(options, uri); return self._imap.createFolder({
}); path: IMAP_KEYS_FOLDER
};
/**
* 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() { }).then(function() {
return self._restDao.get({ self._axe.debug('Successfully created imap folder ' + IMAP_KEYS_FOLDER);
uri: '/privatekey/user/' + options.userId + '/key/' + options.keyId + '?ignoreRecovery=true',
});
}).then(function() {
return true;
}).catch(function(err) { }).catch(function(err) {
// 404: there is no encrypted private key on the server var prettyErr = new Error('Creating imap folder ' + IMAP_KEYS_FOLDER + ' failed: ' + err.message);
if (err.code && err.code !== 200) { self._axe.error(prettyErr);
return false; throw prettyErr;
}
throw err;
}); });
}).then(createMessage).then(function(message) {
// upload to imap folder
return self._imap.uploadMessage({
path: IMAP_KEYS_FOLDER,
message: message
});
});
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));
// 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);
// 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();
}
}; };
/** /**
* Request download for the encrypted private PGP key. * Check if matching private key is stored in IMAP.
* @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) { PrivateKey.prototype.isSynced = function() {
var self = this; return this._fetchMessage({
return new Promise(function(resolve) { userId: this._auth.emailAddress,
if (!options.userId || !options.keyId) { keyId: this._pgp.getKeyId()
throw new Error('Incomplete arguments!'); }).then(function(msg) {
} return !!msg;
resolve(); }).catch(function() {
}).then(function() {
return self._restDao.get({
uri: '/privatekey/user/' + options.userId + '/key/' + options.keyId
});
}).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; return false;
}
throw err;
}); });
}; };
/** /**
* 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.userId The user's email address
* @param {String} options.keyId The private key id * @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]} * @return {Object} {_id:[hex encoded capital 16 char key id], encryptedPrivateKey:[base64 encoded], encryptedUserId: [base64 encoded]}
*/ */
PrivateKey.prototype.download = function(options) { PrivateKey.prototype.download = function(options) {
var self = this; var self = this,
return new Promise(function(resolve) { message;
if (!options.userId || !options.keyId || !options.recoveryToken) {
throw new Error('Incomplete arguments!'); 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() { }).then(function() {
return self._restDao.get({ // parse the message
uri: '/privatekey/user/' + options.userId + '/key/' + options.keyId + '/recovery/' + options.recoveryToken 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() { PublickeyVerifier.prototype.persistKeypair = function() {
if (this.keypair) {
return this._keychain.putUserKeyPair(this.keypair); return this._keychain.putUserKeyPair(this.keypair);
}
return new Promise(function(resolve) {
resolve();
});
}; };
PublickeyVerifier.prototype.verify = function() { PublickeyVerifier.prototype.verify = function() {

View File

@ -9,13 +9,6 @@ ngModule.factory('publicKeyRestDao', function(appConfig) {
return dao; 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 // rest dao for use in the invitation service
ngModule.factory('invitationRestDao', function(appConfig) { ngModule.factory('invitationRestDao', function(appConfig) {
var dao = new RestDAO(); var dao = new RestDAO();

View File

@ -12,7 +12,6 @@
"unlimitedStorage", "unlimitedStorage",
"notifications", "notifications",
"https://keys-test.whiteout.io/", "https://keys-test.whiteout.io/",
"https://keychain-test.whiteout.io/",
"https://settings.whiteout.io/", "https://settings.whiteout.io/",
"https://admin-node.whiteout.io/", "https://admin-node.whiteout.io/",
"https://www.googleapis.com/", "https://www.googleapis.com/",

View File

@ -243,6 +243,7 @@
line-height: 1em; line-height: 1em;
border: 1px solid $color-text-light; border: 1px solid $color-text-light;
text-align: center; text-align: center;
background-color: $color-bg;
svg { svg {
display: inline-block; display: inline-block;
fill: $color-main; fill: $color-main;

View File

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

View File

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

View File

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

View File

@ -5,10 +5,9 @@
</header> </header>
<main class="page__main"> <main class="page__main">
<div ng-show="state.ui === 1"> <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"> <p class="typo-paragraph">
You can either import an existing PGP key or generate a new one. Generate a new encryption key. Your key belongs to you and only you can read encrypted messages.
Your private key remains on your device and is not sent to our servers.
</p> </p>
<form class="form" name="form"> <form class="form" name="form">
@ -31,13 +30,17 @@
Stay up to date on Whiteout Networks products and important announcements. Stay up to date on Whiteout Networks products and important announcements.
</label> </label>
</div> </div>
<div class="form__row"> <div class="form__row">
<button type="submit" ng-click="generateKey()" class="btn" tabindex="3">Generate new key</button> <button type="submit" ng-click="generateKey()" class="btn" tabindex="3">Generate new key</button>
</div> </div>
<div class="form__row">
<button type="button" ng-click="importKey()" class="btn btn--secondary">Import existing key</button>
</div>
</form> </form>
<p class="typo-paragraph">
<a href="#" wo-touch="$event.preventDefault(); importKey()">
Or import an existing PGP key
</a>
</p>
</div> </div>
<div ng-show="state.ui === 2"> <div ng-show="state.ui === 2">

View File

@ -5,33 +5,28 @@
</header> </header>
<main class="page__main"> <main class="page__main">
<h2 class="typo-title">Import PGP key</h2> <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"> <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> <p class="form__error-message" ng-show="errMsg">{{errMsg}}</p>
<div class="form__row"> <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>
<div class="form__row"> <div class="form__row">
<input class="input-text" type="password" ng-model="passphrase" <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>
<div class="spinner-block" ng-show="busy"> <div class="spinner-block" ng-show="busy">
<span class="spinner spinner--big"></span> <span class="spinner spinner--big"></span>
</div> </div>
<div class="form__row"> <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> </div>
</form> </form>
<p class="typo-paragraph"> <p class="typo-paragraph">
<a href="https://whiteout.io/revocation.html" title="Click here to reset your account." target="_blank"> <a href="https://whiteout.io/revocation.html" title="Click here to reset your account." target="_blank">
Lost your keyfile or passphrase? Lost your keyfile or passphrase?

View File

@ -5,45 +5,13 @@
</header> </header>
<main class="page__main"> <main class="page__main">
<div ng-show="step === 1"> <h2 class="typo-title">Enter backup code</h2>
<h2 class="typo-title">Key sync</h2> <p class="typo-paragraph">Please enter the backup code you wrote down during setup to read encrypted messages on this device.</p>
<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="form">
<form class="form" name="tokenForm">
<p class="form__error-message" ng-show="errMsg">{{errMsg}}</p> <p class="form__error-message" ng-show="errMsg">{{errMsg}}</p>
<div class="form__row"> <div class="form__row">
<input type="text" class="input-text input-text--big" ng-class="{'input-text--error':incorrect}" <input type="text" class="input-text" ng-model="code" wo-input-code
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>
<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" required pattern="([a-zA-Z0-9\-]*)" placeholder="0000-0000-0000-0000-0000-0000"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"> autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
</div> </div>
@ -51,13 +19,15 @@
<span class="spinner spinner--big"></span> <span class="spinner spinner--big"></span>
</div> </div>
<div class="form__row"> <div class="form__row">
<button class="btn" type="submit" ng-click="checkCode()">Complete sync</button> <button class="btn btn--big" type="submit" ng-click="checkCode()">Confirm code</button>
</div> </div>
</form> </form>
<p class="typo-paragraph"> <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> <a href="#login-new-device">
Or import PGP key as file
</a>
</p> </p>
</div>
</main> </main>
<div ng-include="'tpl/page-footer.html'"></div> <div ng-include="'tpl/page-footer.html'"></div>

View File

@ -0,0 +1,54 @@
<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">
<p class="form__error-message" ng-show="errMsg">{{errMsg}}</p>
<div class="form__row">
<input type="text" class="input-text" ng-class="{'input-text--error':incorrect}"
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

@ -57,11 +57,6 @@
<svg role="presentation"><use xlink:href="#icon-contact" /></svg> Contacts <svg role="presentation"><use xlink:href="#icon-contact" /></svg> Contacts
</a> </a>
</li> </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> <li>
<a href="#" wo-touch="$event.preventDefault(); state.writer.reportBug()"> <a href="#" wo-touch="$event.preventDefault(); state.writer.reportBug()">
<svg role="presentation"><use xlink:href="#icon-bug" /></svg> Report a bug <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'), Account = require('../../../../src/js/email/account'),
Outbox = require('../../../../src/js/email/outbox'), Outbox = require('../../../../src/js/email/outbox'),
Dialog = require('../../../../src/js/util/dialog'), 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() { 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() { beforeEach(function() {
var account = { var account = {
@ -29,6 +30,7 @@ describe('Navigation Controller unit test', function() {
outboxBoMock.startChecking.returns(); outboxBoMock.startChecking.returns();
dialogStub = sinon.createStubInstance(Dialog); dialogStub = sinon.createStubInstance(Dialog);
notificationStub = sinon.createStubInstance(Notif); notificationStub = sinon.createStubInstance(Notif);
privateKeyStub = sinon.createStubInstance(PrivateKey);
accountMock = sinon.createStubInstance(Account); accountMock = sinon.createStubInstance(Account);
accountMock.list.returns([account]); accountMock.list.returns([account]);
accountMock.isLoggedIn.returns(true); accountMock.isLoggedIn.returns(true);
@ -46,7 +48,8 @@ describe('Navigation Controller unit test', function() {
email: emailDaoMock, email: emailDaoMock,
outbox: outboxBoMock, outbox: outboxBoMock,
notification: notificationStub, notification: notificationStub,
dialog: dialogStub dialog: dialogStub,
privateKey: privateKeyStub
}); });
}); });
}); });
@ -85,4 +88,20 @@ describe('Navigation Controller unit test', function() {
expect(outboxFolder.count).to.equal(5); 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) { it('should redirect to /login-privatekey-download', function(done) {
authMock.init.returns(resolves()); authMock.init.returns(resolves());
authMock.getEmailAddress.returns(resolves({ authMock.getEmailAddress.returns(resolves({
@ -172,7 +156,6 @@ describe('Login Controller unit test', function() {
accountMock.init.returns(resolves({ accountMock.init.returns(resolves({
publicKey: 'publicKey' publicKey: 'publicKey'
})); }));
keychainMock.requestPrivateKeyDownload.returns(resolves(true));
scope.init().then(function() { scope.init().then(function() {
expect(goToStub.withArgs('/login-privatekey-download').called).to.be.true; 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) { it('should redirect to /login-initial', function(done) {
authMock.init.returns(resolves()); authMock.init.returns(resolves());
authMock.getEmailAddress.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.errMsg).to.not.exist;
expect(scope.state.ui).to.equal(2); expect(scope.state.ui).to.equal(2);
expect(newsletterStub.called).to.be.true; 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; expect(emailMock.unlock.calledOnce).to.be.true;
done(); 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() { describe('confirm passphrase', function() {
it('should unlock crypto with a public key on the server', function(done) { it('should unlock crypto with a public key on the server', function(done) {
scope.passphrase = passphrase; scope.passphrase = passphrase;
@ -107,7 +116,7 @@ describe('Login (new device) Controller unit test', function() {
scope.confirmPassphrase().then(function() { scope.confirmPassphrase().then(function() {
expect(emailMock.unlock.calledOnce).to.be.true; expect(emailMock.unlock.calledOnce).to.be.true;
expect(keychainMock.getUserKeyPair.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(); done();
}); });
}); });

View File

@ -3,16 +3,18 @@
var Auth = require('../../../../src/js/service/auth'), var Auth = require('../../../../src/js/service/auth'),
LoginPrivateKeyDownloadCtrl = require('../../../../src/js/controller/login/login-privatekey-download'), LoginPrivateKeyDownloadCtrl = require('../../../../src/js/controller/login/login-privatekey-download'),
Email = require('../../../../src/js/email/email'), 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() { describe('Login Private Key Download Controller unit test', function() {
var scope, location, ctrl, var scope, location, ctrl,
emailDaoMock, authMock, keychainMock, emailDaoMock, authMock, keychainMock, privateKeyStub,
emailAddress = 'fred@foo.com'; emailAddress = 'fred@foo.com';
beforeEach(function(done) { beforeEach(function(done) {
emailDaoMock = sinon.createStubInstance(Email); emailDaoMock = sinon.createStubInstance(Email);
keychainMock = sinon.createStubInstance(Keychain); keychainMock = sinon.createStubInstance(Keychain);
privateKeyStub = sinon.createStubInstance(PrivateKey);
authMock = sinon.createStubInstance(Auth); authMock = sinon.createStubInstance(Auth);
authMock.emailAddress = emailAddress; authMock.emailAddress = emailAddress;
@ -22,8 +24,7 @@ describe('Login Private Key Download Controller unit test', function() {
angular.mock.inject(function($controller, $rootScope, $location) { angular.mock.inject(function($controller, $rootScope, $location) {
scope = $rootScope.$new(); scope = $rootScope.$new();
scope.state = {}; scope.state = {};
scope.tokenForm = {}; scope.form = {};
scope.codeForm = {};
location = $location; location = $location;
ctrl = $controller(LoginPrivateKeyDownloadCtrl, { ctrl = $controller(LoginPrivateKeyDownloadCtrl, {
$location: location, $location: location,
@ -32,7 +33,8 @@ describe('Login Private Key Download Controller unit test', function() {
$q: window.qMock, $q: window.qMock,
auth: authMock, auth: authMock,
email: emailDaoMock, email: emailDaoMock,
keychain: keychainMock keychain: keychainMock,
privateKey: privateKeyStub
}); });
done(); done();
}); });
@ -40,75 +42,23 @@ describe('Login Private Key Download Controller unit test', function() {
afterEach(function() {}); afterEach(function() {});
describe('initialization', function() {
it('should work', function() {
expect(scope.step).to.equal(1);
});
});
describe('checkToken', function() {
var testKeypair = {
publicKey: {
_id: 'id'
}
};
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() { describe('checkCode', function() {
beforeEach(function() { var encryptedPrivateKey = {
scope.code = '012345';
scope.encryptedPrivateKey = {
encryptedPrivateKey: 'encryptedPrivateKey' encryptedPrivateKey: 'encryptedPrivateKey'
}; };
scope.cachedKeypair = { var cachedKeypair = {
publicKey: { publicKey: {
_id: 'keyId' _id: 'keyId'
} }
}; };
var privkey = {
_id: cachedKeypair.publicKey._id,
userId: emailAddress,
encryptedKey: 'PRIVATE PGP BLOCK'
};
beforeEach(function() {
scope.code = '012345';
sinon.stub(scope, 'goTo'); sinon.stub(scope, 'goTo');
}); });
@ -116,41 +66,49 @@ describe('Login Private Key Download Controller unit test', function() {
scope.goTo.restore(); scope.goTo.restore();
}); });
it('should fail on decryptAndStorePrivateKeyLocally', function(done) { it('should fail on privateKey.init', function(done) {
keychainMock.decryptAndStorePrivateKeyLocally.returns(rejects(new Error('asdf'))); privateKeyStub.init.returns(rejects(new Error('asdf')));
scope.checkCode().then(function() { scope.checkCode().then(function() {
expect(scope.errMsg).to.exist; expect(scope.errMsg).to.match(/asdf/);
expect(keychainMock.decryptAndStorePrivateKeyLocally.calledOnce).to.be.true; expect(privateKeyStub.init.calledOnce).to.be.true;
done(); done();
}); });
}); });
it('should goto /login-existing on emailDao.unlock fail', function(done) { it('should work with empty passphrase', function(done) {
keychainMock.decryptAndStorePrivateKeyLocally.returns(resolves({ privateKeyStub.init.returns(resolves());
encryptedKey: 'keyArmored' keychainMock.getUserKeyPair.withArgs(emailAddress).returns(resolves(cachedKeypair));
})); privateKeyStub.download.withArgs({
emailDaoMock.unlock.returns(rejects(new Error('asdf'))); userId: emailAddress,
keyId: cachedKeypair.publicKey._id
}).returns(resolves(encryptedPrivateKey));
privateKeyStub.decrypt.returns(resolves(privkey));
emailDaoMock.unlock.returns(resolves());
authMock.storeCredentials.returns(resolves());
privateKeyStub.destroy.returns(resolves());
scope.checkCode().then(function() {
expect(scope.errMsg).to.not.exist;
expect(scope.goTo.withArgs('/account').calledOnce).to.be.true;
done();
});
});
it('should work with passphrase', function(done) {
privateKeyStub.init.returns(resolves());
keychainMock.getUserKeyPair.withArgs(emailAddress).returns(resolves(cachedKeypair));
privateKeyStub.download.withArgs({
userId: emailAddress,
keyId: cachedKeypair.publicKey._id
}).returns(resolves(encryptedPrivateKey));
privateKeyStub.decrypt.returns(resolves(privkey));
emailDaoMock.unlock.returns(rejects(new Error()));
authMock.storeCredentials.returns(resolves());
privateKeyStub.destroy.returns(resolves());
scope.checkCode().then(function() { scope.checkCode().then(function() {
expect(scope.goTo.withArgs('/login-existing').calledOnce).to.be.true; expect(scope.goTo.withArgs('/login-existing').calledOnce).to.be.true;
expect(keychainMock.decryptAndStorePrivateKeyLocally.calledOnce).to.be.true;
expect(emailDaoMock.unlock.calledOnce).to.be.true;
done();
});
});
it('should goto /account on emailDao.unlock success', function(done) {
keychainMock.decryptAndStorePrivateKeyLocally.returns(resolves({
encryptedKey: 'keyArmored'
}));
emailDaoMock.unlock.returns(resolves());
authMock.storeCredentials.returns(resolves());
scope.checkCode().then(function() {
expect(scope.goTo.withArgs('/account').calledOnce).to.be.true;
expect(keychainMock.decryptAndStorePrivateKeyLocally.calledOnce).to.be.true;
expect(emailDaoMock.unlock.calledOnce).to.be.true;
done(); done();
}); });
}); });

View File

@ -0,0 +1,78 @@
'use strict';
var Auth = require('../../../../src/js/service/auth'),
LoginPrivateKeyUploadCtrl = require('../../../../src/js/controller/login/login-privatekey-upload'),
PrivateKey = require('../../../../src/js/service/privatekey');
describe('Login Private Key Upload Controller unit test', function() {
var scope, location, ctrl,
authMock, privateKeyStub,
emailAddress = 'fred@foo.com';
beforeEach(function(done) {
privateKeyStub = sinon.createStubInstance(PrivateKey);
authMock = sinon.createStubInstance(Auth);
authMock.emailAddress = emailAddress;
angular.module('login-privatekey-download-test', ['woServices']);
angular.mock.module('login-privatekey-download-test');
angular.mock.inject(function($controller, $rootScope, $location) {
scope = $rootScope.$new();
scope.state = {};
scope.form = {};
location = $location;
ctrl = $controller(LoginPrivateKeyUploadCtrl, {
$location: location,
$scope: scope,
$routeParams: {},
$q: window.qMock,
auth: authMock,
privateKey: privateKeyStub
});
done();
});
});
afterEach(function() {});
describe('init', function() {
it('should work', function() {
expect(scope.step).to.equal(1);
expect(scope.code).to.exist;
expect(scope.displayedCode).to.exist;
expect(scope.inputCode).to.equal('');
});
});
describe('encryptAndUploadKey', function() {
var encryptedPrivateKey = {
encryptedPrivateKey: 'encryptedPrivateKey'
};
beforeEach(function() {
scope.inputCode = scope.code;
sinon.spy(location, 'path');
});
it('should fail for invalid code', function() {
scope.inputCode = 'asdf';
scope.encryptAndUploadKey().then(function() {
expect(scope.errMsg).to.match(/go back and check/);
});
});
it('should work', function(done) {
privateKeyStub.init.returns(resolves());
privateKeyStub.encrypt.withArgs(scope.code).returns(resolves(encryptedPrivateKey));
privateKeyStub.upload.returns(resolves());
privateKeyStub.destroy.returns(resolves());
scope.encryptAndUploadKey().then(function() {
expect(scope.errMsg).to.not.exist;
location.path.calledWith('/login-verify-public-key');
done();
});
});
});
});

View File

@ -3,7 +3,7 @@
var Auth = require('../../../../src/js/service/auth'), var Auth = require('../../../../src/js/service/auth'),
Dialog = require('../../../../src/js/util/dialog'), Dialog = require('../../../../src/js/util/dialog'),
PublicKeyVerifier = require('../../../../src/js/service/publickey-verifier'), 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'); PublicKeyVerifierCtrl = require('../../../../src/js/controller/login/login-verify-public-key');
describe('Public Key Verification Controller unit test', function() { describe('Public Key Verification Controller unit test', function() {
@ -11,7 +11,7 @@ describe('Public Key Verification Controller unit test', function() {
var scope, location; var scope, location;
// Stubs & Fixture // Stubs & Fixture
var auth, verifier, dialogStub, keychain; var auth, verifier, dialogStub, publicKeyStub;
var emailAddress = 'foo@foo.com'; var emailAddress = 'foo@foo.com';
// SUT // SUT
@ -22,8 +22,9 @@ describe('Public Key Verification Controller unit test', function() {
auth = sinon.createStubInstance(Auth); auth = sinon.createStubInstance(Auth);
verifier = sinon.createStubInstance(PublicKeyVerifier); verifier = sinon.createStubInstance(PublicKeyVerifier);
dialogStub = sinon.createStubInstance(Dialog); dialogStub = sinon.createStubInstance(Dialog);
keychain = sinon.createStubInstance(KeychainDAO); publicKeyStub = sinon.createStubInstance(PublicKey);
verifier.uploadPublicKey.returns(resolves());
auth.emailAddress = emailAddress; auth.emailAddress = emailAddress;
// setup the controller // setup the controller
@ -39,7 +40,7 @@ describe('Public Key Verification Controller unit test', function() {
auth: auth, auth: auth,
publickeyVerifier: verifier, publickeyVerifier: verifier,
dialog: dialogStub, dialog: dialogStub,
keychain: keychain, publicKey: publicKeyStub,
appConfig: { appConfig: {
string: { string: {
publickeyVerificationSkipTitle: 'foo', publickeyVerificationSkipTitle: 'foo',
@ -56,14 +57,14 @@ describe('Public Key Verification Controller unit test', function() {
it('should verify', function(done) { it('should verify', function(done) {
var credentials = {}; var credentials = {};
keychain.getUserKeyPair.withArgs(emailAddress).returns(resolves({})); publicKeyStub.getByUserId.withArgs(emailAddress).returns(resolves());
auth.getCredentials.returns(resolves(credentials)); auth.getCredentials.returns(resolves(credentials));
verifier.configure.withArgs(credentials).returns(resolves()); verifier.configure.withArgs(credentials).returns(resolves());
verifier.verify.withArgs().returns(resolves()); verifier.verify.withArgs().returns(resolves());
verifier.persistKeypair.returns(resolves()); verifier.persistKeypair.returns(resolves());
scope.verify().then(function() { 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(auth.getCredentials.calledOnce).to.be.true;
expect(verifier.configure.calledOnce).to.be.true; expect(verifier.configure.calledOnce).to.be.true;
expect(verifier.verify.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) { it('should skip verification when key is already verified', function(done) {
keychain.getUserKeyPair.withArgs(emailAddress).returns(resolves({ publicKeyStub.getByUserId.withArgs(emailAddress).returns(resolves({
publicKey: {} publicKey: {}
})); }));
scope.verify().then(function() { 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(auth.getCredentials.called).to.be.false;
expect(verifier.configure.called).to.be.false; expect(verifier.configure.called).to.be.false;
expect(verifier.verify.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() { describe('upload public key', function() {
it('should upload key', function(done) { it('should upload key', function(done) {
var keypair = { var keypair = {

View File

@ -1,239 +1,337 @@
'use strict'; 'use strict';
var RestDAO = require('../../../src/js/service/rest'), var Auth = require('../../../src/js/service/auth'),
PrivateKeyDAO = require('../../../src/js/service/privatekey'), PrivateKey = require('../../../src/js/service/privatekey'),
appConfig = require('../../../src/js/app-config'); 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() { describe('Private Key DAO unit tests', function() {
var privkeyDao, restDaoStub, var privkeyDao, authStub, pgpStub, cryptoStub, imapClientStub,
emailAddress = 'test@example.com', 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() { beforeEach(function() {
restDaoStub = sinon.createStubInstance(RestDAO); authStub = sinon.createStubInstance(Auth);
privkeyDao = new PrivateKeyDAO(restDaoStub, appConfig); 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() {}); afterEach(function() {});
describe('requestDeviceRegistration', function() { describe('destroy', function() {
it('should work', function(done) {
privkeyDao.destroy().then(function() {
expect(imapClientStub.logout.calledOnce).to.be.true;
done();
});
});
});
describe('encrypt', function() {
it('should fail due to invalid args', function(done) { it('should fail due to invalid args', function(done) {
privkeyDao.requestDeviceRegistration({}).catch(function(err) { privkeyDao.encrypt().catch(function(err) {
expect(err).to.exist; expect(err.message).to.match(/Incomplete/);
done(); done();
}); });
}); });
it('should work', function(done) { it('should work', function(done) {
restDaoStub.post.returns(resolves({ cryptoStub.deriveKey.returns(resolves('derivedKey'));
encryptedRegSessionKey: 'asdf' pgpStub.exportKeys.returns(resolves({
keyId: keyId,
privateKeyArmored: 'PGP BLOCK'
})); }));
cryptoStub.encrypt.returns(resolves(encryptedPrivateKey));
privkeyDao.requestDeviceRegistration({ privkeyDao.encrypt('asdf').then(function(encryptedKey) {
userId: emailAddress, expect(encryptedKey._id).to.equal(keyId);
deviceName: deviceName expect(encryptedKey.encryptedPrivateKey).to.equal(encryptedPrivateKey);
}).then(function(sessionKey) { expect(encryptedKey.salt).to.exist;
expect(sessionKey).to.exist; expect(encryptedKey.iv).to.exist;
done(); 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() { describe('upload', function() {
it('should fail due to invalid args', function(done) { it('should fail due to invalid args', function(done) {
privkeyDao.upload({}).catch(function(err) { privkeyDao.upload({}).catch(function(err) {
expect(err).to.exist; expect(err.message).to.match(/Incomplete/);
done(); done();
}); });
}); });
it('should work', function(done) { it('should work', function(done) {
var options = { imapClientStub.createFolder.returns(resolves());
_id: '12345', imapClientStub.uploadMessage.returns(resolves());
privkeyDao.upload({
_id: keyId,
userId: emailAddress, userId: emailAddress,
encryptedPrivateKey: 'asdf', encryptedPrivateKey: encryptedPrivateKey,
sessionId: '1', salt: salt,
salt: 'salt', iv: iv
iv: 'iv' }).then(function() {
}; expect(imapClientStub.uploadMessage.calledOnce).to.be.true;
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;
done(); done();
}); });
}); });
}); });
describe('hasPrivateKey', function() { describe('isSynced', function() {
it('should fail due to invalid args', function(done) { beforeEach(function() {
privkeyDao.hasPrivateKey({}).catch(function(err) { sinon.stub(privkeyDao, '_fetchMessage');
expect(err).to.exist; });
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(); done();
}); });
}); });
it('should not find a key', function(done) { it('should not be synced', function(done) {
var keyId = '12345'; privkeyDao._fetchMessage.returns(resolves());
restDaoStub.get.withArgs({ privkeyDao.isSynced().then(function(synced) {
uri: '/privatekey/user/' + emailAddress + '/key/' + keyId + '?ignoreRecovery=true' expect(synced).to.be.false;
}).returns(rejects({
code: 404
}));
privkeyDao.hasPrivateKey({
userId: emailAddress,
keyId: keyId
}).then(function(found) {
expect(found).to.be.false;
done(); done();
}); });
}); });
it('should work', function(done) { it('should not be synced in case of error', function(done) {
var keyId = '12345'; privkeyDao._fetchMessage.returns(rejects(new Error()));
restDaoStub.get.withArgs({ privkeyDao.isSynced().then(function(synced) {
uri: '/privatekey/user/' + emailAddress + '/key/' + keyId + '?ignoreRecovery=true' expect(synced).to.be.false;
}).returns(resolves());
privkeyDao.hasPrivateKey({
userId: emailAddress,
keyId: keyId
}).then(function(found) {
expect(found).to.be.true;
done(); done();
}); });
}); });
}); });
describe('download', function() { describe('download', function() {
it('should fail due to invalid args', function(done) { 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==';
privkeyDao.download({}).catch(function(err) { var root = [{
expect(err).to.exist; 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(); done();
}); });
}); });
it('should work', function(done) { it('should work', function(done) {
var key = { privkeyDao._fetchMessage.returns(resolves({}));
_id: '12345' imapClientStub.getBodyParts.returns(resolves());
}; privkeyDao._parse.returns(resolves(root));
restDaoStub.get.withArgs({
uri: '/privatekey/user/' + emailAddress + '/key/' + key._id + '/recovery/token'
}).returns(resolves());
privkeyDao.download({ privkeyDao.download({
userId: emailAddress, userId: emailAddress,
keyId: key._id, keyId: keyId
recoveryToken: 'token' }).then(function(privkey) {
}).then(done); 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();
});
}); });
}); });