From 99a6cda40d8c75bb6a7e6d3c091e3ec4a0653cc9 Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Tue, 3 Dec 2013 13:26:29 +0100 Subject: [PATCH] add silent public key verification --- src/js/app-config.js | 3 +- src/js/dao/email-dao-2.js | 65 ++++++- test/new-unit/email-dao-2-test.js | 282 +++++++++++++++++++++++++++++- 3 files changed, 346 insertions(+), 4 deletions(-) diff --git a/src/js/app-config.js b/src/js/app-config.js index a7646c4..e40c56c 100644 --- a/src/js/app-config.js +++ b/src/js/app-config.js @@ -38,7 +38,8 @@ define(function(require) { }, checkOutboxInterval: 5000, iconPath: '/img/icon.png', - verificationUrl: '/verify/' + verificationUrl: '/verify/', + verificationUuidLength: 36 }; /** diff --git a/src/js/dao/email-dao-2.js b/src/js/dao/email-dao-2.js index 4f5554a..56fd7c0 100644 --- a/src/js/dao/email-dao-2.js +++ b/src/js/dao/email-dao-2.js @@ -3,8 +3,8 @@ define(function(require) { var util = require('cryptoLib/util'), _ = require('underscore'), - str = require('js/app-config').string; - // config = require('js/app-config').config; + str = require('js/app-config').string, + config = require('js/app-config').config; var EmailDAO = function(keychain, imapClient, smtpClient, crypto, devicestorage) { var self = this; @@ -375,6 +375,19 @@ define(function(require) { return; } + if (isVerificationMail(message)) { + verify(message, function(err) { + if (err) { + self._account.busy = false; + callback(err); + return; + } + + after(); + }); + return; + } + // add the encrypted message to the local storage self._localStoreMessages({ folder: folder.path, @@ -425,6 +438,54 @@ define(function(require) { return delta; } + function isVerificationMail(email) { + return email.subject === str.subjectPrefix + str.verificationSubject; + } + + function verify(email, localCallback) { + var uuid, index, verifyUrlPrefix = config.cloudUrl + config.verificationUrl; + + if (!email.unread) { + // don't bother if the email was already marked as read + localCallback(); + return; + } + + index = email.body.indexOf(verifyUrlPrefix); + if (index === -1) { + // there's no url in the email, so forget about that. + localCallback(); + return; + } + + + uuid = email.body.substr(index + verifyUrlPrefix.length, config.verificationUuidLength); + self._keychain.verifyPublicKey(uuid, function(err) { + if (err) { + localCallback({ + errMsg: 'Verifying your public key failed: ' + err.errMsg + }); + return; + } + + // public key has been verified, mark the message as read, delete it, and ignore it in the future + self.markRead({ + folder: options.folder, + uid: email.uid + }, function(err) { + if (err) { + localCallback(err); + return; + } + + self._imapDeleteMessage({ + folder: options.folder, + uid: email.uid + }, localCallback); + }); + }); + } + function handleMessage(message, localCallback) { if (containsArmoredCiphertext(message)) { decrypt(message, localCallback); diff --git a/test/new-unit/email-dao-2-test.js b/test/new-unit/email-dao-2-test.js index 2e4fedf..8963202 100644 --- a/test/new-unit/email-dao-2-test.js +++ b/test/new-unit/email-dao-2-test.js @@ -14,7 +14,7 @@ define(function(require) { var dao, keychainStub, imapClientStub, smtpClientStub, pgpStub, devicestorageStub; var emailAddress, passphrase, asymKeySize, mockkeyId, dummyEncryptedMail, - dummyDecryptedMail, mockKeyPair, account, publicKey; + dummyDecryptedMail, mockKeyPair, account, publicKey, verificationMail, verificationUuid; beforeEach(function() { emailAddress = 'asdf@asdf.com'; @@ -32,6 +32,19 @@ define(function(require) { subject: '[whiteout] qweasd', body: '-----BEGIN PGP MESSAGE-----\nasd\n-----END PGP MESSAGE-----' }; + verificationUuid = 'OMFG_FUCKING_BASTARD_UUID_FROM_HELL!'; + verificationMail = { + from: [{ + name: 'Whiteout Test', + address: 'whiteout.test@t-online.de' + }], // sender address + to: [{ + address: 'safewithme.testuser@gmail.com' + }], // list of receivers + subject: "[whiteout] New public key uploaded", // Subject line + body: 'yadda yadda bla blabla foo bar https://keys.whiteout.io/verify/' + verificationUuid, // plaintext body + unread: true + }; dummyDecryptedMail = { uid: 1234, from: [{ @@ -1057,6 +1070,7 @@ define(function(require) { done(); }); }); + it('should error while storing messages from the remote locally', function(done) { var invocations, folder, localListStub, imapListStub, imapGetStub, localStoreStub; @@ -1096,6 +1110,7 @@ define(function(require) { done(); }); }); + it('should error while fetching messages from the remote', function(done) { var invocations, folder, localListStub, imapListStub, imapGetStub, localStoreStub; @@ -1135,6 +1150,271 @@ define(function(require) { done(); }); }); + + it('should verify an authentication mail', function(done) { + var invocations, folder, localListStub, imapListStub, + imapGetStub, markReadStub, imapDeleteStub; + + invocations = 0; + folder = 'FOLDAAAA'; + dao._account.folders = [{ + type: 'Folder', + path: folder, + messages: [] + }]; + + localListStub = sinon.stub(dao, '_localListMessages').yields(null, []); + imapListStub = sinon.stub(dao, '_imapListMessages').yields(null, [verificationMail]); + imapGetStub = sinon.stub(dao, '_imapGetMessage').yields(null, verificationMail); + keychainStub.verifyPublicKey.withArgs(verificationUuid).yields(); + markReadStub = sinon.stub(dao, 'markRead').withArgs({ + folder: folder, + uid: verificationMail.uid + }).yields(); + imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage').withArgs({ + folder: folder, + uid: verificationMail.uid + }).yields(); + + dao.sync({ + folder: folder + }, function(err) { + expect(err).to.not.exist; + + if (invocations === 0) { + expect(dao._account.busy).to.be.true; + invocations++; + return; + } + + expect(dao._account.busy).to.be.false; + expect(dao._account.folders[0].messages).to.be.empty; + expect(localListStub.calledOnce).to.be.true; + expect(imapListStub.calledOnce).to.be.true; + expect(imapGetStub.calledOnce).to.be.true; + expect(keychainStub.verifyPublicKey.calledOnce).to.be.true; + expect(markReadStub.calledOnce).to.be.true; + expect(imapDeleteStub.calledOnce).to.be.true; + + done(); + }); + }); + + it('should fail during deletion of an authentication mail', function(done) { + var invocations, folder, localListStub, imapListStub, + imapGetStub, markReadStub, imapDeleteStub; + + invocations = 0; + folder = 'FOLDAAAA'; + dao._account.folders = [{ + type: 'Folder', + path: folder, + messages: [] + }]; + + localListStub = sinon.stub(dao, '_localListMessages').yields(null, []); + imapListStub = sinon.stub(dao, '_imapListMessages').yields(null, [verificationMail]); + imapGetStub = sinon.stub(dao, '_imapGetMessage').yields(null, verificationMail); + keychainStub.verifyPublicKey.yields(); + markReadStub = sinon.stub(dao, 'markRead').yields(); + imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage').yields({}); + + dao.sync({ + folder: folder + }, function(err) { + + if (invocations === 0) { + expect(err).to.not.exist; + expect(dao._account.busy).to.be.true; + invocations++; + return; + } + + expect(err).to.exist; + expect(dao._account.busy).to.be.false; + expect(dao._account.folders[0].messages).to.be.empty; + expect(localListStub.calledOnce).to.be.true; + expect(imapListStub.calledOnce).to.be.true; + expect(imapGetStub.calledOnce).to.be.true; + expect(keychainStub.verifyPublicKey.calledOnce).to.be.true; + expect(markReadStub.calledOnce).to.be.true; + expect(imapDeleteStub.calledOnce).to.be.true; + + done(); + }); + }); + it('should fail during marking an authentication mail read', function(done) { + var invocations, folder, localListStub, imapListStub, + imapGetStub, markReadStub, imapDeleteStub; + + invocations = 0; + folder = 'FOLDAAAA'; + dao._account.folders = [{ + type: 'Folder', + path: folder, + messages: [] + }]; + + localListStub = sinon.stub(dao, '_localListMessages').yields(null, []); + imapListStub = sinon.stub(dao, '_imapListMessages').yields(null, [verificationMail]); + imapGetStub = sinon.stub(dao, '_imapGetMessage').yields(null, verificationMail); + keychainStub.verifyPublicKey.yields(); + markReadStub = sinon.stub(dao, 'markRead').yields({}); + imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage'); + + dao.sync({ + folder: folder + }, function(err) { + + if (invocations === 0) { + expect(err).to.not.exist; + expect(dao._account.busy).to.be.true; + invocations++; + return; + } + + expect(err).to.exist; + expect(dao._account.busy).to.be.false; + expect(dao._account.folders[0].messages).to.be.empty; + expect(localListStub.calledOnce).to.be.true; + expect(imapListStub.calledOnce).to.be.true; + expect(imapGetStub.calledOnce).to.be.true; + expect(keychainStub.verifyPublicKey.calledOnce).to.be.true; + expect(markReadStub.calledOnce).to.be.true; + expect(imapDeleteStub.called).to.be.false; + + done(); + }); + }); + it('should fail during verifying authentication', function(done) { + var invocations, folder, localListStub, imapListStub, + imapGetStub, markReadStub, imapDeleteStub; + + invocations = 0; + folder = 'FOLDAAAA'; + dao._account.folders = [{ + type: 'Folder', + path: folder, + messages: [] + }]; + + localListStub = sinon.stub(dao, '_localListMessages').yields(null, []); + imapListStub = sinon.stub(dao, '_imapListMessages').yields(null, [verificationMail]); + imapGetStub = sinon.stub(dao, '_imapGetMessage').yields(null, verificationMail); + keychainStub.verifyPublicKey.yields({}); + markReadStub = sinon.stub(dao, 'markRead'); + imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage'); + + dao.sync({ + folder: folder + }, function(err) { + + if (invocations === 0) { + expect(err).to.not.exist; + expect(dao._account.busy).to.be.true; + invocations++; + return; + } + + expect(err).to.exist; + expect(dao._account.busy).to.be.false; + expect(dao._account.folders[0].messages).to.be.empty; + expect(localListStub.calledOnce).to.be.true; + expect(imapListStub.calledOnce).to.be.true; + expect(imapGetStub.calledOnce).to.be.true; + expect(keychainStub.verifyPublicKey.calledOnce).to.be.true; + expect(markReadStub.called).to.be.false; + expect(imapDeleteStub.called).to.be.false; + + done(); + }); + }); + it('should not bother about read authentication mails', function(done) { + var invocations, folder, localListStub, imapListStub, + imapGetStub, markReadStub, imapDeleteStub; + + invocations = 0; + folder = 'FOLDAAAA'; + dao._account.folders = [{ + type: 'Folder', + path: folder, + messages: [] + }]; + + verificationMail.unread = false; + + localListStub = sinon.stub(dao, '_localListMessages').yields(null, []); + imapListStub = sinon.stub(dao, '_imapListMessages').yields(null, [verificationMail]); + imapGetStub = sinon.stub(dao, '_imapGetMessage').yields(null, verificationMail); + markReadStub = sinon.stub(dao, 'markRead'); + imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage'); + + dao.sync({ + folder: folder + }, function(err) { + expect(err).to.not.exist; + + if (invocations === 0) { + expect(dao._account.busy).to.be.true; + invocations++; + return; + } + + expect(dao._account.busy).to.be.false; + expect(dao._account.folders[0].messages).to.be.empty; + expect(localListStub.calledOnce).to.be.true; + expect(imapListStub.calledOnce).to.be.true; + expect(imapGetStub.calledOnce).to.be.true; + expect(keychainStub.verifyPublicKey.called).to.be.false; + expect(markReadStub.called).to.be.false; + expect(imapDeleteStub.called).to.be.false; + + done(); + }); + }); + it('should not bother about read authentication mails', function(done) { + var invocations, folder, localListStub, imapListStub, + imapGetStub, markReadStub, imapDeleteStub; + + invocations = 0; + folder = 'FOLDAAAA'; + dao._account.folders = [{ + type: 'Folder', + path: folder, + messages: [] + }]; + + verificationMail.body = 'url? there is no url.'; + + localListStub = sinon.stub(dao, '_localListMessages').yields(null, []); + imapListStub = sinon.stub(dao, '_imapListMessages').yields(null, [verificationMail]); + imapGetStub = sinon.stub(dao, '_imapGetMessage').yields(null, verificationMail); + markReadStub = sinon.stub(dao, 'markRead'); + imapDeleteStub = sinon.stub(dao, '_imapDeleteMessage'); + + dao.sync({ + folder: folder + }, function(err) { + expect(err).to.not.exist; + + if (invocations === 0) { + expect(dao._account.busy).to.be.true; + invocations++; + return; + } + + expect(dao._account.busy).to.be.false; + expect(dao._account.folders[0].messages).to.be.empty; + expect(localListStub.calledOnce).to.be.true; + expect(imapListStub.calledOnce).to.be.true; + expect(imapGetStub.calledOnce).to.be.true; + expect(keychainStub.verifyPublicKey.called).to.be.false; + expect(markReadStub.called).to.be.false; + expect(imapDeleteStub.called).to.be.false; + + done(); + }); + }); }); describe('markAsRead', function() {