mirror of
https://github.com/moparisthebest/mail
synced 2024-12-21 23:08:50 -05:00
[WO-860] Introduce publickey-verifier
This commit is contained in:
parent
0304bbf8fe
commit
1d4a9414bb
@ -178,6 +178,7 @@ module.exports = function(grunt) {
|
||||
'test/unit/service/newsletter-service-test.js',
|
||||
'test/unit/service/mail-config-service-test.js',
|
||||
'test/unit/service/invitation-dao-test.js',
|
||||
'test/unit/service/publickey-verifier-test.js',
|
||||
'test/unit/email/outbox-bo-test.js',
|
||||
'test/unit/email/email-dao-test.js',
|
||||
'test/unit/email/account-test.js',
|
||||
@ -189,6 +190,7 @@ 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-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',
|
||||
@ -210,7 +212,8 @@ module.exports = function(grunt) {
|
||||
files: {
|
||||
'test/integration/index.browserified.js': [
|
||||
'test/main.js',
|
||||
'test/integration/email-dao-test.js'
|
||||
'test/integration/email-dao-test.js',
|
||||
'test/integration/publickey-verifier-test.js'
|
||||
]
|
||||
},
|
||||
options: browserifyOpt
|
||||
|
@ -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-verify-public-key', {
|
||||
templateUrl: 'tpl/login-verify-public-key.html',
|
||||
controller: require('./controller/login/login-verify-public-key')
|
||||
});
|
||||
$routeProvider.when('/login-existing', {
|
||||
templateUrl: 'tpl/login-existing.html',
|
||||
controller: require('./controller/login/login-existing')
|
||||
|
@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
var LoginInitialCtrl = function($scope, $location, $routeParams, $q, newsletter, email, auth) {
|
||||
var LoginInitialCtrl = function($scope, $location, $routeParams, $q, newsletter, email, auth, publickeyVerifier) {
|
||||
!$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
|
||||
|
||||
var emailAddress = auth.emailAddress;
|
||||
@ -60,13 +60,10 @@ var LoginInitialCtrl = function($scope, $location, $routeParams, $q, newsletter,
|
||||
passphrase: undefined
|
||||
});
|
||||
|
||||
}).then(function() {
|
||||
// persist credentials locally
|
||||
return auth.storeCredentials();
|
||||
|
||||
}).then(function() {
|
||||
// go to main account screen
|
||||
$location.path('/account');
|
||||
}).then(function(keypair) {
|
||||
// go to public key verification
|
||||
publickeyVerifier.keypair = keypair;
|
||||
$location.path('/login-verify-public-key');
|
||||
|
||||
}).catch(displayError);
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, auth, pgp, keychain) {
|
||||
var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, auth, pgp, keychain, publickeyVerifier) {
|
||||
!$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
|
||||
|
||||
$scope.incorrect = false;
|
||||
@ -12,6 +12,7 @@ var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, aut
|
||||
}
|
||||
|
||||
var userId = auth.emailAddress,
|
||||
pubKeyNeedsVerification = false,
|
||||
keypair;
|
||||
|
||||
return $q(function(resolve) {
|
||||
@ -61,6 +62,7 @@ var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, aut
|
||||
userIds: pubKeyParams.userIds,
|
||||
publicKey: $scope.key.publicKeyArmored
|
||||
};
|
||||
pubKeyNeedsVerification = true; // this public key needs to be authenticated
|
||||
}
|
||||
|
||||
// import and validate keypair
|
||||
@ -72,17 +74,21 @@ var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, aut
|
||||
throw err;
|
||||
});
|
||||
|
||||
}).then(function() {
|
||||
// perist keys locally
|
||||
return keychain.putUserKeyPair(keypair);
|
||||
}).then(function(keypair) {
|
||||
if (!pubKeyNeedsVerification) {
|
||||
// persist credentials and key and go to main account screen
|
||||
return keychain.putUserKeyPair(keypair).then(function() {
|
||||
return auth.storeCredentials();
|
||||
}).then(function() {
|
||||
$location.path('/account');
|
||||
});
|
||||
}
|
||||
|
||||
}).then(function() {
|
||||
// persist credentials locally
|
||||
return auth.storeCredentials();
|
||||
|
||||
}).then(function() {
|
||||
// go to main account screen
|
||||
$location.path('/account');
|
||||
// go to public key verification
|
||||
publickeyVerifier.keypair = keypair;
|
||||
return keychain.uploadPublicKey(keypair.publicKey).then(function() {
|
||||
$location.path('/login-verify-public-key');
|
||||
});
|
||||
|
||||
}).catch(displayError);
|
||||
};
|
||||
|
100
src/js/controller/login/login-verify-public-key.js
Normal file
100
src/js/controller/login/login-verify-public-key.js
Normal file
@ -0,0 +1,100 @@
|
||||
'use strict';
|
||||
|
||||
var RETRY_INTERVAL = 10000;
|
||||
|
||||
var PublicKeyVerifierCtrl = function($scope, $location, $q, $timeout, $interval, auth, publickeyVerifier, keychain) {
|
||||
$scope.retries = 0;
|
||||
|
||||
/**
|
||||
* Runs a verification attempt
|
||||
*/
|
||||
$scope.verify = function() {
|
||||
if ($scope.busy) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.busy = true;
|
||||
disarmTimeouts();
|
||||
|
||||
return $q(function(resolve) {
|
||||
// updates the GUI
|
||||
resolve();
|
||||
|
||||
}).then(function() {
|
||||
// pre-flight check: is there already a public key for the user?
|
||||
return keychain.getUserKeyPair(auth.emailAddress);
|
||||
|
||||
}).then(function(keypair) {
|
||||
if (!keypair || !keypair.publicKey) {
|
||||
// no pubkey, need to do the roundtrip
|
||||
return verifyImap();
|
||||
}
|
||||
|
||||
// pubkey has already been verified, we're done here
|
||||
return success();
|
||||
|
||||
}).catch(function(error) {
|
||||
$scope.busy = false;
|
||||
$scope.errMsg = error.message; // display error
|
||||
|
||||
scheduleVerification(); // schedule next verification attempt
|
||||
});
|
||||
|
||||
function verifyImap() {
|
||||
// retrieve the credentials
|
||||
return auth.getCredentials().then(function(credentials) {
|
||||
return publickeyVerifier.configure(credentials); // configure imap
|
||||
|
||||
}).then(function() {
|
||||
return publickeyVerifier.verify(); // connect to imap to look for the message
|
||||
|
||||
}).then(function() {
|
||||
return success();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function success() {
|
||||
return $q(function(resolve) {
|
||||
resolve();
|
||||
|
||||
}).then(function() {
|
||||
// persist keypair
|
||||
return publickeyVerifier.persistKeypair();
|
||||
|
||||
}).then(function() {
|
||||
// persist credentials locally (needs private key to encrypt imap password)
|
||||
return auth.storeCredentials();
|
||||
|
||||
}).then(function() {
|
||||
$location.path('/account'); // go to main account screen
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* schedules next verification attempt in RETRY_INTERVAL ms (scope.timeout)
|
||||
* and sets up a countdown timer for the ui (scope.countdown)
|
||||
*/
|
||||
function scheduleVerification() {
|
||||
$scope.timeout = setTimeout($scope.verify, RETRY_INTERVAL);
|
||||
|
||||
// shows the countdown timer, decrements each second
|
||||
$scope.countdown = RETRY_INTERVAL / 1000;
|
||||
$scope.countdownDecrement = setInterval(function() {
|
||||
if ($scope.countdown > 0) {
|
||||
$timeout(function() {
|
||||
$scope.countdown--;
|
||||
}, 0);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function disarmTimeouts() {
|
||||
clearTimeout($scope.timeout);
|
||||
clearInterval($scope.countdownDecrement);
|
||||
}
|
||||
|
||||
scheduleVerification();
|
||||
};
|
||||
|
||||
module.exports = PublicKeyVerifierCtrl;
|
@ -122,7 +122,7 @@ Email.prototype.unlock = function(options) {
|
||||
|
||||
}).then(function() {
|
||||
// persist newly generated keypair
|
||||
var newKeypair = {
|
||||
return {
|
||||
publicKey: {
|
||||
_id: generatedKeypair.keyId,
|
||||
userId: self._account.emailAddress,
|
||||
@ -135,8 +135,6 @@ Email.prototype.unlock = function(options) {
|
||||
}
|
||||
};
|
||||
|
||||
return self._keychain.putUserKeyPair(newKeypair);
|
||||
|
||||
}).then(setPrivateKey);
|
||||
|
||||
function handleExistingKeypair(keypair) {
|
||||
@ -160,6 +158,7 @@ Email.prototype.unlock = function(options) {
|
||||
if (!matchingPrivUserId || !matchingPubUserId || keypair.privateKey.userId !== self._account.emailAddress || keypair.publicKey.userId !== self._account.emailAddress) {
|
||||
throw new Error('User IDs dont match!');
|
||||
}
|
||||
|
||||
resolve();
|
||||
|
||||
}).then(function() {
|
||||
@ -168,14 +167,17 @@ Email.prototype.unlock = function(options) {
|
||||
passphrase: options.passphrase,
|
||||
privateKeyArmored: keypair.privateKey.encryptedKey,
|
||||
publicKeyArmored: keypair.publicKey.publicKey
|
||||
}).then(function() {
|
||||
return keypair;
|
||||
});
|
||||
|
||||
}).then(setPrivateKey);
|
||||
}
|
||||
|
||||
function setPrivateKey() {
|
||||
function setPrivateKey(keypair) {
|
||||
// set decrypted privateKey to pgpMailer
|
||||
self._pgpbuilder._privateKey = self._pgp._privateKey;
|
||||
return keypair;
|
||||
}
|
||||
};
|
||||
|
||||
@ -259,15 +261,11 @@ Email.prototype.refreshFolder = function(options) {
|
||||
/**
|
||||
* Fetches a message's headers from IMAP.
|
||||
*
|
||||
* NB! If we fetch a message whose subject line correspond's to that of a verification message,
|
||||
* we try to verify that, and if that worked, we delete the verified message from IMAP.
|
||||
*
|
||||
* @param {Object} options.folder The folder for which to fetch the message
|
||||
*/
|
||||
Email.prototype.fetchMessages = function(options) {
|
||||
var self = this,
|
||||
folder = options.folder,
|
||||
messages;
|
||||
folder = options.folder;
|
||||
|
||||
self.busy();
|
||||
|
||||
@ -279,42 +277,18 @@ Email.prototype.fetchMessages = function(options) {
|
||||
// list the messages starting from the lowest new uid to the highest new uid
|
||||
return self._imapListMessages(options);
|
||||
|
||||
}).then(function(msgs) {
|
||||
messages = msgs;
|
||||
// if there are verification messages in the synced messages, handle it
|
||||
var verificationMessages = _.filter(messages, function(message) {
|
||||
return message.subject === str.verificationSubject;
|
||||
});
|
||||
|
||||
// if there are verification messages, continue after we've tried to verify
|
||||
if (verificationMessages.length > 0) {
|
||||
var jobs = [];
|
||||
verificationMessages.forEach(function(verificationMessage) {
|
||||
var promise = handleVerification(verificationMessage).then(function() {
|
||||
// if verification worked, we remove the mail from the list.
|
||||
messages.splice(messages.indexOf(verificationMessage), 1);
|
||||
}).catch(function() {
|
||||
// if it was NOT a valid verification mail, do nothing
|
||||
// if an error occurred and the mail was a valid verification mail,
|
||||
// keep the mail in the list so the user can see it and verify manually
|
||||
});
|
||||
jobs.push(promise);
|
||||
});
|
||||
return Promise.all(jobs);
|
||||
}
|
||||
|
||||
}).then(function() {
|
||||
}).then(function(messages) {
|
||||
if (_.isEmpty(messages)) {
|
||||
// nothing to do, we're done here
|
||||
return;
|
||||
}
|
||||
|
||||
// persist the encrypted message to the local storage
|
||||
// persist the messages to the local storage
|
||||
return self._localStoreMessages({
|
||||
folder: folder,
|
||||
emails: messages
|
||||
}).then(function() {
|
||||
// this enables us to already show the attachment clip in the message list ui
|
||||
// show the attachment clip in the message list ui
|
||||
messages.forEach(function(message) {
|
||||
message.attachments = message.bodyParts.filter(function(bodyPart) {
|
||||
return bodyPart.type === MSG_PART_TYPE_ATTACHMENT;
|
||||
@ -338,40 +312,6 @@ Email.prototype.fetchMessages = function(options) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// Handles verification of public keys, deletion of messages with verified keys
|
||||
function handleVerification(message) {
|
||||
return self._getBodyParts({
|
||||
folder: folder,
|
||||
uid: message.uid,
|
||||
bodyParts: message.bodyParts
|
||||
}).then(function(parsedBodyParts) {
|
||||
var body = _.pluck(filterBodyParts(parsedBodyParts, MSG_PART_TYPE_TEXT), MSG_PART_ATTR_CONTENT).join('\n'),
|
||||
verificationUrlPrefix = config.keyServerUrl + config.verificationUrl,
|
||||
uuid = body.split(verificationUrlPrefix).pop().substr(0, config.verificationUuidLength),
|
||||
uuidRegex = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/;
|
||||
|
||||
// there's no valid uuid in the message, so forget about it
|
||||
if (!uuidRegex.test(uuid)) {
|
||||
throw new Error('No public key verifier found!');
|
||||
}
|
||||
|
||||
// there's a valid uuid in the message, so try to verify it
|
||||
return self._keychain.verifyPublicKey(uuid).catch(function(err) {
|
||||
throw new Error('Verifying your public key failed: ' + err.message);
|
||||
});
|
||||
|
||||
}).then(function() {
|
||||
// public key has been verified, delete the message
|
||||
return self._imapDeleteMessage({
|
||||
folder: folder,
|
||||
uid: message.uid
|
||||
}).catch(function() {
|
||||
// if we could successfully not delete the message or not doesn't matter.
|
||||
// just don't show it in whiteout and keep quiet about it
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@ -1573,8 +1513,8 @@ Email.prototype._getBodyParts = function(options) {
|
||||
return self._imapClient.getBodyParts(options);
|
||||
}).then(function() {
|
||||
if (options.bodyParts.filter(function(bodyPart) {
|
||||
return !(bodyPart.raw || bodyPart.content);
|
||||
}).length) {
|
||||
return !(bodyPart.raw || bodyPart.content);
|
||||
}).length) {
|
||||
var error = new Error('Can not get the contents of this message. It has already been deleted!');
|
||||
error.hide = true;
|
||||
throw error;
|
||||
|
@ -14,4 +14,5 @@ require('./admin');
|
||||
require('./lawnchair');
|
||||
require('./devicestorage');
|
||||
require('./auth');
|
||||
require('./keychain');
|
||||
require('./keychain');
|
||||
require('./publickey-verifier');
|
@ -659,7 +659,7 @@ Keychain.prototype.putUserKeyPair = function(keypair) {
|
||||
// validate input
|
||||
if (!keypair || !keypair.publicKey || !keypair.privateKey || !keypair.publicKey.userId || keypair.publicKey.userId !== keypair.privateKey.userId) {
|
||||
return new Promise(function() {
|
||||
throw new Error('Incorrect input!');
|
||||
throw new Error('Cannot put user key pair: Incorrect input!');
|
||||
});
|
||||
}
|
||||
|
||||
@ -676,6 +676,24 @@ Keychain.prototype.putUserKeyPair = function(keypair) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Uploads the public key
|
||||
* @param {Object} publicKey The user's public key
|
||||
* @return {Promise}
|
||||
*/
|
||||
Keychain.prototype.uploadPublicKey = function(publicKey) {
|
||||
var self = this;
|
||||
|
||||
// validate input
|
||||
if (!publicKey || !publicKey.userId || !publicKey.publicKey) {
|
||||
return new Promise(function() {
|
||||
throw new Error('Cannot upload user key pair: Incorrect input!');
|
||||
});
|
||||
}
|
||||
|
||||
return self._publicKeyDao.put(publicKey);
|
||||
};
|
||||
|
||||
//
|
||||
// Helper functions
|
||||
//
|
||||
|
233
src/js/service/publickey-verifier.js
Normal file
233
src/js/service/publickey-verifier.js
Normal file
@ -0,0 +1,233 @@
|
||||
'use strict';
|
||||
|
||||
var MSG_PART_ATTR_CONTENT = 'content';
|
||||
var MSG_PART_TYPE_TEXT = 'text';
|
||||
|
||||
var ngModule = angular.module('woServices');
|
||||
ngModule.service('publickeyVerifier', PublickeyVerifier);
|
||||
module.exports = PublickeyVerifier;
|
||||
|
||||
var ImapClient = require('imap-client');
|
||||
|
||||
function PublickeyVerifier(auth, appConfig, mailreader, keychain) {
|
||||
this._appConfig = appConfig;
|
||||
this._mailreader = mailreader;
|
||||
this._keychain = keychain;
|
||||
this._auth = auth;
|
||||
this._workerPath = appConfig.config.workerPath + '/tcp-socket-tls-worker.min.js';
|
||||
this._keyServerUrl = this._appConfig.config.keyServerUrl;
|
||||
}
|
||||
|
||||
//
|
||||
// Public API
|
||||
//
|
||||
|
||||
PublickeyVerifier.prototype.configure = function() {
|
||||
var self = this;
|
||||
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
||||
PublickeyVerifier.prototype.persistKeypair = function() {
|
||||
return this._keychain.putUserKeyPair(this.keypair);
|
||||
};
|
||||
|
||||
PublickeyVerifier.prototype.verify = function() {
|
||||
var self = this,
|
||||
verificationSuccessful = false;
|
||||
|
||||
// have to wrap it in a promise to catch .onError of imap-client
|
||||
return new Promise(function(resolve, reject) {
|
||||
self._imap.onError = reject;
|
||||
|
||||
// login
|
||||
self._imap.login().then(function() {
|
||||
// list folders
|
||||
return self._imap.listWellKnownFolders();
|
||||
}).then(function(wellKnownFolders) {
|
||||
var paths = []; // gathers paths
|
||||
|
||||
// extract the paths from the folder arrays
|
||||
for (var folderType in wellKnownFolders) {
|
||||
if (wellKnownFolders.hasOwnProperty(folderType) && Array.isArray(wellKnownFolders[folderType])) {
|
||||
paths = paths.concat(_.pluck(wellKnownFolders[folderType], 'path'));
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
|
||||
}).then(function(paths) {
|
||||
return self._searchAll(paths); // search
|
||||
|
||||
}).then(function(candidates) {
|
||||
if (!candidates.length) {
|
||||
// nothing here to potentially verify
|
||||
verificationSuccessful = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// verify everything that looks like a verification mail
|
||||
return self._verifyAll(candidates).then(function(success) {
|
||||
verificationSuccessful = success;
|
||||
});
|
||||
|
||||
}).then(function() {
|
||||
// at this point, we don't care about errors anymore
|
||||
self._imap.onError = function() {};
|
||||
self._imap.logout();
|
||||
|
||||
if (!verificationSuccessful) {
|
||||
// nothing unexpected went wrong, but no public key could be verified
|
||||
throw new Error('Could not verify public key');
|
||||
}
|
||||
|
||||
resolve(); // we're done
|
||||
|
||||
}).catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
PublickeyVerifier.prototype._searchAll = function(paths) {
|
||||
var self = this,
|
||||
candidates = []; // gather matching uids
|
||||
|
||||
// async for-loop inside a then-able
|
||||
return new Promise(next);
|
||||
|
||||
// search each path for the relevant email
|
||||
function next(resolve) {
|
||||
if (!paths.length) {
|
||||
resolve(candidates);
|
||||
return;
|
||||
}
|
||||
|
||||
var path = paths.shift();
|
||||
self._imap.search({
|
||||
path: path,
|
||||
header: ['Subject', self._appConfig.string.verificationSubject]
|
||||
}).then(function(uids) {
|
||||
uids.forEach(function(uid) {
|
||||
candidates.push({
|
||||
path: path,
|
||||
uid: uid
|
||||
});
|
||||
});
|
||||
next(resolve); // keep on searching
|
||||
}).catch(function() {
|
||||
next(resolve); // if there's an error, just search the next inbox
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
PublickeyVerifier.prototype._verifyAll = function(candidates) {
|
||||
var self = this;
|
||||
|
||||
// async for-loop inside a then-able
|
||||
return new Promise(next);
|
||||
|
||||
function next(resolve) {
|
||||
if (!candidates.length) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var candidate = candidates.shift();
|
||||
self._verify(candidate.path, candidate.uid).then(function(success) {
|
||||
if (success) {
|
||||
resolve(success); // we're done here
|
||||
} else {
|
||||
next(resolve);
|
||||
}
|
||||
}).catch(function() {
|
||||
next(resolve); // ignore
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
PublickeyVerifier.prototype._verify = function(path, uid) {
|
||||
var self = this,
|
||||
message;
|
||||
|
||||
// get the metadata for the message
|
||||
return self._imap.listMessages({
|
||||
path: path,
|
||||
firstUid: uid,
|
||||
lastUid: uid
|
||||
}).then(function(messages) {
|
||||
if (!messages.length) {
|
||||
// message has been deleted in the meantime
|
||||
throw new Error('Message has already been deleted');
|
||||
}
|
||||
|
||||
// remember in scope
|
||||
message = messages[0];
|
||||
|
||||
}).then(function() {
|
||||
// get the body for the message
|
||||
return self._imap.getBodyParts({
|
||||
path: path,
|
||||
uid: uid,
|
||||
bodyParts: message.bodyParts
|
||||
});
|
||||
|
||||
}).then(function() {
|
||||
// parse the message
|
||||
return new Promise(function(resolve, reject) {
|
||||
self._mailreader.parse(message, function(err, root) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(root);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}).then(function(root) {
|
||||
// extract the nonce
|
||||
var body = _.pluck(filterBodyParts(root, MSG_PART_TYPE_TEXT), MSG_PART_ATTR_CONTENT).join('\n'),
|
||||
verificationUrlPrefix = self._keyServerUrl + self._appConfig.config.verificationUrl,
|
||||
uuid = body.split(verificationUrlPrefix).pop().substr(0, self._appConfig.config.verificationUuidLength),
|
||||
uuidRegex = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/;
|
||||
|
||||
// there's no valid uuid in the message, so forget about it
|
||||
if (!uuidRegex.test(uuid)) {
|
||||
throw new Error('No public key verifier found!');
|
||||
}
|
||||
|
||||
// there's a valid uuid in the message, so try to verify it
|
||||
return self._keychain.verifyPublicKey(uuid).catch(function(err) {
|
||||
throw new Error('Verifying your public key failed: ' + err.message);
|
||||
});
|
||||
|
||||
}).then(function() {
|
||||
return self._imap.deleteMessage({
|
||||
path: path,
|
||||
uid: uid
|
||||
}).catch(function() {}); // ignore error here
|
||||
}).then(function() {
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
36
src/tpl/login-verify-public-key.html
Normal file
36
src/tpl/login-verify-public-key.html
Normal file
@ -0,0 +1,36 @@
|
||||
<section class="page" ng-class="{'u-waiting-cursor': busy}">
|
||||
<div class="page__canvas">
|
||||
<header class="page__header">
|
||||
<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>
|
||||
<div ng-show="!busy">
|
||||
<p class="typo-paragraph">
|
||||
Verifying your email address in {{countdown}} seconds.
|
||||
</p>
|
||||
|
||||
<form class="form" name="form">
|
||||
<p class="form__error-message" ng-show="errMsg">{{errMsg}}</p>
|
||||
<div class="form__row">
|
||||
<button type="submit" ng-click="verify()" class="btn" tabindex="1">Verify now</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div ng-show="busy">
|
||||
<p class="typo-paragraph">
|
||||
This could take a moment. Please be patient, you'll be forwarded to your inbox when verification is successful.
|
||||
</p>
|
||||
<div class="spinner-block spinner-block--standalone">
|
||||
<span class="spinner spinner--big"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
<div ng-include="'tpl/page-footer.html'"></div>
|
||||
</div>
|
||||
</section>
|
153
test/integration/publickey-verifier-test.js
Normal file
153
test/integration/publickey-verifier-test.js
Normal file
@ -0,0 +1,153 @@
|
||||
'use strict';
|
||||
|
||||
var ImapClient = require('imap-client'),
|
||||
BrowserCrow = require('browsercrow'),
|
||||
mailreader = require('mailreader'),
|
||||
config = require('../../src/js/app-config'),
|
||||
str = config.string;
|
||||
|
||||
describe('Public-Key Verifier integration tests', function() {
|
||||
this.timeout(10 * 1000);
|
||||
|
||||
var verifier; // SUT
|
||||
var imapServer, keyId, workingUUID, outdatedUUID; // fixture
|
||||
var imapClient, auth, keychain; // stubs
|
||||
|
||||
beforeEach(function(done) {
|
||||
|
||||
//
|
||||
// Test data
|
||||
//
|
||||
|
||||
keyId = '1234DEADBEEF';
|
||||
workingUUID = '8314D2BF-82E5-4862-A614-1EA8CD582485';
|
||||
outdatedUUID = 'CA8BD44B-E4C5-4D48-82AB-33DA2E488CF7';
|
||||
|
||||
//
|
||||
// Test server setup
|
||||
//
|
||||
|
||||
var testAccount = {
|
||||
user: 'safewithme.testuser@gmail.com',
|
||||
pass: 'passphrase',
|
||||
xoauth2: 'testtoken'
|
||||
};
|
||||
|
||||
var serverUsers = {};
|
||||
serverUsers[testAccount.user] = {
|
||||
password: testAccount.pass,
|
||||
xoauth2: {
|
||||
accessToken: testAccount.xoauth2,
|
||||
sessionTimeout: 3600 * 1000
|
||||
}
|
||||
};
|
||||
|
||||
imapServer = new BrowserCrow({
|
||||
debug: false,
|
||||
plugins: ['sasl-ir', 'xoauth2', 'special-use', 'id', 'idle', 'unselect', 'enable', 'condstore'],
|
||||
id: {
|
||||
name: 'browsercrow',
|
||||
version: '0.1.0'
|
||||
},
|
||||
storage: {
|
||||
'INBOX': {
|
||||
messages: [{
|
||||
raw: 'Message-id: <a>\r\nSubject: ' + str.verificationSubject + '\r\n\r\nhttps://keys.whiteout.io/verify/' + outdatedUUID,
|
||||
uid: 100
|
||||
}, {
|
||||
raw: 'Message-id: <a>\r\nSubject: ' + str.verificationSubject + '\r\n\r\nhttps://keys.whiteout.io/verify/' + workingUUID,
|
||||
uid: 200
|
||||
}]
|
||||
},
|
||||
'': {
|
||||
separator: '/',
|
||||
folders: {
|
||||
'[Gmail]': {
|
||||
flags: ['\\Noselect'],
|
||||
folders: {
|
||||
'All Mail': {
|
||||
'special-use': '\\All'
|
||||
},
|
||||
Drafts: {
|
||||
'special-use': '\\Drafts'
|
||||
},
|
||||
Important: {
|
||||
'special-use': '\\Important'
|
||||
},
|
||||
'Sent Mail': {
|
||||
'special-use': '\\Sent'
|
||||
},
|
||||
Spam: {
|
||||
'special-use': '\\Junk'
|
||||
},
|
||||
Starred: {
|
||||
'special-use': '\\Flagged'
|
||||
},
|
||||
Trash: {
|
||||
'special-use': '\\Trash'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
users: serverUsers
|
||||
});
|
||||
|
||||
// don't multithread, Function.prototype.bind() is broken in phantomjs in web workers
|
||||
window.Worker = undefined;
|
||||
sinon.stub(mailreader, 'startWorker', function() {});
|
||||
|
||||
// build and inject angular services
|
||||
angular.module('email-integration-test', ['woEmail']);
|
||||
angular.mock.module('email-integration-test');
|
||||
angular.mock.inject(function($injector) {
|
||||
verifier = $injector.get('publickeyVerifier');
|
||||
setup();
|
||||
});
|
||||
|
||||
function setup() {
|
||||
auth = verifier._auth;
|
||||
auth.setCredentials({
|
||||
emailAddress: testAccount.user,
|
||||
password: 'asd',
|
||||
smtp: {}, // host and port don't matter here since we're using
|
||||
imap: {} // a preconfigured smtpclient with mocked tcp sockets
|
||||
});
|
||||
|
||||
// avoid firing up a whole http
|
||||
keychain = verifier._keychain;
|
||||
keychain.verifyPublicKey = function(uuid) {
|
||||
return new Promise(function(res, rej) {
|
||||
if (uuid === workingUUID) {
|
||||
res();
|
||||
} else {
|
||||
rej();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// create imap/smtp clients with stubbed tcp sockets
|
||||
imapClient = new ImapClient({
|
||||
auth: {
|
||||
user: testAccount.user,
|
||||
xoauth2: testAccount.xoauth2
|
||||
},
|
||||
secure: true
|
||||
});
|
||||
imapClient._client.client._TCPSocket = imapServer.createTCPSocket();
|
||||
|
||||
auth._initialized = true;
|
||||
verifier._imap = imapClient;
|
||||
verifier._keyId = keyId;
|
||||
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
describe('#verify', function() {
|
||||
it('should verify a key', function(done) {
|
||||
verifier.verify(keyId).then(done);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,17 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
var Auth = require('../../../../src/js/service/auth'),
|
||||
PublicKeyVerifier = require('../../../../src/js/service/publickey-verifier'),
|
||||
LoginInitialCtrl = require('../../../../src/js/controller/login/login-initial'),
|
||||
Email = require('../../../../src/js/email/email');
|
||||
|
||||
describe('Login (initial user) Controller unit test', function() {
|
||||
var scope, ctrl, location, emailMock, authMock, newsletterStub,
|
||||
var scope, ctrl, location, emailMock, authMock, newsletterStub, verifierMock,
|
||||
emailAddress = 'fred@foo.com',
|
||||
keyId, expectedKeyId;
|
||||
|
||||
beforeEach(function() {
|
||||
emailMock = sinon.createStubInstance(Email);
|
||||
authMock = sinon.createStubInstance(Auth);
|
||||
verifierMock = sinon.createStubInstance(PublicKeyVerifier);
|
||||
|
||||
keyId = '9FEB47936E712926';
|
||||
expectedKeyId = '6E712926';
|
||||
@ -32,6 +34,7 @@ describe('Login (initial user) Controller unit test', function() {
|
||||
$routeParams: {},
|
||||
$q: window.qMock,
|
||||
newsletter: newsletter,
|
||||
publickeyVerifier: verifierMock,
|
||||
email: emailMock,
|
||||
auth: authMock
|
||||
});
|
||||
@ -102,14 +105,14 @@ describe('Login (initial user) Controller unit test', function() {
|
||||
emailMock.unlock.withArgs({
|
||||
passphrase: undefined,
|
||||
realname: authMock.realname
|
||||
}).returns(resolves());
|
||||
}).returns(resolves('foofoo'));
|
||||
authMock.storeCredentials.returns(resolves());
|
||||
|
||||
scope.generateKey().then(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('/account');
|
||||
expect(location.$$path).to.equal('/login-verify-public-key');
|
||||
expect(emailMock.unlock.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
|
@ -3,6 +3,7 @@
|
||||
var PGP = require('../../../../src/js/crypto/pgp'),
|
||||
LoginNewDeviceCtrl = require('../../../../src/js/controller/login/login-new-device'),
|
||||
KeychainDAO = require('../../../../src/js/service/keychain'),
|
||||
PublicKeyVerifier = require('../../../../src/js/service/publickey-verifier'),
|
||||
EmailDAO = require('../../../../src/js/email/email'),
|
||||
Auth = require('../../../../src/js/service/auth');
|
||||
|
||||
@ -11,11 +12,14 @@ describe('Login (new device) Controller unit test', function() {
|
||||
emailAddress = 'fred@foo.com',
|
||||
passphrase = 'asd',
|
||||
keyId,
|
||||
keychainMock;
|
||||
location,
|
||||
keychainMock,
|
||||
verifierMock;
|
||||
|
||||
beforeEach(function() {
|
||||
emailMock = sinon.createStubInstance(EmailDAO);
|
||||
authMock = sinon.createStubInstance(Auth);
|
||||
verifierMock = sinon.createStubInstance(PublicKeyVerifier);
|
||||
|
||||
keyId = '9FEB47936E712926';
|
||||
keychainMock = sinon.createStubInstance(KeychainDAO);
|
||||
@ -26,8 +30,10 @@ describe('Login (new device) Controller unit test', function() {
|
||||
|
||||
angular.module('loginnewdevicetest', ['woServices']);
|
||||
angular.mock.module('loginnewdevicetest');
|
||||
angular.mock.inject(function($rootScope, $controller) {
|
||||
angular.mock.inject(function($rootScope, $location, $controller) {
|
||||
scope = $rootScope.$new();
|
||||
location = $location;
|
||||
|
||||
scope.state = {
|
||||
ui: {}
|
||||
};
|
||||
@ -39,6 +45,7 @@ describe('Login (new device) Controller unit test', function() {
|
||||
email: emailMock,
|
||||
auth: authMock,
|
||||
pgp: pgpMock,
|
||||
publickeyVerifier: verifierMock,
|
||||
keychain: keychainMock
|
||||
});
|
||||
});
|
||||
@ -69,12 +76,13 @@ describe('Login (new device) Controller unit test', function() {
|
||||
_id: keyId,
|
||||
publicKey: 'a'
|
||||
}));
|
||||
emailMock.unlock.withArgs(sinon.match.any, passphrase).returns(resolves());
|
||||
emailMock.unlock.returns(resolves('asd'));
|
||||
keychainMock.putUserKeyPair.returns(resolves());
|
||||
|
||||
scope.confirmPassphrase().then(function() {
|
||||
expect(emailMock.unlock.calledOnce).to.be.true;
|
||||
expect(keychainMock.getUserKeyPair.calledOnce).to.be.true;
|
||||
expect(location.$$path).to.equal('/account');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@ -92,12 +100,14 @@ describe('Login (new device) Controller unit test', function() {
|
||||
});
|
||||
|
||||
keychainMock.getUserKeyPair.withArgs(emailAddress).returns(resolves());
|
||||
emailMock.unlock.withArgs(sinon.match.any, passphrase).returns(resolves());
|
||||
keychainMock.uploadPublicKey.returns(resolves());
|
||||
emailMock.unlock.returns(resolves('asd'));
|
||||
keychainMock.putUserKeyPair.returns(resolves());
|
||||
|
||||
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');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
113
test/unit/controller/login/login-verify-public-key-ctrl-test.js
Normal file
113
test/unit/controller/login/login-verify-public-key-ctrl-test.js
Normal file
@ -0,0 +1,113 @@
|
||||
'use strict';
|
||||
|
||||
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'),
|
||||
PublicKeyVerifierCtrl = require('../../../../src/js/controller/login/login-verify-public-key');
|
||||
|
||||
describe('Public Key Verification Controller unit test', function() {
|
||||
// Angular parameters
|
||||
var scope, location;
|
||||
|
||||
// Stubs & Fixture
|
||||
var auth, verifier, dialogStub, keychain;
|
||||
var emailAddress = 'foo@foo.com';
|
||||
|
||||
// SUT
|
||||
var verificationCtrl;
|
||||
|
||||
beforeEach(function() {
|
||||
// remeber pre-test state to restore later
|
||||
auth = sinon.createStubInstance(Auth);
|
||||
verifier = sinon.createStubInstance(PublicKeyVerifier);
|
||||
dialogStub = sinon.createStubInstance(Dialog);
|
||||
keychain = sinon.createStubInstance(KeychainDAO);
|
||||
|
||||
auth.emailAddress = emailAddress;
|
||||
|
||||
// setup the controller
|
||||
angular.module('publickeyverificationtest', []);
|
||||
angular.mock.module('publickeyverificationtest');
|
||||
angular.mock.inject(function($rootScope, $controller, $location) {
|
||||
scope = $rootScope.$new();
|
||||
location = $location;
|
||||
|
||||
verificationCtrl = $controller(PublicKeyVerifierCtrl, {
|
||||
$scope: scope,
|
||||
$q: window.qMock,
|
||||
auth: auth,
|
||||
publickeyVerifier: verifier,
|
||||
dialog: dialogStub,
|
||||
keychain: keychain,
|
||||
appConfig: {
|
||||
string: {
|
||||
publickeyVerificationSkipTitle: 'foo',
|
||||
publickeyVerificationSkipMessage: 'bar'
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {});
|
||||
|
||||
describe('#verify', function() {
|
||||
it('should verify', function(done) {
|
||||
var credentials = {};
|
||||
|
||||
keychain.getUserKeyPair.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(auth.getCredentials.calledOnce).to.be.true;
|
||||
expect(verifier.configure.calledOnce).to.be.true;
|
||||
expect(verifier.verify.calledOnce).to.be.true;
|
||||
expect(verifier.persistKeypair.calledOnce).to.be.true;
|
||||
expect(location.$$path).to.equal('/account');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip verification when key is already verified', function(done) {
|
||||
keychain.getUserKeyPair.withArgs(emailAddress).returns(resolves({
|
||||
publicKey: {}
|
||||
}));
|
||||
|
||||
scope.verify().then(function() {
|
||||
expect(keychain.getUserKeyPair.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;
|
||||
expect(location.$$path).to.equal('/account');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not verify', function(done) {
|
||||
var credentials = {};
|
||||
|
||||
auth.getCredentials.returns(resolves(credentials));
|
||||
verifier.configure.withArgs(credentials).returns(resolves());
|
||||
verifier.verify.withArgs().returns(rejects(new Error('foo')));
|
||||
|
||||
scope.verify().then(function() {
|
||||
expect(auth.getCredentials.calledOnce).to.be.true;
|
||||
expect(verifier.configure.calledOnce).to.be.true;
|
||||
expect(verifier.verify.calledOnce).to.be.true;
|
||||
expect(scope.errMsg).to.equal('foo');
|
||||
|
||||
clearTimeout(scope.timeout);
|
||||
clearInterval(scope.countdownDecrement);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -216,7 +216,6 @@ describe('Email DAO unit tests', function() {
|
||||
privateKeyArmored: mockKeyPair.privateKey.encryptedKey,
|
||||
publicKeyArmored: mockKeyPair.publicKey.publicKey
|
||||
}).returns(resolves());
|
||||
keychainStub.putUserKeyPair.withArgs().returns(resolves());
|
||||
|
||||
dao.unlock({
|
||||
realname: name,
|
||||
@ -224,30 +223,6 @@ describe('Email DAO unit tests', function() {
|
||||
}).then(function() {
|
||||
expect(pgpStub.generateKeys.calledOnce).to.be.true;
|
||||
expect(pgpStub.importKeys.calledOnce).to.be.true;
|
||||
expect(keychainStub.putUserKeyPair.calledOnce).to.be.true;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail when persisting fails', function(done) {
|
||||
var keypair = {
|
||||
keyId: 123,
|
||||
publicKeyArmored: 'qwerty',
|
||||
privateKeyArmored: 'asdfgh'
|
||||
};
|
||||
pgpStub.generateKeys.returns(resolves(keypair));
|
||||
pgpStub.importKeys.withArgs().returns(resolves());
|
||||
keychainStub.putUserKeyPair.returns(rejects({}));
|
||||
|
||||
dao.unlock({
|
||||
passphrase: passphrase
|
||||
}).catch(function(err) {
|
||||
expect(err).to.exist;
|
||||
|
||||
expect(pgpStub.generateKeys.calledOnce).to.be.true;
|
||||
expect(pgpStub.importKeys.calledOnce).to.be.true;
|
||||
expect(keychainStub.putUserKeyPair.calledOnce).to.be.true;
|
||||
|
||||
done();
|
||||
});
|
||||
@ -387,7 +362,7 @@ describe('Email DAO unit tests', function() {
|
||||
|
||||
describe('#fetchMessages', function() {
|
||||
var imapListStub, imapGetStub, imapDeleteStub, localStoreStub;
|
||||
var opts, message, validUuid, corruptedUuid, verificationSubject;
|
||||
var opts, message;
|
||||
var notified;
|
||||
|
||||
beforeEach(function() {
|
||||
@ -407,9 +382,6 @@ describe('Email DAO unit tests', function() {
|
||||
unread: true,
|
||||
bodyParts: []
|
||||
};
|
||||
validUuid = '9A858952-17EE-4273-9E74-D309EAFDFAFB';
|
||||
corruptedUuid = 'OMFG_FUCKING_BASTARD_UUID_FROM_HELL!';
|
||||
verificationSubject = "[whiteout] New public key uploaded";
|
||||
|
||||
notified = false;
|
||||
dao.onIncomingMessage = function(newMessages) {
|
||||
@ -455,103 +427,6 @@ describe('Email DAO unit tests', function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should verify verification mails', function(done) {
|
||||
message.subject = verificationSubject;
|
||||
|
||||
imapListStub.withArgs(opts).returns(resolves([message]));
|
||||
|
||||
imapGetStub.withArgs({
|
||||
folder: inboxFolder,
|
||||
uid: message.uid,
|
||||
bodyParts: message.bodyParts
|
||||
}).returns(resolves([{
|
||||
type: 'text',
|
||||
content: '' + cfg.keyServerUrl + cfg.verificationUrl + validUuid
|
||||
}]));
|
||||
|
||||
keychainStub.verifyPublicKey.withArgs(validUuid).returns(resolves());
|
||||
|
||||
imapDeleteStub.withArgs({
|
||||
folder: inboxFolder,
|
||||
uid: message.uid
|
||||
}).returns(resolves());
|
||||
|
||||
dao.fetchMessages(opts).then(function() {
|
||||
expect(inboxFolder.messages).to.not.contain(message);
|
||||
expect(notified).to.be.false;
|
||||
expect(imapListStub.calledOnce).to.be.true;
|
||||
expect(imapGetStub.calledOnce).to.be.true;
|
||||
expect(keychainStub.verifyPublicKey.calledOnce).to.be.true;
|
||||
expect(imapDeleteStub.calledOnce).to.be.true;
|
||||
expect(localStoreStub.called).to.be.false;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not verify invalid verification mails', function(done) {
|
||||
message.subject = verificationSubject;
|
||||
|
||||
imapListStub.withArgs(opts).returns(resolves([message]));
|
||||
|
||||
imapGetStub.withArgs({
|
||||
folder: inboxFolder,
|
||||
uid: message.uid,
|
||||
bodyParts: message.bodyParts
|
||||
}).returns(resolves([{
|
||||
type: 'text',
|
||||
content: '' + cfg.keyServerUrl + cfg.verificationUrl + corruptedUuid
|
||||
}]));
|
||||
|
||||
localStoreStub.withArgs({
|
||||
folder: inboxFolder,
|
||||
emails: [message]
|
||||
}).returns(resolves());
|
||||
|
||||
dao.fetchMessages(opts).then(function() {
|
||||
expect(inboxFolder.messages).to.contain(message);
|
||||
expect(notified).to.be.true;
|
||||
expect(imapListStub.calledOnce).to.be.true;
|
||||
expect(imapGetStub.calledOnce).to.be.true;
|
||||
expect(keychainStub.verifyPublicKey.called).to.be.false;
|
||||
expect(imapDeleteStub.called).to.be.false;
|
||||
expect(localStoreStub.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display verification mail when verification failed', function(done) {
|
||||
message.subject = verificationSubject;
|
||||
|
||||
imapListStub.withArgs(opts).returns(resolves([message]));
|
||||
|
||||
imapGetStub.withArgs({
|
||||
folder: inboxFolder,
|
||||
uid: message.uid,
|
||||
bodyParts: message.bodyParts
|
||||
}).returns(resolves([{
|
||||
type: 'text',
|
||||
content: '' + cfg.keyServerUrl + cfg.verificationUrl + validUuid
|
||||
}]));
|
||||
|
||||
keychainStub.verifyPublicKey.withArgs(validUuid).returns(rejects({}));
|
||||
|
||||
localStoreStub.withArgs({
|
||||
folder: inboxFolder,
|
||||
emails: [message]
|
||||
}).returns(resolves());
|
||||
|
||||
dao.fetchMessages(opts).then(function() {
|
||||
expect(inboxFolder.messages).to.contain(message);
|
||||
expect(notified).to.be.true;
|
||||
expect(imapListStub.calledOnce).to.be.true;
|
||||
expect(imapGetStub.calledOnce).to.be.true;
|
||||
expect(keychainStub.verifyPublicKey.calledOnce).to.be.true;
|
||||
expect(imapDeleteStub.called).to.be.false;
|
||||
expect(localStoreStub.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#deleteMessage', function() {
|
||||
|
@ -1356,6 +1356,29 @@ describe('Keychain DAO unit tests', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('upload public key', function() {
|
||||
it('should upload key', function(done) {
|
||||
var keypair = {
|
||||
publicKey: {
|
||||
_id: '12345',
|
||||
userId: testUser,
|
||||
publicKey: 'asdf'
|
||||
},
|
||||
privateKey: {
|
||||
_id: '12345',
|
||||
encryptedKey: 'qwer'
|
||||
}
|
||||
};
|
||||
|
||||
pubkeyDaoStub.put.withArgs(keypair.publicKey).returns(resolves());
|
||||
|
||||
keychainDao.uploadPublicKey(keypair.publicKey).then(function() {
|
||||
expect(pubkeyDaoStub.put.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('put user keypair', function() {
|
||||
it('should fail', function(done) {
|
||||
var keypair = {
|
||||
|
210
test/unit/service/publickey-verifier-test.js
Normal file
210
test/unit/service/publickey-verifier-test.js
Normal file
@ -0,0 +1,210 @@
|
||||
'use strict';
|
||||
|
||||
var mailreader = require('mailreader'),
|
||||
KeychainDAO = require('../../../src/js/service/keychain'),
|
||||
ImapClient = require('imap-client'),
|
||||
PublickeyVerifier = require('../../../src/js/service/publickey-verifier'),
|
||||
appConfig = require('../../../src/js/app-config');
|
||||
|
||||
describe('Public-Key Verifier', function() {
|
||||
var verifier;
|
||||
var imapStub, parseStub, keychainStub, credentials, workerPath;
|
||||
|
||||
beforeEach(function() {
|
||||
//
|
||||
// Stubs
|
||||
//
|
||||
|
||||
workerPath = '../lib/tcp-socket-tls-worker.min.js';
|
||||
imapStub = sinon.createStubInstance(ImapClient);
|
||||
parseStub = sinon.stub(mailreader, 'parse');
|
||||
keychainStub = sinon.createStubInstance(KeychainDAO);
|
||||
|
||||
//
|
||||
// Fixture
|
||||
//
|
||||
credentials = {
|
||||
imap: {
|
||||
host: 'asd',
|
||||
port: 1234,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: 'user',
|
||||
pass: 'pass'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Setup SUT
|
||||
//
|
||||
verifier = new PublickeyVerifier({}, appConfig, mailreader, keychainStub);
|
||||
verifier._imap = imapStub;
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
mailreader.parse.restore();
|
||||
});
|
||||
|
||||
describe('#check', function() {
|
||||
var FOLDER_TYPE_INBOX = 'Inbox',
|
||||
FOLDER_TYPE_SENT = 'Sent',
|
||||
FOLDER_TYPE_DRAFTS = 'Drafts',
|
||||
FOLDER_TYPE_TRASH = 'Trash',
|
||||
FOLDER_TYPE_FLAGGED = 'Flagged';
|
||||
|
||||
var messages,
|
||||
folders,
|
||||
searches,
|
||||
workingUUID,
|
||||
outdatedUUID;
|
||||
|
||||
beforeEach(function() {
|
||||
folders = {};
|
||||
searches = {};
|
||||
|
||||
[FOLDER_TYPE_INBOX, FOLDER_TYPE_SENT, FOLDER_TYPE_DRAFTS, FOLDER_TYPE_TRASH, FOLDER_TYPE_FLAGGED].forEach(function(type) {
|
||||
folders[type] = [{
|
||||
path: type
|
||||
}];
|
||||
searches[type] = {
|
||||
path: type,
|
||||
header: ['Subject', appConfig.string.verificationSubject]
|
||||
};
|
||||
});
|
||||
|
||||
workingUUID = '8314D2BF-82E5-4862-A614-1EA8CD582485';
|
||||
outdatedUUID = 'CA8BD44B-E4C5-4D48-82AB-33DA2E488CF7';
|
||||
messages = [{
|
||||
uid: 123,
|
||||
bodyParts: [{
|
||||
type: 'text',
|
||||
content: 'https://keys.whiteout.io/verify/' + workingUUID
|
||||
}]
|
||||
}, {
|
||||
uid: 456,
|
||||
bodyParts: [{
|
||||
type: 'text',
|
||||
content: 'https://keys.whiteout.io/verify/' + outdatedUUID
|
||||
}]
|
||||
}, {
|
||||
uid: 789,
|
||||
bodyParts: [{
|
||||
type: 'text',
|
||||
content: 'foobar'
|
||||
}]
|
||||
}];
|
||||
});
|
||||
|
||||
it('should verify a key', function(done) {
|
||||
// log in
|
||||
imapStub.login.returns(resolves());
|
||||
|
||||
// list the folders
|
||||
imapStub.listWellKnownFolders.returns(resolves(folders));
|
||||
|
||||
// return matching uids for inbox, flagged, and sent, otherwise no matches
|
||||
imapStub.search.returns(resolves([]));
|
||||
imapStub.search.withArgs(searches[FOLDER_TYPE_INBOX]).returns(resolves([messages[1].uid]));
|
||||
imapStub.search.withArgs(searches[FOLDER_TYPE_FLAGGED]).returns(resolves([messages[0].uid]));
|
||||
imapStub.search.withArgs(searches[FOLDER_TYPE_SENT]).returns(resolves([messages[2].uid]));
|
||||
|
||||
// fetch message metadata from inbox, flagged, and sent
|
||||
imapStub.listMessages.withArgs({
|
||||
path: FOLDER_TYPE_INBOX,
|
||||
firstUid: messages[1].uid,
|
||||
lastUid: messages[1].uid
|
||||
}).returns(resolves([messages[1]]));
|
||||
imapStub.listMessages.withArgs({
|
||||
path: FOLDER_TYPE_FLAGGED,
|
||||
firstUid: messages[0].uid,
|
||||
lastUid: messages[0].uid
|
||||
}).returns(resolves([messages[0]]));
|
||||
imapStub.listMessages.withArgs({
|
||||
path: FOLDER_TYPE_SENT,
|
||||
firstUid: messages[2].uid,
|
||||
lastUid: messages[2].uid
|
||||
}).returns(resolves([messages[2]]));
|
||||
|
||||
// fetch message metadata from inbox, flagged, and sent
|
||||
imapStub.getBodyParts.withArgs({
|
||||
path: FOLDER_TYPE_INBOX,
|
||||
uid: messages[1].uid,
|
||||
bodyParts: messages[1].bodyParts
|
||||
}).returns(resolves(messages[1].bodyParts));
|
||||
imapStub.getBodyParts.withArgs({
|
||||
path: FOLDER_TYPE_FLAGGED,
|
||||
uid: messages[0].uid,
|
||||
bodyParts: messages[0].bodyParts
|
||||
}).returns(resolves(messages[0].bodyParts));
|
||||
imapStub.getBodyParts.withArgs({
|
||||
path: FOLDER_TYPE_SENT,
|
||||
uid: messages[2].uid,
|
||||
bodyParts: messages[2].bodyParts
|
||||
}).returns(resolves(messages[2].bodyParts));
|
||||
|
||||
// parse messages (already have body parts, so this is essentially a no-op)
|
||||
parseStub.withArgs(messages[0]).yields(null, messages[0].bodyParts);
|
||||
parseStub.withArgs(messages[1]).yields(null, messages[1].bodyParts);
|
||||
parseStub.withArgs(messages[2]).yields(null, messages[2].bodyParts);
|
||||
|
||||
// delete the verification message from the inbox
|
||||
imapStub.deleteMessage.withArgs({
|
||||
path: FOLDER_TYPE_FLAGGED,
|
||||
uid: messages[0].uid
|
||||
}).returns(resolves());
|
||||
|
||||
keychainStub.verifyPublicKey.withArgs(workingUUID).returns(resolves());
|
||||
keychainStub.verifyPublicKey.withArgs(outdatedUUID).returns(rejects(new Error('foo')));
|
||||
|
||||
// logout ... duh
|
||||
imapStub.logout.returns(resolves());
|
||||
|
||||
// run the test
|
||||
verifier.verify().then(function() {
|
||||
// verification
|
||||
expect(parseStub.callCount).to.equal(3);
|
||||
expect(imapStub.login.callCount).to.equal(1);
|
||||
expect(imapStub.listWellKnownFolders.callCount).to.equal(1);
|
||||
expect(imapStub.search.callCount).to.be.at.least(5);
|
||||
expect(imapStub.listMessages.callCount).to.equal(3);
|
||||
expect(imapStub.getBodyParts.callCount).to.equal(3);
|
||||
expect(imapStub.deleteMessage.callCount).to.equal(1);
|
||||
expect(imapStub.logout.callCount).to.equal(1);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not find a verifiable key', function(done) {
|
||||
// log in
|
||||
imapStub.login.returns(resolves());
|
||||
|
||||
// list the folders
|
||||
imapStub.listWellKnownFolders.returns(resolves(folders));
|
||||
|
||||
// return matching uids for inbox, flagged, and sent, otherwise no matches
|
||||
imapStub.search.returns(resolves([]));
|
||||
|
||||
// logout ... duh
|
||||
imapStub.logout.returns(resolves());
|
||||
|
||||
// run the test
|
||||
verifier.verify().catch(function(error) {
|
||||
expect(error.message).to.equal('Could not verify public key');
|
||||
|
||||
// verification
|
||||
expect(imapStub.login.callCount).to.equal(1);
|
||||
expect(imapStub.listWellKnownFolders.callCount).to.equal(1);
|
||||
expect(imapStub.search.callCount).to.be.at.least(5);
|
||||
expect(imapStub.listMessages.callCount).to.equal(0);
|
||||
expect(imapStub.getBodyParts.callCount).to.equal(0);
|
||||
expect(imapStub.deleteMessage.callCount).to.equal(0);
|
||||
expect(imapStub.logout.callCount).to.equal(1);
|
||||
expect(parseStub.callCount).to.equal(0);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user