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

Implement encrypted private key imap sync
master
Tankred Hase 8 years ago
commit c9981239c8

@ -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": "~0.12.0",
"jquery": "~2.1.1",
"mailbuild": "^0.3.7",
"mailreader": "~0.4.0",
"mocha": "^1.21.4",
"ng-infinite-scroll": "~1.1.2",
@ -75,4 +76,4 @@
"time-grunt": "^1.0.0",
"wo-smtpclient": "~0.6.0"
}
}
}

@ -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,45 @@ var NavigationCtrl = function($scope, $location, $q, account, email, outbox, not
// start checking outbox periodically
outbox.startChecking($scope.onOutboxUpdate);
}
$scope.checkKeySyncStatus = function() {
return $q(function(resolve) {
resolve();
}).then(function() {
// login to imap
return privateKey.init();
}).then(function() {
// check key sync status
return privateKey.isSynced();
}).then(function(synced) {
if (!synced) {
dialog.confirm({
title: 'Key backup',
message: 'Your private key is not backed up. Back up now?',
positiveBtnStr: 'Backup',
negativeBtnStr: 'Not now',
showNegativeBtn: true,
callback: function(granted) {
if (granted) {
// logout of the current session
email.onDisconnect().then(function() {
// send to key upload screen
$timeout(function() {
$location.path('/login-privatekey-upload');
});
});
}
}
});
}
// logout of imap
return privateKey.destroy();
}).catch(axe.error);
};
};
module.exports = NavigationCtrl;

