mirror of
https://github.com/moparisthebest/mail
synced 2024-11-28 20:02:16 -05:00
[WO-895] Implement encrypted private key imap sync
* Add copy and paste private key import during setup * Simplify key setup screen (login-initial) import option * Make checkbox background color white
This commit is contained in:
parent
220b8af509
commit
809de91354
@ -190,11 +190,11 @@ module.exports = function(grunt) {
|
||||
'test/unit/controller/login/login-initial-ctrl-test.js',
|
||||
'test/unit/controller/login/login-new-device-ctrl-test.js',
|
||||
'test/unit/controller/login/login-privatekey-download-ctrl-test.js',
|
||||
'test/unit/controller/login/login-privatekey-upload-ctrl-test.js',
|
||||
'test/unit/controller/login/login-verify-public-key-ctrl-test.js',
|
||||
'test/unit/controller/login/login-set-credentials-ctrl-test.js',
|
||||
'test/unit/controller/login/login-ctrl-test.js',
|
||||
'test/unit/controller/app/dialog-ctrl-test.js',
|
||||
'test/unit/controller/app/privatekey-upload-ctrl-test.js',
|
||||
'test/unit/controller/app/publickey-import-ctrl-test.js',
|
||||
'test/unit/controller/app/account-ctrl-test.js',
|
||||
'test/unit/controller/app/set-passphrase-ctrl-test.js',
|
||||
@ -677,8 +677,7 @@ module.exports = function(grunt) {
|
||||
patchManifest({
|
||||
version: version,
|
||||
deleteKey: true,
|
||||
keyServer: 'https://keys.whiteout.io/',
|
||||
keychainServer: 'https://keychain.whiteout.io/'
|
||||
keyServer: 'https://keys.whiteout.io/'
|
||||
});
|
||||
});
|
||||
|
||||
@ -700,10 +699,6 @@ module.exports = function(grunt) {
|
||||
var ksIndex = manifest.permissions.indexOf('https://keys-test.whiteout.io/');
|
||||
manifest.permissions[ksIndex] = options.keyServer;
|
||||
}
|
||||
if (options.keychainServer) {
|
||||
var kcsIndex = manifest.permissions.indexOf('https://keychain-test.whiteout.io/');
|
||||
manifest.permissions[kcsIndex] = options.keychainServer;
|
||||
}
|
||||
if (options.deleteKey) {
|
||||
delete manifest.key;
|
||||
}
|
||||
|
@ -62,8 +62,9 @@
|
||||
"grunt-svgmin": "~1.0.0",
|
||||
"grunt-svgstore": "~0.3.4",
|
||||
"iframe-resizer": "^2.8.3",
|
||||
"imap-client": "~0.11.0",
|
||||
"imap-client": "https://github.com/whiteout-io/imap-client/tarball/dev/WO-885",
|
||||
"jquery": "~2.1.1",
|
||||
"mailbuild": "^0.3.7",
|
||||
"mailreader": "~0.4.0",
|
||||
"mocha": "^1.21.4",
|
||||
"ng-infinite-scroll": "~1.1.2",
|
||||
|
@ -15,7 +15,6 @@ appCfg.config = {
|
||||
pgpComment: 'Whiteout Mail - https://whiteout.io',
|
||||
keyServerUrl: 'https://keys.whiteout.io',
|
||||
hkpUrl: 'http://keyserver.ubuntu.com',
|
||||
privkeyServerUrl: 'https://keychain.whiteout.io',
|
||||
adminUrl: 'https://admin-node.whiteout.io',
|
||||
settingsUrl: 'https://settings.whiteout.io/autodiscovery/',
|
||||
mailServer: {
|
||||
@ -70,8 +69,6 @@ function setConfigParams(manifest) {
|
||||
|
||||
// get key server base url
|
||||
cfg.keyServerUrl = getUrl('https://keys');
|
||||
// get keychain server base url
|
||||
cfg.privkeyServerUrl = getUrl('https://keychain');
|
||||
// get the app version
|
||||
cfg.appVersion = manifest.version;
|
||||
}
|
||||
|
@ -68,6 +68,10 @@ app.config(function($routeProvider, $animateProvider) {
|
||||
templateUrl: 'tpl/login-set-credentials.html',
|
||||
controller: require('./controller/login/login-set-credentials')
|
||||
});
|
||||
$routeProvider.when('/login-privatekey-upload', {
|
||||
templateUrl: 'tpl/login-privatekey-upload.html',
|
||||
controller: require('./controller/login/login-privatekey-upload')
|
||||
});
|
||||
$routeProvider.when('/login-verify-public-key', {
|
||||
templateUrl: 'tpl/login-verify-public-key.html',
|
||||
controller: require('./controller/login/login-verify-public-key')
|
||||
@ -114,7 +118,6 @@ app.controller('WriteCtrl', require('./controller/app/write'));
|
||||
app.controller('MailListCtrl', require('./controller/app/mail-list'));
|
||||
app.controller('AccountCtrl', require('./controller/app/account'));
|
||||
app.controller('SetPassphraseCtrl', require('./controller/app/set-passphrase'));
|
||||
app.controller('PrivateKeyUploadCtrl', require('./controller/app/privatekey-upload'));
|
||||
app.controller('PublicKeyImportCtrl', require('./controller/app/publickey-import'));
|
||||
app.controller('ContactsCtrl', require('./controller/app/contacts'));
|
||||
app.controller('AboutCtrl', require('./controller/app/about'));
|
||||
|
@ -11,7 +11,7 @@ var NOTIFICATION_SENT_TIMEOUT = 2000;
|
||||
// Controller
|
||||
//
|
||||
|
||||
var NavigationCtrl = function($scope, $location, $q, account, email, outbox, notification, appConfig, dialog, dummy) {
|
||||
var NavigationCtrl = function($scope, $location, $q, $timeout, account, email, outbox, notification, appConfig, dialog, dummy, privateKey, axe) {
|
||||
if (!$location.search().dev && !account.isLoggedIn()) {
|
||||
$location.path('/'); // init app
|
||||
return;
|
||||
@ -149,6 +149,9 @@ var NavigationCtrl = function($scope, $location, $q, account, email, outbox, not
|
||||
if (!$scope.state.nav.currentFolder) {
|
||||
$scope.navigate(0);
|
||||
}
|
||||
|
||||
// check if the private PGP key is synced
|
||||
$scope.checkKeySyncStatus();
|
||||
});
|
||||
|
||||
//
|
||||
@ -178,6 +181,42 @@ var NavigationCtrl = function($scope, $location, $q, account, email, outbox, not
|
||||
// start checking outbox periodically
|
||||
outbox.startChecking($scope.onOutboxUpdate);
|
||||
}
|
||||
|
||||
$scope.checkKeySyncStatus = function() {
|
||||
return $q(function(resolve) {
|
||||
resolve();
|
||||
|
||||
}).then(function() {
|
||||
// login to imap
|
||||
return privateKey.init();
|
||||
|
||||
}).then(function() {
|
||||
// check key sync status
|
||||
return privateKey.isSynced();
|
||||
|
||||
}).then(function(synced) {
|
||||
if (!synced) {
|
||||
dialog.confirm({
|
||||
title: 'Key backup',
|
||||
message: 'Your private key is not backed up. Back up now?',
|
||||
positiveBtnStr: 'Backup',
|
||||
negativeBtnStr: 'Not now',
|
||||
showNegativeBtn: true,
|
||||
callback: function(granted) {
|
||||
if (granted) {
|
||||
// send to key upload screen
|
||||
$timeout(function() {
|
||||
$location.path('/login-privatekey-upload');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// logout of imap
|
||||
return privateKey.destroy();
|
||||
|
||||
}).catch(axe.error);
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = NavigationCtrl;
|
@ -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;
|
@ -61,9 +61,9 @@ var LoginInitialCtrl = function($scope, $location, $routeParams, $q, newsletter,
|
||||
});
|
||||
|
||||
}).then(function(keypair) {
|
||||
// go to public key verification
|
||||
// remember keypair for storing after public key verification
|
||||
publickeyVerifier.keypair = keypair;
|
||||
$location.path('/login-verify-public-key');
|
||||
$location.path('/login-privatekey-upload');
|
||||
|
||||
}).catch(displayError);
|
||||
};
|
||||
|
@ -5,6 +5,13 @@ var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, aut
|
||||
|
||||
$scope.incorrect = false;
|
||||
|
||||
$scope.pasteKey = function(pasted) {
|
||||
var index = pasted.indexOf('-----BEGIN PGP PRIVATE KEY BLOCK-----');
|
||||
$scope.key = {
|
||||
privateKeyArmored: pasted.substring(index, pasted.length).trim()
|
||||
};
|
||||
};
|
||||
|
||||
$scope.confirmPassphrase = function() {
|
||||
if ($scope.form.$invalid || !$scope.key) {
|
||||
$scope.errMsg = 'Please fill out all required fields!';
|
||||
@ -84,11 +91,10 @@ var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, aut
|
||||
});
|
||||
}
|
||||
|
||||
// go to public key verification
|
||||
// remember keypair for public key verification
|
||||
publickeyVerifier.keypair = keypair;
|
||||
return keychain.uploadPublicKey(keypair.publicKey).then(function() {
|
||||
$location.path('/login-verify-public-key');
|
||||
});
|
||||
// upload private key and then go to public key verification
|
||||
$location.path('/login-privatekey-upload');
|
||||
|
||||
}).catch(displayError);
|
||||
};
|
||||
|
@ -1,20 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams, $q, auth, email, keychain) {
|
||||
var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams, $q, auth, email, privateKey, keychain) {
|
||||
!$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
|
||||
|
||||
$scope.step = 1;
|
||||
|
||||
//
|
||||
// Token
|
||||
// scope functions
|
||||
//
|
||||
|
||||
$scope.checkToken = function() {
|
||||
if ($scope.tokenForm.$invalid) {
|
||||
$scope.errMsg = 'Please enter a valid recovery token!';
|
||||
$scope.checkCode = function() {
|
||||
if ($scope.form.$invalid) {
|
||||
$scope.errMsg = 'Please fill out all required fields!';
|
||||
return;
|
||||
}
|
||||
|
||||
var cachedKeypair;
|
||||
var userId = auth.emailAddress;
|
||||
|
||||
return $q(function(resolve) {
|
||||
@ -22,65 +21,52 @@ var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams, $q,
|
||||
$scope.errMsg = undefined;
|
||||
resolve();
|
||||
|
||||
}).then(function() {
|
||||
// login to imap
|
||||
return privateKey.init();
|
||||
|
||||
}).then(function() {
|
||||
// get public key id for reference
|
||||
return keychain.getUserKeyPair(userId);
|
||||
|
||||
}).then(function(keypair) {
|
||||
// remember for storage later
|
||||
$scope.cachedKeypair = keypair;
|
||||
return keychain.downloadPrivateKey({
|
||||
cachedKeypair = keypair;
|
||||
return privateKey.download({
|
||||
userId: userId,
|
||||
keyId: keypair.publicKey._id,
|
||||
recoveryToken: $scope.recoveryToken.toUpperCase()
|
||||
keyId: keypair.publicKey._id
|
||||
});
|
||||
|
||||
}).then(function(encryptedPrivateKey) {
|
||||
$scope.encryptedPrivateKey = encryptedPrivateKey;
|
||||
$scope.busy = false;
|
||||
$scope.step++;
|
||||
}).then(function(encryptedKey) {
|
||||
// set decryption code
|
||||
encryptedKey.code = $scope.code.toUpperCase();
|
||||
// decrypt the downloaded encrypted private key
|
||||
return privateKey.decrypt(encryptedKey);
|
||||
|
||||
}).catch(displayError);
|
||||
};
|
||||
|
||||
//
|
||||
// Keychain code
|
||||
//
|
||||
|
||||
$scope.checkCode = function() {
|
||||
if ($scope.codeForm.$invalid) {
|
||||
$scope.errMsg = 'Please fill out all required fields!';
|
||||
return;
|
||||
}
|
||||
|
||||
var options = $scope.encryptedPrivateKey;
|
||||
options.code = $scope.code.toUpperCase();
|
||||
|
||||
return $q(function(resolve) {
|
||||
$scope.busy = true;
|
||||
$scope.errMsg = undefined;
|
||||
resolve();
|
||||
}).then(function(privkey) {
|
||||
// add private key to cached keypair object
|
||||
cachedKeypair.privateKey = privkey;
|
||||
// store the decrypted private key locally
|
||||
return keychain.putUserKeyPair(cachedKeypair);
|
||||
|
||||
}).then(function() {
|
||||
return keychain.decryptAndStorePrivateKeyLocally(options);
|
||||
|
||||
}).then(function(privateKey) {
|
||||
// add private key to cached keypair object
|
||||
$scope.cachedKeypair.privateKey = privateKey;
|
||||
// try empty passphrase
|
||||
return email.unlock({
|
||||
keypair: $scope.cachedKeypair,
|
||||
keypair: cachedKeypair,
|
||||
passphrase: undefined
|
||||
}).catch(function(err) {
|
||||
}).catch(function() {
|
||||
// passphrase incorrct ... go to passphrase login screen
|
||||
$scope.goTo('/login-existing');
|
||||
throw err;
|
||||
});
|
||||
|
||||
}).then(function() {
|
||||
// passphrase is corrent ...
|
||||
return auth.storeCredentials();
|
||||
|
||||
}).then(function() {
|
||||
// logout of imap
|
||||
return privateKey.destroy();
|
||||
|
||||
}).then(function() {
|
||||
// continue to main app
|
||||
$scope.goTo('/account');
|
||||
|
84
src/js/controller/login/login-privatekey-upload.js
Normal file
84
src/js/controller/login/login-privatekey-upload.js
Normal file
@ -0,0 +1,84 @@
|
||||
'use strict';
|
||||
|
||||
var util = require('crypto-lib').util;
|
||||
|
||||
var LoginPrivateKeyUploadCtrl = function($scope, $location, $routeParams, $q, auth, privateKey) {
|
||||
!$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
|
||||
|
||||
//
|
||||
// scope state
|
||||
//
|
||||
|
||||
// go to step 1
|
||||
$scope.step = 1;
|
||||
// generate new code for the user
|
||||
$scope.code = util.randomString(24);
|
||||
$scope.displayedCode = $scope.code.replace(/.{4}/g, "$&-").replace(/-$/, '');
|
||||
// clear input field of any previous artifacts
|
||||
$scope.inputCode = '';
|
||||
|
||||
//
|
||||
// scope functions
|
||||
//
|
||||
|
||||
$scope.encryptAndUploadKey = function() {
|
||||
if ($scope.inputCode.toUpperCase() !== $scope.code) {
|
||||
$scope.errMsg = 'The code does not match. Please go back and check the generated code.';
|
||||
return;
|
||||
}
|
||||
|
||||
// register device to keychain service
|
||||
return $q(function(resolve) {
|
||||
$scope.busy = true;
|
||||
$scope.errMsg = undefined;
|
||||
$scope.incorrect = false;
|
||||
resolve();
|
||||
|
||||
}).then(function() {
|
||||
// login to imap
|
||||
return privateKey.init();
|
||||
|
||||
}).then(function() {
|
||||
// encrypt the private key
|
||||
return privateKey.encrypt($scope.code);
|
||||
|
||||
}).then(function(encryptedPayload) {
|
||||
// set user id to encrypted payload
|
||||
encryptedPayload.userId = auth.emailAddress;
|
||||
|
||||
// encrypt private PGP key using code and upload
|
||||
return privateKey.upload(encryptedPayload);
|
||||
|
||||
}).then(function() {
|
||||
// logout of imap
|
||||
return privateKey.destroy();
|
||||
|
||||
}).then(function() {
|
||||
// continue to public key verification
|
||||
$location.path('/login-verify-public-key');
|
||||
|
||||
}).catch(displayError);
|
||||
};
|
||||
|
||||
$scope.goForward = function() {
|
||||
$scope.step++;
|
||||
};
|
||||
|
||||
$scope.goBack = function() {
|
||||
if ($scope.step > 1) {
|
||||
$scope.step--;
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// helper functions
|
||||
//
|
||||
|
||||
function displayError(err) {
|
||||
$scope.busy = false;
|
||||
$scope.incorrect = true;
|
||||
$scope.errMsg = err.errMsg || err.message;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = LoginPrivateKeyUploadCtrl;
|
@ -2,7 +2,7 @@
|
||||
|
||||
var RETRY_INTERVAL = 10000;
|
||||
|
||||
var PublicKeyVerifierCtrl = function($scope, $location, $q, $timeout, $interval, auth, publickeyVerifier, keychain) {
|
||||
var PublicKeyVerifierCtrl = function($scope, $location, $q, $timeout, $interval, auth, publickeyVerifier, publicKey) {
|
||||
$scope.retries = 0;
|
||||
|
||||
/**
|
||||
@ -22,10 +22,10 @@ var PublicKeyVerifierCtrl = function($scope, $location, $q, $timeout, $interval,
|
||||
|
||||
}).then(function() {
|
||||
// pre-flight check: is there already a public key for the user?
|
||||
return keychain.getUserKeyPair(auth.emailAddress);
|
||||
return publicKey.getByUserId(auth.emailAddress);
|
||||
|
||||
}).then(function(keypair) {
|
||||
if (!keypair || !keypair.publicKey) {
|
||||
}).then(function(cloudPubkey) {
|
||||
if (!cloudPubkey || (cloudPubkey && cloudPubkey.source)) {
|
||||
// no pubkey, need to do the roundtrip
|
||||
return verifyImap();
|
||||
}
|
||||
@ -94,7 +94,8 @@ var PublicKeyVerifierCtrl = function($scope, $location, $q, $timeout, $interval,
|
||||
clearInterval($scope.countdownDecrement);
|
||||
}
|
||||
|
||||
scheduleVerification();
|
||||
// upload public key and then schedule verifcation
|
||||
publickeyVerifier.uploadPublicKey().then(scheduleVerification);
|
||||
};
|
||||
|
||||
module.exports = PublicKeyVerifierCtrl;
|
@ -52,19 +52,8 @@ var LoginCtrl = function($scope, $timeout, $location, updateHandler, account, au
|
||||
});
|
||||
|
||||
} else if (availableKeys && availableKeys.publicKey && !availableKeys.privateKey) {
|
||||
// check if private key is synced
|
||||
return keychain.requestPrivateKeyDownload({
|
||||
userId: availableKeys.publicKey.userId,
|
||||
keyId: availableKeys.publicKey._id,
|
||||
}).then(function(privateKeySynced) {
|
||||
if (privateKeySynced) {
|
||||
// private key is synced, proceed to download
|
||||
return $scope.goTo('/login-privatekey-download');
|
||||
} else {
|
||||
// no private key, import key file
|
||||
return $scope.goTo('/login-new-device');
|
||||
}
|
||||
});
|
||||
// proceed to private key download
|
||||
return $scope.goTo('/login-privatekey-download');
|
||||
|
||||
} else {
|
||||
// no public key available, start onboarding process
|
||||
|
@ -4,6 +4,7 @@ angular.module('woEmail', ['woAppConfig', 'woUtil', 'woServices', 'woCrypto']);
|
||||
|
||||
require('./mailreader');
|
||||
require('./pgpbuilder');
|
||||
require('./mailbuild');
|
||||
require('./email');
|
||||
require('./outbox');
|
||||
require('./account');
|
||||
|
8
src/js/email/mailbuild.js
Normal file
8
src/js/email/mailbuild.js
Normal file
@ -0,0 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
var Mailbuild = require('mailbuild');
|
||||
|
||||
var ngModule = angular.module('woEmail');
|
||||
ngModule.factory('mailbuild', function() {
|
||||
return Mailbuild;
|
||||
});
|
@ -4,12 +4,8 @@ var ngModule = angular.module('woServices');
|
||||
ngModule.service('keychain', Keychain);
|
||||
module.exports = Keychain;
|
||||
|
||||
var util = require('crypto-lib').util;
|
||||
|
||||
var DB_PUBLICKEY = 'publickey',
|
||||
DB_PRIVATEKEY = 'privatekey',
|
||||
DB_DEVICENAME = 'devicename',
|
||||
DB_DEVICE_SECRET = 'devicesecret';
|
||||
DB_PRIVATEKEY = 'privatekey';
|
||||
|
||||
/**
|
||||
* A high-level Data-Access Api for handling Keypair synchronization
|
||||
@ -199,390 +195,6 @@ Keychain.prototype.getReceiverPublicKey = function(userId) {
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Device registration functions
|
||||
//
|
||||
|
||||
/**
|
||||
* Set the device's memorable name e.g 'iPhone Work'
|
||||
* @param {String} deviceName The device name
|
||||
*/
|
||||
Keychain.prototype.setDeviceName = function(deviceName) {
|
||||
if (!deviceName) {
|
||||
return new Promise(function() {
|
||||
throw new Error('Please set a device name!');
|
||||
});
|
||||
}
|
||||
return this._lawnchairDAO.persist(DB_DEVICENAME, deviceName);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the device' memorable name from local storage. Throws an error if not set
|
||||
* @return {String} The device name
|
||||
*/
|
||||
Keychain.prototype.getDeviceName = function() {
|
||||
// check if deviceName is already persisted in storage
|
||||
return this._lawnchairDAO.read(DB_DEVICENAME).then(function(deviceName) {
|
||||
if (!deviceName) {
|
||||
throw new Error('Device name not set!');
|
||||
}
|
||||
return deviceName;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Geneate a device specific key and secret to authenticate to the private key service.
|
||||
*/
|
||||
Keychain.prototype.getDeviceSecret = function() {
|
||||
var self = this,
|
||||
config = self._appConfig.config;
|
||||
|
||||
// generate random deviceSecret or get from storage
|
||||
return self._lawnchairDAO.read(DB_DEVICE_SECRET).then(function(storedDevSecret) {
|
||||
if (storedDevSecret) {
|
||||
// a device key is already available locally
|
||||
return storedDevSecret;
|
||||
}
|
||||
|
||||
// generate random deviceSecret
|
||||
var deviceSecret = util.random(config.symKeySize);
|
||||
// persist deviceSecret to local storage (in plaintext)
|
||||
return self._lawnchairDAO.persist(DB_DEVICE_SECRET, deviceSecret).then(function() {
|
||||
return deviceSecret;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Register the device on the private key server. This will give the device access to upload an encrypted private key.
|
||||
* @param {String} options.userId The user's email address
|
||||
*/
|
||||
Keychain.prototype.registerDevice = function(options) {
|
||||
var self = this,
|
||||
devName,
|
||||
config = self._appConfig.config;
|
||||
|
||||
// check if deviceName is already persisted in storage
|
||||
return self.getDeviceName().then(function(deviceName) {
|
||||
return requestDeviceRegistration(deviceName);
|
||||
});
|
||||
|
||||
function requestDeviceRegistration(deviceName) {
|
||||
devName = deviceName;
|
||||
|
||||
// request device registration session key
|
||||
return self._privateKeyDao.requestDeviceRegistration({
|
||||
userId: options.userId,
|
||||
deviceName: deviceName
|
||||
}).then(function(regSessionKey) {
|
||||
if (!regSessionKey.encryptedRegSessionKey) {
|
||||
throw new Error('Invalid format for session key!');
|
||||
}
|
||||
|
||||
return decryptSessionKey(regSessionKey);
|
||||
});
|
||||
}
|
||||
|
||||
function decryptSessionKey(regSessionKey) {
|
||||
return self.lookupPublicKey(config.serverPrivateKeyId).then(function(serverPubkey) {
|
||||
if (!serverPubkey || !serverPubkey.publicKey) {
|
||||
throw new Error('Server public key for device registration not found!');
|
||||
}
|
||||
|
||||
// decrypt the session key
|
||||
var ct = regSessionKey.encryptedRegSessionKey;
|
||||
return self._pgp.decrypt(ct, serverPubkey.publicKey).then(function(pt) {
|
||||
if (!pt.signaturesValid) {
|
||||
throw new Error('Verifying PGP signature failed!');
|
||||
}
|
||||
|
||||
return uploadDeviceSecret(pt.decrypted);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function uploadDeviceSecret(regSessionKey) {
|
||||
// generate iv
|
||||
var iv = util.random(config.symIvSize);
|
||||
// read device secret from local storage
|
||||
return self.getDeviceSecret().then(function(deviceSecret) {
|
||||
// encrypt deviceSecret
|
||||
return self._crypto.encrypt(deviceSecret, regSessionKey, iv);
|
||||
|
||||
}).then(function(encryptedDeviceSecret) {
|
||||
// upload encryptedDeviceSecret
|
||||
return self._privateKeyDao.uploadDeviceSecret({
|
||||
userId: options.userId,
|
||||
deviceName: devName,
|
||||
encryptedDeviceSecret: encryptedDeviceSecret,
|
||||
iv: iv
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Private key functions
|
||||
//
|
||||
|
||||
/**
|
||||
* Authenticate to the private key server (required before private PGP key upload).
|
||||
* @param {String} userId The user's email address
|
||||
* @return {Object} {sessionId:String, sessionKey:[base64 encoded]}
|
||||
*/
|
||||
Keychain.prototype._authenticateToPrivateKeyServer = function(userId) {
|
||||
var self = this,
|
||||
sessionId,
|
||||
config = self._appConfig.config;
|
||||
|
||||
// request auth session key required for upload
|
||||
return self._privateKeyDao.requestAuthSessionKey({
|
||||
userId: userId
|
||||
}).then(function(authSessionKey) {
|
||||
if (!authSessionKey.encryptedAuthSessionKey || !authSessionKey.encryptedChallenge || !authSessionKey.sessionId) {
|
||||
throw new Error('Invalid format for session key!');
|
||||
}
|
||||
|
||||
// remember session id for verification
|
||||
sessionId = authSessionKey.sessionId;
|
||||
|
||||
return decryptSessionKey(authSessionKey);
|
||||
});
|
||||
|
||||
function decryptSessionKey(authSessionKey) {
|
||||
var ptSessionKey, ptChallenge, serverPubkey;
|
||||
return self.lookupPublicKey(config.serverPrivateKeyId).then(function(pubkey) {
|
||||
if (!pubkey || !pubkey.publicKey) {
|
||||
throw new Error('Server public key for authentication not found!');
|
||||
}
|
||||
|
||||
serverPubkey = pubkey;
|
||||
// decrypt the session key
|
||||
var ct1 = authSessionKey.encryptedAuthSessionKey;
|
||||
return self._pgp.decrypt(ct1, serverPubkey.publicKey);
|
||||
|
||||
}).then(function(pt) {
|
||||
if (!pt.signaturesValid) {
|
||||
throw new Error('Verifying PGP signature failed!');
|
||||
}
|
||||
|
||||
ptSessionKey = pt.decrypted;
|
||||
// decrypt the challenge
|
||||
var ct2 = authSessionKey.encryptedChallenge;
|
||||
return self._pgp.decrypt(ct2, serverPubkey.publicKey);
|
||||
|
||||
}).then(function(pt) {
|
||||
if (!pt.signaturesValid) {
|
||||
throw new Error('Verifying PGP signature failed!');
|
||||
}
|
||||
|
||||
ptChallenge = pt.decrypted;
|
||||
return encryptChallenge(ptSessionKey, ptChallenge);
|
||||
});
|
||||
}
|
||||
|
||||
function encryptChallenge(sessionKey, challenge) {
|
||||
var deviceSecret, encryptedChallenge;
|
||||
var iv = util.random(config.symIvSize);
|
||||
// get device secret
|
||||
return self.getDeviceSecret().then(function(secret) {
|
||||
deviceSecret = secret;
|
||||
// encrypt the challenge
|
||||
return self._crypto.encrypt(challenge, sessionKey, iv);
|
||||
|
||||
}).then(function(ct) {
|
||||
encryptedChallenge = ct;
|
||||
// encrypt the device secret
|
||||
return self._crypto.encrypt(deviceSecret, sessionKey, iv);
|
||||
|
||||
}).then(function(encryptedDeviceSecret) {
|
||||
return replyChallenge({
|
||||
encryptedChallenge: encryptedChallenge,
|
||||
encryptedDeviceSecret: encryptedDeviceSecret,
|
||||
iv: iv
|
||||
}, sessionKey);
|
||||
});
|
||||
}
|
||||
|
||||
function replyChallenge(response, sessionKey) {
|
||||
// respond to challenge by uploading the with the session key encrypted challenge
|
||||
return self._privateKeyDao.verifyAuthentication({
|
||||
userId: userId,
|
||||
sessionId: sessionId,
|
||||
encryptedChallenge: response.encryptedChallenge,
|
||||
encryptedDeviceSecret: response.encryptedDeviceSecret,
|
||||
iv: response.iv
|
||||
}).then(function() {
|
||||
return {
|
||||
sessionId: sessionId,
|
||||
sessionKey: sessionKey
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Encrypt and upload the private PGP key to the server.
|
||||
* @param {String} options.userId The user's email address
|
||||
* @param {String} options.code The randomly generated or self selected code used to derive the key for the encryption of the private PGP key
|
||||
*/
|
||||
Keychain.prototype.uploadPrivateKey = function(options) {
|
||||
var self = this,
|
||||
config = self._appConfig.config,
|
||||
keySize = config.symKeySize,
|
||||
salt;
|
||||
|
||||
if (!options.userId || !options.code) {
|
||||
return new Promise(function() {
|
||||
throw new Error('Incomplete arguments!');
|
||||
});
|
||||
}
|
||||
|
||||
return deriveKey(options.code);
|
||||
|
||||
function deriveKey(code) {
|
||||
// generate random salt
|
||||
salt = util.random(keySize);
|
||||
// derive key from the code using PBKDF2
|
||||
return self._crypto.deriveKey(code, salt, keySize).then(function(key) {
|
||||
return encryptPrivateKey(key);
|
||||
});
|
||||
}
|
||||
|
||||
function encryptPrivateKey(encryptionKey) {
|
||||
var privkeyId, pgpBlock,
|
||||
iv = util.random(config.symIvSize);
|
||||
|
||||
// get private key from local storage
|
||||
return self.getUserKeyPair(options.userId).then(function(keypair) {
|
||||
privkeyId = keypair.privateKey._id;
|
||||
pgpBlock = keypair.privateKey.encryptedKey;
|
||||
|
||||
// encrypt the private key with the derived key
|
||||
return self._crypto.encrypt(pgpBlock, encryptionKey, iv);
|
||||
|
||||
}).then(function(ct) {
|
||||
return uploadPrivateKey({
|
||||
_id: privkeyId,
|
||||
userId: options.userId,
|
||||
encryptedPrivateKey: ct,
|
||||
salt: salt,
|
||||
iv: iv
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function uploadPrivateKey(payload) {
|
||||
var pt = payload.encryptedPrivateKey,
|
||||
iv = payload.iv;
|
||||
|
||||
// authenticate to server for upload
|
||||
return self._authenticateToPrivateKeyServer(options.userId).then(function(authSessionKey) {
|
||||
// set sessionId
|
||||
payload.sessionId = authSessionKey.sessionId;
|
||||
// encrypt encryptedPrivateKey again using authSessionKey
|
||||
var key = authSessionKey.sessionKey;
|
||||
return self._crypto.encrypt(pt, key, iv);
|
||||
|
||||
}).then(function(ct) {
|
||||
// replace the encryptedPrivateKey with the double wrapped ciphertext
|
||||
payload.encryptedPrivateKey = ct;
|
||||
// upload the encrypted priavet key
|
||||
return self._privateKeyDao.upload(payload);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Request downloading the user's encrypted private key. This will initiate the server to send the recovery token via email/sms to the user.
|
||||
* @param {String} options.userId The user's email address
|
||||
* @param {String} options.keyId The private PGP key id
|
||||
*/
|
||||
Keychain.prototype.requestPrivateKeyDownload = function(options) {
|
||||
return this._privateKeyDao.requestDownload(options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Query if an encrypted private PGP key exists on the server without initializing the recovery procedure
|
||||
* @param {String} options.userId The user's email address
|
||||
* @param {String} options.keyId The private PGP key id
|
||||
*/
|
||||
Keychain.prototype.hasPrivateKey = function(options) {
|
||||
return this._privateKeyDao.hasPrivateKey(options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Download the encrypted private PGP key from the server using the recovery token.
|
||||
* @param {String} options.userId The user's email address
|
||||
* @param {String} options.keyId The user's email address
|
||||
* @param {String} options.recoveryToken The recovery token acquired via email/sms from the key server
|
||||
*/
|
||||
Keychain.prototype.downloadPrivateKey = function(options) {
|
||||
return this._privateKeyDao.download(options);
|
||||
};
|
||||
|
||||
/**
|
||||
* This is called after the encrypted private key has successfully been downloaded and it's ready to be decrypted and stored in localstorage.
|
||||
* @param {String} options._id The private PGP key id
|
||||
* @param {String} options.userId The user's email address
|
||||
* @param {String} options.code The randomly generated or self selected code used to derive the key for the decryption of the private PGP key
|
||||
* @param {String} options.encryptedPrivateKey The encrypted private PGP key
|
||||
* @param {String} options.salt The salt required to derive the code derived key
|
||||
* @param {String} options.iv The iv used to encrypt the private PGP key
|
||||
*/
|
||||
Keychain.prototype.decryptAndStorePrivateKeyLocally = function(options) {
|
||||
var self = this,
|
||||
code = options.code,
|
||||
salt = options.salt,
|
||||
config = self._appConfig.config,
|
||||
keySize = config.symKeySize;
|
||||
|
||||
if (!options._id || !options.userId || !options.code || !options.salt || !options.encryptedPrivateKey || !options.iv) {
|
||||
return new Promise(function() {
|
||||
throw new Error('Incomplete arguments!');
|
||||
});
|
||||
}
|
||||
|
||||
// derive key from the code and the salt using PBKDF2
|
||||
return self._crypto.deriveKey(code, salt, keySize).then(function(key) {
|
||||
return decryptAndStore(key);
|
||||
});
|
||||
|
||||
function decryptAndStore(derivedKey) {
|
||||
// decrypt the private key with the derived key
|
||||
var ct = options.encryptedPrivateKey,
|
||||
iv = options.iv;
|
||||
|
||||
return self._crypto.decrypt(ct, derivedKey, iv).then(function(privateKeyArmored) {
|
||||
// validate pgp key
|
||||
var keyParams;
|
||||
try {
|
||||
keyParams = self._pgp.getKeyParams(privateKeyArmored);
|
||||
} catch (e) {
|
||||
throw new Error('Error parsing private PGP key!');
|
||||
}
|
||||
|
||||
if (keyParams._id !== options._id || keyParams.userId !== options.userId) {
|
||||
throw new Error('Private key parameters don\'t match with public key\'s!');
|
||||
}
|
||||
|
||||
var keyObject = {
|
||||
_id: options._id,
|
||||
userId: options.userId,
|
||||
encryptedKey: privateKeyArmored
|
||||
};
|
||||
|
||||
// store private key locally
|
||||
return self.saveLocalPrivateKey(keyObject).then(function() {
|
||||
return keyObject;
|
||||
});
|
||||
|
||||
}).catch(function() {
|
||||
throw new Error('Invalid keychain code!');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Keypair functions
|
||||
//
|
||||
|
@ -4,96 +4,89 @@ var ngModule = angular.module('woServices');
|
||||
ngModule.service('privateKey', PrivateKey);
|
||||
module.exports = PrivateKey;
|
||||
|
||||
function PrivateKey(privateKeyRestDao) {
|
||||
this._restDao = privateKeyRestDao;
|
||||
var ImapClient = require('imap-client');
|
||||
var util = require('crypto-lib').util;
|
||||
|
||||
var IMAP_KEYS_FOLDER = 'openpgp_keys';
|
||||
var MIME_TYPE = 'application/x.encrypted-pgp-key';
|
||||
var MSG_PART_TYPE_ATTACHMENT = 'attachment';
|
||||
|
||||
function PrivateKey(auth, mailbuild, mailreader, appConfig, pgp, crypto, axe) {
|
||||
this._auth = auth;
|
||||
this._Mailbuild = mailbuild;
|
||||
this._mailreader = mailreader;
|
||||
this._appConfig = appConfig;
|
||||
this._pgp = pgp;
|
||||
this._crypto = crypto;
|
||||
this._axe = axe;
|
||||
}
|
||||
|
||||
//
|
||||
// Device registration functions
|
||||
//
|
||||
|
||||
/**
|
||||
* Request registration of a new device by fetching registration session key.
|
||||
* @param {String} options.userId The user's email address
|
||||
* @param {String} options.deviceName The device's memorable name
|
||||
* @return {Object} {encryptedRegSessionKey:[base64]}
|
||||
* Configure the local imap client used for key-sync with credentials from the auth module.
|
||||
*/
|
||||
PrivateKey.prototype.requestDeviceRegistration = function(options) {
|
||||
PrivateKey.prototype.init = function() {
|
||||
var self = this;
|
||||
return new Promise(function(resolve) {
|
||||
if (!options.userId || !options.deviceName) {
|
||||
throw new Error('Incomplete arguments!');
|
||||
}
|
||||
resolve();
|
||||
|
||||
}).then(function() {
|
||||
var uri = '/device/user/' + options.userId + '/devicename/' + options.deviceName;
|
||||
return self._restDao.post(undefined, uri);
|
||||
return self._auth.getCredentials().then(function(credentials) {
|
||||
// tls socket worker path for multithreaded tls in non-native tls environments
|
||||
credentials.imap.tlsWorkerPath = self._appConfig.config.workerPath + '/tcp-socket-tls-worker.min.js';
|
||||
self._imap = new ImapClient(credentials.imap);
|
||||
self._imap.onError = self._axe.error;
|
||||
// login to the imap server
|
||||
return self._imap.login();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticate device registration by uploading the deviceSecret encrypted with the regSessionKeys.
|
||||
* @param {String} options.userId The user's email address
|
||||
* @param {String} options.deviceName The device's memorable name
|
||||
* @param {String} options.encryptedDeviceSecret The base64 encoded encrypted device secret
|
||||
* @param {String} options.iv The iv used for encryption
|
||||
* Cleanup by logging out of the imap client.
|
||||
*/
|
||||
PrivateKey.prototype.uploadDeviceSecret = function(options) {
|
||||
var self = this;
|
||||
PrivateKey.prototype.destroy = function() {
|
||||
this._imap.login();
|
||||
// don't wait for logout to complete
|
||||
return new Promise(function(resolve) {
|
||||
if (!options.userId || !options.deviceName || !options.encryptedDeviceSecret || !options.iv) {
|
||||
throw new Error('Incomplete arguments!');
|
||||
}
|
||||
resolve();
|
||||
|
||||
}).then(function() {
|
||||
var uri = '/device/user/' + options.userId + '/devicename/' + options.deviceName;
|
||||
return self._restDao.put(options, uri);
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Private key functions
|
||||
//
|
||||
|
||||
/**
|
||||
* Request authSessionKeys required for upload the encrypted private PGP key.
|
||||
* @param {String} options.userId The user's email address
|
||||
* @return {Object} {sessionId, encryptedAuthSessionKey:[base64 encoded], encryptedChallenge:[base64 encoded]}
|
||||
*/
|
||||
PrivateKey.prototype.requestAuthSessionKey = function(options) {
|
||||
var self = this;
|
||||
return new Promise(function(resolve) {
|
||||
if (!options.userId) {
|
||||
throw new Error('Incomplete arguments!');
|
||||
}
|
||||
resolve();
|
||||
|
||||
}).then(function() {
|
||||
var uri = '/auth/user/' + options.userId;
|
||||
return self._restDao.post(undefined, uri);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Verifiy authentication by uploading the challenge and deviceSecret encrypted with the authSessionKeys as a response.
|
||||
* @param {String} options.userId The user's email address
|
||||
* @param {String} options.encryptedChallenge The server's base64 encoded challenge encrypted using the authSessionKey
|
||||
* @param {String} options.encryptedDeviceSecret The server's base64 encoded deviceSecret encrypted using the authSessionKey
|
||||
* @param {String} options.iv The iv used for encryption
|
||||
* Encrypt and upload the private PGP key to the server.
|
||||
* @param {String} code The randomly generated or self selected code used to derive the key for the encryption of the private PGP key
|
||||
*/
|
||||
PrivateKey.prototype.verifyAuthentication = function(options) {
|
||||
var self = this;
|
||||
return new Promise(function(resolve) {
|
||||
if (!options.userId || !options.sessionId || !options.encryptedChallenge || !options.encryptedDeviceSecret || !options.iv) {
|
||||
throw new Error('Incomplete arguments!');
|
||||
}
|
||||
resolve();
|
||||
PrivateKey.prototype.encrypt = function(code) {
|
||||
var self = this,
|
||||
config = self._appConfig.config,
|
||||
keySize = config.symKeySize,
|
||||
encryptionKey, salt, iv, privkeyId;
|
||||
|
||||
}).then(function() {
|
||||
var uri = '/auth/user/' + options.userId + '/session/' + options.sessionId;
|
||||
return self._restDao.put(options, uri);
|
||||
if (!code) {
|
||||
return new Promise(function() {
|
||||
throw new Error('Incomplete arguments!');
|
||||
});
|
||||
}
|
||||
|
||||
// generate random salt and iv
|
||||
salt = util.random(keySize);
|
||||
iv = util.random(config.symIvSize);
|
||||
|
||||
// derive key from the code using PBKDF2
|
||||
return self._crypto.deriveKey(code, salt, keySize).then(function(key) {
|
||||
encryptionKey = key;
|
||||
|
||||
// get private key from local storage
|
||||
return self._pgp.exportKeys();
|
||||
}).then(function(keypair) {
|
||||
privkeyId = keypair.keyId;
|
||||
|
||||
// encrypt the private key with the derived key
|
||||
return self._crypto.encrypt(keypair.privateKeyArmored, encryptionKey, iv);
|
||||
|
||||
}).then(function(ct) {
|
||||
return {
|
||||
_id: privkeyId,
|
||||
encryptedPrivateKey: ct,
|
||||
salt: salt,
|
||||
iv: iv
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@ -102,104 +95,244 @@ PrivateKey.prototype.verifyAuthentication = function(options) {
|
||||
* @param {String} options._id The hex encoded capital 16 char key id
|
||||
* @param {String} options.userId The user's email address
|
||||
* @param {String} options.encryptedPrivateKey The base64 encoded encrypted private PGP key
|
||||
* @param {String} options.sessionId The session id
|
||||
*/
|
||||
PrivateKey.prototype.upload = function(options) {
|
||||
var self = this;
|
||||
|
||||
return new Promise(function(resolve) {
|
||||
if (!options._id || !options.userId || !options.encryptedPrivateKey || !options.sessionId || !options.salt || !options.iv) {
|
||||
if (!options._id || !options.userId || !options.encryptedPrivateKey || !options.salt || !options.iv) {
|
||||
throw new Error('Incomplete arguments!');
|
||||
}
|
||||
resolve();
|
||||
|
||||
}).then(function() {
|
||||
var uri = '/privatekey/user/' + options.userId + '/session/' + options.sessionId;
|
||||
return self._restDao.post(options, uri);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Query if an encrypted private PGP key exists on the server without initializing the recovery procedure.
|
||||
* @param {String} options.userId The user's email address
|
||||
* @param {String} options.keyId The private PGP key id
|
||||
* @return {Boolean} whether the key was found on the server or not.
|
||||
*/
|
||||
PrivateKey.prototype.hasPrivateKey = function(options) {
|
||||
var self = this;
|
||||
return new Promise(function(resolve) {
|
||||
if (!options.userId || !options.keyId) {
|
||||
throw new Error('Incomplete arguments!');
|
||||
}
|
||||
resolve();
|
||||
|
||||
}).then(function() {
|
||||
return self._restDao.get({
|
||||
uri: '/privatekey/user/' + options.userId + '/key/' + options.keyId + '?ignoreRecovery=true',
|
||||
// create imap folder
|
||||
return self._imap.createFolder({
|
||||
path: IMAP_KEYS_FOLDER
|
||||
}).then(function() {
|
||||
self._axe.debug('Successfully created imap folder ' + IMAP_KEYS_FOLDER);
|
||||
}).catch(function() {
|
||||
self._axe.debug('Creating imap folder ' + IMAP_KEYS_FOLDER + ' failed. Probably already available.');
|
||||
});
|
||||
|
||||
}).then(function() {
|
||||
return true;
|
||||
|
||||
}).catch(function(err) {
|
||||
// 404: there is no encrypted private key on the server
|
||||
if (err.code && err.code !== 200) {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Request download for the encrypted private PGP key.
|
||||
* @param {String} options.userId The user's email address
|
||||
* @param {String} options.keyId The private PGP key id
|
||||
* @return {Boolean} whether the key was found on the server or not.
|
||||
*/
|
||||
PrivateKey.prototype.requestDownload = function(options) {
|
||||
var self = this;
|
||||
return new Promise(function(resolve) {
|
||||
if (!options.userId || !options.keyId) {
|
||||
throw new Error('Incomplete arguments!');
|
||||
}
|
||||
resolve();
|
||||
|
||||
}).then(function() {
|
||||
return self._restDao.get({
|
||||
uri: '/privatekey/user/' + options.userId + '/key/' + options.keyId
|
||||
}).then(createMessage).then(function(message) {
|
||||
// upload to imap folder
|
||||
return self._imap.uploadMessage({
|
||||
path: IMAP_KEYS_FOLDER,
|
||||
message: message
|
||||
});
|
||||
});
|
||||
|
||||
}).then(function() {
|
||||
return true;
|
||||
function createMessage() {
|
||||
var encryptedKeyBuf = util.binStr2Uint8Arr(util.base642Str(options.encryptedPrivateKey));
|
||||
var saltBuf = util.binStr2Uint8Arr(util.base642Str(options.salt));
|
||||
var ivBuf = util.binStr2Uint8Arr(util.base642Str(options.iv));
|
||||
|
||||
}).catch(function(err) {
|
||||
// 404: there is no encrypted private key on the server
|
||||
if (err.code && err.code !== 200) {
|
||||
return false;
|
||||
}
|
||||
// allocate payload buffer for sync
|
||||
var payloadBuf = new Uint8Array(1 + saltBuf.length + ivBuf.length + encryptedKeyBuf.length);
|
||||
var offset = 0;
|
||||
// set version byte
|
||||
payloadBuf[offset] = 0x01; // version 1 of the key-sync protocol
|
||||
offset++;
|
||||
// copy salt bytes
|
||||
payloadBuf.set(saltBuf, offset);
|
||||
offset += saltBuf.length;
|
||||
// copy iv bytes
|
||||
payloadBuf.set(ivBuf, offset);
|
||||
offset += ivBuf.length;
|
||||
// copy encrypted key bytes
|
||||
payloadBuf.set(encryptedKeyBuf, offset);
|
||||
|
||||
throw err;
|
||||
// create MIME tree
|
||||
var rootNode = options.rootNode || new self._Mailbuild();
|
||||
rootNode.setHeader({
|
||||
subject: options._id,
|
||||
from: options.userId,
|
||||
to: options.userId,
|
||||
'content-type': MIME_TYPE + '; charset=us-ascii',
|
||||
'content-transfer-encoding': 'base64'
|
||||
});
|
||||
rootNode.setContent(payloadBuf);
|
||||
|
||||
return rootNode.build();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if matching private key is stored in IMAP.
|
||||
*/
|
||||
PrivateKey.prototype.isSynced = function() {
|
||||
return this._fetchMessage({
|
||||
userId: this._auth.emailAddress,
|
||||
keyId: this._pgp.getKeyId()
|
||||
}).then(function(msg) {
|
||||
return !!msg;
|
||||
}).catch(function() {
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Verify the download request for the private PGP key using the recovery token sent via email. This downloads the actual encrypted private key.
|
||||
* Verify the download request for the private PGP key.
|
||||
* @param {String} options.userId The user's email address
|
||||
* @param {String} options.keyId The private key id
|
||||
* @param {String} options.recoveryToken The token proving the user own the email account
|
||||
* @return {Object} {_id:[hex encoded capital 16 char key id], encryptedPrivateKey:[base64 encoded], encryptedUserId: [base64 encoded]}
|
||||
*/
|
||||
PrivateKey.prototype.download = function(options) {
|
||||
var self = this;
|
||||
return new Promise(function(resolve) {
|
||||
if (!options.userId || !options.keyId || !options.recoveryToken) {
|
||||
throw new Error('Incomplete arguments!');
|
||||
var self = this,
|
||||
message;
|
||||
|
||||
return self._fetchMessage(options).then(function(msg) {
|
||||
if (!msg) {
|
||||
throw new Error('Private key not synced!');
|
||||
}
|
||||
resolve();
|
||||
|
||||
message = msg;
|
||||
}).then(function() {
|
||||
// get the body for the message
|
||||
return self._imap.getBodyParts({
|
||||
path: IMAP_KEYS_FOLDER,
|
||||
uid: message.uid,
|
||||
bodyParts: message.bodyParts
|
||||
});
|
||||
|
||||
}).then(function() {
|
||||
return self._restDao.get({
|
||||
uri: '/privatekey/user/' + options.userId + '/key/' + options.keyId + '/recovery/' + options.recoveryToken
|
||||
// parse the message
|
||||
return self._parse(message);
|
||||
|
||||
}).then(function(root) {
|
||||
var payloadBuf = filterBodyParts(root, MSG_PART_TYPE_ATTACHMENT)[0].content;
|
||||
var offset = 0;
|
||||
var SALT_LEN = 32;
|
||||
var IV_LEN = 12;
|
||||
|
||||
// check version
|
||||
var version = payloadBuf[offset];
|
||||
offset++;
|
||||
if (version !== 1) {
|
||||
throw new Error('Unsupported key sync protocol version!');
|
||||
}
|
||||
// salt
|
||||
var saltBuf = payloadBuf.subarray(offset, offset + SALT_LEN);
|
||||
offset += SALT_LEN;
|
||||
// iv
|
||||
var ivBuf = payloadBuf.subarray(offset, offset + IV_LEN);
|
||||
offset += IV_LEN;
|
||||
// encrypted private key
|
||||
var encryptedKeyBuf = payloadBuf.subarray(offset, payloadBuf.length);
|
||||
|
||||
return {
|
||||
_id: options.keyId,
|
||||
userId: options.userId,
|
||||
encryptedPrivateKey: util.str2Base64(util.uint8Arr2BinStr(encryptedKeyBuf)),
|
||||
salt: util.str2Base64(util.uint8Arr2BinStr(saltBuf)),
|
||||
iv: util.str2Base64(util.uint8Arr2BinStr(ivBuf))
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* This is called after the encrypted private key has successfully been downloaded and it's ready to be decrypted and stored in localstorage.
|
||||
* @param {String} options._id The private PGP key id
|
||||
* @param {String} options.userId The user's email address
|
||||
* @param {String} options.code The randomly generated or self selected code used to derive the key for the decryption of the private PGP key
|
||||
* @param {String} options.encryptedPrivateKey The encrypted private PGP key
|
||||
* @param {String} options.salt The salt required to derive the code derived key
|
||||
* @param {String} options.iv The iv used to encrypt the private PGP key
|
||||
*/
|
||||
PrivateKey.prototype.decrypt = function(options) {
|
||||
var self = this,
|
||||
config = self._appConfig.config,
|
||||
keySize = config.symKeySize;
|
||||
|
||||
if (!options._id || !options.userId || !options.code || !options.salt || !options.encryptedPrivateKey || !options.iv) {
|
||||
return new Promise(function() {
|
||||
throw new Error('Incomplete arguments!');
|
||||
});
|
||||
}
|
||||
|
||||
// derive key from the code and the salt using PBKDF2
|
||||
return self._crypto.deriveKey(options.code, options.salt, keySize).then(function(derivedKey) {
|
||||
// decrypt the private key with the derived key
|
||||
return self._crypto.decrypt(options.encryptedPrivateKey, derivedKey, options.iv).catch(function() {
|
||||
throw new Error('Invalid backup code!');
|
||||
});
|
||||
|
||||
}).then(function(privateKeyArmored) {
|
||||
// validate pgp key
|
||||
var keyParams;
|
||||
try {
|
||||
keyParams = self._pgp.getKeyParams(privateKeyArmored);
|
||||
} catch (e) {
|
||||
throw new Error('Error parsing private PGP key!');
|
||||
}
|
||||
|
||||
if (keyParams._id !== options._id || keyParams.userId !== options.userId) {
|
||||
throw new Error('Private key parameters don\'t match with public key\'s!');
|
||||
}
|
||||
|
||||
return {
|
||||
_id: options._id,
|
||||
userId: options.userId,
|
||||
encryptedKey: privateKeyArmored
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
PrivateKey.prototype._fetchMessage = function(options) {
|
||||
var self = this;
|
||||
|
||||
if (!options.userId || !options.keyId) {
|
||||
return new Promise(function() {
|
||||
throw new Error('Incomplete arguments!');
|
||||
});
|
||||
}
|
||||
|
||||
// get the metadata for the message
|
||||
return self._imap.listMessages({
|
||||
path: IMAP_KEYS_FOLDER,
|
||||
}).then(function(messages) {
|
||||
if (!messages.length) {
|
||||
// message has been deleted in the meantime
|
||||
return;
|
||||
}
|
||||
|
||||
// get matching private key if multiple keys uloaded
|
||||
return _.findWhere(messages, {
|
||||
subject: options.keyId
|
||||
});
|
||||
}).catch(function() {
|
||||
throw new Error('Imap folder ' + IMAP_KEYS_FOLDER + ' does not exist for key sync!');
|
||||
});
|
||||
};
|
||||
|
||||
PrivateKey.prototype._parse = function(options) {
|
||||
var self = this;
|
||||
return new Promise(function(resolve, reject) {
|
||||
self._mailreader.parse(options, function(err, root) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(root);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function that recursively traverses the body parts tree. Looks for bodyParts that match the provided type and aggregates them
|
||||
*
|
||||
* @param {Array} bodyParts The bodyParts array
|
||||
* @param {String} type The type to look up
|
||||
* @param {undefined} result Leave undefined, only used for recursion
|
||||
*/
|
||||
function filterBodyParts(bodyParts, type, result) {
|
||||
result = result || [];
|
||||
bodyParts.forEach(function(part) {
|
||||
if (part.type === type) {
|
||||
result.push(part);
|
||||
} else if (Array.isArray(part.content)) {
|
||||
filterBodyParts(part.content, type, result);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
@ -32,8 +32,22 @@ PublickeyVerifier.prototype.configure = function() {
|
||||
});
|
||||
};
|
||||
|
||||
PublickeyVerifier.prototype.uploadPublicKey = function() {
|
||||
if (this.keypair) {
|
||||
return this._keychain.uploadPublicKey(this.keypair.publicKey);
|
||||
}
|
||||
return new Promise(function(resolve) {
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
|
||||
PublickeyVerifier.prototype.persistKeypair = function() {
|
||||
return this._keychain.putUserKeyPair(this.keypair);
|
||||
if (this.keypair) {
|
||||
return this._keychain.putUserKeyPair(this.keypair);
|
||||
}
|
||||
return new Promise(function(resolve) {
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
|
||||
PublickeyVerifier.prototype.verify = function() {
|
||||
|
@ -9,13 +9,6 @@ ngModule.factory('publicKeyRestDao', function(appConfig) {
|
||||
return dao;
|
||||
});
|
||||
|
||||
// rest dao for use in the private key service
|
||||
ngModule.factory('privateKeyRestDao', function(appConfig) {
|
||||
var dao = new RestDAO();
|
||||
dao.setBaseUri(appConfig.config.privkeyServerUrl);
|
||||
return dao;
|
||||
});
|
||||
|
||||
// rest dao for use in the invitation service
|
||||
ngModule.factory('invitationRestDao', function(appConfig) {
|
||||
var dao = new RestDAO();
|
||||
|
@ -12,7 +12,6 @@
|
||||
"unlimitedStorage",
|
||||
"notifications",
|
||||
"https://keys-test.whiteout.io/",
|
||||
"https://keychain-test.whiteout.io/",
|
||||
"https://settings.whiteout.io/",
|
||||
"https://admin-node.whiteout.io/",
|
||||
"https://www.googleapis.com/",
|
||||
|
@ -243,6 +243,7 @@
|
||||
line-height: 1em;
|
||||
border: 1px solid $color-text-light;
|
||||
text-align: center;
|
||||
background-color: $color-bg;
|
||||
svg {
|
||||
display: inline-block;
|
||||
fill: $color-main;
|
||||
|
@ -27,4 +27,5 @@
|
||||
.typo-code {
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
user-select: text;
|
||||
}
|
@ -32,6 +32,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
.toolbar__label {
|
||||
@include respond-to(xs-only) {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__main {
|
||||
flex-grow: 1;
|
||||
margin: 0 auto 20px;
|
||||
|
@ -24,9 +24,6 @@
|
||||
<div class="lightbox lightbox--dialog" ng-class="{'lightbox--show': state.lightbox === 'set-passphrase'}"
|
||||
ng-include="'tpl/set-passphrase.html'"></div>
|
||||
|
||||
<div class="lightbox" ng-class="{'lightbox--show': state.lightbox === 'privatekey-upload'}"
|
||||
ng-include="'tpl/privatekey-upload.html'"></div>
|
||||
|
||||
<div class="lightbox" ng-class="{'lightbox--show': state.lightbox === 'publickey-import'}"
|
||||
ng-include="'tpl/publickey-import.html'"></div>
|
||||
|
||||
|
@ -5,10 +5,9 @@
|
||||
</header>
|
||||
<main class="page__main">
|
||||
<div ng-show="state.ui === 1">
|
||||
<h2 class="typo-title">PGP key</h2>
|
||||
<h2 class="typo-title">Setup encryption key</h2>
|
||||
<p class="typo-paragraph">
|
||||
You can either import an existing PGP key or generate a new one.
|
||||
Your private key remains on your device and is not sent to our servers.
|
||||
Generate a new encryption key. Your key belongs to you and only you can read encrypted messages.
|
||||
</p>
|
||||
|
||||
<form class="form" name="form">
|
||||
@ -31,13 +30,17 @@
|
||||
Stay up to date on Whiteout Networks products and important announcements.
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form__row">
|
||||
<button type="submit" ng-click="generateKey()" class="btn" tabindex="3">Generate new key</button>
|
||||
</div>
|
||||
<div class="form__row">
|
||||
<button type="button" ng-click="importKey()" class="btn btn--secondary">Import existing key</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p class="typo-paragraph">
|
||||
<a href="#" wo-touch="$event.preventDefault(); importKey()">
|
||||
Or import an existing PGP key
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div ng-show="state.ui === 2">
|
||||
|
@ -5,33 +5,28 @@
|
||||
</header>
|
||||
<main class="page__main">
|
||||
<h2 class="typo-title">Import PGP key</h2>
|
||||
<p class="typo-paragraph">Please import an existing key from the file system.</p>
|
||||
<p class="typo-paragraph">Please import an existing key. You can import a key via copy/paste or from the filesystem.</p>
|
||||
|
||||
<form class="form" name="form">
|
||||
<fieldset class="form-fieldset form-fieldset--standalone">
|
||||
<legend>On a mobile device?</legend>
|
||||
<p class="typo-paragraph">
|
||||
If you cannot import your key via a USB stick, you can setup <em>Key sync</em>
|
||||
on a desktop PC to securely transfer your PGP key over the Whiteout cloud.
|
||||
<a href="https://blog.whiteout.io/2014/07/07/secure-pgp-key-sync-a-proposal/" target="_blank">Learn more</a>.
|
||||
</p>
|
||||
</fieldset>
|
||||
|
||||
<p class="form__error-message" ng-show="errMsg">{{errMsg}}</p>
|
||||
<div class="form__row">
|
||||
<input class="input-file" type="file" file-reader tabindex="1" required>
|
||||
<textarea class="textarea" placeholder="Paste PRIVATE PGP KEY BLOCK here..." ng-model="pastedKey" ng-change="pasteKey(pastedKey)" tabindex="1"></textarea>
|
||||
</div>
|
||||
<div class="form__row">
|
||||
<input class="input-file" type="file" file-reader tabindex="2">
|
||||
</div>
|
||||
<div class="form__row">
|
||||
<input class="input-text" type="password" ng-model="passphrase"
|
||||
ng-class="{'input-text--error':incorrect}" placeholder="Passphrase" tabindex="2">
|
||||
ng-class="{'input-text--error':incorrect}" placeholder="Passphrase" tabindex="3">
|
||||
</div>
|
||||
<div class="spinner-block" ng-show="busy">
|
||||
<span class="spinner spinner--big"></span>
|
||||
</div>
|
||||
<div class="form__row">
|
||||
<button type="submit" ng-click="confirmPassphrase()" class="btn" tabindex="3">Import</button>
|
||||
<button type="submit" ng-click="confirmPassphrase()" class="btn" tabindex="4">Import</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p class="typo-paragraph">
|
||||
<a href="https://whiteout.io/revocation.html" title="Click here to reset your account." target="_blank">
|
||||
Lost your keyfile or passphrase?
|
||||
|
@ -5,59 +5,29 @@
|
||||
</header>
|
||||
<main class="page__main">
|
||||
|
||||
<div ng-show="step === 1">
|
||||
<h2 class="typo-title">Key sync</h2>
|
||||
<p class="typo-paragraph">We have sent you an email containing a recovery token. Please copy and paste the token below to download your key.</p>
|
||||
<form class="form" name="tokenForm">
|
||||
<p class="form__error-message" ng-show="errMsg">{{errMsg}}</p>
|
||||
<h2 class="typo-title">Enter backup code</h2>
|
||||
<p class="typo-paragraph">Please enter the backup code you wrote down during setup to read encrypted messages on this device.</p>
|
||||
<form class="form" name="form">
|
||||
<p class="form__error-message" ng-show="errMsg">{{errMsg}}</p>
|
||||
|
||||
<div class="form__row">
|
||||
<input type="text" class="input-text input-text--big" ng-class="{'input-text--error':incorrect}"
|
||||
size="6" maxlength="6" ng-model="recoveryToken" placeholder="Token" wo-focus-me="step === 1" pattern="([a-zA-Z0-9]*)" required>
|
||||
</div>
|
||||
<div class="spinner-block" ng-show="busy">
|
||||
<span class="spinner spinner--big"></span>
|
||||
</div>
|
||||
<div class="form__row">
|
||||
<button class="btn btn--big" type="submit" ng-click="checkToken()">Confirm token</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="form__row">
|
||||
<input type="text" class="input-text" ng-model="code" wo-input-code
|
||||
required pattern="([a-zA-Z0-9\-]*)" placeholder="0000-0000-0000-0000-0000-0000"
|
||||
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
|
||||
</div>
|
||||
<div class="spinner-block" ng-show="busy">
|
||||
<span class="spinner spinner--big"></span>
|
||||
</div>
|
||||
<div class="form__row">
|
||||
<button class="btn btn--big" type="submit" ng-click="checkCode()">Confirm code</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form class="form">
|
||||
<fieldset class="form-fieldset">
|
||||
<legend>Got USB?</legend>
|
||||
<p class="typo-paragraph">
|
||||
You can also import the key file manually if you're on a device with USB access and your key is on a flash drive.
|
||||
</p>
|
||||
</fieldset>
|
||||
<div class="form__row">
|
||||
<a class="btn btn--secondary" href="#login-new-device">Import key file</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div ng-show="step === 2">
|
||||
<h2 class="typo-title">Key sync</h2>
|
||||
<p class="typo-paragraph">Please enter the keychain code you wrote down during sync setup.</p>
|
||||
<form class="form" name="codeForm">
|
||||
<p class="form__error-message" ng-show="errMsg">{{errMsg}}</p>
|
||||
|
||||
<div class="form__row">
|
||||
<input type="text" class="input-text" ng-model="code" wo-input-code wo-focus-me="step === 2"
|
||||
required pattern="([a-zA-Z0-9\-]*)" placeholder="0000-0000-0000-0000-0000-0000"
|
||||
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
|
||||
</div>
|
||||
<div class="spinner-block" ng-show="busy">
|
||||
<span class="spinner spinner--big"></span>
|
||||
</div>
|
||||
<div class="form__row">
|
||||
<button class="btn" type="submit" ng-click="checkCode()">Complete sync</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="typo-paragraph">
|
||||
<a href="https://whiteout.io/revocation.html" title="Click here to reset your account." target="_blank">Lost your keychain code?</a>
|
||||
</p>
|
||||
</div>
|
||||
<p class="typo-paragraph">
|
||||
<a href="#login-new-device">
|
||||
Or import PGP key as file
|
||||
</a>
|
||||
</p>
|
||||
|
||||
</main>
|
||||
<div ng-include="'tpl/page-footer.html'"></div>
|
||||
|
51
src/tpl/login-privatekey-upload.html
Normal file
51
src/tpl/login-privatekey-upload.html
Normal file
@ -0,0 +1,51 @@
|
||||
<section class="page" ng-class="{'u-waiting-cursor': busy}">
|
||||
<div class="page__canvas">
|
||||
<div class="toolbar" ng-show="step > 1">
|
||||
<a class="toolbar__label" href="#" wo-touch="$event.preventDefault(); goBack()"><svg><use xlink:href="#icon-back" /></svg> Back</a>
|
||||
</div>
|
||||
<header class="page__header">
|
||||
<img src="img/whiteout_logo.svg" alt="whiteout.io">
|
||||
</header>
|
||||
|
||||
<main class="page__main">
|
||||
<h2 class="typo-title">Backup code</h2>
|
||||
|
||||
<div ng-show="step === 1">
|
||||
<p class="typo-paragraph">
|
||||
Your backup code can be used to securely backup and synchronize your encryption key between devices.
|
||||
</p>
|
||||
<p class="typo-paragraph">
|
||||
<code class="typo-code">{{displayedCode}}</code>
|
||||
</p>
|
||||
<p class="typo-paragraph">
|
||||
Please write down your backup code and keep it in a safe place. Whiteout Networks cannot recover a lost code.
|
||||
</p>
|
||||
|
||||
<form class="form">
|
||||
<div class="form__row">
|
||||
<button class="btn btn--big" type="submit" ng-click="goForward()">Continue</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div ng-show="step === 2">
|
||||
<p class="typo-paragraph">Please confirm the backup code you have written down.</p>
|
||||
<form class="form">
|
||||
<div class="form__row">
|
||||
<input type="text" class="input-text" ng-model="inputCode" wo-input-code wo-focus-me="step === 2"
|
||||
required pattern="([a-zA-Z0-9\-]*)" placeholder="0000-0000-0000-0000-0000-0000"
|
||||
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
|
||||
</div>
|
||||
<div class="spinner-block" ng-show="busy">
|
||||
<span class="spinner spinner--big"></span>
|
||||
</div>
|
||||
<div class="form__row">
|
||||
<button class="btn btn--big" type="submit" ng-click="encryptAndUploadKey()">Confirm code</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
<div ng-include="'tpl/page-footer.html'"></div>
|
||||
</div>
|
||||
</section>
|
@ -4,10 +4,10 @@
|
||||
<img src="img/whiteout_logo.svg" alt="whiteout.io">
|
||||
</header>
|
||||
<main class="page__main">
|
||||
<h2 class="typo-title">Email address verification</h2>
|
||||
<p class="typo-paragraph">
|
||||
We will now automatically verify your email address with a confirmation message we've sent you.
|
||||
</p>
|
||||
<h2 class="typo-title">Email address verification</h2>
|
||||
<p class="typo-paragraph">
|
||||
We will now automatically verify your email address with a confirmation message we've sent you.
|
||||
</p>
|
||||
<div ng-show="!busy">
|
||||
<p class="typo-paragraph">
|
||||
Verifying your email address in {{countdown}} seconds.
|
||||
|
@ -57,11 +57,6 @@
|
||||
<svg role="presentation"><use xlink:href="#icon-contact" /></svg> Contacts
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" wo-touch="$event.preventDefault(); state.privateKeyUpload.toggle(true)">
|
||||
<svg role="presentation"><use xlink:href="#icon-key" /></svg> Key sync (experimental)
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" wo-touch="$event.preventDefault(); state.writer.reportBug()">
|
||||
<svg role="presentation"><use xlink:href="#icon-bug" /></svg> Report a bug
|
||||
|
@ -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>
|
@ -5,10 +5,11 @@ var NavigationCtrl = require('../../../../src/js/controller/app/navigation'),
|
||||
Account = require('../../../../src/js/email/account'),
|
||||
Outbox = require('../../../../src/js/email/outbox'),
|
||||
Dialog = require('../../../../src/js/util/dialog'),
|
||||
Notif = require('../../../../src/js/util/notification');
|
||||
Notif = require('../../../../src/js/util/notification'),
|
||||
PrivateKey = require('../../../../src/js/service/privatekey');
|
||||
|
||||
describe('Navigation Controller unit test', function() {
|
||||
var scope, ctrl, emailDaoMock, accountMock, notificationStub, dialogStub, outboxBoMock, outboxFolder;
|
||||
var scope, ctrl, emailDaoMock, accountMock, notificationStub, privateKeyStub, dialogStub, outboxBoMock, outboxFolder;
|
||||
|
||||
beforeEach(function() {
|
||||
var account = {
|
||||
@ -29,6 +30,7 @@ describe('Navigation Controller unit test', function() {
|
||||
outboxBoMock.startChecking.returns();
|
||||
dialogStub = sinon.createStubInstance(Dialog);
|
||||
notificationStub = sinon.createStubInstance(Notif);
|
||||
privateKeyStub = sinon.createStubInstance(PrivateKey);
|
||||
accountMock = sinon.createStubInstance(Account);
|
||||
accountMock.list.returns([account]);
|
||||
accountMock.isLoggedIn.returns(true);
|
||||
@ -46,7 +48,8 @@ describe('Navigation Controller unit test', function() {
|
||||
email: emailDaoMock,
|
||||
outbox: outboxBoMock,
|
||||
notification: notificationStub,
|
||||
dialog: dialogStub
|
||||
dialog: dialogStub,
|
||||
privateKey: privateKeyStub
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -85,4 +88,20 @@ describe('Navigation Controller unit test', function() {
|
||||
expect(outboxFolder.count).to.equal(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkKeySyncStatus', function() {
|
||||
it('should work', function(done) {
|
||||
privateKeyStub.init.returns(resolves());
|
||||
privateKeyStub.isSynced.returns(resolves());
|
||||
privateKeyStub.destroy.returns(resolves());
|
||||
|
||||
scope.checkKeySyncStatus().then(done);
|
||||
});
|
||||
|
||||
it('should fail silently', function(done) {
|
||||
privateKeyStub.init.returns(rejects());
|
||||
|
||||
scope.checkKeySyncStatus().then(done);
|
||||
});
|
||||
});
|
||||
});
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -148,22 +148,6 @@ describe('Login Controller unit test', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail for keychain.requestPrivateKeyDownload', function(done) {
|
||||
authMock.init.returns(resolves());
|
||||
authMock.getEmailAddress.returns(resolves({
|
||||
emailAddress: emailAddress
|
||||
}));
|
||||
accountMock.init.returns(resolves({
|
||||
publicKey: 'publicKey'
|
||||
}));
|
||||
keychainMock.requestPrivateKeyDownload.returns(rejects(new Error()));
|
||||
|
||||
scope.init().then(function() {
|
||||
expect(dialogMock.error.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect to /login-privatekey-download', function(done) {
|
||||
authMock.init.returns(resolves());
|
||||
authMock.getEmailAddress.returns(resolves({
|
||||
@ -172,7 +156,6 @@ describe('Login Controller unit test', function() {
|
||||
accountMock.init.returns(resolves({
|
||||
publicKey: 'publicKey'
|
||||
}));
|
||||
keychainMock.requestPrivateKeyDownload.returns(resolves(true));
|
||||
|
||||
scope.init().then(function() {
|
||||
expect(goToStub.withArgs('/login-privatekey-download').called).to.be.true;
|
||||
@ -181,23 +164,6 @@ describe('Login Controller unit test', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect to /login-new-device', function(done) {
|
||||
authMock.init.returns(resolves());
|
||||
authMock.getEmailAddress.returns(resolves({
|
||||
emailAddress: emailAddress
|
||||
}));
|
||||
accountMock.init.returns(resolves({
|
||||
publicKey: 'publicKey'
|
||||
}));
|
||||
keychainMock.requestPrivateKeyDownload.returns(resolves());
|
||||
|
||||
scope.init().then(function() {
|
||||
expect(goToStub.withArgs('/login-new-device').called).to.be.true;
|
||||
expect(goToStub.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect to /login-initial', function(done) {
|
||||
authMock.init.returns(resolves());
|
||||
authMock.getEmailAddress.returns(resolves({
|
||||
|
@ -112,7 +112,7 @@ describe('Login (initial user) Controller unit test', function() {
|
||||
expect(scope.errMsg).to.not.exist;
|
||||
expect(scope.state.ui).to.equal(2);
|
||||
expect(newsletterStub.called).to.be.true;
|
||||
expect(location.$$path).to.equal('/login-verify-public-key');
|
||||
expect(location.$$path).to.equal('/login-privatekey-upload');
|
||||
expect(emailMock.unlock.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
|
@ -60,6 +60,15 @@ describe('Login (new device) Controller unit test', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('pasteKey', function() {
|
||||
it('should work', function() {
|
||||
var keyStr = '-----BEGIN PGP PRIVATE KEY BLOCK----- asdf -----END PGP PRIVATE KEY BLOCK-----';
|
||||
scope.pasteKey(keyStr);
|
||||
|
||||
expect(scope.key.privateKeyArmored).to.equal(keyStr);
|
||||
});
|
||||
});
|
||||
|
||||
describe('confirm passphrase', function() {
|
||||
it('should unlock crypto with a public key on the server', function(done) {
|
||||
scope.passphrase = passphrase;
|
||||
@ -107,7 +116,7 @@ describe('Login (new device) Controller unit test', function() {
|
||||
scope.confirmPassphrase().then(function() {
|
||||
expect(emailMock.unlock.calledOnce).to.be.true;
|
||||
expect(keychainMock.getUserKeyPair.calledOnce).to.be.true;
|
||||
expect(location.$$path).to.equal('/login-verify-public-key');
|
||||
expect(location.$$path).to.equal('/login-privatekey-upload');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
@ -3,16 +3,18 @@
|
||||
var Auth = require('../../../../src/js/service/auth'),
|
||||
LoginPrivateKeyDownloadCtrl = require('../../../../src/js/controller/login/login-privatekey-download'),
|
||||
Email = require('../../../../src/js/email/email'),
|
||||
Keychain = require('../../../../src/js/service/keychain');
|
||||
Keychain = require('../../../../src/js/service/keychain'),
|
||||
PrivateKey = require('../../../../src/js/service/privatekey');
|
||||
|
||||
describe('Login Private Key Download Controller unit test', function() {
|
||||
var scope, location, ctrl,
|
||||
emailDaoMock, authMock, keychainMock,
|
||||
emailDaoMock, authMock, keychainMock, privateKeyStub,
|
||||
emailAddress = 'fred@foo.com';
|
||||
|
||||
beforeEach(function(done) {
|
||||
emailDaoMock = sinon.createStubInstance(Email);
|
||||
keychainMock = sinon.createStubInstance(Keychain);
|
||||
privateKeyStub = sinon.createStubInstance(PrivateKey);
|
||||
authMock = sinon.createStubInstance(Auth);
|
||||
|
||||
authMock.emailAddress = emailAddress;
|
||||
@ -22,8 +24,7 @@ describe('Login Private Key Download Controller unit test', function() {
|
||||
angular.mock.inject(function($controller, $rootScope, $location) {
|
||||
scope = $rootScope.$new();
|
||||
scope.state = {};
|
||||
scope.tokenForm = {};
|
||||
scope.codeForm = {};
|
||||
scope.form = {};
|
||||
location = $location;
|
||||
ctrl = $controller(LoginPrivateKeyDownloadCtrl, {
|
||||
$location: location,
|
||||
@ -32,7 +33,8 @@ describe('Login Private Key Download Controller unit test', function() {
|
||||
$q: window.qMock,
|
||||
auth: authMock,
|
||||
email: emailDaoMock,
|
||||
keychain: keychainMock
|
||||
keychain: keychainMock,
|
||||
privateKey: privateKeyStub
|
||||
});
|
||||
done();
|
||||
});
|
||||
@ -40,117 +42,74 @@ describe('Login Private Key Download Controller unit test', function() {
|
||||
|
||||
afterEach(function() {});
|
||||
|
||||
describe('initialization', function() {
|
||||
it('should work', function() {
|
||||
expect(scope.step).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkToken', function() {
|
||||
var testKeypair = {
|
||||
describe('checkCode', function() {
|
||||
var encryptedPrivateKey = {
|
||||
encryptedPrivateKey: 'encryptedPrivateKey'
|
||||
};
|
||||
var cachedKeypair = {
|
||||
publicKey: {
|
||||
_id: 'id'
|
||||
_id: 'keyId'
|
||||
}
|
||||
};
|
||||
var privkey = {
|
||||
_id: cachedKeypair.publicKey._id,
|
||||
userId: emailAddress,
|
||||
encryptedKey: 'PRIVATE PGP BLOCK'
|
||||
};
|
||||
|
||||
it('should fail for empty recovery token', function() {
|
||||
scope.tokenForm.$invalid = true;
|
||||
|
||||
scope.checkToken();
|
||||
|
||||
expect(keychainMock.getUserKeyPair.calledOnce).to.be.false;
|
||||
expect(scope.errMsg).to.exist;
|
||||
});
|
||||
|
||||
it('should fail in keychain.getUserKeyPair', function(done) {
|
||||
keychainMock.getUserKeyPair.returns(rejects(new Error('asdf')));
|
||||
|
||||
scope.checkToken().then(function() {
|
||||
expect(scope.errMsg).to.exist;
|
||||
expect(keychainMock.getUserKeyPair.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail in keychain.downloadPrivateKey', function(done) {
|
||||
keychainMock.getUserKeyPair.returns(resolves(testKeypair));
|
||||
keychainMock.downloadPrivateKey.returns(rejects(new Error('asdf')));
|
||||
scope.recoveryToken = 'token';
|
||||
|
||||
scope.checkToken().then(function() {
|
||||
expect(scope.errMsg).to.exist;
|
||||
expect(keychainMock.getUserKeyPair.calledOnce).to.be.true;
|
||||
expect(keychainMock.downloadPrivateKey.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work', function(done) {
|
||||
keychainMock.getUserKeyPair.returns(resolves(testKeypair));
|
||||
keychainMock.downloadPrivateKey.returns(resolves('encryptedPrivateKey'));
|
||||
scope.recoveryToken = 'token';
|
||||
|
||||
scope.checkToken().then(function() {
|
||||
expect(scope.encryptedPrivateKey).to.equal('encryptedPrivateKey');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkCode', function() {
|
||||
beforeEach(function() {
|
||||
scope.code = '012345';
|
||||
|
||||
scope.encryptedPrivateKey = {
|
||||
encryptedPrivateKey: 'encryptedPrivateKey'
|
||||
};
|
||||
scope.cachedKeypair = {
|
||||
publicKey: {
|
||||
_id: 'keyId'
|
||||
}
|
||||
};
|
||||
|
||||
sinon.stub(scope, 'goTo');
|
||||
});
|
||||
afterEach(function() {
|
||||
scope.goTo.restore();
|
||||
});
|
||||
|
||||
it('should fail on decryptAndStorePrivateKeyLocally', function(done) {
|
||||
keychainMock.decryptAndStorePrivateKeyLocally.returns(rejects(new Error('asdf')));
|
||||
it('should fail on privateKey.init', function(done) {
|
||||
privateKeyStub.init.returns(rejects(new Error('asdf')));
|
||||
|
||||
scope.checkCode().then(function() {
|
||||
expect(scope.errMsg).to.exist;
|
||||
expect(keychainMock.decryptAndStorePrivateKeyLocally.calledOnce).to.be.true;
|
||||
expect(scope.errMsg).to.match(/asdf/);
|
||||
expect(privateKeyStub.init.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should goto /login-existing on emailDao.unlock fail', function(done) {
|
||||
keychainMock.decryptAndStorePrivateKeyLocally.returns(resolves({
|
||||
encryptedKey: 'keyArmored'
|
||||
}));
|
||||
emailDaoMock.unlock.returns(rejects(new Error('asdf')));
|
||||
|
||||
scope.checkCode().then(function() {
|
||||
expect(scope.goTo.withArgs('/login-existing').calledOnce).to.be.true;
|
||||
expect(keychainMock.decryptAndStorePrivateKeyLocally.calledOnce).to.be.true;
|
||||
expect(emailDaoMock.unlock.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should goto /account on emailDao.unlock success', function(done) {
|
||||
keychainMock.decryptAndStorePrivateKeyLocally.returns(resolves({
|
||||
encryptedKey: 'keyArmored'
|
||||
}));
|
||||
it('should work with empty passphrase', function(done) {
|
||||
privateKeyStub.init.returns(resolves());
|
||||
keychainMock.getUserKeyPair.withArgs(emailAddress).returns(resolves(cachedKeypair));
|
||||
privateKeyStub.download.withArgs({
|
||||
userId: emailAddress,
|
||||
keyId: cachedKeypair.publicKey._id
|
||||
}).returns(resolves(encryptedPrivateKey));
|
||||
privateKeyStub.decrypt.returns(resolves(privkey));
|
||||
emailDaoMock.unlock.returns(resolves());
|
||||
authMock.storeCredentials.returns(resolves());
|
||||
privateKeyStub.destroy.returns(resolves());
|
||||
|
||||
scope.checkCode().then(function() {
|
||||
expect(scope.errMsg).to.not.exist;
|
||||
expect(scope.goTo.withArgs('/account').calledOnce).to.be.true;
|
||||
expect(keychainMock.decryptAndStorePrivateKeyLocally.calledOnce).to.be.true;
|
||||
expect(emailDaoMock.unlock.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with passphrase', function(done) {
|
||||
privateKeyStub.init.returns(resolves());
|
||||
keychainMock.getUserKeyPair.withArgs(emailAddress).returns(resolves(cachedKeypair));
|
||||
privateKeyStub.download.withArgs({
|
||||
userId: emailAddress,
|
||||
keyId: cachedKeypair.publicKey._id
|
||||
}).returns(resolves(encryptedPrivateKey));
|
||||
privateKeyStub.decrypt.returns(resolves(privkey));
|
||||
emailDaoMock.unlock.returns(rejects());
|
||||
authMock.storeCredentials.returns(resolves());
|
||||
privateKeyStub.destroy.returns(resolves());
|
||||
|
||||
scope.checkCode().then(function() {
|
||||
expect(scope.errMsg).to.not.exist;
|
||||
expect(scope.goTo.withArgs('/login-existing').calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,77 @@
|
||||
'use strict';
|
||||
|
||||
var Auth = require('../../../../src/js/service/auth'),
|
||||
LoginPrivateKeyUploadCtrl = require('../../../../src/js/controller/login/login-privatekey-upload'),
|
||||
PrivateKey = require('../../../../src/js/service/privatekey');
|
||||
|
||||
describe('Login Private Key Upload Controller unit test', function() {
|
||||
var scope, location, ctrl,
|
||||
authMock, privateKeyStub,
|
||||
emailAddress = 'fred@foo.com';
|
||||
|
||||
beforeEach(function(done) {
|
||||
privateKeyStub = sinon.createStubInstance(PrivateKey);
|
||||
authMock = sinon.createStubInstance(Auth);
|
||||
|
||||
authMock.emailAddress = emailAddress;
|
||||
|
||||
angular.module('login-privatekey-download-test', ['woServices']);
|
||||
angular.mock.module('login-privatekey-download-test');
|
||||
angular.mock.inject(function($controller, $rootScope, $location) {
|
||||
scope = $rootScope.$new();
|
||||
scope.state = {};
|
||||
scope.form = {};
|
||||
location = $location;
|
||||
ctrl = $controller(LoginPrivateKeyUploadCtrl, {
|
||||
$location: location,
|
||||
$scope: scope,
|
||||
$routeParams: {},
|
||||
$q: window.qMock,
|
||||
auth: authMock,
|
||||
privateKey: privateKeyStub
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {});
|
||||
|
||||
describe('init', function() {
|
||||
it('should work', function() {
|
||||
expect(scope.step).to.equal(1);
|
||||
expect(scope.code).to.exist;
|
||||
expect(scope.displayedCode).to.exist;
|
||||
expect(scope.inputCode).to.equal('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('encryptAndUploadKey', function() {
|
||||
var encryptedPrivateKey = {
|
||||
encryptedPrivateKey: 'encryptedPrivateKey'
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
scope.inputCode = scope.code;
|
||||
sinon.spy(location, 'path');
|
||||
});
|
||||
|
||||
it('should fail for invalid code', function() {
|
||||
scope.inputCode = 'asdf';
|
||||
scope.encryptAndUploadKey();
|
||||
expect(scope.errMsg).to.match(/go back and check/);
|
||||
});
|
||||
|
||||
it('should work', function(done) {
|
||||
privateKeyStub.init.returns(resolves());
|
||||
privateKeyStub.encrypt.withArgs(scope.code).returns(resolves(encryptedPrivateKey));
|
||||
privateKeyStub.upload.returns(resolves());
|
||||
privateKeyStub.destroy.returns(resolves());
|
||||
|
||||
scope.encryptAndUploadKey().then(function() {
|
||||
expect(scope.errMsg).to.not.exist;
|
||||
location.path.calledWith('/login-verify-public-key');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -3,7 +3,7 @@
|
||||
var Auth = require('../../../../src/js/service/auth'),
|
||||
Dialog = require('../../../../src/js/util/dialog'),
|
||||
PublicKeyVerifier = require('../../../../src/js/service/publickey-verifier'),
|
||||
KeychainDAO = require('../../../../src/js/service/keychain'),
|
||||
PublicKey = require('../../../../src/js/service/publickey'),
|
||||
PublicKeyVerifierCtrl = require('../../../../src/js/controller/login/login-verify-public-key');
|
||||
|
||||
describe('Public Key Verification Controller unit test', function() {
|
||||
@ -11,7 +11,7 @@ describe('Public Key Verification Controller unit test', function() {
|
||||
var scope, location;
|
||||
|
||||
// Stubs & Fixture
|
||||
var auth, verifier, dialogStub, keychain;
|
||||
var auth, verifier, dialogStub, publicKeyStub;
|
||||
var emailAddress = 'foo@foo.com';
|
||||
|
||||
// SUT
|
||||
@ -22,8 +22,9 @@ describe('Public Key Verification Controller unit test', function() {
|
||||
auth = sinon.createStubInstance(Auth);
|
||||
verifier = sinon.createStubInstance(PublicKeyVerifier);
|
||||
dialogStub = sinon.createStubInstance(Dialog);
|
||||
keychain = sinon.createStubInstance(KeychainDAO);
|
||||
publicKeyStub = sinon.createStubInstance(PublicKey);
|
||||
|
||||
verifier.uploadPublicKey.returns(resolves());
|
||||
auth.emailAddress = emailAddress;
|
||||
|
||||
// setup the controller
|
||||
@ -39,7 +40,7 @@ describe('Public Key Verification Controller unit test', function() {
|
||||
auth: auth,
|
||||
publickeyVerifier: verifier,
|
||||
dialog: dialogStub,
|
||||
keychain: keychain,
|
||||
publicKey: publicKeyStub,
|
||||
appConfig: {
|
||||
string: {
|
||||
publickeyVerificationSkipTitle: 'foo',
|
||||
@ -56,14 +57,14 @@ describe('Public Key Verification Controller unit test', function() {
|
||||
it('should verify', function(done) {
|
||||
var credentials = {};
|
||||
|
||||
keychain.getUserKeyPair.withArgs(emailAddress).returns(resolves({}));
|
||||
publicKeyStub.getByUserId.withArgs(emailAddress).returns(resolves());
|
||||
auth.getCredentials.returns(resolves(credentials));
|
||||
verifier.configure.withArgs(credentials).returns(resolves());
|
||||
verifier.verify.withArgs().returns(resolves());
|
||||
verifier.persistKeypair.returns(resolves());
|
||||
|
||||
scope.verify().then(function() {
|
||||
expect(keychain.getUserKeyPair.calledOnce).to.be.true;
|
||||
expect(publicKeyStub.getByUserId.calledOnce).to.be.true;
|
||||
expect(auth.getCredentials.calledOnce).to.be.true;
|
||||
expect(verifier.configure.calledOnce).to.be.true;
|
||||
expect(verifier.verify.calledOnce).to.be.true;
|
||||
@ -75,12 +76,12 @@ describe('Public Key Verification Controller unit test', function() {
|
||||
});
|
||||
|
||||
it('should skip verification when key is already verified', function(done) {
|
||||
keychain.getUserKeyPair.withArgs(emailAddress).returns(resolves({
|
||||
publicKeyStub.getByUserId.withArgs(emailAddress).returns(resolves({
|
||||
publicKey: {}
|
||||
}));
|
||||
|
||||
scope.verify().then(function() {
|
||||
expect(keychain.getUserKeyPair.calledOnce).to.be.true;
|
||||
expect(publicKeyStub.getByUserId.calledOnce).to.be.true;
|
||||
expect(auth.getCredentials.called).to.be.false;
|
||||
expect(verifier.configure.called).to.be.false;
|
||||
expect(verifier.verify.called).to.be.false;
|
||||
|
@ -598,764 +598,6 @@ describe('Keychain DAO unit tests', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('setDeviceName', function() {
|
||||
it('should work', function(done) {
|
||||
lawnchairDaoStub.persist.returns(resolves());
|
||||
|
||||
keychainDao.setDeviceName('iPhone').then(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDeviceName', function() {
|
||||
it('should fail when device name is not set', function(done) {
|
||||
lawnchairDaoStub.read.withArgs('devicename').returns(resolves());
|
||||
|
||||
keychainDao.getDeviceName().catch(function(err) {
|
||||
expect(err.message).to.equal('Device name not set!');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail due to error when reading device name', function(done) {
|
||||
lawnchairDaoStub.read.withArgs('devicename').returns(rejects(42));
|
||||
|
||||
keychainDao.getDeviceName().catch(function(err) {
|
||||
expect(err).to.equal(42);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work', function(done) {
|
||||
lawnchairDaoStub.read.withArgs('devicename').returns(resolves('iPhone'));
|
||||
|
||||
keychainDao.getDeviceName().then(function(deviceName) {
|
||||
expect(deviceName).to.equal('iPhone');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDeviceSecret', function() {
|
||||
it('should fail due to error when reading device secret', function(done) {
|
||||
lawnchairDaoStub.read.withArgs('devicename').returns(resolves('iPhone'));
|
||||
lawnchairDaoStub.read.withArgs('devicesecret').returns(rejects(42));
|
||||
|
||||
keychainDao.getDeviceSecret().catch(function(err) {
|
||||
expect(err).to.equal(42);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail due to error when storing device secret', function(done) {
|
||||
lawnchairDaoStub.read.withArgs('devicename').returns(resolves('iPhone'));
|
||||
lawnchairDaoStub.read.withArgs('devicesecret').returns(resolves());
|
||||
lawnchairDaoStub.persist.withArgs('devicesecret').returns(rejects(42));
|
||||
|
||||
keychainDao.getDeviceSecret().catch(function(err) {
|
||||
expect(err).to.equal(42);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work when device secret is not set', function(done) {
|
||||
lawnchairDaoStub.read.withArgs('devicename').returns(resolves('iPhone'));
|
||||
lawnchairDaoStub.read.withArgs('devicesecret').returns(resolves());
|
||||
lawnchairDaoStub.persist.withArgs('devicesecret').returns(resolves());
|
||||
|
||||
keychainDao.getDeviceSecret().then(function(deviceSecret) {
|
||||
expect(deviceSecret).to.exist;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work when device secret is set', function(done) {
|
||||
lawnchairDaoStub.read.withArgs('devicename').returns(resolves('iPhone'));
|
||||
lawnchairDaoStub.read.withArgs('devicesecret').returns(resolves('secret'));
|
||||
|
||||
keychainDao.getDeviceSecret().then(function(deviceSecret) {
|
||||
expect(deviceSecret).to.equal('secret');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('registerDevice', function() {
|
||||
var getDeviceNameStub, lookupPublicKeyStub, getDeviceSecretStub;
|
||||
|
||||
beforeEach(function() {
|
||||
getDeviceNameStub = sinon.stub(keychainDao, 'getDeviceName');
|
||||
lookupPublicKeyStub = sinon.stub(keychainDao, 'lookupPublicKey');
|
||||
getDeviceSecretStub = sinon.stub(keychainDao, 'getDeviceSecret');
|
||||
});
|
||||
afterEach(function() {
|
||||
getDeviceNameStub.restore();
|
||||
lookupPublicKeyStub.restore();
|
||||
getDeviceSecretStub.restore();
|
||||
});
|
||||
|
||||
it('should fail when reading devicename', function(done) {
|
||||
getDeviceNameStub.returns(rejects(42));
|
||||
|
||||
keychainDao.registerDevice({}).catch(function(err) {
|
||||
expect(err).to.equal(42);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail in requestDeviceRegistration', function(done) {
|
||||
getDeviceNameStub.returns(resolves('iPhone'));
|
||||
|
||||
privkeyDaoStub.requestDeviceRegistration.withArgs({
|
||||
userId: testUser,
|
||||
deviceName: 'iPhone'
|
||||
}).returns(rejects(42));
|
||||
|
||||
keychainDao.registerDevice({
|
||||
userId: testUser
|
||||
}).catch(function(err) {
|
||||
expect(err).to.equal(42);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail due to invalid requestDeviceRegistration return value', function(done) {
|
||||
getDeviceNameStub.returns(resolves('iPhone'));
|
||||
|
||||
privkeyDaoStub.requestDeviceRegistration.withArgs({
|
||||
userId: testUser,
|
||||
deviceName: 'iPhone'
|
||||
}).returns(resolves({}));
|
||||
|
||||
keychainDao.registerDevice({
|
||||
userId: testUser
|
||||
}).catch(function(err) {
|
||||
expect(err.message).to.equal('Invalid format for session key!');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail in lookupPublicKey', function(done) {
|
||||
getDeviceNameStub.returns(resolves('iPhone'));
|
||||
|
||||
privkeyDaoStub.requestDeviceRegistration.withArgs({
|
||||
userId: testUser,
|
||||
deviceName: 'iPhone'
|
||||
}).returns(resolves({
|
||||
encryptedRegSessionKey: 'asdf'
|
||||
}));
|
||||
|
||||
lookupPublicKeyStub.returns(rejects(42));
|
||||
|
||||
keychainDao.registerDevice({
|
||||
userId: testUser
|
||||
}).catch(function(err) {
|
||||
expect(err).to.equal(42);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail when server public key not found', function(done) {
|
||||
getDeviceNameStub.returns(resolves('iPhone'));
|
||||
|
||||
privkeyDaoStub.requestDeviceRegistration.withArgs({
|
||||
userId: testUser,
|
||||
deviceName: 'iPhone'
|
||||
}).returns(resolves({
|
||||
encryptedRegSessionKey: 'asdf'
|
||||
}));
|
||||
|
||||
lookupPublicKeyStub.returns(resolves());
|
||||
|
||||
keychainDao.registerDevice({
|
||||
userId: testUser
|
||||
}).catch(function(err) {
|
||||
expect(err).to.exist;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail in decrypt', function(done) {
|
||||
getDeviceNameStub.returns(resolves('iPhone'));
|
||||
|
||||
privkeyDaoStub.requestDeviceRegistration.withArgs({
|
||||
userId: testUser,
|
||||
deviceName: 'iPhone'
|
||||
}).returns(resolves({
|
||||
encryptedRegSessionKey: 'asdf'
|
||||
}));
|
||||
|
||||
lookupPublicKeyStub.returns(resolves({
|
||||
publicKey: 'pubkey'
|
||||
}));
|
||||
pgpStub.decrypt.withArgs('asdf', 'pubkey').returns(rejects(42));
|
||||
|
||||
keychainDao.registerDevice({
|
||||
userId: testUser
|
||||
}).catch(function(err) {
|
||||
expect(err).to.equal(42);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail in getDeviceSecret', function(done) {
|
||||
getDeviceNameStub.returns(resolves('iPhone'));
|
||||
|
||||
privkeyDaoStub.requestDeviceRegistration.withArgs({
|
||||
userId: testUser,
|
||||
deviceName: 'iPhone'
|
||||
}).returns(resolves({
|
||||
encryptedRegSessionKey: 'asdf'
|
||||
}));
|
||||
|
||||
lookupPublicKeyStub.returns(resolves({
|
||||
publicKey: 'pubkey'
|
||||
}));
|
||||
pgpStub.decrypt.withArgs('asdf', 'pubkey').returns(resolves({
|
||||
decrypted: 'decrypted',
|
||||
signaturesValid: true
|
||||
}));
|
||||
getDeviceSecretStub.returns(rejects(42));
|
||||
|
||||
keychainDao.registerDevice({
|
||||
userId: testUser
|
||||
}).catch(function(err) {
|
||||
expect(err).to.equal(42);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail in encrypt', function(done) {
|
||||
getDeviceNameStub.returns(resolves('iPhone'));
|
||||
|
||||
privkeyDaoStub.requestDeviceRegistration.withArgs({
|
||||
userId: testUser,
|
||||
deviceName: 'iPhone'
|
||||
}).returns(resolves({
|
||||
encryptedRegSessionKey: 'asdf'
|
||||
}));
|
||||
|
||||
lookupPublicKeyStub.returns(resolves({
|
||||
publicKey: 'pubkey'
|
||||
}));
|
||||
pgpStub.decrypt.withArgs('asdf', 'pubkey').returns(resolves({
|
||||
decrypted: 'decrypted',
|
||||
signaturesValid: true
|
||||
}));
|
||||
getDeviceSecretStub.returns(resolves('secret'));
|
||||
cryptoStub.encrypt.withArgs('secret', 'decrypted').returns(rejects(42));
|
||||
|
||||
keychainDao.registerDevice({
|
||||
userId: testUser
|
||||
}).catch(function(err) {
|
||||
expect(err).to.equal(42);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work', function(done) {
|
||||
getDeviceNameStub.returns(resolves('iPhone'));
|
||||
|
||||
privkeyDaoStub.requestDeviceRegistration.withArgs({
|
||||
userId: testUser,
|
||||
deviceName: 'iPhone'
|
||||
}).returns(resolves({
|
||||
encryptedRegSessionKey: 'asdf'
|
||||
}));
|
||||
|
||||
lookupPublicKeyStub.returns(resolves({
|
||||
publicKey: 'pubkey'
|
||||
}));
|
||||
pgpStub.decrypt.withArgs('asdf', 'pubkey').returns(resolves({
|
||||
decrypted: 'decrypted',
|
||||
signaturesValid: true
|
||||
}));
|
||||
getDeviceSecretStub.returns(resolves('secret'));
|
||||
cryptoStub.encrypt.withArgs('secret', 'decrypted').returns(resolves('encryptedDeviceSecret'));
|
||||
privkeyDaoStub.uploadDeviceSecret.returns(resolves());
|
||||
|
||||
keychainDao.registerDevice({
|
||||
userId: testUser
|
||||
}).then(function() {
|
||||
expect(privkeyDaoStub.uploadDeviceSecret.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('_authenticateToPrivateKeyServer', function() {
|
||||
var lookupPublicKeyStub, getDeviceSecretStub;
|
||||
|
||||
beforeEach(function() {
|
||||
lookupPublicKeyStub = sinon.stub(keychainDao, 'lookupPublicKey');
|
||||
getDeviceSecretStub = sinon.stub(keychainDao, 'getDeviceSecret');
|
||||
});
|
||||
afterEach(function() {
|
||||
lookupPublicKeyStub.restore();
|
||||
getDeviceSecretStub.restore();
|
||||
});
|
||||
|
||||
it('should fail due to privkeyDao.requestAuthSessionKey', function(done) {
|
||||
privkeyDaoStub.requestAuthSessionKey.withArgs({
|
||||
userId: testUser
|
||||
}).returns(rejects(42));
|
||||
|
||||
keychainDao._authenticateToPrivateKeyServer(testUser).catch(function(err) {
|
||||
expect(err).to.equal(42);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail due to privkeyDao.requestAuthSessionKey response', function(done) {
|
||||
privkeyDaoStub.requestAuthSessionKey.returns(resolves({}));
|
||||
|
||||
keychainDao._authenticateToPrivateKeyServer(testUser).catch(function(err) {
|
||||
expect(err).to.exist;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail due to lookupPublicKey', function(done) {
|
||||
privkeyDaoStub.requestAuthSessionKey.returns(resolves({
|
||||
encryptedAuthSessionKey: 'encryptedAuthSessionKey',
|
||||
encryptedChallenge: 'encryptedChallenge',
|
||||
sessionId: 'sessionId'
|
||||
}));
|
||||
|
||||
lookupPublicKeyStub.returns(rejects(42));
|
||||
|
||||
keychainDao._authenticateToPrivateKeyServer(testUser).catch(function(err) {
|
||||
expect(err).to.exist;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail due to pgp.decrypt', function(done) {
|
||||
privkeyDaoStub.requestAuthSessionKey.returns(resolves({
|
||||
encryptedAuthSessionKey: 'encryptedAuthSessionKey',
|
||||
encryptedChallenge: 'encryptedChallenge',
|
||||
sessionId: 'sessionId'
|
||||
}));
|
||||
|
||||
lookupPublicKeyStub.returns(resolves({
|
||||
publickKey: 'publicKey'
|
||||
}));
|
||||
|
||||
pgpStub.decrypt.returns(rejects(42));
|
||||
|
||||
keychainDao._authenticateToPrivateKeyServer(testUser).catch(function(err) {
|
||||
expect(err).to.exist;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail due to getDeviceSecret', function(done) {
|
||||
privkeyDaoStub.requestAuthSessionKey.returns(resolves({
|
||||
encryptedAuthSessionKey: 'encryptedAuthSessionKey',
|
||||
encryptedChallenge: 'encryptedChallenge',
|
||||
sessionId: 'sessionId'
|
||||
}));
|
||||
|
||||
lookupPublicKeyStub.returns(resolves({
|
||||
publickKey: 'publicKey'
|
||||
}));
|
||||
|
||||
pgpStub.decrypt.returns(resolves({
|
||||
decrypted: 'decryptedStuff'
|
||||
}));
|
||||
getDeviceSecretStub.returns(rejects(42));
|
||||
|
||||
keychainDao._authenticateToPrivateKeyServer(testUser).catch(function(err) {
|
||||
expect(err).to.exist;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail due to crypto.encrypt', function(done) {
|
||||
privkeyDaoStub.requestAuthSessionKey.returns(resolves({
|
||||
encryptedAuthSessionKey: 'encryptedAuthSessionKey',
|
||||
encryptedChallenge: 'encryptedChallenge',
|
||||
sessionId: 'sessionId'
|
||||
}));
|
||||
|
||||
lookupPublicKeyStub.returns(resolves({
|
||||
publickKey: 'publicKey'
|
||||
}));
|
||||
|
||||
pgpStub.decrypt.returns(resolves({
|
||||
decrypted: 'decryptedStuff'
|
||||
}));
|
||||
getDeviceSecretStub.returns(resolves('deviceSecret'));
|
||||
cryptoStub.encrypt.returns(rejects(42));
|
||||
|
||||
keychainDao._authenticateToPrivateKeyServer(testUser).catch(function(err) {
|
||||
expect(err).to.exist;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail due to privkeyDao.verifyAuthentication', function(done) {
|
||||
privkeyDaoStub.requestAuthSessionKey.returns(resolves({
|
||||
encryptedAuthSessionKey: 'encryptedAuthSessionKey',
|
||||
encryptedChallenge: 'encryptedChallenge',
|
||||
sessionId: 'sessionId'
|
||||
}));
|
||||
|
||||
lookupPublicKeyStub.returns(resolves({
|
||||
publickKey: 'publicKey'
|
||||
}));
|
||||
|
||||
pgpStub.decrypt.returns(resolves({
|
||||
decrypted: 'decryptedStuff',
|
||||
signaturesValid: true
|
||||
}));
|
||||
getDeviceSecretStub.returns(resolves('deviceSecret'));
|
||||
cryptoStub.encrypt.returns(resolves('encryptedStuff'));
|
||||
privkeyDaoStub.verifyAuthentication.returns(rejects(42));
|
||||
|
||||
keychainDao._authenticateToPrivateKeyServer(testUser).catch(function(err) {
|
||||
expect(err).to.exist;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail due to server public key nto found', function(done) {
|
||||
privkeyDaoStub.requestAuthSessionKey.returns(resolves({
|
||||
encryptedAuthSessionKey: 'encryptedAuthSessionKey',
|
||||
encryptedChallenge: 'encryptedChallenge',
|
||||
sessionId: 'sessionId'
|
||||
}));
|
||||
|
||||
lookupPublicKeyStub.returns(resolves());
|
||||
|
||||
pgpStub.decrypt.returns(resolves({
|
||||
decrypted: 'decryptedStuff',
|
||||
signaturesValid: true
|
||||
}));
|
||||
getDeviceSecretStub.returns(resolves('deviceSecret'));
|
||||
cryptoStub.encrypt.returns(resolves('encryptedStuff'));
|
||||
privkeyDaoStub.verifyAuthentication.returns(resolves());
|
||||
|
||||
keychainDao._authenticateToPrivateKeyServer(testUser).catch(function(err) {
|
||||
expect(err).to.exist;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work', function(done) {
|
||||
privkeyDaoStub.requestAuthSessionKey.returns(resolves({
|
||||
encryptedAuthSessionKey: 'encryptedAuthSessionKey',
|
||||
encryptedChallenge: 'encryptedChallenge',
|
||||
sessionId: 'sessionId'
|
||||
}));
|
||||
|
||||
lookupPublicKeyStub.returns(resolves({
|
||||
publicKey: 'publicKey'
|
||||
}));
|
||||
|
||||
pgpStub.decrypt.returns(resolves({
|
||||
decrypted: 'decryptedStuff',
|
||||
signaturesValid: true
|
||||
}));
|
||||
getDeviceSecretStub.returns(resolves('deviceSecret'));
|
||||
cryptoStub.encrypt.returns(resolves('encryptedStuff'));
|
||||
privkeyDaoStub.verifyAuthentication.returns(resolves());
|
||||
|
||||
keychainDao._authenticateToPrivateKeyServer(testUser).then(function(authSessionKey) {
|
||||
expect(authSessionKey).to.deep.equal({
|
||||
sessionKey: 'decryptedStuff',
|
||||
sessionId: 'sessionId'
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadPrivateKey', function() {
|
||||
var getUserKeyPairStub, _authenticateToPrivateKeyServerStub;
|
||||
|
||||
beforeEach(function() {
|
||||
getUserKeyPairStub = sinon.stub(keychainDao, 'getUserKeyPair');
|
||||
_authenticateToPrivateKeyServerStub = sinon.stub(keychainDao, '_authenticateToPrivateKeyServer');
|
||||
});
|
||||
afterEach(function() {
|
||||
getUserKeyPairStub.restore();
|
||||
_authenticateToPrivateKeyServerStub.restore();
|
||||
});
|
||||
|
||||
it('should fail due to missing args', function(done) {
|
||||
keychainDao.uploadPrivateKey({}).catch(function(err) {
|
||||
expect(err).to.exist;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail due to error in derive key', function(done) {
|
||||
cryptoStub.deriveKey.returns(rejects(42));
|
||||
|
||||
keychainDao.uploadPrivateKey({
|
||||
code: 'code',
|
||||
userId: testUser
|
||||
}).catch(function(err) {
|
||||
expect(err).to.exist;
|
||||
expect(cryptoStub.deriveKey.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail due to error in getUserKeyPair', function(done) {
|
||||
cryptoStub.deriveKey.returns(resolves('derivedKey'));
|
||||
getUserKeyPairStub.returns(rejects(42));
|
||||
|
||||
keychainDao.uploadPrivateKey({
|
||||
code: 'code',
|
||||
userId: testUser
|
||||
}).catch(function(err) {
|
||||
expect(err).to.exist;
|
||||
expect(cryptoStub.deriveKey.calledOnce).to.be.true;
|
||||
expect(getUserKeyPairStub.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail due to error in crypto.encrypt', function(done) {
|
||||
cryptoStub.deriveKey.returns(resolves('derivedKey'));
|
||||
getUserKeyPairStub.returns(resolves({
|
||||
privateKey: {
|
||||
_id: 'pgpKeyId',
|
||||
encryptedKey: 'pgpKey'
|
||||
}
|
||||
}));
|
||||
cryptoStub.encrypt.returns(rejects(42));
|
||||
|
||||
keychainDao.uploadPrivateKey({
|
||||
code: 'code',
|
||||
userId: testUser
|
||||
}).catch(function(err) {
|
||||
expect(err).to.exist;
|
||||
expect(cryptoStub.deriveKey.calledOnce).to.be.true;
|
||||
expect(getUserKeyPairStub.calledOnce).to.be.true;
|
||||
expect(cryptoStub.encrypt.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail due to error in _authenticateToPrivateKeyServer', function(done) {
|
||||
cryptoStub.deriveKey.returns(resolves('derivedKey'));
|
||||
getUserKeyPairStub.returns(resolves({
|
||||
privateKey: {
|
||||
_id: 'pgpKeyId',
|
||||
encryptedKey: 'pgpKey'
|
||||
}
|
||||
}));
|
||||
cryptoStub.encrypt.returns(resolves('encryptedPgpKey'));
|
||||
_authenticateToPrivateKeyServerStub.returns(rejects(42));
|
||||
|
||||
keychainDao.uploadPrivateKey({
|
||||
code: 'code',
|
||||
userId: testUser
|
||||
}).catch(function(err) {
|
||||
expect(err).to.exist;
|
||||
expect(cryptoStub.deriveKey.calledOnce).to.be.true;
|
||||
expect(getUserKeyPairStub.calledOnce).to.be.true;
|
||||
expect(cryptoStub.encrypt.calledOnce).to.be.true;
|
||||
expect(_authenticateToPrivateKeyServerStub.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail due to error in cryptoStub.encrypt', function(done) {
|
||||
cryptoStub.deriveKey.returns(resolves('derivedKey'));
|
||||
getUserKeyPairStub.returns(resolves({
|
||||
privateKey: {
|
||||
_id: 'pgpKeyId',
|
||||
encryptedKey: 'pgpKey'
|
||||
}
|
||||
}));
|
||||
cryptoStub.encrypt.withArgs('pgpKey').returns(resolves('encryptedPgpKey'));
|
||||
_authenticateToPrivateKeyServerStub.returns(resolves({
|
||||
sessionId: 'sessionId',
|
||||
sessionKey: 'sessionKey'
|
||||
}));
|
||||
cryptoStub.encrypt.withArgs('encryptedPgpKey').returns(rejects(42));
|
||||
|
||||
keychainDao.uploadPrivateKey({
|
||||
code: 'code',
|
||||
userId: testUser
|
||||
}).catch(function(err) {
|
||||
expect(err).to.exist;
|
||||
expect(cryptoStub.deriveKey.calledOnce).to.be.true;
|
||||
expect(getUserKeyPairStub.calledOnce).to.be.true;
|
||||
expect(cryptoStub.encrypt.calledTwice).to.be.true;
|
||||
expect(_authenticateToPrivateKeyServerStub.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work', function(done) {
|
||||
cryptoStub.deriveKey.returns(resolves('derivedKey'));
|
||||
getUserKeyPairStub.returns(resolves({
|
||||
privateKey: {
|
||||
_id: 'pgpKeyId',
|
||||
encryptedKey: 'pgpKey'
|
||||
}
|
||||
}));
|
||||
cryptoStub.encrypt.withArgs('pgpKey').returns(resolves('encryptedPgpKey'));
|
||||
_authenticateToPrivateKeyServerStub.returns(resolves({
|
||||
sessionId: 'sessionId',
|
||||
sessionKey: 'sessionKey'
|
||||
}));
|
||||
cryptoStub.encrypt.withArgs('encryptedPgpKey').returns(resolves('doubleEncryptedPgpKey'));
|
||||
privkeyDaoStub.upload.returns(resolves());
|
||||
|
||||
keychainDao.uploadPrivateKey({
|
||||
code: 'code',
|
||||
userId: testUser
|
||||
}).then(function() {
|
||||
expect(cryptoStub.deriveKey.calledOnce).to.be.true;
|
||||
expect(getUserKeyPairStub.calledOnce).to.be.true;
|
||||
expect(cryptoStub.encrypt.calledTwice).to.be.true;
|
||||
expect(_authenticateToPrivateKeyServerStub.calledOnce).to.be.true;
|
||||
expect(privkeyDaoStub.upload.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestPrivateKeyDownload', function() {
|
||||
it('should work', function(done) {
|
||||
var options = {
|
||||
userId: testUser,
|
||||
keyId: 'someId'
|
||||
};
|
||||
|
||||
privkeyDaoStub.requestDownload.withArgs(options).returns(resolves());
|
||||
keychainDao.requestPrivateKeyDownload(options).then(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasPrivateKey', function() {
|
||||
it('should work', function(done) {
|
||||
var options = {
|
||||
userId: testUser,
|
||||
keyId: 'someId'
|
||||
};
|
||||
|
||||
privkeyDaoStub.hasPrivateKey.withArgs(options).returns(resolves());
|
||||
keychainDao.hasPrivateKey(options).then(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadPrivateKey', function() {
|
||||
it('should work', function(done) {
|
||||
var options = {
|
||||
recoveryToken: 'token'
|
||||
};
|
||||
|
||||
privkeyDaoStub.download.withArgs(options).returns(resolves());
|
||||
keychainDao.downloadPrivateKey(options).then(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('decryptAndStorePrivateKeyLocally', function() {
|
||||
var saveLocalPrivateKeyStub, testData;
|
||||
|
||||
beforeEach(function() {
|
||||
testData = {
|
||||
_id: 'keyId',
|
||||
userId: testUser,
|
||||
encryptedPrivateKey: 'encryptedPrivateKey',
|
||||
code: 'code',
|
||||
salt: 'salt',
|
||||
iv: 'iv'
|
||||
};
|
||||
|
||||
saveLocalPrivateKeyStub = sinon.stub(keychainDao, 'saveLocalPrivateKey');
|
||||
});
|
||||
afterEach(function() {
|
||||
saveLocalPrivateKeyStub.restore();
|
||||
});
|
||||
|
||||
it('should fail due to invlaid args', function(done) {
|
||||
keychainDao.decryptAndStorePrivateKeyLocally({}).catch(function(err) {
|
||||
expect(err).to.exist;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail due to crypto.deriveKey', function(done) {
|
||||
cryptoStub.deriveKey.returns(rejects(42));
|
||||
|
||||
keychainDao.decryptAndStorePrivateKeyLocally(testData).catch(function(err) {
|
||||
expect(err).to.exist;
|
||||
expect(cryptoStub.deriveKey.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail due to crypto.decrypt', function(done) {
|
||||
cryptoStub.deriveKey.returns(resolves('derivedKey'));
|
||||
cryptoStub.decrypt.returns(rejects(42));
|
||||
|
||||
keychainDao.decryptAndStorePrivateKeyLocally(testData).catch(function(err) {
|
||||
expect(err).to.exist;
|
||||
expect(cryptoStub.deriveKey.calledOnce).to.be.true;
|
||||
expect(cryptoStub.decrypt.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail due to pgp.getKeyParams', function(done) {
|
||||
cryptoStub.deriveKey.returns(resolves('derivedKey'));
|
||||
cryptoStub.decrypt.returns(resolves('privateKeyArmored'));
|
||||
pgpStub.getKeyParams.returns(rejects(new Error()));
|
||||
|
||||
keychainDao.decryptAndStorePrivateKeyLocally(testData).catch(function(err) {
|
||||
expect(err).to.exist;
|
||||
expect(cryptoStub.deriveKey.calledOnce).to.be.true;
|
||||
expect(cryptoStub.decrypt.calledOnce).to.be.true;
|
||||
expect(pgpStub.getKeyParams.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail due to saveLocalPrivateKey', function(done) {
|
||||
cryptoStub.deriveKey.returns(resolves('derivedKey'));
|
||||
cryptoStub.decrypt.returns(resolves('privateKeyArmored'));
|
||||
pgpStub.getKeyParams.returns(testData);
|
||||
saveLocalPrivateKeyStub.returns(rejects(42));
|
||||
|
||||
keychainDao.decryptAndStorePrivateKeyLocally(testData).catch(function(err) {
|
||||
expect(err).to.exist;
|
||||
expect(cryptoStub.deriveKey.calledOnce).to.be.true;
|
||||
expect(cryptoStub.decrypt.calledOnce).to.be.true;
|
||||
expect(pgpStub.getKeyParams.calledOnce).to.be.true;
|
||||
expect(saveLocalPrivateKeyStub.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work', function(done) {
|
||||
cryptoStub.deriveKey.returns(resolves('derivedKey'));
|
||||
cryptoStub.decrypt.returns(resolves('privateKeyArmored'));
|
||||
pgpStub.getKeyParams.returns(testData);
|
||||
saveLocalPrivateKeyStub.returns(resolves());
|
||||
|
||||
keychainDao.decryptAndStorePrivateKeyLocally(testData).then(function(keyObject) {
|
||||
expect(keyObject).to.deep.equal({
|
||||
_id: 'keyId',
|
||||
userId: testUser,
|
||||
encryptedKey: 'privateKeyArmored'
|
||||
});
|
||||
expect(cryptoStub.deriveKey.calledOnce).to.be.true;
|
||||
expect(cryptoStub.decrypt.calledOnce).to.be.true;
|
||||
expect(pgpStub.getKeyParams.calledOnce).to.be.true;
|
||||
expect(saveLocalPrivateKeyStub.calledOnce).to.be.true;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('upload public key', function() {
|
||||
it('should upload key', function(done) {
|
||||
var keypair = {
|
||||
|
@ -1,239 +1,337 @@
|
||||
'use strict';
|
||||
|
||||
var RestDAO = require('../../../src/js/service/rest'),
|
||||
PrivateKeyDAO = require('../../../src/js/service/privatekey'),
|
||||
appConfig = require('../../../src/js/app-config');
|
||||
var Auth = require('../../../src/js/service/auth'),
|
||||
PrivateKey = require('../../../src/js/service/privatekey'),
|
||||
PGP = require('../../../src/js/crypto/pgp'),
|
||||
Crypto = require('../../../src/js/crypto/crypto'),
|
||||
axe = require('axe-logger'),
|
||||
appConfig = require('../../../src/js/app-config'),
|
||||
util = require('crypto-lib').util,
|
||||
Mailbuild = require('mailbuild'),
|
||||
mailreader = require('mailreader'),
|
||||
ImapClient = require('imap-client');
|
||||
|
||||
describe('Private Key DAO unit tests', function() {
|
||||
|
||||
var privkeyDao, restDaoStub,
|
||||
var privkeyDao, authStub, pgpStub, cryptoStub, imapClientStub,
|
||||
emailAddress = 'test@example.com',
|
||||
deviceName = 'iPhone Work';
|
||||
keyId = '12345',
|
||||
salt = util.random(appConfig.config.symKeySize),
|
||||
iv = util.random(appConfig.config.symIvSize),
|
||||
encryptedPrivateKey = util.random(1024 * 8);
|
||||
|
||||
beforeEach(function() {
|
||||
restDaoStub = sinon.createStubInstance(RestDAO);
|
||||
privkeyDao = new PrivateKeyDAO(restDaoStub, appConfig);
|
||||
authStub = sinon.createStubInstance(Auth);
|
||||
authStub.emailAddress = emailAddress;
|
||||
pgpStub = sinon.createStubInstance(PGP);
|
||||
cryptoStub = sinon.createStubInstance(Crypto);
|
||||
privkeyDao = new PrivateKey(authStub, Mailbuild, mailreader, appConfig, pgpStub, cryptoStub, axe);
|
||||
imapClientStub = sinon.createStubInstance(ImapClient);
|
||||
privkeyDao._imap = imapClientStub;
|
||||
});
|
||||
|
||||
afterEach(function() {});
|
||||
|
||||
describe('requestDeviceRegistration', function() {
|
||||
describe('destroy', function() {
|
||||
it('should work', function(done) {
|
||||
privkeyDao.destroy().then(function() {
|
||||
expect(imapClientStub.login.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('encrypt', function() {
|
||||
it('should fail due to invalid args', function(done) {
|
||||
privkeyDao.requestDeviceRegistration({}).catch(function(err) {
|
||||
expect(err).to.exist;
|
||||
privkeyDao.encrypt().catch(function(err) {
|
||||
expect(err.message).to.match(/Incomplete/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work', function(done) {
|
||||
restDaoStub.post.returns(resolves({
|
||||
encryptedRegSessionKey: 'asdf'
|
||||
cryptoStub.deriveKey.returns(resolves('derivedKey'));
|
||||
pgpStub.exportKeys.returns(resolves({
|
||||
keyId: keyId,
|
||||
privateKeyArmored: 'PGP BLOCK'
|
||||
}));
|
||||
cryptoStub.encrypt.returns(resolves(encryptedPrivateKey));
|
||||
|
||||
privkeyDao.requestDeviceRegistration({
|
||||
userId: emailAddress,
|
||||
deviceName: deviceName
|
||||
}).then(function(sessionKey) {
|
||||
expect(sessionKey).to.exist;
|
||||
privkeyDao.encrypt('asdf').then(function(encryptedKey) {
|
||||
expect(encryptedKey._id).to.equal(keyId);
|
||||
expect(encryptedKey.encryptedPrivateKey).to.equal(encryptedPrivateKey);
|
||||
expect(encryptedKey.salt).to.exist;
|
||||
expect(encryptedKey.iv).to.exist;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadDeviceSecret', function() {
|
||||
it('should fail due to invalid args', function(done) {
|
||||
privkeyDao.uploadDeviceSecret({}).catch(function(err) {
|
||||
expect(err).to.exist;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work', function(done) {
|
||||
restDaoStub.put.returns(resolves());
|
||||
|
||||
privkeyDao.uploadDeviceSecret({
|
||||
userId: emailAddress,
|
||||
deviceName: deviceName,
|
||||
encryptedDeviceSecret: 'asdf',
|
||||
iv: 'iv'
|
||||
}).then(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestAuthSessionKey', function() {
|
||||
it('should fail due to invalid args', function(done) {
|
||||
privkeyDao.requestAuthSessionKey({}).catch(function(err) {
|
||||
expect(err).to.exist;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work', function(done) {
|
||||
restDaoStub.post.withArgs(undefined, '/auth/user/' + emailAddress).returns(resolves());
|
||||
|
||||
privkeyDao.requestAuthSessionKey({
|
||||
userId: emailAddress
|
||||
}).then(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyAuthentication', function() {
|
||||
it('should fail due to invalid args', function(done) {
|
||||
privkeyDao.verifyAuthentication({}).catch(function(err) {
|
||||
expect(err).to.exist;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work', function(done) {
|
||||
var sessionId = '1';
|
||||
|
||||
var options = {
|
||||
userId: emailAddress,
|
||||
sessionId: sessionId,
|
||||
encryptedChallenge: 'asdf',
|
||||
encryptedDeviceSecret: 'qwer',
|
||||
iv: ' iv'
|
||||
};
|
||||
|
||||
restDaoStub.put.withArgs(options, '/auth/user/' + emailAddress + '/session/' + sessionId).returns(resolves());
|
||||
|
||||
privkeyDao.verifyAuthentication(options).then(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('upload', function() {
|
||||
it('should fail due to invalid args', function(done) {
|
||||
privkeyDao.upload({}).catch(function(err) {
|
||||
expect(err).to.exist;
|
||||
expect(err.message).to.match(/Incomplete/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work', function(done) {
|
||||
var options = {
|
||||
_id: '12345',
|
||||
imapClientStub.createFolder.returns(resolves());
|
||||
imapClientStub.uploadMessage.returns(resolves());
|
||||
|
||||
privkeyDao.upload({
|
||||
_id: keyId,
|
||||
userId: emailAddress,
|
||||
encryptedPrivateKey: 'asdf',
|
||||
sessionId: '1',
|
||||
salt: 'salt',
|
||||
iv: 'iv'
|
||||
};
|
||||
|
||||
restDaoStub.post.withArgs(options, '/privatekey/user/' + emailAddress + '/session/' + options.sessionId).returns(resolves());
|
||||
|
||||
privkeyDao.upload(options).then(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestDownload', function() {
|
||||
it('should fail due to invalid args', function(done) {
|
||||
privkeyDao.requestDownload({}).catch(function(err) {
|
||||
expect(err).to.exist;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not find a key', function(done) {
|
||||
var keyId = '12345';
|
||||
|
||||
restDaoStub.get.withArgs({
|
||||
uri: '/privatekey/user/' + emailAddress + '/key/' + keyId
|
||||
}).returns(rejects({
|
||||
code: 404
|
||||
}));
|
||||
|
||||
privkeyDao.requestDownload({
|
||||
userId: emailAddress,
|
||||
keyId: keyId
|
||||
}).then(function(found) {
|
||||
expect(found).to.be.false;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work', function(done) {
|
||||
var keyId = '12345';
|
||||
|
||||
restDaoStub.get.withArgs({
|
||||
uri: '/privatekey/user/' + emailAddress + '/key/' + keyId
|
||||
}).returns(resolves());
|
||||
|
||||
privkeyDao.requestDownload({
|
||||
userId: emailAddress,
|
||||
keyId: keyId
|
||||
}).then(function(found) {
|
||||
expect(found).to.be.true;
|
||||
encryptedPrivateKey: encryptedPrivateKey,
|
||||
salt: salt,
|
||||
iv: iv
|
||||
}).then(function() {
|
||||
expect(imapClientStub.uploadMessage.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasPrivateKey', function() {
|
||||
it('should fail due to invalid args', function(done) {
|
||||
privkeyDao.hasPrivateKey({}).catch(function(err) {
|
||||
expect(err).to.exist;
|
||||
describe('isSynced', function() {
|
||||
beforeEach(function() {
|
||||
sinon.stub(privkeyDao, '_fetchMessage');
|
||||
});
|
||||
afterEach(function() {
|
||||
privkeyDao._fetchMessage.restore();
|
||||
});
|
||||
|
||||
it('should be synced', function(done) {
|
||||
privkeyDao._fetchMessage.returns(resolves({}));
|
||||
|
||||
privkeyDao.isSynced().then(function(synced) {
|
||||
expect(synced).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not find a key', function(done) {
|
||||
var keyId = '12345';
|
||||
it('should not be synced', function(done) {
|
||||
privkeyDao._fetchMessage.returns(resolves());
|
||||
|
||||
restDaoStub.get.withArgs({
|
||||
uri: '/privatekey/user/' + emailAddress + '/key/' + keyId + '?ignoreRecovery=true'
|
||||
}).returns(rejects({
|
||||
code: 404
|
||||
}));
|
||||
|
||||
privkeyDao.hasPrivateKey({
|
||||
userId: emailAddress,
|
||||
keyId: keyId
|
||||
}).then(function(found) {
|
||||
expect(found).to.be.false;
|
||||
privkeyDao.isSynced().then(function(synced) {
|
||||
expect(synced).to.be.false;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work', function(done) {
|
||||
var keyId = '12345';
|
||||
it('should not be synced in case of error', function(done) {
|
||||
privkeyDao._fetchMessage.returns(rejects(new Error()));
|
||||
|
||||
restDaoStub.get.withArgs({
|
||||
uri: '/privatekey/user/' + emailAddress + '/key/' + keyId + '?ignoreRecovery=true'
|
||||
}).returns(resolves());
|
||||
|
||||
privkeyDao.hasPrivateKey({
|
||||
userId: emailAddress,
|
||||
keyId: keyId
|
||||
}).then(function(found) {
|
||||
expect(found).to.be.true;
|
||||
privkeyDao.isSynced().then(function(synced) {
|
||||
expect(synced).to.be.false;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('download', function() {
|
||||
it('should fail due to invalid args', function(done) {
|
||||
privkeyDao.download({}).catch(function(err) {
|
||||
expect(err).to.exist;
|
||||
var base64Content = 'AYzsvV+hGMMT4BIl/XFjbl60BaM5DpDYVNyKPnoZ4ZyW1qy1udkQR7VUeNKJw5v2gWOqc3y6KHkZIqybOVro6e8tzhK1Fvpz+rgmME0tbrrh/Dd6QMBXb9c6ZAzgbLdq0sxftqXO9GoxINAVcfGN/MkcOIhonEjIsLSaYY2WLuGOLp8ZNdgO0tPxfcdd/f1hVXH2JRYmkOwStH3y2uYDmUhEWWeLfP2vF57F4NgtK2Ln4Ypn4VDx1SWtI6E1IMpwchpwXssBwzY2uWKUPNbWEwEYDU6pleWCKphc2YBp0ohJg1HfE+Et9/8wsZtQAjTiigZuovRd5ABd6LkCCuPNenmzKvR5os8fbe9HDsAiDYl5OrA1iGTWVcAKec1OWxRWKn3Ktt/v+W39gxvmA6OOSuPkA3PF+1rY2lU05busVlNVmNmv6vY3LTJz4J/jVPP7Bn6+Wl/BwdGC7OagZCORmDUujk4AaIz5y+x/hgS6g9yY8oaY5EGdFCxRpS7aptqiBNIXIpuxGtKZpP3bmjI4pIcVb4xTA57SFTE7czfvlvTjvBSCQP7MGYCNC+SbDRgt1beyM8uUrKiuLTWK+YJ6rvcIvOIEqvUBDR7ak+9S6+fyxw033vNHfQSAagIUC1eq+c8yoUzvtSRISOMEbu7MnjI5i4AQrD5yfJDJdp5NTpZ0Dz3fW3RVmMhghTGN3ch+6vVwkzO2ik11EGTqwaLfOgZuwunEonXLT4v4fJjIFvsl+hMab0keksuW1G8AQCdkNcgDfxMTIz6S/k51yVIGE2DZo1e1LTc7pu8gOCNHtuNMuwzDTZuutWdd0P93ZL7W6j1eq33DShX2zeuxk5S28crn6DdlK5QBYMSpECU1JDKRu1QMBNtiEgGlJaVOi1AQ+cDdZKthMYfJ0MPHCeRyQFMpEYkYUBfhBMnGaiDTmDFMvEy0WGhabKChhtdBF/rlyug8Kx9M60lx1t9dYVbxSmWkqWgUZ36vRwQXPVEWWmRROHtG/V9+CSPCCa9heDDqqj8nKzL0vK9kBG8nh2XlPAVg7ICicTLw0u93pz4US3pRKwfMys1mQNV0z0k0uXB1zJZqDrIsCihcUMC2vFOXg+dNlanPXeP8AMp3ojuMPAClIt+bxTyrjZ7MV0mkDuaWUaeEq2xuaU5cKlG1Aam6vSb3jmURgEzOk1onlkGrCfVUTne18W8V6KL7iG+lX+331baiZVGoMUXT++0T05KYBdTRYL0OZ0P3OPRqilPpxaZCY0NG5rJxC5ij0vnu9ECAvN3xSdiRF7SobVSVFIdc32aY24nLKv8/gSnROgmQbAqeCMOz0bULRyVTe0lzSXBcCgu5gK+KEo8p38trTSJ/S95sKQnyNbrnz2QOIXvzxLrL6/nnC/4pwxXKZ6XqB/2zLVfiJRjUQ1NUC0xDXA==';
|
||||
var root = [{
|
||||
type: 'attachment',
|
||||
content: util.binStr2Uint8Arr(util.base642Str(base64Content))
|
||||
}];
|
||||
|
||||
beforeEach(function() {
|
||||
sinon.stub(privkeyDao, '_fetchMessage');
|
||||
sinon.stub(privkeyDao, '_parse');
|
||||
});
|
||||
afterEach(function() {
|
||||
privkeyDao._fetchMessage.restore();
|
||||
privkeyDao._parse.restore();
|
||||
});
|
||||
|
||||
it('should fail if key not synced', function(done) {
|
||||
privkeyDao._fetchMessage.returns(resolves());
|
||||
|
||||
privkeyDao.download({
|
||||
userId: emailAddress,
|
||||
keyId: keyId
|
||||
}).catch(function(err) {
|
||||
expect(err.message).to.match(/not synced/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work', function(done) {
|
||||
var key = {
|
||||
_id: '12345'
|
||||
};
|
||||
|
||||
restDaoStub.get.withArgs({
|
||||
uri: '/privatekey/user/' + emailAddress + '/key/' + key._id + '/recovery/token'
|
||||
}).returns(resolves());
|
||||
privkeyDao._fetchMessage.returns(resolves({}));
|
||||
imapClientStub.getBodyParts.returns(resolves());
|
||||
privkeyDao._parse.returns(resolves(root));
|
||||
|
||||
privkeyDao.download({
|
||||
userId: emailAddress,
|
||||
keyId: key._id,
|
||||
recoveryToken: 'token'
|
||||
}).then(done);
|
||||
keyId: keyId
|
||||
}).then(function(privkey) {
|
||||
expect(privkey._id).to.equal(keyId);
|
||||
expect(privkey.userId).to.equal(emailAddress);
|
||||
expect(privkey.encryptedPrivateKey).to.exist;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('decrypt', function() {
|
||||
it('should fail due to invalid args', function(done) {
|
||||
privkeyDao.decrypt({}).catch(function(err) {
|
||||
expect(err.message).to.match(/Incomplete/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail for invalid code', function(done) {
|
||||
cryptoStub.deriveKey.returns(resolves('derivedKey'));
|
||||
cryptoStub.decrypt.returns(rejects(new Error()));
|
||||
|
||||
privkeyDao.decrypt({
|
||||
_id: keyId,
|
||||
userId: emailAddress,
|
||||
code: 'asdf',
|
||||
encryptedPrivateKey: encryptedPrivateKey,
|
||||
salt: salt,
|
||||
iv: iv
|
||||
}).catch(function(err) {
|
||||
expect(err.message).to.match(/Invalid/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail for invalid key params', function(done) {
|
||||
cryptoStub.deriveKey.returns(resolves('derivedKey'));
|
||||
cryptoStub.decrypt.returns(resolves('PGP BLOCK'));
|
||||
pgpStub.getKeyParams.returns({
|
||||
_id: '7890',
|
||||
userId: emailAddress
|
||||
});
|
||||
|
||||
privkeyDao.decrypt({
|
||||
_id: keyId,
|
||||
userId: emailAddress,
|
||||
code: 'asdf',
|
||||
encryptedPrivateKey: encryptedPrivateKey,
|
||||
salt: salt,
|
||||
iv: iv
|
||||
}).catch(function(err) {
|
||||
expect(err.message).to.match(/key parameters/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work', function(done) {
|
||||
cryptoStub.deriveKey.returns(resolves('derivedKey'));
|
||||
cryptoStub.decrypt.returns(resolves('PGP BLOCK'));
|
||||
pgpStub.getKeyParams.returns({
|
||||
_id: keyId,
|
||||
userId: emailAddress
|
||||
});
|
||||
|
||||
privkeyDao.decrypt({
|
||||
_id: keyId,
|
||||
userId: emailAddress,
|
||||
code: 'asdf',
|
||||
encryptedPrivateKey: encryptedPrivateKey,
|
||||
salt: salt,
|
||||
iv: iv
|
||||
}).then(function(privkey) {
|
||||
expect(privkey._id).to.equal(keyId);
|
||||
expect(privkey.userId).to.equal(emailAddress);
|
||||
expect(privkey.encryptedKey).to.equal('PGP BLOCK');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('_fetchMessage', function() {
|
||||
it('should fail due to invalid args', function(done) {
|
||||
privkeyDao._fetchMessage({}).catch(function(err) {
|
||||
expect(err.message).to.match(/Incomplete/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail if imap folder does not exist', function(done) {
|
||||
imapClientStub.listMessages.returns(rejects(new Error()));
|
||||
|
||||
privkeyDao._fetchMessage({
|
||||
userId: emailAddress,
|
||||
keyId: keyId
|
||||
}).catch(function(err) {
|
||||
expect(err.message).to.match(/Imap folder/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work', function(done) {
|
||||
imapClientStub.listMessages.returns(resolves([{
|
||||
subject: keyId
|
||||
}]));
|
||||
|
||||
privkeyDao._fetchMessage({
|
||||
userId: emailAddress,
|
||||
keyId: keyId
|
||||
}).then(function(msg) {
|
||||
expect(msg.subject).to.equal(keyId);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work for not matching message', function(done) {
|
||||
imapClientStub.listMessages.returns(resolves([{
|
||||
subject: '7890'
|
||||
}]));
|
||||
|
||||
privkeyDao._fetchMessage({
|
||||
userId: emailAddress,
|
||||
keyId: keyId
|
||||
}).then(function(msg) {
|
||||
expect(msg).to.not.exist;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work for no messages', function(done) {
|
||||
imapClientStub.listMessages.returns(resolves([]));
|
||||
|
||||
privkeyDao._fetchMessage({
|
||||
userId: emailAddress,
|
||||
keyId: keyId
|
||||
}).then(function(msg) {
|
||||
expect(msg).to.not.exist;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('_parse', function() {
|
||||
var root = {
|
||||
foo: 'bar'
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
sinon.stub(mailreader, 'parse');
|
||||
});
|
||||
afterEach(function() {
|
||||
mailreader.parse.restore();
|
||||
});
|
||||
|
||||
it('should fail', function(done) {
|
||||
mailreader.parse.yields(new Error('asdf'));
|
||||
|
||||
privkeyDao._parse().catch(function(err) {
|
||||
expect(err.message).to.match(/asdf/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work', function(done) {
|
||||
mailreader.parse.yields(null, root);
|
||||
|
||||
privkeyDao._parse().then(function(res) {
|
||||
expect(res).to.equal(root);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user