diff --git a/src/js/app-config.js b/src/js/app-config.js
index 38be9a9..e6df9f4 100644
--- a/src/js/app-config.js
+++ b/src/js/app-config.js
@@ -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;
diff --git a/src/js/app-controller.js b/src/js/app-controller.js
index 0ad3a41..b799ee4 100644
--- a/src/js/app-controller.js
+++ b/src/js/app-controller.js
@@ -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);
diff --git a/src/js/controller/mail-list.js b/src/js/controller/mail-list.js
index e5e3a0e..0759cae 100644
--- a/src/js/controller/mail-list.js
+++ b/src/js/controller/mail-list.js
@@ -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);
};
/**
diff --git a/src/js/controller/write.js b/src/js/controller/write.js
index ed96ef8..845289a 100644
--- a/src/js/controller/write.js
+++ b/src/js/controller/write.js
@@ -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;
diff --git a/src/js/dao/keychain-dao.js b/src/js/dao/keychain-dao.js
index 7669460..2ee4e69 100644
--- a/src/js/dao/keychain-dao.js
+++ b/src/js/dao/keychain-dao.js
@@ -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);
diff --git a/src/js/dao/publickey-dao.js b/src/js/dao/publickey-dao.js
index 0674d76..a2ebba0 100644
--- a/src/js/dao/publickey-dao.js
+++ b/src/js/dao/publickey-dao.js
@@ -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);
});
};
diff --git a/src/js/dao/rest-dao.js b/src/js/dao/rest-dao.js
index 581747d..78f4236 100644
--- a/src/js/dao/rest-dao.js
+++ b/src/js/dao/rest-dao.js
@@ -68,7 +68,7 @@ define(function(require) {
xhr.onerror = function() {
callback({
- code: 404,
+ code: 42,
errMsg: 'Error calling GET on ' + options.uri
});
};
diff --git a/src/js/util/error.js b/src/js/util/error.js
index ba9b8bd..de4b80e 100644
--- a/src/js/util/error.js
+++ b/src/js/util/error.js
@@ -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) {
diff --git a/src/tpl/dialog.html b/src/tpl/dialog.html
index cc24300..ae98177 100644
--- a/src/tpl/dialog.html
+++ b/src/tpl/dialog.html
@@ -7,7 +7,8 @@