mirror of https://github.com/moparisthebest/mail
Merge pull request #313 from whiteout-io/dev/WO-860
[WO-860] Introduce publickey-verifier
This commit is contained in:
commit
518ceec0ef
|
@ -178,6 +178,7 @@ module.exports = function(grunt) {
|
||||||
'test/unit/service/newsletter-service-test.js',
|
'test/unit/service/newsletter-service-test.js',
|
||||||
'test/unit/service/mail-config-service-test.js',
|
'test/unit/service/mail-config-service-test.js',
|
||||||
'test/unit/service/invitation-dao-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/outbox-bo-test.js',
|
||||||
'test/unit/email/email-dao-test.js',
|
'test/unit/email/email-dao-test.js',
|
||||||
'test/unit/email/account-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-initial-ctrl-test.js',
|
||||||
'test/unit/controller/login/login-new-device-ctrl-test.js',
|
'test/unit/controller/login/login-new-device-ctrl-test.js',
|
||||||
'test/unit/controller/login/login-privatekey-download-ctrl-test.js',
|
'test/unit/controller/login/login-privatekey-download-ctrl-test.js',
|
||||||
|
'test/unit/controller/login/login-verify-public-key-ctrl-test.js',
|
||||||
'test/unit/controller/login/login-set-credentials-ctrl-test.js',
|
'test/unit/controller/login/login-set-credentials-ctrl-test.js',
|
||||||
'test/unit/controller/login/login-ctrl-test.js',
|
'test/unit/controller/login/login-ctrl-test.js',
|
||||||
'test/unit/controller/app/dialog-ctrl-test.js',
|
'test/unit/controller/app/dialog-ctrl-test.js',
|
||||||
|
@ -210,7 +212,8 @@ module.exports = function(grunt) {
|
||||||
files: {
|
files: {
|
||||||
'test/integration/index.browserified.js': [
|
'test/integration/index.browserified.js': [
|
||||||
'test/main.js',
|
'test/main.js',
|
||||||
'test/integration/email-dao-test.js'
|
'test/integration/email-dao-test.js',
|
||||||
|
'test/integration/publickey-verifier-test.js'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
options: browserifyOpt
|
options: browserifyOpt
|
||||||
|
|
|
@ -68,6 +68,10 @@ app.config(function($routeProvider, $animateProvider) {
|
||||||
templateUrl: 'tpl/login-set-credentials.html',
|
templateUrl: 'tpl/login-set-credentials.html',
|
||||||
controller: require('./controller/login/login-set-credentials')
|
controller: require('./controller/login/login-set-credentials')
|
||||||
});
|
});
|
||||||
|
$routeProvider.when('/login-verify-public-key', {
|
||||||
|
templateUrl: 'tpl/login-verify-public-key.html',
|
||||||
|
controller: require('./controller/login/login-verify-public-key')
|
||||||
|
});
|
||||||
$routeProvider.when('/login-existing', {
|
$routeProvider.when('/login-existing', {
|
||||||
templateUrl: 'tpl/login-existing.html',
|
templateUrl: 'tpl/login-existing.html',
|
||||||
controller: require('./controller/login/login-existing')
|
controller: require('./controller/login/login-existing')
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
'use strict';
|
'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
|
!$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
|
||||||
|
|
||||||
var emailAddress = auth.emailAddress;
|
var emailAddress = auth.emailAddress;
|
||||||
|
@ -60,13 +60,10 @@ var LoginInitialCtrl = function($scope, $location, $routeParams, $q, newsletter,
|
||||||
passphrase: undefined
|
passphrase: undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
}).then(function() {
|
}).then(function(keypair) {
|
||||||
// persist credentials locally
|
// go to public key verification
|
||||||
return auth.storeCredentials();
|
publickeyVerifier.keypair = keypair;
|
||||||
|
$location.path('/login-verify-public-key');
|
||||||
}).then(function() {
|
|
||||||
// go to main account screen
|
|
||||||
$location.path('/account');
|
|
||||||
|
|
||||||
}).catch(displayError);
|
}).catch(displayError);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
'use strict';
|
'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
|
!$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
|
||||||
|
|
||||||
$scope.incorrect = false;
|
$scope.incorrect = false;
|
||||||
|
@ -12,6 +12,7 @@ var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, aut
|
||||||
}
|
}
|
||||||
|
|
||||||
var userId = auth.emailAddress,
|
var userId = auth.emailAddress,
|
||||||
|
pubKeyNeedsVerification = false,
|
||||||
keypair;
|
keypair;
|
||||||
|
|
||||||
return $q(function(resolve) {
|
return $q(function(resolve) {
|
||||||
|
@ -61,6 +62,7 @@ var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, aut
|
||||||
userIds: pubKeyParams.userIds,
|
userIds: pubKeyParams.userIds,
|
||||||
publicKey: $scope.key.publicKeyArmored
|
publicKey: $scope.key.publicKeyArmored
|
||||||
};
|
};
|
||||||
|
pubKeyNeedsVerification = true; // this public key needs to be authenticated
|
||||||
}
|
}
|
||||||
|
|
||||||
// import and validate keypair
|
// import and validate keypair
|
||||||
|
@ -72,17 +74,21 @@ var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, aut
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
}).then(function() {
|
}).then(function(keypair) {
|
||||||
// perist keys locally
|
if (!pubKeyNeedsVerification) {
|
||||||
return keychain.putUserKeyPair(keypair);
|
// 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() {
|
// go to public key verification
|
||||||
// persist credentials locally
|
publickeyVerifier.keypair = keypair;
|
||||||
return auth.storeCredentials();
|
return keychain.uploadPublicKey(keypair.publicKey).then(function() {
|
||||||
|
$location.path('/login-verify-public-key');
|
||||||
}).then(function() {
|
});
|
||||||
// go to main account screen
|
|
||||||
$location.path('/account');
|
|
||||||
|
|
||||||
}).catch(displayError);
|
}).catch(displayError);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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() {
|
}).then(function() {
|
||||||
// persist newly generated keypair
|
// persist newly generated keypair
|
||||||
var newKeypair = {
|
return {
|
||||||
publicKey: {
|
publicKey: {
|
||||||
_id: generatedKeypair.keyId,
|
_id: generatedKeypair.keyId,
|
||||||
userId: self._account.emailAddress,
|
userId: self._account.emailAddress,
|
||||||
|
@ -135,8 +135,6 @@ Email.prototype.unlock = function(options) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return self._keychain.putUserKeyPair(newKeypair);
|
|
||||||
|
|
||||||
}).then(setPrivateKey);
|
}).then(setPrivateKey);
|
||||||
|
|
||||||
function handleExistingKeypair(keypair) {
|
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) {
|
if (!matchingPrivUserId || !matchingPubUserId || keypair.privateKey.userId !== self._account.emailAddress || keypair.publicKey.userId !== self._account.emailAddress) {
|
||||||
throw new Error('User IDs dont match!');
|
throw new Error('User IDs dont match!');
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
|
|
||||||
}).then(function() {
|
}).then(function() {
|
||||||
|
@ -168,14 +167,17 @@ Email.prototype.unlock = function(options) {
|
||||||
passphrase: options.passphrase,
|
passphrase: options.passphrase,
|
||||||
privateKeyArmored: keypair.privateKey.encryptedKey,
|
privateKeyArmored: keypair.privateKey.encryptedKey,
|
||||||
publicKeyArmored: keypair.publicKey.publicKey
|
publicKeyArmored: keypair.publicKey.publicKey
|
||||||
|
}).then(function() {
|
||||||
|
return keypair;
|
||||||
});
|
});
|
||||||
|
|
||||||
}).then(setPrivateKey);
|
}).then(setPrivateKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPrivateKey() {
|
function setPrivateKey(keypair) {
|
||||||
// set decrypted privateKey to pgpMailer
|
// set decrypted privateKey to pgpMailer
|
||||||
self._pgpbuilder._privateKey = self._pgp._privateKey;
|
self._pgpbuilder._privateKey = self._pgp._privateKey;
|
||||||
|
return keypair;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -259,15 +261,11 @@ Email.prototype.refreshFolder = function(options) {
|
||||||
/**
|
/**
|
||||||
* Fetches a message's headers from IMAP.
|
* 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
|
* @param {Object} options.folder The folder for which to fetch the message
|
||||||
*/
|
*/
|
||||||
Email.prototype.fetchMessages = function(options) {
|
Email.prototype.fetchMessages = function(options) {
|
||||||
var self = this,
|
var self = this,
|
||||||
folder = options.folder,
|
folder = options.folder;
|
||||||
messages;
|
|
||||||
|
|
||||||
self.busy();
|
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
|
// list the messages starting from the lowest new uid to the highest new uid
|
||||||
return self._imapListMessages(options);
|
return self._imapListMessages(options);
|
||||||
|
|
||||||
}).then(function(msgs) {
|
}).then(function(messages) {
|
||||||
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() {
|
|
||||||
if (_.isEmpty(messages)) {
|
if (_.isEmpty(messages)) {
|
||||||
// nothing to do, we're done here
|
// nothing to do, we're done here
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// persist the encrypted message to the local storage
|
// persist the messages to the local storage
|
||||||
return self._localStoreMessages({
|
return self._localStoreMessages({
|
||||||
folder: folder,
|
folder: folder,
|
||||||
emails: messages
|
emails: messages
|
||||||
}).then(function() {
|
}).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) {
|
messages.forEach(function(message) {
|
||||||
message.attachments = message.bodyParts.filter(function(bodyPart) {
|
message.attachments = message.bodyParts.filter(function(bodyPart) {
|
||||||
return bodyPart.type === MSG_PART_TYPE_ATTACHMENT;
|
return bodyPart.type === MSG_PART_TYPE_ATTACHMENT;
|
||||||
|
@ -338,40 +312,6 @@ Email.prototype.fetchMessages = function(options) {
|
||||||
throw err;
|
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);
|
return self._imapClient.getBodyParts(options);
|
||||||
}).then(function() {
|
}).then(function() {
|
||||||
if (options.bodyParts.filter(function(bodyPart) {
|
if (options.bodyParts.filter(function(bodyPart) {
|
||||||
return !(bodyPart.raw || bodyPart.content);
|
return !(bodyPart.raw || bodyPart.content);
|
||||||
}).length) {
|
}).length) {
|
||||||
var error = new Error('Can not get the contents of this message. It has already been deleted!');
|
var error = new Error('Can not get the contents of this message. It has already been deleted!');
|
||||||
error.hide = true;
|
error.hide = true;
|
||||||
throw error;
|
throw error;
|
||||||
|
|
|
@ -14,4 +14,5 @@ require('./admin');
|
||||||
require('./lawnchair');
|
require('./lawnchair');
|
||||||
require('./devicestorage');
|
require('./devicestorage');
|
||||||
require('./auth');
|
require('./auth');
|
||||||
require('./keychain');
|
require('./keychain');
|
||||||
|
require('./publickey-verifier');
|
|
@ -659,7 +659,7 @@ Keychain.prototype.putUserKeyPair = function(keypair) {
|
||||||
// validate input
|
// validate input
|
||||||
if (!keypair || !keypair.publicKey || !keypair.privateKey || !keypair.publicKey.userId || keypair.publicKey.userId !== keypair.privateKey.userId) {
|
if (!keypair || !keypair.publicKey || !keypair.privateKey || !keypair.publicKey.userId || keypair.publicKey.userId !== keypair.privateKey.userId) {
|
||||||
return new Promise(function() {
|
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
|
// Helper functions
|
||||||
//
|
//
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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>
|
|
@ -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';
|
'use strict';
|
||||||
|
|
||||||
var Auth = require('../../../../src/js/service/auth'),
|
var Auth = require('../../../../src/js/service/auth'),
|
||||||
|
PublicKeyVerifier = require('../../../../src/js/service/publickey-verifier'),
|
||||||
LoginInitialCtrl = require('../../../../src/js/controller/login/login-initial'),
|
LoginInitialCtrl = require('../../../../src/js/controller/login/login-initial'),
|
||||||
Email = require('../../../../src/js/email/email');
|
Email = require('../../../../src/js/email/email');
|
||||||
|
|
||||||
describe('Login (initial user) Controller unit test', function() {
|
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',
|
emailAddress = 'fred@foo.com',
|
||||||
keyId, expectedKeyId;
|
keyId, expectedKeyId;
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
emailMock = sinon.createStubInstance(Email);
|
emailMock = sinon.createStubInstance(Email);
|
||||||
authMock = sinon.createStubInstance(Auth);
|
authMock = sinon.createStubInstance(Auth);
|
||||||
|
verifierMock = sinon.createStubInstance(PublicKeyVerifier);
|
||||||
|
|
||||||
keyId = '9FEB47936E712926';
|
keyId = '9FEB47936E712926';
|
||||||
expectedKeyId = '6E712926';
|
expectedKeyId = '6E712926';
|
||||||
|
@ -32,6 +34,7 @@ describe('Login (initial user) Controller unit test', function() {
|
||||||
$routeParams: {},
|
$routeParams: {},
|
||||||
$q: window.qMock,
|
$q: window.qMock,
|
||||||
newsletter: newsletter,
|
newsletter: newsletter,
|
||||||
|
publickeyVerifier: verifierMock,
|
||||||
email: emailMock,
|
email: emailMock,
|
||||||
auth: authMock
|
auth: authMock
|
||||||
});
|
});
|
||||||
|
@ -102,14 +105,14 @@ describe('Login (initial user) Controller unit test', function() {
|
||||||
emailMock.unlock.withArgs({
|
emailMock.unlock.withArgs({
|
||||||
passphrase: undefined,
|
passphrase: undefined,
|
||||||
realname: authMock.realname
|
realname: authMock.realname
|
||||||
}).returns(resolves());
|
}).returns(resolves('foofoo'));
|
||||||
authMock.storeCredentials.returns(resolves());
|
authMock.storeCredentials.returns(resolves());
|
||||||
|
|
||||||
scope.generateKey().then(function() {
|
scope.generateKey().then(function() {
|
||||||
expect(scope.errMsg).to.not.exist;
|
expect(scope.errMsg).to.not.exist;
|
||||||
expect(scope.state.ui).to.equal(2);
|
expect(scope.state.ui).to.equal(2);
|
||||||
expect(newsletterStub.called).to.be.true;
|
expect(newsletterStub.called).to.be.true;
|
||||||
expect(location.$$path).to.equal('/account');
|
expect(location.$$path).to.equal('/login-verify-public-key');
|
||||||
expect(emailMock.unlock.calledOnce).to.be.true;
|
expect(emailMock.unlock.calledOnce).to.be.true;
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
var PGP = require('../../../../src/js/crypto/pgp'),
|
var PGP = require('../../../../src/js/crypto/pgp'),
|
||||||
LoginNewDeviceCtrl = require('../../../../src/js/controller/login/login-new-device'),
|
LoginNewDeviceCtrl = require('../../../../src/js/controller/login/login-new-device'),
|
||||||
KeychainDAO = require('../../../../src/js/service/keychain'),
|
KeychainDAO = require('../../../../src/js/service/keychain'),
|
||||||
|
PublicKeyVerifier = require('../../../../src/js/service/publickey-verifier'),
|
||||||
EmailDAO = require('../../../../src/js/email/email'),
|
EmailDAO = require('../../../../src/js/email/email'),
|
||||||
Auth = require('../../../../src/js/service/auth');
|
Auth = require('../../../../src/js/service/auth');
|
||||||
|
|
||||||
|
@ -11,11 +12,14 @@ describe('Login (new device) Controller unit test', function() {
|
||||||
emailAddress = 'fred@foo.com',
|
emailAddress = 'fred@foo.com',
|
||||||
passphrase = 'asd',
|
passphrase = 'asd',
|
||||||
keyId,
|
keyId,
|
||||||
keychainMock;
|
location,
|
||||||
|
keychainMock,
|
||||||
|
verifierMock;
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
emailMock = sinon.createStubInstance(EmailDAO);
|
emailMock = sinon.createStubInstance(EmailDAO);
|
||||||
authMock = sinon.createStubInstance(Auth);
|
authMock = sinon.createStubInstance(Auth);
|
||||||
|
verifierMock = sinon.createStubInstance(PublicKeyVerifier);
|
||||||
|
|
||||||
keyId = '9FEB47936E712926';
|
keyId = '9FEB47936E712926';
|
||||||
keychainMock = sinon.createStubInstance(KeychainDAO);
|
keychainMock = sinon.createStubInstance(KeychainDAO);
|
||||||
|
@ -26,8 +30,10 @@ describe('Login (new device) Controller unit test', function() {
|
||||||
|
|
||||||
angular.module('loginnewdevicetest', ['woServices']);
|
angular.module('loginnewdevicetest', ['woServices']);
|
||||||
angular.mock.module('loginnewdevicetest');
|
angular.mock.module('loginnewdevicetest');
|
||||||
angular.mock.inject(function($rootScope, $controller) {
|
angular.mock.inject(function($rootScope, $location, $controller) {
|
||||||
scope = $rootScope.$new();
|
scope = $rootScope.$new();
|
||||||
|
location = $location;
|
||||||
|
|
||||||
scope.state = {
|
scope.state = {
|
||||||
ui: {}
|
ui: {}
|
||||||
};
|
};
|
||||||
|
@ -39,6 +45,7 @@ describe('Login (new device) Controller unit test', function() {
|
||||||
email: emailMock,
|
email: emailMock,
|
||||||
auth: authMock,
|
auth: authMock,
|
||||||
pgp: pgpMock,
|
pgp: pgpMock,
|
||||||
|
publickeyVerifier: verifierMock,
|
||||||
keychain: keychainMock
|
keychain: keychainMock
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -69,12 +76,13 @@ describe('Login (new device) Controller unit test', function() {
|
||||||
_id: keyId,
|
_id: keyId,
|
||||||
publicKey: 'a'
|
publicKey: 'a'
|
||||||
}));
|
}));
|
||||||
emailMock.unlock.withArgs(sinon.match.any, passphrase).returns(resolves());
|
emailMock.unlock.returns(resolves('asd'));
|
||||||
keychainMock.putUserKeyPair.returns(resolves());
|
keychainMock.putUserKeyPair.returns(resolves());
|
||||||
|
|
||||||
scope.confirmPassphrase().then(function() {
|
scope.confirmPassphrase().then(function() {
|
||||||
expect(emailMock.unlock.calledOnce).to.be.true;
|
expect(emailMock.unlock.calledOnce).to.be.true;
|
||||||
expect(keychainMock.getUserKeyPair.calledOnce).to.be.true;
|
expect(keychainMock.getUserKeyPair.calledOnce).to.be.true;
|
||||||
|
expect(location.$$path).to.equal('/account');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -92,12 +100,14 @@ describe('Login (new device) Controller unit test', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
keychainMock.getUserKeyPair.withArgs(emailAddress).returns(resolves());
|
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());
|
keychainMock.putUserKeyPair.returns(resolves());
|
||||||
|
|
||||||
scope.confirmPassphrase().then(function() {
|
scope.confirmPassphrase().then(function() {
|
||||||
expect(emailMock.unlock.calledOnce).to.be.true;
|
expect(emailMock.unlock.calledOnce).to.be.true;
|
||||||
expect(keychainMock.getUserKeyPair.calledOnce).to.be.true;
|
expect(keychainMock.getUserKeyPair.calledOnce).to.be.true;
|
||||||
|
expect(location.$$path).to.equal('/login-verify-public-key');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
privateKeyArmored: mockKeyPair.privateKey.encryptedKey,
|
||||||
publicKeyArmored: mockKeyPair.publicKey.publicKey
|
publicKeyArmored: mockKeyPair.publicKey.publicKey
|
||||||
}).returns(resolves());
|
}).returns(resolves());
|
||||||
keychainStub.putUserKeyPair.withArgs().returns(resolves());
|
|
||||||
|
|
||||||
dao.unlock({
|
dao.unlock({
|
||||||
realname: name,
|
realname: name,
|
||||||
|
@ -224,30 +223,6 @@ describe('Email DAO unit tests', function() {
|
||||||
}).then(function() {
|
}).then(function() {
|
||||||
expect(pgpStub.generateKeys.calledOnce).to.be.true;
|
expect(pgpStub.generateKeys.calledOnce).to.be.true;
|
||||||
expect(pgpStub.importKeys.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();
|
done();
|
||||||
});
|
});
|
||||||
|
@ -387,7 +362,7 @@ describe('Email DAO unit tests', function() {
|
||||||
|
|
||||||
describe('#fetchMessages', function() {
|
describe('#fetchMessages', function() {
|
||||||
var imapListStub, imapGetStub, imapDeleteStub, localStoreStub;
|
var imapListStub, imapGetStub, imapDeleteStub, localStoreStub;
|
||||||
var opts, message, validUuid, corruptedUuid, verificationSubject;
|
var opts, message;
|
||||||
var notified;
|
var notified;
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
|
@ -407,9 +382,6 @@ describe('Email DAO unit tests', function() {
|
||||||
unread: true,
|
unread: true,
|
||||||
bodyParts: []
|
bodyParts: []
|
||||||
};
|
};
|
||||||
validUuid = '9A858952-17EE-4273-9E74-D309EAFDFAFB';
|
|
||||||
corruptedUuid = 'OMFG_FUCKING_BASTARD_UUID_FROM_HELL!';
|
|
||||||
verificationSubject = "[whiteout] New public key uploaded";
|
|
||||||
|
|
||||||
notified = false;
|
notified = false;
|
||||||
dao.onIncomingMessage = function(newMessages) {
|
dao.onIncomingMessage = function(newMessages) {
|
||||||
|
@ -455,103 +427,6 @@ describe('Email DAO unit tests', function() {
|
||||||
done();
|
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() {
|
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() {
|
describe('put user keypair', function() {
|
||||||
it('should fail', function(done) {
|
it('should fail', function(done) {
|
||||||
var keypair = {
|
var keypair = {
|
||||||
|
|
|
@ -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