@ -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,54 +21,38 @@ var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams, $q,
$scope.errMsg = undefined;
resolve();
}).then(function() {
// login to imap
return privateKey.init();
}).then(function() {
// get public key id for reference
return keychain.getUserKeyPair(userId);
}).then(function(keypair) {
// remember for storage later
$scope.cachedKeypair = keypair;
return keychain.downloadPrivateKey({
cachedKeypair = keypair;
return privateKey.download({
userId: userId,
keyId: keypair.publicKey._id,
recoveryToken: $scope.recoveryToken.toUpperCase()
keyId: keypair.publicKey._id
});
}).then(function(encryptedPrivateKey) {
$scope.encryptedPrivateKey = encryptedPrivateKey;
$scope.busy = false;
$scope.step++;
}).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();
}).then(function(encryptedKey) {
// set decryption code
encryptedKey.code = $scope.code.toUpperCase();
// decrypt the downloaded encrypted private key
return privateKey.decrypt(encryptedKey);
return $q(function(resolve) {
$scope.busy = true;
$scope.errMsg = undefined;
resolve();
}).then(function(privkey) {
// add private key to cached keypair object
cachedKeypair.privateKey = privkey;
// store the decrypted private key locally
return keychain.putUserKeyPair(cachedKeypair);
}).then(function() {
return keychain.decryptAndStorePrivateKeyLocally(options);
}).then(function(privateKey) {
// add private key to cached keypair object
$scope.cachedKeypair.privateKey = privateKey;
// try empty passphrase
return email.unlock({
keypair: $scope.cachedKeypair,
keypair: cachedKeypair,
passphrase: undefined
}).catch(function(err) {
// passphrase incorrct ... go to passphrase login screen
@ -81,6 +64,10 @@ var LoginPrivateKeyDownloadCtrl = function($scope, $location, $routeParams, $q,
// passphrase is corrent ...
return auth.storeCredentials();
}).then(function() {
// logout of imap
return privateKey.destroy();
}).then(function() {
// continue to main app
$scope.goTo('/account');

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

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

@ -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';
//
// Device registration functions
//
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;
}
/**
* Request registration of a new device by fetching registration session key.
* @param {String} options.userId The user's email address
* @param {String} options.deviceName The device's memorable name
* @return {Object} {encryptedRegSessionKey:[base64]}
* Configure the local imap client used for key-sync with credentials from the auth module.
*/
PrivateKey.prototype.requestDeviceRegistration = function(options) {
PrivateKey.prototype.init = function() {
var self = this;
return new Promise(function(resolve) {
if (!options.userId || !options.deviceName) {
throw new Error('Incomplete arguments!');
}
resolve();
}).then(function() {
var uri = '/device/user/' + options.userId + '/devicename/' + options.deviceName;
return self._restDao.post(undefined, uri);
return self._auth.getCredentials().then(function(credentials) {
// tls socket worker path for multithreaded tls in non-native tls environments
credentials.imap.tlsWorkerPath = self._appConfig.config.workerPath + '/tcp-socket-tls-worker.min.js';
self._imap = new ImapClient(credentials.imap);
self._imap.onError = self._axe.error;
// login to the imap server
return self._imap.login();
});
};
/**
* Authenticate device registration by uploading the deviceSecret encrypted with the regSessionKeys.
* @param {String} options.userId The user's email address
* @param {String} options.deviceName The device's memorable name
* @param {String} options.encryptedDeviceSecret The base64 encoded encrypted device secret
* @param {String} options.iv The iv used for encryption
* Cleanup by logging out of the imap client.
*/
PrivateKey.prototype.uploadDeviceSecret = function(options) {
var self = this;
PrivateKey.prototype.destroy = function() {
this._imap.logout();
// don't wait for logout to complete
return new Promise(function(resolve) {
if (!options.userId || !options.deviceName || !options.encryptedDeviceSecret || !options.iv) {
throw new Error('Incomplete arguments!');
}
resolve();
}).then(function() {
var uri = '/device/user/' + options.userId + '/devicename/' + options.deviceName;
return self._restDao.put(options, uri);
});
};
//
// Private key functions
//
/**
* Request authSessionKeys required for upload the encrypted private PGP key.
* @param {String} options.userId The user's email address
* @return {Object} {sessionId, encryptedAuthSessionKey:[base64 encoded], encryptedChallenge:[base64 encoded]}
* 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.requestAuthSessionKey = function(options) {
var self = this;
return new Promise(function(resolve) {
if (!options.userId) {
PrivateKey.prototype.encrypt = function(code) {
var self = this,
config = self._appConfig.config,
keySize = config.symKeySize,
encryptionKey, salt, iv, privkeyId;
if (!code) {
return new Promise(function() {
throw new Error('Incomplete arguments!');
}
resolve();
});
}
}).then(function() {
var uri = '/auth/user/' + options.userId;
return self._restDao.post(undefined, uri);
});
};
// generate random salt and iv
salt = util.random(keySize);
iv = util.random(config.symIvSize);
/**
* 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
*/
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();
// derive key from the code using PBKDF2
return self._crypto.deriveKey(code, salt, keySize).then(function(key) {
encryptionKey = key;
}).then(function() {
var uri = '/auth/user/' + options.userId + '/session/' + options.sessionId;
return self._restDao.put(options, uri);
// get private key from local storage
return self._pgp.exportKeys();
}).then(function(keypair) {
privkeyId = keypair.keyId;
// encrypt the private key with the derived key
return self._crypto.encrypt(keypair.privateKeyArmored, encryptionKey, iv);
}).then(function(ct) {
return {
_id: privkeyId,
encryptedPrivateKey: ct,
salt: salt,
iv: iv
};
});
};
@ -102,104 +95,246 @@ PrivateKey.prototype.verifyAuthentication = function(options) {
* @param {String} options._id The hex encoded capital 16 char key id
* @param {String} options.userId The user's email address
* @param {String} options.encryptedPrivateKey The base64 encoded encrypted private PGP key
* @param {String} options.sessionId The session id
*/
PrivateKey.prototype.upload = function(options) {
var self = this;
return new Promise(function(resolve) {
if (!options._id || !options.userId || !options.encryptedPrivateKey || !options.sessionId || !options.salt || !options.iv) {
if (!options._id || !options.userId || !options.encryptedPrivateKey || !options.salt || !options.iv) {
throw new Error('Incomplete arguments!');
}
resolve();
}).then(function() {
var uri = '/privatekey/user/' + options.userId + '/session/' + options.sessionId;
return self._restDao.post(options, uri);
// create imap folder
return self._imap.createFolder({
path: IMAP_KEYS_FOLDER
}).then(function() {
self._axe.debug('Successfully created imap folder ' + IMAP_KEYS_FOLDER);
}).catch(function(err) {
var prettyErr = new Error('Creating imap folder ' + IMAP_KEYS_FOLDER + ' failed: ' + err.message);
self._axe.error(prettyErr);
throw prettyErr;
});
}).then(createMessage).then(function(message) {
// upload to imap folder
return self._imap.uploadMessage({
path: IMAP_KEYS_FOLDER,
message: message
});
});
function createMessage() {
var encryptedKeyBuf = util.binStr2Uint8Arr(util.base642Str(options.encryptedPrivateKey));
var saltBuf = util.binStr2Uint8Arr(util.base642Str(options.salt));
var ivBuf = util.binStr2Uint8Arr(util.base642Str(options.iv));
// allocate payload buffer for sync
var payloadBuf = new Uint8Array(1 + saltBuf.length + ivBuf.length + encryptedKeyBuf.length);
var offset = 0;
// set version byte
payloadBuf[offset] = 0x01; // version 1 of the key-sync protocol
offset++;
// copy salt bytes
payloadBuf.set(saltBuf, offset);
offset += saltBuf.length;
// copy iv bytes
payloadBuf.set(ivBuf, offset);
offset += ivBuf.length;
// copy encrypted key bytes
payloadBuf.set(encryptedKeyBuf, offset);
// create MIME tree
var rootNode = options.rootNode || new self._Mailbuild();
rootNode.setHeader({
subject: options._id,
from: options.userId,
to: options.userId,
'content-type': MIME_TYPE + '; charset=us-ascii',
'content-transfer-encoding': 'base64'
});
rootNode.setContent(payloadBuf);
return rootNode.build();
}
};
/**
* 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.
* Check if matching private key is stored in IMAP.
*/
PrivateKey.prototype.hasPrivateKey = function(options) {
var self = this;
return new Promise(function(resolve) {
if (!options.userId || !options.keyId) {
throw new Error('Incomplete arguments!');
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.
* @param {String} options.userId The user's email address
* @param {String} options.keyId The private key id
* @return {Object} {_id:[hex encoded capital 16 char key id], encryptedPrivateKey:[base64 encoded], encryptedUserId: [base64 encoded]}
*/
PrivateKey.prototype.download = function(options) {
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() {
return self._restDao.get({
uri: '/privatekey/user/' + options.userId + '/key/' + options.keyId + '?ignoreRecovery=true',
// get the body for the message
return self._imap.getBodyParts({
path: IMAP_KEYS_FOLDER,
uid: message.uid,
bodyParts: message.bodyParts
});
}).then(function() {
return true;
// 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;
}).catch(function(err) {
// 404: there is no encrypted private key on the server
if (err.code && err.code !== 200) {
return false;
// 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);
throw err;
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))
};
});
};
/**
* 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.
* 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.requestDownload = function(options) {
var self = this;
return new Promise(function(resolve) {
if (!options.userId || !options.keyId) {
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!');
}
resolve();