optimized email sync from cloud to do RSA validation and decryption only once

This commit is contained in:
Tankred Hase 2013-06-26 17:37:21 +02:00
parent 09a104ce80
commit 6097000f9f
6 changed files with 175 additions and 52 deletions

View File

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

View File

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

View File

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

View File

@ -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) {

View File

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

View File

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