diff --git a/src/js/crypto/aes-cbc.js b/src/js/crypto/aes-cbc.js index 8137f39..91e317a 100644 --- a/src/js/crypto/aes-cbc.js +++ b/src/js/crypto/aes-cbc.js @@ -16,6 +16,11 @@ * @return [String] The base64 encoded ciphertext */ this.encrypt = function(plaintext, key, iv) { + // validate args + if (!plaintext || !key || !iv) { + throw new Error("Missing args for encryption!"); + } + // decode args to utf8 and encrypt var cipher = forge.aes.createEncryptionCipher(utl.decode64(key)); cipher.start(utl.decode64(iv)); @@ -34,6 +39,11 @@ * @return [String] The decrypted plaintext in UTF-16 */ this.decrypt = function(ciphertext, key, iv) { + // validate args + if (!ciphertext || !key || !iv) { + throw new Error("Missing args for decryption!"); + } + // decode args input to utf8 decrypt var cipher = forge.aes.createDecryptionCipher(utl.decode64(key)); cipher.start(utl.decode64(iv)); diff --git a/src/js/crypto/crypto-batch-worker.js b/src/js/crypto/crypto-batch-worker.js index 87ded59..e476624 100644 --- a/src/js/crypto/crypto-batch-worker.js +++ b/src/js/crypto/crypto-batch-worker.js @@ -28,6 +28,14 @@ // start decryption output = batch.decryptListForUser(i.list, i.senderPubkeys, i.receiverPrivkey); + } else if (i.type === 'reencrypt' && i.senderPubkeys && i.receiverPrivkey && i.list && i.symKey) { + // start validation and re-encryption + output = batch.reencryptListKeysForUser(i.list, i.senderPubkeys, i.receiverPrivkey, i.symKey); + + } else if (i.type === 'decryptItems' && i.symKey && i.list) { + // start decryption + output = batch.decryptKeysAndList(i.list, i.symKey); + } else { throw 'Not all arguments for web worker crypto are defined!'; } diff --git a/src/js/crypto/crypto-batch.js b/src/js/crypto/crypto-batch.js index b592ac7..76ca295 100644 --- a/src/js/crypto/crypto-batch.js +++ b/src/js/crypto/crypto-batch.js @@ -97,7 +97,7 @@ * Decrypt and verify a list of item keys using RSA * @param list [Array] The list of items to decrypt * @param senderPubkeys [Array] A list of public keys used to verify - * @param receiverPrivkey [Array] The receiver's private key used to decrypt + * @param receiverPrivkey [String] The receiver's private key used to decrypt */ this.decryptListKeysForUser = function(list, senderPubkeys, receiverPrivkey) { var senderPk, @@ -120,6 +120,27 @@ return list; }; + /** + * Decrypt a list of item keys using RSA and the encrypt them again using AES + * @param list [Array] The list of items to decrypt + * @param senderPubkeys [Array] A list of public keys used to verify + * @param receiverPrivkey [String] The receiver's private key used to decrypt + * @param symKey [String] The symmetric key used to re-encrypt the item key + */ + this.reencryptListKeysForUser = function(list, senderPubkeys, receiverPrivkey, symKey) { + // verify and decrypt item keys using RSA + this.decryptListKeysForUser(list, senderPubkeys, receiverPrivkey); + + list.forEach(function(i) { + // re-encrypt item key using aes + i.encryptedKey = aes.encrypt(i.key, symKey, i.iv); + + delete i.key; + }); + + return list; + }; + /** * Decrypt an item using AES * @param i [Object] The item to decrypt @@ -149,6 +170,32 @@ return list; }; + /** + * Decrypt keys and items using AES + * @param list [Array] The list of items to decrypt + * @param symKey [String] The symmetric key used to re-encrypt the item key + */ + this.decryptKeysAndList = function(list, symKey) { + var self = this, + j; + + list.forEach(function(i) { + // decrypt item key + i.key = aes.decrypt(i.encryptedKey, symKey, i.iv); + // decrypt item for user + self.decryptItem(i); + + delete i.encryptedKey; + }); + + // set plaintext as list item + for (j = 0; j < list.length; j++) { + list[j] = list[j].plaintext; + } + + return list; + }; + /** * Decrypt and verify an item using AES and RSA * @param i [Object] The item to decrypt diff --git a/src/js/crypto/crypto.js b/src/js/crypto/crypto.js index 1460525..f5da833 100644 --- a/src/js/crypto/crypto.js +++ b/src/js/crypto/crypto.js @@ -9,6 +9,8 @@ define(['cryptoLib/util', 'cryptoLib/aes-cbc', 'cryptoLib/rsa', 'cryptoLib/crypt var self = {}; + var passBasedKey; + /** * Initializes the crypto modules by fetching the user's * encrypted secret key from storage and storing it in memory. @@ -34,6 +36,9 @@ define(['cryptoLib/util', 'cryptoLib/aes-cbc', 'cryptoLib/rsa', 'cryptoLib/crypt return; } + // remember pbkdf2 for later use + passBasedKey = derivedKey; + // check if key exists if (!args.storedKeypair) { // generate keys, encrypt and persist if none exists @@ -239,6 +244,42 @@ define(['cryptoLib/util', 'cryptoLib/aes-cbc', 'cryptoLib/rsa', 'cryptoLib/crypt }); }; + // + // Re-encrypt keys item and items seperately + // + + self.reencryptListKeysForUser = function(list, senderPubkeys, callback) { + var keypair = rsa.exportKeys(); + var receiverPrivkey = { + _id: keypair._id, + privateKey: keypair.privkeyPem + }; + + startWorker('/crypto/crypto-batch-worker.js', { + type: 'reencrypt', + list: list, + receiverPrivkey: receiverPrivkey, + senderPubkeys: senderPubkeys, + symKey: passBasedKey + }, callback, function() { + return cryptoBatch.reencryptListKeysForUser(list, senderPubkeys, receiverPrivkey, passBasedKey); + }); + }; + + self.decryptKeysAndList = function(list, callback) { + startWorker('/crypto/crypto-batch-worker.js', { + type: 'decryptItems', + list: list, + symKey: passBasedKey + }, callback, function() { + return cryptoBatch.decryptKeysAndList(list, passBasedKey); + }); + }; + + // + // helper functions + // + function startWorker(script, args, callback, noWorker) { // check for WebWorker support if (window.Worker) { diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index 78a33dc..d4952f3 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -80,7 +80,7 @@ define(['underscore', 'cryptoLib/util', 'js/crypto/crypto', 'js/dao/lawnchair-da * @param num [Number] The number of items to fetch (null means fetch all) */ self.listItems = function(folderName, offset, num, callback) { - var collection, folder, already, pubkeyIds = []; + var collection, folder; // check if items are in memory already (account.folders model) folder = self.account.get('folders').where({ @@ -99,45 +99,23 @@ define(['underscore', 'cryptoLib/util', 'js/crypto/crypto', 'js/dao/lawnchair-da return; } - // 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 - }); - } - }); - - // fetch public keys from keychain - keychain.getPublicKeys(pubkeyIds, function(err, senderPubkeys) { + // decrypt list + crypto.decryptKeysAndList(encryptedList, function(err, decryptedList) { if (err) { callback(err); return; } - // decrypt list - crypto.decryptListForUser(encryptedList, senderPubkeys, function(err, decryptedList) { - if (err) { - callback(err); - return; - } - - // cache collection in folder memory - if (decryptedList.length > 0) { - folder = new app.model.Folder({ - name: folderName - }); - folder.set('items', decryptedList); - self.account.get('folders').add(folder); - } - - callback(null, decryptedList); - }); + // cache collection in folder memory + if (decryptedList.length > 0) { + folder = new app.model.Folder({ + name: folderName + }); + folder.set('items', decryptedList); + self.account.get('folders').add(folder); + } + callback(null, decryptedList); }); }); @@ -153,9 +131,9 @@ define(['underscore', 'cryptoLib/util', 'js/crypto/crypto', 'js/dao/lawnchair-da * @param folderName [String] The name of the folder e.g. 'inbox' */ self.syncFromCloud = function(folderName, callback) { - var folder; + var folder, already, pubkeyIds = []; - cloudstorage.listEncryptedItems('email', self.account.get('emailAddress'), folderName, function(err, data) { + cloudstorage.listEncryptedItems('email', self.account.get('emailAddress'), folderName, function(err, encryptedList) { // return if an error occured if (err) { callback({ @@ -164,19 +142,53 @@ define(['underscore', 'cryptoLib/util', 'js/crypto/crypto', 'js/dao/lawnchair-da }); // error return; } + if (encryptedList.length === 0) { + callback(); + return; + } // TODO: remove old folder items from devicestorage - // persist encrypted list in device storage - devicestorage.storeEcryptedList(data, 'email_' + folderName, function() { - // remove cached folder in account model - folder = self.account.get('folders').where({ - name: folderName - })[0]; - if (folder) { - self.account.get('folders').remove(folder); + // 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 + }); } - callback(); + }); + + // fetch public keys from keychain + keychain.getPublicKeys(pubkeyIds, function(err, senderPubkeys) { + if (err) { + callback(err); + return; + } + + // verfiy signatures and re-encrypt item keys + crypto.reencryptListKeysForUser(encryptedList, senderPubkeys, function(err, encryptedKeyList) { + if (err) { + callback(err); + return; + } + + // persist encrypted list in device storage + devicestorage.storeEcryptedList(encryptedKeyList, 'email_' + folderName, function() { + // remove cached folder in account model + folder = self.account.get('folders').where({ + name: folderName + })[0]; + if (folder) { + self.account.get('folders').remove(folder); + } + callback(); + }); + }); + }); }); }; diff --git a/test/unit/email-dao-test.js b/test/unit/email-dao-test.js index dc03d69..524b1e3 100644 --- a/test/unit/email-dao-test.js +++ b/test/unit/email-dao-test.js @@ -17,7 +17,7 @@ define(['js/dao/email-dao', 'js/dao/keychain-dao', 'js/dao/lawnchair-dao', // init dependencies jsonDao.init(emaildaoTest.user); // cloud storage stub - var cloudstorageStub = { + emaildaoTest.cloudstorageStub = { putPublicKey: function(pk, callback) { callback(); }, @@ -28,8 +28,8 @@ define(['js/dao/email-dao', 'js/dao/keychain-dao', 'js/dao/lawnchair-dao', callback(); } }; - emaildaoTest.keychain = new KeychainDAO(cloudstorageStub); - emaildaoTest.emailDao = new EmailDAO(cloudstorageStub, emaildaoTest.keychain); + emaildaoTest.keychain = new KeychainDAO(emaildaoTest.cloudstorageStub); + emaildaoTest.emailDao = new EmailDAO(emaildaoTest.cloudstorageStub, emaildaoTest.keychain); // generate test data emaildaoTest.list = testData.getEmailCollection(100); @@ -54,7 +54,7 @@ define(['js/dao/email-dao', 'js/dao/keychain-dao', 'js/dao/lawnchair-dao', }); }); - asyncTest("Persist test emails", 4, function() { + asyncTest("Persist test emails (stubbed sync from cloud)", 4, function() { emaildaoTest.keychain.getUserKeyPair(emaildaoTest.user, function(err, keypair) { ok(!err && keypair, 'Fetch keypair from keychain'); @@ -69,8 +69,13 @@ define(['js/dao/email-dao', 'js/dao/keychain-dao', 'js/dao/lawnchair-dao', encryptedList[i].sentDate = emaildaoTest.list.at(i).get('sentDate'); } - storage.storeEcryptedList(encryptedList, 'email_inbox', function() { - ok(true, 'Store encrypted list'); + // set encrypted test list as return value for cloud storage stub + emaildaoTest.cloudstorageStub.listEncryptedItems = function(type, emailAddress, folderName, callback) { + callback(null, encryptedList); + }; + + emaildaoTest.emailDao.syncFromCloud('inbox', function() { + ok(true, 'Stored encrypted list'); start(); });