integrate pgp into email dao and app

This commit is contained in:
Tankred Hase 2013-10-12 03:19:01 +02:00
parent 66a4921573
commit 79c9d134d3
8 changed files with 169 additions and 256 deletions

View File

@ -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'
};

View File

@ -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);
};

View File

@ -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(),

View File

@ -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;
}

View File

@ -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);
});
});
};

View File

@ -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;
/**

View File

@ -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();

View File

@ -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();
});
});