diff --git a/src/js/app-config.js b/src/js/app-config.js index d883675..fdf0e18 100644 --- a/src/js/app-config.js +++ b/src/js/app-config.js @@ -36,8 +36,8 @@ define([], function() { app.string = { subject: '[whiteout] Encrypted message', message: 'this is a private conversation. To read my encrypted message below, simply install Whiteout Mail for Chrome. The app is really easy to use and automatically encrypts sent emails, so that only the two of us can read them: https://chrome.google.com/webstore/detail/whiteout-mail/jjgghafhamholjigjoghcfcekhkonijg', - cryptPrefix: '-----BEGIN ENCRYPTED MESSAGE-----', - cryptSuffix: '-----END ENCRYPTED MESSAGE-----', + cryptPrefix: '-----BEGIN PGP MESSAGE-----', + cryptSuffix: '-----END PGP MESSAGE-----', signature: 'Sent securely from whiteout mail', webSite: 'http://whiteout.io' }; diff --git a/src/js/app-controller.js b/src/js/app-controller.js index 553a3c6..5971ba6 100644 --- a/src/js/app-controller.js +++ b/src/js/app-controller.js @@ -5,14 +5,13 @@ define(function(require) { 'use strict'; var $ = require('jquery'), - util = require('cryptoLib/util'), ImapClient = require('imap-client'), SmtpClient = require('smtp-client'), EmailDAO = require('js/dao/email-dao'), KeychainDAO = require('js/dao/keychain-dao'), cloudstorage = require('js/dao/cloudstorage-dao'), DeviceStorageDAO = require('js/dao/devicestorage-dao'), - Crypto = require('js/crypto/crypto'), + PGP = require('js/crypto/pgp'), config = require('js/app-config').config; require('cordova'); @@ -65,18 +64,8 @@ define(function(require) { return; } - self.getSalt(function(err, salt) { - if (err || !salt) { - callback({ - errMsg: 'Error gettin salt on login!', - err: err - }); - return; - } - - // login using the received email address - self.login(emailAddress, password, salt, token, callback); - }); + // login using the received email address + self.login(emailAddress, password, token, callback); }); } ); @@ -132,45 +121,12 @@ define(function(require) { } }; - /** - * Fetch a random salt from the app storage or generate a new one - */ - self.getSalt = function(callback) { - var itemKey = 'salt', - salt; - - self._appConfigStore.listItems(itemKey, 0, null, function(err, cachedItems) { - if (err) { - callback(err); - return; - } - - // generate random salt if non exists - if (!cachedItems || cachedItems.length < 1) { - salt = util.random(config.symKeySize); - - // store the salt locally - self._appConfigStore.storeList([salt], itemKey, function(err) { - if (err) { - callback(err); - return; - } - - callback(null, salt); - }); - return; - } - - callback(null, cachedItems[0]); - }); - }; - /** * Instanciate the mail email data access object and its dependencies. Login to imap on init. */ - self.login = function(userId, password, salt, token, callback) { + self.login = function(userId, password, token, callback) { var auth, imapOptions, smtpOptions, - keychain, imapClient, smtpClient, crypto, userStorage; + keychain, imapClient, smtpClient, pgp, userStorage; // create mail credentials objects for imap/smtp auth = { @@ -197,17 +153,14 @@ define(function(require) { keychain = new KeychainDAO(cloudstorage); imapClient = new ImapClient(imapOptions); smtpClient = new SmtpClient(smtpOptions); - crypto = new Crypto(); + pgp = new PGP(); userStorage = new DeviceStorageDAO(); - self._emailDao = new EmailDAO(keychain, imapClient, smtpClient, crypto, userStorage); + self._emailDao = new EmailDAO(keychain, imapClient, smtpClient, pgp, userStorage); // init email dao var account = { emailAddress: userId, - symKeySize: config.symKeySize, - symIvSize: config.symIvSize, - asymKeySize: config.asymKeySize, - salt: salt + asymKeySize: config.asymKeySize }; self._emailDao.init(account, password, callback); }; diff --git a/src/js/crypto/pgp.js b/src/js/crypto/pgp.js index efdc032..e9d82a1 100644 --- a/src/js/crypto/pgp.js +++ b/src/js/crypto/pgp.js @@ -25,7 +25,15 @@ define(function(require) { } // generate keypair (keytype 1=RSA) - keys = openpgp.generate_key_pair(1, options.keySize, options.emailAddress, options.passphrase); + try { + keys = openpgp.generate_key_pair(1, options.keySize, options.emailAddress, options.passphrase); + } catch (e) { + callback({ + errMsg: 'Keygeneration failed!', + err: e + }); + return; + } callback(null, { keyId: util.hexstrdump(keys.privateKey.getKeyId()).toUpperCase(), diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index cc70235..2206965 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -22,7 +22,7 @@ define(function(require) { /** * Inits all dependencies */ - EmailDAO.prototype.init = function(account, password, callback) { + EmailDAO.prototype.init = function(account, passphrase, callback) { var self = this; self._account = account; @@ -55,25 +55,41 @@ define(function(require) { } function initCrypto(storedKeypair) { - self._crypto.init({ - emailAddress: emailAddress, - password: password, - salt: self._account.salt, - keySize: self._account.symKeySize, - rsaKeySize: self._account.asymKeySize, - storedKeypair: storedKeypair + if (storedKeypair && storedKeypair.privateKey && storedKeypair.publicKey) { + // import existing key pair into crypto module + self._crypto.importKeys({ + passphrase: passphrase, + privateKeyArmored: storedKeypair.privateKey.encryptedKey, + publicKeyArmored: storedKeypair.publicKey.publicKey + }, callback); + return; + } + + // no keypair for is stored for the user... generate a new one + self._crypto.generateKeys({ + emailAddress: self._account.emailAddress, + keySize: self._account.asymKeySize, + passphrase: passphrase }, function(err, generatedKeypair) { if (err) { callback(err); return; } - if (generatedKeypair) { - // persist newly generated keypair - self._keychain.putUserKeyPair(generatedKeypair, callback); - } else { - callback(); - } + // persist newly generated keypair + var newKeypair = { + publicKey: { + _id: generatedKeypair.keyId, + userId: self._account.emailAddress, + publicKey: generatedKeypair.publicKeyArmored + }, + privateKey: { + _id: generatedKeypair.keyId, + userId: self._account.emailAddress, + encryptedKey: generatedKeypair.privateKeyArmored + } + }; + self._keychain.putUserKeyPair(newKeypair, callback); }); } }; @@ -172,7 +188,6 @@ define(function(require) { */ EmailDAO.prototype.listMessages = function(options, callback) { var self = this, - displayList = [], encryptedList = []; // validate options @@ -193,8 +208,6 @@ define(function(require) { // find encrypted items emails.forEach(function(i) { if (typeof i.body === 'string' && i.body.indexOf(str.cryptPrefix) !== -1 && i.body.indexOf(str.cryptSuffix) !== -1) { - // add item to plaintext list for display later - displayList.push(i); // parse ct object from ascii armored message block encryptedList.push(parseMessageBlock(i)); } @@ -206,34 +219,18 @@ define(function(require) { } // decrypt items - decryptList(encryptedList, function(err, decryptedList) { - if (err) { - callback(err); - return; - } - - // replace encrypted subject and body - for (var j = 0; j < displayList.length; j++) { - displayList[j].subject = decryptedList[j].subject; - displayList[j].body = decryptedList[j].body; - } - - // return only decrypted items - callback(null, displayList); - }); + decryptList(encryptedList, callback); }); function parseMessageBlock(email) { - var ctMessageBase64, ctMessageJson, ctMessage; + var messageBlock; // parse email body for encrypted message block try { - // get base64 encoded message block - ctMessageBase64 = email.body.split(str.cryptPrefix)[1].split(str.cryptSuffix)[0].trim(); - // decode bae64 - ctMessageJson = atob(ctMessageBase64); - // parse json string to get ciphertext object - ctMessage = JSON.parse(ctMessageJson); + // get ascii armored message block by prefix and suffix + messageBlock = email.body.split(str.cryptPrefix)[1].split(str.cryptSuffix)[0]; + // add prefix and suffix again + email.body = str.cryptPrefix + messageBlock + str.cryptSuffix; } catch (e) { callback({ errMsg: 'Error parsing encrypted message block!' @@ -241,40 +238,32 @@ define(function(require) { return; } - return ctMessage; + return email; } - function decryptList(encryptedList, callback) { - var already, pubkeyIds = []; - - // gather public key ids required to verify signatures - encryptedList.forEach(function(i) { - already = null; - already = _.findWhere(pubkeyIds, { - _id: i.senderPk - }); - if (!already) { - pubkeyIds.push({ - _id: i.senderPk - }); - } + function decryptList(list, callback) { + var after = _.after(list.length, function() { + callback(null, list); }); - // fetch public keys from keychain - self._keychain.getPublicKeys(pubkeyIds, function(err, senderPubkeys) { - if (err) { - callback(err); - return; - } + list.forEach(function(i) { + // gather public keys required to verify signatures + var sender = i.from[0].address; + self._keychain.getReveiverPublicKey(sender, function(err, senderPubkey) { - // verfiy signatures and re-encrypt item keys - self._crypto.decryptListForUser(encryptedList, senderPubkeys, function(err, decryptedList) { - if (err) { - callback(err); - return; - } + // decrypt and verfiy signatures + self._crypto.decrypt(i.body, senderPubkey.publicKey, function(err, decrypted) { + if (err) { + callback(err); + return; + } - callback(null, decryptedList); + decrypted = JSON.parse(decrypted); + i.subject = decrypted.subject; + i.body = decrypted.body; + + after(); + }); }); }); } @@ -507,7 +496,7 @@ define(function(require) { } // public key found... encrypt and send - self.encryptForUser(email, receiverPubkey, callback); + self.encryptForUser(email, receiverPubkey.publicKey, callback); }); }; @@ -516,101 +505,41 @@ define(function(require) { */ EmailDAO.prototype.encryptForUser = function(email, receiverPubkey, callback) { var self = this, - ptItems = bundleForEncryption(email), + pt = JSON.stringify(email), receiverPubkeys = [receiverPubkey]; - // encrypt the email - self._crypto.encryptListForUser(ptItems, receiverPubkeys, function(err, encryptedList) { + // get own public key so send message can be read + self._crypto.exportKeys(function(err, ownKeys) { if (err) { callback(err); return; } - // bundle encrypted email together for sending - bundleEncryptedItems(email, encryptedList); + // add own public key to receiver list + receiverPubkeys.push(ownKeys.publicKeyArmored); + // encrypt the email + self._crypto.encrypt(pt, receiverPubkeys, function(err, ct) { + if (err) { + callback(err); + return; + } - self.send(email, callback); + // bundle encrypted email together for sending + frameEncryptedMessage(email, ct); + + self.send(email, callback); + }); }); }; - /** - * Encrypt an email symmetrically for a new user, write the secret one time key to the cloudstorage REST service, and send the email client side via SMTP. - */ - EmailDAO.prototype.encryptForNewUser = function(email, callback) { - var self = this, - ptItems = bundleForEncryption(email); - - self._crypto.symEncryptList(ptItems, function(err, result) { - if (err) { - callback(err); - return; - } - - // bundle encrypted email together for sending - bundleEncryptedItems(email, result.list); - - // TODO: write result.key to REST endpoint - - self.send(email, callback); - }); - }; - - /** - * Give the email a newly generated UUID, remove its attachments, and bundle all plaintext items to a batchable array for encryption. - */ - - function bundleForEncryption(email) { - var ptItems = [email]; - - // generate a new UUID for the new email - email.id = util.UUID(); - - // add attachment to encryption batch and remove from email object - if (email.attachments) { - email.attachments.forEach(function(attachment) { - attachment.id = email.id; - ptItems.push(attachment); - }); - delete email.attachments; - } - - return ptItems; - } - - /** - * Frame the encrypted email message and append the encrypted attachments. - */ - - function bundleEncryptedItems(email, encryptedList) { - var i; - - // replace body and subject of the email with encrypted versions - email = frameEncryptedMessage(email, encryptedList[0]); - - // add encrypted attachments - if (encryptedList.length > 1) { - email.attachments = []; - } - for (i = 1; i < encryptedList.length; i++) { - email.attachments.push({ - fileName: 'Encrypted Attachment ' + i, - contentType: 'application/octet-stream', - uint8Array: util.binStr2Uint8Arr(JSON.stringify(encryptedList[i])) - }); - } - } - /** * Frames an encrypted message in base64 Format. */ function frameEncryptedMessage(email, ct) { - var to, greeting, ctBase64; + var to, greeting; - var SUBJECT = str.subject, - MESSAGE = str.message + '\n\n\n', - PREFIX = str.cryptPrefix + '\n', - SUFFIX = '\n' + str.cryptSuffix, + var MESSAGE = str.message + '\n\n\n', SIGNATURE = '\n\n\n' + str.signature + '\n' + str.webSite + '\n\n'; // get first name of recipient @@ -618,9 +547,8 @@ define(function(require) { greeting = 'Hi ' + to + ',\n\n'; // build encrypted text body - ctBase64 = btoa(JSON.stringify(ct)); - email.body = greeting + MESSAGE + PREFIX + ctBase64 + SUFFIX + SIGNATURE; - email.subject = SUBJECT; + email.body = greeting + MESSAGE + ct + SIGNATURE; + email.subject = str.subject; return email; } diff --git a/src/js/dao/keychain-dao.js b/src/js/dao/keychain-dao.js index 4dc6625..a14f60d 100644 --- a/src/js/dao/keychain-dao.js +++ b/src/js/dao/keychain-dao.js @@ -210,23 +210,7 @@ define(['underscore', 'js/dao/lawnchair-dao'], function(_, jsonDao) { } // store private key locally - self.saveLocalPrivateKey(keypair.privateKey, function(err) { - if (err) { - callback(err); - return; - } - - // persist private key in cloud storage - self._cloudstorage.putPrivateKey(keypair.privateKey, function(err) { - // validate result - if (err) { - callback(err); - return; - } - - callback(null); - }); - }); + self.saveLocalPrivateKey(keypair.privateKey, callback); }); }); }; diff --git a/src/lib/openpgp/openpgp.js b/src/lib/openpgp/openpgp.js index 71a2e27..d7d2213 100644 --- a/src/lib/openpgp/openpgp.js +++ b/src/lib/openpgp/openpgp.js @@ -7424,7 +7424,7 @@ function openpgp_config() { keyserver: "keyserver.linux.it" // "pgp.mit.edu:11371" }; - this.versionstring ="OpenPGP.js v.1.20131011"; + this.versionstring ="OpenPGP.js v.1.20131012"; this.commentstring ="http://openpgpjs.org"; /** * Reads the config out of the HTML5 local storage @@ -7432,7 +7432,10 @@ function openpgp_config() { * if config is null the default config will be used */ function read() { - var cf = JSON.parse(window.localStorage.getItem("config")); + var cf = null; + if (typeof chrome === 'undefined' || typeof chrome.runtime === 'undefined') { + cf = JSON.parse(window.localStorage.getItem("config")); + } if (cf == null) { this.config = this.default_config; this.write(); @@ -7450,7 +7453,9 @@ function openpgp_config() { * Writes the config to HTML5 local storage */ function write() { - window.localStorage.setItem("config",JSON.stringify(this.config)); + if (typeof chrome === 'undefined' || typeof chrome.runtime === 'undefined') { + window.localStorage.setItem("config",JSON.stringify(this.config)); + } } this.read = read; @@ -8463,8 +8468,11 @@ function openpgp_keyring() { * This method is called by openpgp.init(). */ function init() { - var sprivatekeys = JSON.parse(window.localStorage.getItem("privatekeys")); - var spublickeys = JSON.parse(window.localStorage.getItem("publickeys")); + var sprivatekeys, spublickeys; + if (typeof chrome === 'undefined' || typeof chrome.runtime === 'undefined') { + sprivatekeys = JSON.parse(window.localStorage.getItem("privatekeys")); + spublickeys = JSON.parse(window.localStorage.getItem("publickeys")); + } if (sprivatekeys == null || sprivatekeys.length == 0) { sprivatekeys = new Array(); } @@ -8513,8 +8521,11 @@ function openpgp_keyring() { for (var i = 0; i < this.publicKeys.length; i++) { pub[i] = this.publicKeys[i].armored; } - window.localStorage.setItem("privatekeys",JSON.stringify(priv)); - window.localStorage.setItem("publickeys",JSON.stringify(pub)); + + if (typeof chrome === 'undefined' || typeof chrome.runtime === 'undefined') { + window.localStorage.setItem("privatekeys",JSON.stringify(priv)); + window.localStorage.setItem("publickeys",JSON.stringify(pub)); + } } this.store = store; /** diff --git a/test/new-unit/app-controller-test.js b/test/new-unit/app-controller-test.js index 6528dea..2ba952d 100644 --- a/test/new-unit/app-controller-test.js +++ b/test/new-unit/app-controller-test.js @@ -15,7 +15,7 @@ define(function(require) { describe('App Controller unit tests', function() { beforeEach(function() { - sinon.stub(controller, 'login', function(userId, password, salt, token, callback) { + sinon.stub(controller, 'login', function(userId, password, token, callback) { controller._emailDao = sinon.createStubInstance(EmailDAO); callback(); }); @@ -61,8 +61,8 @@ define(function(require) { controller.fetchOAuthToken(appControllerTest.passphrase, function(err) { expect(err).to.not.exist; - expect(controller._appConfigStore.listItems.calledTwice).to.be.true; - expect(controller._appConfigStore.storeList.calledTwice).to.be.true; + expect(controller._appConfigStore.listItems.calledOnce).to.be.true; + expect(controller._appConfigStore.storeList.calledOnce).to.be.true; expect(window.chrome.identity.getAuthToken.calledOnce).to.be.true; expect($.ajax.calledOnce).to.be.true; done(); @@ -74,7 +74,7 @@ define(function(require) { controller.fetchOAuthToken(appControllerTest.passphrase, function(err) { expect(err).to.not.exist; - expect(controller._appConfigStore.listItems.calledTwice).to.be.true; + expect(controller._appConfigStore.listItems.calledOnce).to.be.true; expect(window.chrome.identity.getAuthToken.calledOnce).to.be.true; expect($.ajax.called).to.be.false; done(); diff --git a/test/new-unit/email-dao-test.js b/test/new-unit/email-dao-test.js index 2c46d32..49a3d44 100644 --- a/test/new-unit/email-dao-test.js +++ b/test/new-unit/email-dao-test.js @@ -6,7 +6,7 @@ define(function(require) { DeviceStorageDAO = require('js/dao/devicestorage-dao'), SmtpClient = require('smtp-client'), ImapClient = require('imap-client'), - Crypto = require('js/crypto/crypto'), + PGP = require('js/crypto/pgp'), app = require('js/app-config'), expect = chai.expect; @@ -23,7 +23,7 @@ define(function(require) { describe('Email DAO unit tests', function() { var emailDao, account, - keychainStub, imapClientStub, smtpClientStub, cryptoStub, devicestorageStub; + keychainStub, imapClientStub, smtpClientStub, pgpStub, devicestorageStub; beforeEach(function() { // init dummy object @@ -49,10 +49,10 @@ define(function(require) { keychainStub = sinon.createStubInstance(KeychainDAO); imapClientStub = sinon.createStubInstance(ImapClient); smtpClientStub = sinon.createStubInstance(SmtpClient); - cryptoStub = sinon.createStubInstance(Crypto); + pgpStub = sinon.createStubInstance(PGP); devicestorageStub = sinon.createStubInstance(DeviceStorageDAO); - emailDao = new EmailDAO(keychainStub, imapClientStub, smtpClientStub, cryptoStub, devicestorageStub); + emailDao = new EmailDAO(keychainStub, imapClientStub, smtpClientStub, pgpStub, devicestorageStub); }); afterEach(function() {}); @@ -72,18 +72,43 @@ define(function(require) { it('should init with new keygen', function(done) { devicestorageStub.init.yields(); keychainStub.getUserKeyPair.yields(); - cryptoStub.init.yields(null, {}); + pgpStub.generateKeys.yields(null, {}); keychainStub.putUserKeyPair.yields(); emailDao.init(account, emaildaoTest.passphrase, function(err) { expect(devicestorageStub.init.calledOnce).to.be.true; expect(keychainStub.getUserKeyPair.calledOnce).to.be.true; - expect(cryptoStub.init.calledOnce).to.be.true; + expect(pgpStub.generateKeys.calledOnce).to.be.true; expect(keychainStub.putUserKeyPair.calledOnce).to.be.true; expect(err).to.not.exist; done(); }); }); + + it('should init with stored keygen', function(done) { + devicestorageStub.init.yields(); + keychainStub.getUserKeyPair.yields(null, { + publicKey: { + _id: 'keyId', + userId: emaildaoTest.user, + publicKey: 'publicKeyArmored' + }, + privateKey: { + _id: 'keyId', + userId: emaildaoTest.user, + encryptedKey: 'privateKeyArmored' + } + }); + pgpStub.importKeys.yields(); + + emailDao.init(account, emaildaoTest.passphrase, function(err) { + expect(devicestorageStub.init.calledOnce).to.be.true; + expect(keychainStub.getUserKeyPair.calledOnce).to.be.true; + expect(pgpStub.importKeys.calledOnce).to.be.true; + expect(err).to.not.exist; + done(); + }); + }); }); describe('login', function() { @@ -110,13 +135,13 @@ define(function(require) { beforeEach(function(done) { devicestorageStub.init.yields(); keychainStub.getUserKeyPair.yields(); - cryptoStub.init.yields(null, {}); + pgpStub.generateKeys.yields(null, {}); keychainStub.putUserKeyPair.yields(); emailDao.init(account, emaildaoTest.passphrase, function(err) { expect(devicestorageStub.init.calledOnce).to.be.true; expect(keychainStub.getUserKeyPair.calledOnce).to.be.true; - expect(cryptoStub.init.calledOnce).to.be.true; + expect(pgpStub.generateKeys.calledOnce).to.be.true; expect(keychainStub.putUserKeyPair.calledOnce).to.be.true; expect(err).to.not.exist; done(); @@ -175,11 +200,14 @@ define(function(require) { userId: "safewithme.testuser@gmail.com", publicKey: publicKey }); + pgpStub.exportKeys.yields(null, {}); + pgpStub.encrypt.yields(null, 'asdfasfd'); smtpClientStub.send.yields(); - cryptoStub.encryptListForUser.yields(null, []); emailDao.smtpSend(dummyMail, function(err) { expect(keychainStub.getReveiverPublicKey.calledOnce).to.be.true; + expect(pgpStub.exportKeys.calledOnce).to.be.true; + expect(pgpStub.encrypt.calledOnce).to.be.true; expect(smtpClientStub.send.calledOnce).to.be.true; smtpClientStub.send.calledWith(sinon.match(function(o) { return typeof o.attachments === 'undefined'; @@ -200,11 +228,14 @@ define(function(require) { userId: "safewithme.testuser@gmail.com", publicKey: publicKey }); + pgpStub.exportKeys.yields(null, {}); + pgpStub.encrypt.yields(null, 'asdfasfd'); smtpClientStub.send.yields(); - cryptoStub.encryptListForUser.yields(null, [{}, {}]); emailDao.smtpSend(dummyMail, function(err) { expect(keychainStub.getReveiverPublicKey.calledOnce).to.be.true; + expect(pgpStub.exportKeys.calledOnce).to.be.true; + expect(pgpStub.encrypt.calledOnce).to.be.true; expect(smtpClientStub.send.calledOnce).to.be.true; smtpClientStub.send.calledWith(sinon.match(function(o) { var ptAt = dummyMail.attachments[0]; @@ -408,19 +439,17 @@ define(function(require) { describe('IMAP: list messages from local storage', function() { it('should work', function(done) { - - devicestorageStub.listItems.yields(null, [{ - body: app.string.cryptPrefix + btoa(JSON.stringify({})) + app.string.cryptSuffix - }]); - keychainStub.getPublicKeys.yields(null, [{ + dummyMail.body = app.string.cryptPrefix + btoa('asdf') + app.string.cryptSuffix; + devicestorageStub.listItems.yields(null, [dummyMail, dummyMail]); + keychainStub.getReveiverPublicKey.yields(null, { _id: "fcf8b4aa-5d09-4089-8b4f-e3bc5091daf3", userId: "safewithme.testuser@gmail.com", publicKey: publicKey - }]); - cryptoStub.decryptListForUser.yields(null, [{ + }); + pgpStub.decrypt.yields(null, JSON.stringify({ body: 'test body', subject: 'test subject' - }]); + })); emailDao.listMessages({ folder: 'INBOX', @@ -428,10 +457,10 @@ define(function(require) { num: 2 }, function(err, emails) { expect(devicestorageStub.listItems.calledOnce).to.be.true; - expect(keychainStub.getPublicKeys.calledOnce).to.be.true; - expect(cryptoStub.decryptListForUser.calledOnce).to.be.true; + expect(keychainStub.getReveiverPublicKey.calledTwice).to.be.true; + expect(pgpStub.decrypt.calledTwice).to.be.true; expect(err).to.not.exist; - expect(emails.length).to.equal(1); + expect(emails.length).to.equal(2); done(); }); });