From 1f89219353bba17861f7f47249907111d90d2a3e Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Thu, 28 Nov 2013 11:36:14 +0100 Subject: [PATCH] introduce email dao 2 --- src/js/dao/email-dao-2.js | 724 ++++++++++++++++++++++++++++++ test/new-unit/email-dao-2-test.js | 221 +++++++++ test/new-unit/main.js | 1 + 3 files changed, 946 insertions(+) create mode 100644 src/js/dao/email-dao-2.js create mode 100644 test/new-unit/email-dao-2-test.js diff --git a/src/js/dao/email-dao-2.js b/src/js/dao/email-dao-2.js new file mode 100644 index 0000000..a04f9e5 --- /dev/null +++ b/src/js/dao/email-dao-2.js @@ -0,0 +1,724 @@ +define(function(require) { + 'use strict'; + + var util = require('cryptoLib/util'); + // _ = require('underscore'), + // str = require('js/app-config').string, + // config = require('js/app-config').config; + + var EmailDAO = function(keychain, imapClient, smtpClient, crypto, devicestorage) { + var self = this; + + self._keychain = keychain; + self._imapClient = imapClient; + self._smtpClient = smtpClient; + self._crypto = crypto; + self._devicestorage = devicestorage; + + // delegation-esque pattern to mitigate between node-style events and plain js + self._imapClient.onIncomingMessage = function(message) { + if (typeof self.onIncomingMessage === 'function') { + self.onIncomingMessage(message); + } + }; + }; + + // + // Housekeeping APIs + // + + EmailDAO.prototype.init = function(options, callback) { + var self = this; + + self._account = options.account; + + // validate email address + var emailAddress = self._account.emailAddress; + if (!util.validateEmailAddress(emailAddress)) { + callback({ + errMsg: 'The user email address must be specified!' + }); + return; + } + + // init keychain and then crypto module + initKeychain(); + + function initKeychain() { + // init user's local database + self._devicestorage.init(emailAddress, function() { + // call getUserKeyPair to read/sync keypair with devicestorage/cloud + self._keychain.getUserKeyPair(emailAddress, function(err, storedKeypair) { + if (err) { + callback(err); + return; + } + callback(null, storedKeypair); + }); + }); + } + }; + + + EmailDAO.prototype.unlock = function(options, callback) { + var self = this; + + if (options.keypair) { + // import existing key pair into crypto module + self._crypto.importKeys({ + passphrase: options.passphrase, + privateKeyArmored: options.keypair.privateKey.encryptedKey, + publicKeyArmored: options.keypair.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: options.passphrase + }, function(err, generatedKeypair) { + if (err) { + callback(err); + return; + } + + // import the new key pair into crypto module + self._crypto.importKeys({ + passphrase: options.passphrase, + privateKeyArmored: generatedKeypair.privateKeyArmored, + publicKeyArmored: generatedKeypair.publicKeyArmored + }, function(err) { + if (err) { + callback(err); + return; + } + + // 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); + }); + }); + }; + + // // + // // IMAP Apis + // // + + // /** + // * Login the imap client + // */ + // EmailDAO.prototype.imapLogin = function(callback) { + // var self = this; + + // // login IMAP client if existent + // self._imapClient.login(callback); + // }; + + // /** + // * Cleanup by logging the user off. + // */ + // EmailDAO.prototype.destroy = function(callback) { + // var self = this; + + // self._imapClient.logout(callback); + // }; + + // /** + // * List the folders in the user's IMAP mailbox. + // */ + // EmailDAO.prototype.imapListFolders = function(callback) { + // var self = this, + // dbType = 'folders'; + + // // check local cache + // self._devicestorage.listItems(dbType, 0, null, function(err, stored) { + // if (err) { + // callback(err); + // return; + // } + + // if (!stored || stored.length < 1) { + // // no folders cached... fetch from server + // fetchFromServer(); + // return; + // } + + // callback(null, stored[0]); + // }); + + // function fetchFromServer() { + // var folders; + + // // fetch list from imap server + // self._imapClient.listWellKnownFolders(function(err, wellKnownFolders) { + // if (err) { + // callback(err); + // return; + // } + + // folders = [ + // wellKnownFolders.inbox, + // wellKnownFolders.sent, { + // type: 'Outbox', + // path: 'OUTBOX' + // }, + // wellKnownFolders.drafts, + // wellKnownFolders.trash + // ]; + + // // cache locally + // // persist encrypted list in device storage + // self._devicestorage.storeList([folders], dbType, function(err) { + // if (err) { + // callback(err); + // return; + // } + + // callback(null, folders); + // }); + // }); + // } + // }; + + // /** + // * Get the number of unread message for a folder + // */ + // EmailDAO.prototype.unreadMessages = function(path, callback) { + // var self = this; + + // self._imapClient.unreadMessages(path, callback); + // }; + + // /** + // * Fetch a list of emails from the device's local storage + // */ + // EmailDAO.prototype.listMessages = function(options, callback) { + // var self = this, + // cleartextList = []; + + // // validate options + // if (!options.folder) { + // callback({ + // errMsg: 'Invalid options!' + // }); + // return; + // } + // options.offset = (typeof options.offset === 'undefined') ? 0 : options.offset; + // options.num = (typeof options.num === 'undefined') ? null : options.num; + + // // fetch items from device storage + // self._devicestorage.listItems('email_' + options.folder, options.offset, options.num, function(err, emails) { + // if (err) { + // callback(err); + // return; + // } + + // if (emails.length === 0) { + // callback(null, cleartextList); + // return; + // } + + // var after = _.after(emails.length, function() { + // callback(null, cleartextList); + // }); + + // _.each(emails, function(email) { + // handleMail(email, after); + // }); + // }); + + // function handleMail(email, localCallback) { + // // remove subject filter prefix + // email.subject = email.subject.split(str.subjectPrefix)[1]; + + // // encrypted mail + // if (isPGPMail(email)) { + // email = parseMessageBlock(email); + // decrypt(email, localCallback); + // return; + // } + + // // verification mail + // if (isVerificationMail(email)) { + // verify(email, localCallback); + // return; + // } + + // // cleartext mail + // cleartextList.push(email); + // localCallback(); + // } + + // function isPGPMail(email) { + // return typeof email.body === 'string' && email.body.indexOf(str.cryptPrefix) !== -1 && email.body.indexOf(str.cryptSuffix) !== -1; + // } + + // function isVerificationMail(email) { + // return email.subject === str.verificationSubject; + // } + + // function parseMessageBlock(email) { + // var messageBlock; + + // // parse email body for encrypted message block + // // 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; + + // return email; + // } + + // function decrypt(email, localCallback) { + // // fetch public key required to verify signatures + // var sender = email.from[0].address; + // self._keychain.getReceiverPublicKey(sender, function(err, senderPubkey) { + // if (err) { + // callback(err); + // return; + // } + + // if (!senderPubkey) { + // // this should only happen if a mail from another channel is in the inbox + // setBodyAndContinue('Public key for sender not found!'); + // return; + // } + + // // decrypt and verfiy signatures + // self._pgp.decrypt(email.body, senderPubkey.publicKey, function(err, decrypted) { + // if (err) { + // decrypted = err.errMsg; + // } + + // setBodyAndContinue(decrypted); + // }); + // }); + + // function setBodyAndContinue(text) { + // email.body = text; + // cleartextList.push(email); + // localCallback(); + // } + // } + + // function verify(email, localCallback) { + // var uuid, index, + // verifiyUrlPrefix = config.cloudUrl + config.verificationUrl; + + // if (!email.unread) { + // // don't bother if the email was already marked as read + // localCallback(); + // return; + // } + + // index = email.body.indexOf(verifiyUrlPrefix); + // if (index === -1) { + // localCallback(); + // return; + // } + + // uuid = email.body.split(config.verificationUrl)[1]; + // self._keychain.verifyPublicKey(uuid, function(err) { + // if (err) { + // callback({ + // 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.imapMarkMessageRead({ + // folder: options.folder, + // uid: email.uid + // }, function(err) { + // if (err) { + // // if marking the mail as read failed, don't bother + // localCallback(); + // return; + // } + + // self.imapDeleteMessage({ + // folder: options.folder, + // uid: email.uid + // }, function() { + // localCallback(); + // }); + // }); + // }); + // } + // }; + + // /** + // * High level sync operation for the delta from the user's IMAP inbox + // */ + // EmailDAO.prototype.imapSync = function(options, callback) { + // var self = this, + // dbType = 'email_' + options.folder; + + // fetchList(function(err, emails) { + // if (err) { + // callback(err); + // return; + // } + + // // delete old items from db + // self._devicestorage.removeList(dbType, function(err) { + // if (err) { + // callback(err); + // return; + // } + + // // persist encrypted list in device storage + // self._devicestorage.storeList(emails, dbType, callback); + // }); + // }); + + // function fetchList(callback) { + // var headers = []; + + // // fetch imap folder's message list + // self.imapListMessages({ + // folder: options.folder, + // offset: options.offset, + // num: options.num + // }, function(err, emails) { + // if (err) { + // callback(err); + // return; + // } + + // // find encrypted messages by subject + // emails.forEach(function(i) { + // if (typeof i.subject === 'string' && i.subject.indexOf(str.subjectPrefix) !== -1) { + // headers.push(i); + // } + // }); + + // // fetch message bodies + // fetchBodies(headers, callback); + // }); + // } + + // function fetchBodies(headers, callback) { + // var emails = []; + + // if (headers.length < 1) { + // callback(null, emails); + // return; + // } + + // var after = _.after(headers.length, function() { + // callback(null, emails); + // }); + + // _.each(headers, function(header) { + // self.imapGetMessage({ + // folder: options.folder, + // uid: header.uid + // }, function(err, message) { + // if (err) { + // callback(err); + // return; + // } + + // // set gotten attributes like body to message object containing list meta data like 'unread' or 'replied' + // header.id = message.id; + // header.body = message.body; + // header.html = message.html; + // header.attachments = message.attachments; + + // emails.push(header); + // after(); + // }); + // }); + // } + // }; + + // /** + // * List messages from an imap folder. This will not yet fetch the email body. + // * @param {String} options.folderName The name of the imap folder. + // * @param {Number} options.offset The offset of items to fetch (0 is the last stored item) + // * @param {Number} options.num The number of items to fetch (null means fetch all) + // */ + // EmailDAO.prototype.imapListMessages = function(options, callback) { + // var self = this; + + // self._imapClient.listMessages({ + // path: options.folder, + // offset: options.offset, + // length: options.num + // }, callback); + // }; + + // /** + // * Get an email messsage including the email body from imap + // * @param {String} options.messageId The + // */ + // EmailDAO.prototype.imapGetMessage = function(options, callback) { + // var self = this; + + // self._imapClient.getMessagePreview({ + // path: options.folder, + // uid: options.uid + // }, callback); + // }; + + // EmailDAO.prototype.imapMoveMessage = function(options, callback) { + // var self = this; + + // self._imapClient.moveMessage({ + // path: options.folder, + // uid: options.uid, + // destination: options.destination + // }, moved); + + // function moved(err) { + // if (err) { + // callback(err); + // return; + // } + + // // delete from local db + // self._devicestorage.removeList('email_' + options.folder + '_' + options.uid, callback); + // } + // }; + + // EmailDAO.prototype.imapDeleteMessage = function(options, callback) { + // var self = this; + + // self._imapClient.deleteMessage({ + // path: options.folder, + // uid: options.uid + // }, moved); + + // function moved(err) { + // if (err) { + // callback(err); + // return; + // } + + // // delete from local db + // self._devicestorage.removeList('email_' + options.folder + '_' + options.uid, callback); + // } + // }; + + // EmailDAO.prototype.imapMarkMessageRead = function(options, callback) { + // var self = this; + + // self._imapClient.updateFlags({ + // path: options.folder, + // uid: options.uid, + // unread: false + // }, callback); + // }; + + // EmailDAO.prototype.imapMarkAnswered = function(options, callback) { + // var self = this; + + // self._imapClient.updateFlags({ + // path: options.folder, + // uid: options.uid, + // answered: true + // }, callback); + // }; + + // // + // // SMTP Apis + // // + + // /** + // * Send an email client side via STMP. + // */ + // EmailDAO.prototype.encryptedSend = function(email, callback) { + // var self = this, + // invalidRecipient; + + // // validate the email input + // if (!email.to || !email.from || !email.to[0].address || !email.from[0].address) { + // callback({ + // errMsg: 'Invalid email object!' + // }); + // return; + // } + + // // validate email addresses + // _.each(email.to, function(i) { + // if (!util.validateEmailAddress(i.address)) { + // invalidRecipient = i.address; + // } + // }); + // if (invalidRecipient) { + // callback({ + // errMsg: 'Invalid recipient: ' + invalidRecipient + // }); + // return; + // } + // if (!util.validateEmailAddress(email.from[0].address)) { + // callback({ + // errMsg: 'Invalid sender: ' + email.from + // }); + // return; + // } + + // // only support single recipient for e-2-e encryption + // // check if receiver has a public key + // self._keychain.getReceiverPublicKey(email.to[0].address, function(err, receiverPubkey) { + // if (err) { + // callback(err); + // return; + // } + + // // validate public key + // if (!receiverPubkey) { + // callback({ + // errMsg: 'User has no public key yet!' + // }); + // return; + // } + + // // public key found... encrypt and send + // self.encryptForUser({ + // email: email, + // receiverPubkey: receiverPubkey.publicKey + // }, function(err, email) { + // if (err) { + // callback(err); + // return; + // } + + // self.send(email, callback); + // }); + // }); + // }; + + // /** + // * Encrypt an email asymmetrically for an exisiting user with their public key + // */ + // EmailDAO.prototype.encryptForUser = function(options, callback) { + // var self = this, + // pt = options.email.body, + // receiverPubkeys = options.receiverPubkey ? [options.receiverPubkey] : []; + + // // get own public key so send message can be read + // self._pgp.exportKeys(function(err, ownKeys) { + // if (err) { + // callback(err); + // return; + // } + + // // add own public key to receiver list + // receiverPubkeys.push(ownKeys.publicKeyArmored); + // // encrypt the email + // self._pgp.encrypt(pt, receiverPubkeys, function(err, ct) { + // if (err) { + // callback(err); + // return; + // } + + // // bundle encrypted email together for sending + // frameEncryptedMessage(options.email, ct); + // callback(null, options.email); + // }); + // }); + // }; + + // /** + // * Frames an encrypted message in base64 Format. + // */ + // function frameEncryptedMessage(email, ct) { + // var to, greeting; + + // var MESSAGE = str.message + '\n\n\n', + // SIGNATURE = '\n\n' + str.signature + '\n\n'; + + // // get first name of recipient + // to = (email.to[0].name || email.to[0].address).split('@')[0].split('.')[0].split(' ')[0]; + // greeting = 'Hi ' + to + ',\n\n'; + + // // build encrypted text body + // email.body = greeting + MESSAGE + ct + SIGNATURE; + // email.subject = str.subjectPrefix + email.subject; + // } + + // /** + // * Send an actual message object via smtp + // */ + // EmailDAO.prototype.send = function(email, callback) { + // var self = this; + + // self._smtpClient.send(email, callback); + // }; + + // EmailDAO.prototype.store = function(email, callback) { + // var self = this, + // dbType = 'email_OUTBOX'; + + // email.id = util.UUID(); + + // // encrypt + // self.encryptForUser({ + // email: email + // }, function(err, email) { + // if (err) { + // callback(err); + // return; + // } + + // // store to local storage + // self._devicestorage.storeList([email], dbType, callback); + // }); + // }; + + // EmailDAO.prototype.list = function(callback) { + // var self = this, + // dbType = 'email_OUTBOX'; + + // self._devicestorage.listItems(dbType, 0, null, function(err, mails) { + // if (err) { + // callback(err); + // return; + // } + + // if (mails.length === 0) { + // callback(null, []); + // return; + // } + + // self._pgp.exportKeys(function(err, ownKeys) { + // if (err) { + // callback(err); + // return; + // } + + // var after = _.after(mails.length, function() { + // callback(null, mails); + // }); + + // mails.forEach(function(mail) { + // mail.body = str.cryptPrefix + mail.body.split(str.cryptPrefix)[1].split(str.cryptSuffix)[0] + str.cryptSuffix; + // self._pgp.decrypt(mail.body, ownKeys.publicKeyArmored, function(err, decrypted) { + // mail.body = err ? err.errMsg : decrypted; + // mail.subject = mail.subject.split(str.subjectPrefix)[1]; + // after(); + // }); + // }); + + // }); + // }); + // }; + + return EmailDAO; +}); \ No newline at end of file diff --git a/test/new-unit/email-dao-2-test.js b/test/new-unit/email-dao-2-test.js new file mode 100644 index 0000000..0679a94 --- /dev/null +++ b/test/new-unit/email-dao-2-test.js @@ -0,0 +1,221 @@ +define(function(require) { + 'use strict'; + + var EmailDAO = require('js/dao/email-dao-2'), + KeychainDAO = require('js/dao/keychain-dao'), + ImapClient = require('imap-client'), + SmtpClient = require('smtp-client'), + PGP = require('js/crypto/pgp'), + DeviceStorageDAO = require('js/dao/devicestorage-dao'), + expect = chai.expect; + + + describe('Email DAO 2 unit tests', function() { + var dao, keychainStub, imapClientStub, smtpClientStub, pgpStub, devicestorageStub; + + var emailAddress = 'asdf@asdf.com', + passphrase = 'asdf', + asymKeySize = 2048, + mockkeyId = 1234, + mockKeyPair = { + publicKey: { + _id: mockkeyId, + userId: emailAddress, + publicKey: 'publicpublicpublicpublic' + }, + privateKey: { + _id: mockkeyId, + userId: emailAddress, + encryptedKey: 'privateprivateprivateprivate' + } + }, account = { + emailAddress: emailAddress, + asymKeySize: asymKeySize, + }; + + beforeEach(function() { + keychainStub = sinon.createStubInstance(KeychainDAO); + imapClientStub = sinon.createStubInstance(ImapClient); + smtpClientStub = sinon.createStubInstance(SmtpClient); + pgpStub = sinon.createStubInstance(PGP); + devicestorageStub = sinon.createStubInstance(DeviceStorageDAO); + + dao = new EmailDAO(keychainStub, imapClientStub, smtpClientStub, pgpStub, devicestorageStub); + dao._account = account; + + expect(dao._keychain).to.equal(keychainStub); + expect(dao._imapClient).to.equal(imapClientStub); + expect(dao._smtpClient).to.equal(smtpClientStub); + expect(dao._crypto).to.equal(pgpStub); + expect(dao._devicestorage).to.equal(devicestorageStub); + }); + + afterEach(function() {}); + + describe('init', function() { + beforeEach(function() { + delete dao._account; + }); + + it('should init', function(done) { + devicestorageStub.init.withArgs(emailAddress).yields(); + keychainStub.getUserKeyPair.yields(null, mockKeyPair); + + dao.init({ + account: account + }, function(err, keyPair) { + expect(err).to.not.exist; + expect(keyPair).to.equal(mockKeyPair); + + expect(dao._account).to.equal(account); + expect(devicestorageStub.init.calledOnce).to.be.true; + expect(keychainStub.getUserKeyPair.calledOnce).to.be.true; + + done(); + }); + }); + + it('should fail due to error in getUserKeyPair', function(done) { + devicestorageStub.init.yields(); + keychainStub.getUserKeyPair.yields({}); + + dao.init({ + account: account + }, function(err, keyPair) { + expect(err).to.exist; + expect(keyPair).to.not.exist; + + expect(devicestorageStub.init.calledOnce).to.be.true; + + done(); + }); + }); + }); + + describe('unlock', function() { + it('should unlock', function(done) { + var importMatcher = sinon.match(function(o) { + expect(o.passphrase).to.equal(passphrase); + expect(o.privateKeyArmored).to.equal(mockKeyPair.privateKey.encryptedKey); + expect(o.publicKeyArmored).to.equal(mockKeyPair.publicKey.publicKey); + return true; + }); + + pgpStub.importKeys.withArgs(importMatcher).yields(); + + dao.unlock({ + passphrase: passphrase, + keypair: mockKeyPair + }, function(err) { + expect(err).to.not.exist; + + expect(pgpStub.importKeys.calledOnce).to.be.true; + + done(); + }); + }); + + it('should generate a keypair and unlock', function(done) { + var genKeysMatcher, persistKeysMatcher, importMatcher, keypair; + + keypair = { + keyId: 123, + publicKeyArmored: mockKeyPair.publicKey.publicKey, + privateKeyArmored: mockKeyPair.privateKey.encryptedKey + }; + genKeysMatcher = sinon.match(function(o) { + expect(o.emailAddress).to.equal(emailAddress); + expect(o.keySize).to.equal(asymKeySize); + expect(o.passphrase).to.equal(passphrase); + return true; + }); + importMatcher = sinon.match(function(o) { + expect(o.passphrase).to.equal(passphrase); + expect(o.privateKeyArmored).to.equal(mockKeyPair.privateKey.encryptedKey); + expect(o.publicKeyArmored).to.equal(mockKeyPair.publicKey.publicKey); + return true; + }); + persistKeysMatcher = sinon.match(function(o) { + expect(o).to.deep.equal(mockKeyPair); + return true; + }); + + + pgpStub.generateKeys.withArgs(genKeysMatcher).yields(null, keypair); + pgpStub.importKeys.withArgs(importMatcher).yields(); + keychainStub.putUserKeyPair.withArgs().yields(); + + dao.unlock({ + passphrase: passphrase + }, function(err) { + expect(err).to.not.exist; + + expect(pgpStub.generateKeys.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.yields(null, keypair); + pgpStub.importKeys.withArgs().yields(); + keychainStub.putUserKeyPair.yields({}); + + dao.unlock({ + passphrase: passphrase + }, 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(); + }); + }); + + it('should fail when import fails', function(done) { + var keypair = { + keyId: 123, + publicKeyArmored: 'qwerty', + privateKeyArmored: 'asdfgh' + }; + + pgpStub.generateKeys.withArgs().yields(null, keypair); + pgpStub.importKeys.withArgs().yields({}); + + dao.unlock({ + passphrase: passphrase + }, function(err) { + expect(err).to.exist; + + expect(pgpStub.generateKeys.calledOnce).to.be.true; + expect(pgpStub.importKeys.calledOnce).to.be.true; + + done(); + }); + }); + + it('should fail when generation fails', function(done) { + pgpStub.generateKeys.yields({}); + + dao.unlock({ + passphrase: passphrase + }, function(err) { + expect(err).to.exist; + + expect(pgpStub.generateKeys.calledOnce).to.be.true; + + done(); + }); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/new-unit/main.js b/test/new-unit/main.js index 1d8a653..0787794 100644 --- a/test/new-unit/main.js +++ b/test/new-unit/main.js @@ -31,6 +31,7 @@ function startTests() { require( [ 'test/new-unit/email-dao-test', + 'test/new-unit/email-dao-2-test', 'test/new-unit/app-controller-test', 'test/new-unit/pgp-test', 'test/new-unit/rest-dao-test',