[WO-398] update revoked public keys

This commit is contained in:
Felix Hammerl 2014-05-23 10:52:34 +02:00
parent a730cad49d
commit a29ece8c82
12 changed files with 440 additions and 47 deletions

View File

@ -60,13 +60,16 @@ define(function(require) {
invitationSubject: 'Invitation to a private conversation',
invitationMessage: 'Hi,\n\nI use Whiteout Mail to send and receive encrypted email. I would like to exchange encrypted messages with you as well.\n\nPlease install the Whiteout Mail application. This application makes it easy to read and write messages securely with PGP encryption applied.\n\nGo to the Whiteout Networks homepage to learn more and to download the application: https://whiteout.io\n\n',
message: 'Hi,\n\nthis is a private conversation. To read my encrypted message below, simply open it in Whiteout Mail.\nOpen Whiteout Mail: https://chrome.google.com/webstore/detail/jjgghafhamholjigjoghcfcekhkonijg',
cryptPrefix: '-----BEGIN PGP MESSAGE-----',
cryptSuffix: '-----END PGP MESSAGE-----',
signature: '\n\n\n--\nSent from Whiteout Mail - Email encryption for the rest of us\nhttps://whiteout.io\n\n',
webSite: 'http://whiteout.io',
verificationSubject: '[whiteout] New public key uploaded',
sendBtnClear: 'Send',
sendBtnSecure: 'Send securely'
sendBtnSecure: 'Send securely',
updatePublicKeyTitle: 'Public Key Updated',
updatePublicKeyMsgNewKey: '{0} updated his key and may not be able to read encrypted messages sent with his old key. Update the key?',
updatePublicKeyMsgRemovedKey: '{0} revoked his key and may no longer be able to read encrypted messages. Remove the key?',
updatePublicKeyPosBtn: 'Yes',
updatePublicKeyNegBtn: 'No'
};
return app;

View File

@ -14,7 +14,9 @@ define(function(require) {
ImapClient = require('imap-client'),
RestDAO = require('js/dao/rest-dao'),
EmailDAO = require('js/dao/email-dao'),
config = require('js/app-config').config,
appConfig = require('js/app-config'),
config = appConfig.config,
str = appConfig.string,
KeychainDAO = require('js/dao/keychain-dao'),
PublicKeyDAO = require('js/dao/publickey-dao'),
LawnchairDAO = require('js/dao/lawnchair-dao'),
@ -64,11 +66,25 @@ define(function(require) {
pubkeyDao = new PublicKeyDAO(restDao);
oauth = new OAuth(new RestDAO('https://www.googleapis.com'));
self._keychain = keychain = new KeychainDAO(lawnchairDao, pubkeyDao);
keychain.requestPermissionForKeyUpdate = function(params, callback) {
var message = params.newKey ? str.updatePublicKeyMsgNewKey : str.updatePublicKeyMsgRemovedKey;
message = message.replace('{0}', params.userId);
options.onError({
title: str.updatePublicKeyTitle,
message: message,
positiveBtnStr: str.updatePublicKeyPosBtn,
negativeBtnStr: str.updatePublicKeyNegBtn,
showNegativeBtn: true,
callback: callback
});
};
self._appConfigStore = appConfigStore = new DeviceStorageDAO(new LawnchairDAO());
self._auth = new Auth(appConfigStore, oauth, new RestDAO('/ca'));
self._userStorage = userStorage = new DeviceStorageDAO(lawnchairDao);
self._invitationDao = new InvitationDAO(restDao);
self._keychain = keychain = new KeychainDAO(lawnchairDao, pubkeyDao);
self._crypto = pgp = new PGP();
self._pgpbuilder = pgpbuilder = new PgpBuilder();
self._emailDao = emailDao = new EmailDAO(keychain, pgp, userStorage, pgpbuilder, mailreader);

View File

@ -6,7 +6,7 @@ define(function(require) {
appController = require('js/app-controller'),
IScroll = require('iscroll'),
notification = require('js/util/notification'),
emailDao, outboxBo;
emailDao, outboxBo, keychainDao;
var MailListCtrl = function($scope, $timeout) {
//
@ -15,6 +15,7 @@ define(function(require) {
emailDao = appController._emailDao;
outboxBo = appController._outboxBo;
keychainDao = appController._keychain;
//
// scope functions
@ -55,17 +56,25 @@ define(function(require) {
$scope.state.mailList.selected = email;
$scope.state.read.toggle(true);
emailDao.decryptBody({
message: email
}, $scope.onError);
keychainDao.refreshKeyForUserId(email.from[0].address, onKeyRefreshed);
// if the email is unread, please sync the new state.
// otherweise forget about it.
if (!email.unread) {
return;
function onKeyRefreshed(err) {
if (err) {
$scope.onError(err);
}
emailDao.decryptBody({
message: email
}, $scope.onError);
// if the email is unread, please sync the new state.
// otherweise forget about it.
if (!email.unread) {
return;
}
$scope.toggleUnread(email);
}
$scope.toggleUnread(email);
};
/**

View File

@ -7,7 +7,7 @@ define(function(require) {
aes = require('cryptoLib/aes-cbc'),
util = require('cryptoLib/util'),
str = require('js/app-config').string,
crypto, emailDao, outbox;
crypto, emailDao, outbox, keychainDao;
//
// Controller
@ -17,6 +17,8 @@ define(function(require) {
crypto = appController._crypto;
emailDao = appController._emailDao,
outbox = appController._outboxBo;
keychainDao = appController._keychain;
// set default value so that the popover height is correct on init
$scope.keyId = 'XXXXXXXX';
@ -185,7 +187,8 @@ define(function(require) {
}
// check if to address is contained in known public keys
emailDao._keychain.getReceiverPublicKey(recipient.address, function(err, key) {
// when we write an email, we always need to work with the latest keys available
keychainDao.refreshKeyForUserId(recipient.address, function(err, key) {
if (err) {
$scope.onError(err);
return;

View File

@ -66,6 +66,96 @@ define(function(require) {
});
};
/**
* Checks for public key updates of a given user id
* @param {String} userId The user id (email address) for which to check the key
* @param {Function} callback(error, key) Invoked when the key has been updated or an error occurred
*/
KeychainDAO.prototype.refreshKeyForUserId = function(userId, callback) {
var self = this;
// get the public key corresponding to the userId
self.getReceiverPublicKey(userId, function(err, localKey) {
if (!localKey || !localKey._id) {
// there is no key available, no need to refresh
callback();
return;
}
// check if the key id still exists on the key server
checkKeyExists(localKey);
});
// checks if the user's key has been revoked by looking up the key id
function checkKeyExists(localKey) {
self._publicKeyDao.get(localKey._id, function(err, cloudKey) {
if (err && err.code === 42) {
// we're offline, we're done checking the key
callback(null, localKey);
return;
}
if (err) {
// there was an error, exit and inform
callback(err);
return;
}
if (cloudKey && cloudKey._id === localKey._id) {
// the key is present on the server, all is well
callback(null, localKey);
return;
}
// the key has changed, update the key
updateKey(localKey);
});
}
function updateKey(localKey) {
// look for an updated key for the user id
self._publicKeyDao.getByUserId(userId, function(err, newKey) {
// offline?
if (err && err.code === 42) {
callback(null, localKey);
return;
}
if (err) {
callback(err);
return;
}
// the public key has changed, we need to ask for permission to update the key
self.requestPermissionForKeyUpdate({
userId: userId,
newKey: newKey
}, function(granted) {
if (!granted) {
// permission was not given to update the key, so don't overwrite the old one!
callback(null, localKey);
return;
}
// permission to update the key was given, so delete the old one and persist the new one
self.removeLocalPublicKey(localKey._id, function(err) {
if (err || !newKey) {
// error or no new key to save
callback(err);
return;
}
// persist the new key and return it
self.saveLocalPublicKey(newKey, function(err) {
callback(err, err ? undefined : newKey);
});
});
});
});
}
};
/**
* Look up a reveiver's public key by user id
* @param userId [String] the receiver's email address
@ -92,23 +182,27 @@ define(function(require) {
// no public key by that user id in storage
// find from cloud by email address
self._publicKeyDao.getByUserId(userId, onKeyGotten);
self._publicKeyDao.getByUserId(userId, onKeyReceived);
});
function onKeyGotten(err, cloudPubkey) {
function onKeyReceived(err, cloudPubkey) {
if (err && err.code === 42) {
// offline
callback();
return;
}
if (err) {
callback(err);
return;
}
if (!cloudPubkey) {
// no public key for that user
// public key has been deleted without replacement
callback();
return;
}
// there is a public key for that user already in the cloud...
// save to local storage
self.saveLocalPublicKey(cloudPubkey, function(err) {
if (err) {
callback(err);

View File

@ -34,19 +34,17 @@ define(function() {
this._restDao.get({
uri: uri
}, function(err, key) {
if (err && err.code === 404) {
callback();
return;
}
if (err) {
callback(err);
return;
}
if (!key || !key._id) {
callback({
errMsg: 'No public key for that user!'
});
return;
}
callback(null, key);
callback(null, (key && key._id) ? key : undefined);
});
};

View File

@ -68,7 +68,7 @@ define(function(require) {
xhr.onerror = function() {
callback({
code: 404,
code: 42,
errMsg: 'Error calling GET on ' + options.uri
});
};

View File

@ -2,7 +2,6 @@ define(function() {
'use strict';
var er = {};
er.attachHandler = function(scope) {
scope.onError = function(options) {
if (!options) {
@ -19,7 +18,11 @@ define(function() {
scope.state.dialog = {
open: true,
title: options.title || 'Error',
message: options.errMsg || options.message
message: options.errMsg || options.message,
positiveBtnStr: options.positiveBtnStr || 'Ok',
negativeBtnStr: options.negativeBtnStr || 'Cancel',
showNegativeBtn: options.showNegativeBtn || false,
callback: options.callback
};
// don't call apply for synchronous calls
if (!options.sync) {

View File

@ -7,7 +7,8 @@
<div class="content">
<p>{{state.dialog.message}}</p>
<div class="control">
<button ng-click="confirm(true)" class="btn">Ok</button>
<button ng-click="confirm(false)" class="btn btn-alt" ng-show="state.dialog.showNegativeBtn">{{state.dialog.negativeBtnStr}}</button>
<button ng-click="confirm(true)" class="btn">{{state.dialog.positiveBtnStr}}</button>
</div>
</div><!-- /.content -->
</div><!-- /.lightbox-body -->

View File

@ -56,6 +56,255 @@ define(function(require) {
});
});
describe('refreshKeyForUserId', function() {
var getPubKeyStub,
oldKey = {
_id: 123
}, newKey = {
_id: 456
};
beforeEach(function() {
getPubKeyStub = sinon.stub(keychainDao, 'getReceiverPublicKey');
});
afterEach(function() {
keychainDao.getReceiverPublicKey.restore();
delete keychainDao.requestPermissionForKeyUpdate;
});
it('should not find a key', function(done) {
getPubKeyStub.yields();
keychainDao.refreshKeyForUserId(testUser, function(err, key) {
expect(err).to.not.exist;
expect(key).to.not.exist;
done();
});
});
it('should not update the key when up to date', function(done) {
getPubKeyStub.yields(null, oldKey);
pubkeyDaoStub.get.withArgs(oldKey._id).yields(null, oldKey);
keychainDao.refreshKeyForUserId(testUser, function(err, key) {
expect(err).to.not.exist;
expect(key).to.to.equal(oldKey);
expect(getPubKeyStub.calledOnce).to.be.true;
expect(pubkeyDaoStub.get.calledOnce).to.be.true;
done();
});
});
it('should update key', function(done) {
getPubKeyStub.yields(null, oldKey);
pubkeyDaoStub.get.withArgs(oldKey._id).yields();
pubkeyDaoStub.getByUserId.withArgs(testUser).yields(null, newKey);
keychainDao.requestPermissionForKeyUpdate = function(opts, cb) {
expect(opts.userId).to.equal(testUser);
expect(opts.newKey).to.equal(newKey);
cb(true);
};
lawnchairDaoStub.remove.withArgs('publickey_' + oldKey._id).yields();
lawnchairDaoStub.persist.withArgs('publickey_' + newKey._id, newKey).yields();
keychainDao.refreshKeyForUserId(testUser, function(err, key) {
expect(err).to.not.exist;
expect(key).to.equal(newKey);
expect(getPubKeyStub.calledOnce).to.be.true;
expect(pubkeyDaoStub.get.calledOnce).to.be.true;
expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true;
expect(lawnchairDaoStub.remove.calledOnce).to.be.true;
expect(lawnchairDaoStub.persist.calledOnce).to.be.true;
done();
});
});
it('should remove key', function(done) {
getPubKeyStub.yields(null, oldKey);
pubkeyDaoStub.get.withArgs(oldKey._id).yields();
pubkeyDaoStub.getByUserId.withArgs(testUser).yields();
keychainDao.requestPermissionForKeyUpdate = function(opts, cb) {
expect(opts.userId).to.equal(testUser);
expect(opts.newKey).to.not.exist;
cb(true);
};
lawnchairDaoStub.remove.withArgs('publickey_' + oldKey._id).yields();
keychainDao.refreshKeyForUserId(testUser, function(err, key) {
expect(err).to.not.exist;
expect(key).to.not.exist;
expect(getPubKeyStub.calledOnce).to.be.true;
expect(pubkeyDaoStub.get.calledOnce).to.be.true;
expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true;
expect(lawnchairDaoStub.remove.calledOnce).to.be.true;
expect(lawnchairDaoStub.persist.called).to.be.false;
done();
});
});
it('should go offline while fetching new key', function(done) {
getPubKeyStub.yields(null, oldKey);
pubkeyDaoStub.get.withArgs(oldKey._id).yields();
pubkeyDaoStub.getByUserId.withArgs(testUser).yields({
code: 42
});
keychainDao.refreshKeyForUserId(testUser, function(err, key) {
expect(err).to.not.exist;
expect(key).to.to.equal(oldKey);
expect(getPubKeyStub.calledOnce).to.be.true;
expect(pubkeyDaoStub.get.calledOnce).to.be.true;
expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true;
expect(lawnchairDaoStub.remove.called).to.be.false;
expect(lawnchairDaoStub.persist.called).to.be.false;
done();
});
});
it('should not remove old key on user rejection', function(done) {
getPubKeyStub.yields(null, oldKey);
pubkeyDaoStub.get.withArgs(oldKey._id).yields();
pubkeyDaoStub.getByUserId.withArgs(testUser).yields(null, newKey);
keychainDao.requestPermissionForKeyUpdate = function(opts, cb) {
expect(opts.userId).to.equal(testUser);
expect(opts.newKey).to.exist;
cb(false);
};
keychainDao.refreshKeyForUserId(testUser, function(err, key) {
expect(err).to.not.exist;
expect(key).to.equal(oldKey);
expect(getPubKeyStub.calledOnce).to.be.true;
expect(pubkeyDaoStub.get.calledOnce).to.be.true;
expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true;
expect(lawnchairDaoStub.remove.called).to.be.false;
expect(lawnchairDaoStub.persist.called).to.be.false;
done();
});
});
it('should update not the key when offline', function(done) {
getPubKeyStub.yields(null, oldKey);
pubkeyDaoStub.get.withArgs(oldKey._id).yields({
code: 42
});
keychainDao.refreshKeyForUserId(testUser, function(err, key) {
expect(err).to.not.exist;
expect(key).to.to.equal(oldKey);
expect(getPubKeyStub.calledOnce).to.be.true;
expect(pubkeyDaoStub.get.calledOnce).to.be.true;
expect(pubkeyDaoStub.getByUserId.called).to.be.false;
expect(lawnchairDaoStub.remove.called).to.be.false;
expect(lawnchairDaoStub.persist.called).to.be.false;
done();
});
});
it('should error while persisting new key', function(done) {
getPubKeyStub.yields(null, oldKey);
pubkeyDaoStub.get.withArgs(oldKey._id).yields();
pubkeyDaoStub.getByUserId.withArgs(testUser).yields(null, newKey);
keychainDao.requestPermissionForKeyUpdate = function(opts, cb) {
expect(opts.userId).to.equal(testUser);
expect(opts.newKey).to.equal(newKey);
cb(true);
};
lawnchairDaoStub.remove.withArgs('publickey_' + oldKey._id).yields();
lawnchairDaoStub.persist.yields({});
keychainDao.refreshKeyForUserId(testUser, function(err, key) {
expect(err).to.exist;
expect(key).to.not.exist;
expect(getPubKeyStub.calledOnce).to.be.true;
expect(pubkeyDaoStub.get.calledOnce).to.be.true;
expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true;
expect(lawnchairDaoStub.remove.calledOnce).to.be.true;
expect(lawnchairDaoStub.persist.calledOnce).to.be.true;
done();
});
});
it('should error while deleting old key', function(done) {
getPubKeyStub.yields(null, oldKey);
pubkeyDaoStub.get.withArgs(oldKey._id).yields();
pubkeyDaoStub.getByUserId.withArgs(testUser).yields();
keychainDao.requestPermissionForKeyUpdate = function(opts, cb) {
expect(opts.userId).to.equal(testUser);
cb(true);
};
lawnchairDaoStub.remove.yields({});
keychainDao.refreshKeyForUserId(testUser, function(err, key) {
expect(err).to.exist;
expect(key).to.not.exist;
expect(getPubKeyStub.calledOnce).to.be.true;
expect(pubkeyDaoStub.get.calledOnce).to.be.true;
expect(lawnchairDaoStub.remove.calledOnce).to.be.true;
expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true;
expect(lawnchairDaoStub.persist.called).to.be.false;
done();
});
});
it('should error while persisting new key', function(done) {
getPubKeyStub.yields(null, oldKey);
pubkeyDaoStub.get.withArgs(oldKey._id).yields();
pubkeyDaoStub.getByUserId.withArgs(testUser).yields(null, newKey);
keychainDao.requestPermissionForKeyUpdate = function(opts, cb) {
expect(opts.userId).to.equal(testUser);
expect(opts.newKey).to.equal(newKey);
cb(true);
};
lawnchairDaoStub.remove.withArgs('publickey_' + oldKey._id).yields();
lawnchairDaoStub.persist.yields({});
keychainDao.refreshKeyForUserId(testUser, function(err, key) {
expect(err).to.exist;
expect(key).to.not.exist;
expect(getPubKeyStub.calledOnce).to.be.true;
expect(pubkeyDaoStub.get.calledOnce).to.be.true;
expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true;
expect(lawnchairDaoStub.remove.calledOnce).to.be.true;
expect(lawnchairDaoStub.persist.calledOnce).to.be.true;
done();
});
});
it('should error when get failed', function(done) {
getPubKeyStub.yields(null, oldKey);
pubkeyDaoStub.get.withArgs(oldKey._id).yields({});
keychainDao.refreshKeyForUserId(testUser, function(err, key) {
expect(err).to.exist;
expect(key).to.not.exist;
done();
});
});
});
describe('lookup public key', function() {
it('should fail', function(done) {
keychainDao.lookupPublicKey(undefined, function(err, key) {
@ -182,7 +431,7 @@ define(function(require) {
it('should fail due to error in pubkey dao', function(done) {
lawnchairDaoStub.list.yields();
pubkeyDaoStub.getByUserId.yields(42);
pubkeyDaoStub.getByUserId.yields({});
keychainDao.getReceiverPublicKey(testUser, function(err, key) {
expect(err).to.exist;

View File

@ -19,9 +19,9 @@ define(function(require) {
hasChrome, hasSocket, hasRuntime, hasIdentity;
beforeEach(function() {
hasChrome = !! window.chrome;
hasSocket = !! window.chrome.socket;
hasIdentity = !! window.chrome.identity;
hasChrome = !!window.chrome;
hasSocket = !!window.chrome.socket;
hasIdentity = !!window.chrome.identity;
if (!hasChrome) {
window.chrome = {};
}
@ -62,7 +62,7 @@ define(function(require) {
keychainMock = sinon.createStubInstance(KeychainDAO);
emailDaoMock._keychain = keychainMock;
appController._keychain = keychainMock;
deviceStorageMock = sinon.createStubInstance(DeviceStorageDAO);
emailDaoMock._devicestorage = deviceStorageMock;
@ -224,9 +224,12 @@ define(function(require) {
});
describe('select', function() {
it('should decrypt, focus, and mark an unread mail as read', function() {
it('should decrypt, focus mark an unread mail as read', function() {
var mail = {
unread: true
from: [{
address: 'asd'
}],
unread: true,
};
scope.state = {
nav: {
@ -240,16 +243,23 @@ define(function(require) {
}
};
keychainMock.refreshKeyForUserId.withArgs(mail.from[0].address).yields();
scope.select(mail);
expect(emailDaoMock.decryptBody.calledOnce).to.be.true;
expect(keychainMock.refreshKeyForUserId.calledOnce).to.be.true;
expect(scope.state.mailList.selected).to.equal(mail);
});
it('should decrypt and focus a read mail', function() {
var mail = {
from: [{
address: 'asd'
}],
unread: false
};
scope.state = {
mailList: {},
read: {
@ -262,9 +272,12 @@ define(function(require) {
}
};
keychainMock.refreshKeyForUserId.withArgs(mail.from[0].address).yields();
scope.select(mail);
expect(emailDaoMock.decryptBody.calledOnce).to.be.true;
expect(keychainMock.refreshKeyForUserId.calledOnce).to.be.true;
expect(scope.state.mailList.selected).to.equal(mail);
});
});

View File

@ -12,7 +12,7 @@ define(function(require) {
describe('Write controller unit test', function() {
var ctrl, scope,
origEmailDao, origOutbox,
origEmailDao, origOutbox, origKeychain,
emailDaoMock, keychainMock, outboxMock, emailAddress;
beforeEach(function() {
@ -20,6 +20,7 @@ define(function(require) {
// outbox and email dao to restore it after the tests
origEmailDao = appController._emailDao;
origOutbox = appController._outboxBo;
origKeychain = appController._keychain;
outboxMock = sinon.createStubInstance(OutboxBO);
appController._outboxBo = outboxMock;
@ -33,7 +34,7 @@ define(function(require) {
};
keychainMock = sinon.createStubInstance(KeychainDAO);
emailDaoMock._keychain = keychainMock;
appController._keychain = keychainMock;
angular.module('writetest', []);
mocks.module('writetest');
@ -50,6 +51,7 @@ define(function(require) {
// restore the app controller
appController._emailDao = origEmailDao;
appController._outboxBo = origOutbox;
appController._keychain = origKeychain;
});
describe('scope variables', function() {
@ -215,14 +217,15 @@ define(function(require) {
address: 'asds@example.com'
};
keychainMock.getReceiverPublicKey.withArgs(recipient.address).yields({
keychainMock.refreshKeyForUserId.withArgs(recipient.address).yields({
errMsg: '404 not found yadda yadda'
});
scope.onError = function() {
expect(recipient.key).to.be.undefined;
expect(recipient.secure).to.be.false;
expect(scope.checkSendStatus.callCount).to.equal(1);
expect(keychainMock.getReceiverPublicKey.calledOnce).to.be.true;
expect(keychainMock.refreshKeyForUserId.calledOnce).to.be.true;
done();
};
@ -234,16 +237,17 @@ define(function(require) {
address: 'asdf@example.com'
};
keychainMock.getReceiverPublicKey.yields(null, {
keychainMock.refreshKeyForUserId.withArgs(recipient.address).yields(null, {
userId: 'asdf@example.com'
});
scope.$digest = function() {
expect(recipient.key).to.deep.equal({
userId: 'asdf@example.com'
});
expect(recipient.secure).to.be.true;
expect(scope.checkSendStatus.callCount).to.equal(2);
expect(keychainMock.getReceiverPublicKey.calledOnce).to.be.true;
expect(keychainMock.refreshKeyForUserId.calledOnce).to.be.true;
done();
};