mirror of
https://github.com/moparisthebest/mail
synced 2024-11-22 17:02:17 -05:00
optimized email sync from cloud to do RSA validation and decryption only once
This commit is contained in:
parent
09a104ce80
commit
6097000f9f
@ -16,6 +16,11 @@
|
|||||||
* @return [String] The base64 encoded ciphertext
|
* @return [String] The base64 encoded ciphertext
|
||||||
*/
|
*/
|
||||||
this.encrypt = function(plaintext, key, iv) {
|
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
|
// decode args to utf8 and encrypt
|
||||||
var cipher = forge.aes.createEncryptionCipher(utl.decode64(key));
|
var cipher = forge.aes.createEncryptionCipher(utl.decode64(key));
|
||||||
cipher.start(utl.decode64(iv));
|
cipher.start(utl.decode64(iv));
|
||||||
@ -34,6 +39,11 @@
|
|||||||
* @return [String] The decrypted plaintext in UTF-16
|
* @return [String] The decrypted plaintext in UTF-16
|
||||||
*/
|
*/
|
||||||
this.decrypt = function(ciphertext, key, iv) {
|
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
|
// decode args input to utf8 decrypt
|
||||||
var cipher = forge.aes.createDecryptionCipher(utl.decode64(key));
|
var cipher = forge.aes.createDecryptionCipher(utl.decode64(key));
|
||||||
cipher.start(utl.decode64(iv));
|
cipher.start(utl.decode64(iv));
|
||||||
|
@ -28,6 +28,14 @@
|
|||||||
// start decryption
|
// start decryption
|
||||||
output = batch.decryptListForUser(i.list, i.senderPubkeys, i.receiverPrivkey);
|
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 {
|
} else {
|
||||||
throw 'Not all arguments for web worker crypto are defined!';
|
throw 'Not all arguments for web worker crypto are defined!';
|
||||||
}
|
}
|
||||||
|
@ -97,7 +97,7 @@
|
|||||||
* Decrypt and verify a list of item keys using RSA
|
* Decrypt and verify a list of item keys using RSA
|
||||||
* @param list [Array] The list of items to decrypt
|
* @param list [Array] The list of items to decrypt
|
||||||
* @param senderPubkeys [Array] A list of public keys used to verify
|
* @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) {
|
this.decryptListKeysForUser = function(list, senderPubkeys, receiverPrivkey) {
|
||||||
var senderPk,
|
var senderPk,
|
||||||
@ -120,6 +120,27 @@
|
|||||||
return list;
|
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
|
* Decrypt an item using AES
|
||||||
* @param i [Object] The item to decrypt
|
* @param i [Object] The item to decrypt
|
||||||
@ -149,6 +170,32 @@
|
|||||||
return list;
|
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
|
* Decrypt and verify an item using AES and RSA
|
||||||
* @param i [Object] The item to decrypt
|
* @param i [Object] The item to decrypt
|
||||||
|
@ -9,6 +9,8 @@ define(['cryptoLib/util', 'cryptoLib/aes-cbc', 'cryptoLib/rsa', 'cryptoLib/crypt
|
|||||||
|
|
||||||
var self = {};
|
var self = {};
|
||||||
|
|
||||||
|
var passBasedKey;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the crypto modules by fetching the user's
|
* Initializes the crypto modules by fetching the user's
|
||||||
* encrypted secret key from storage and storing it in memory.
|
* 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remember pbkdf2 for later use
|
||||||
|
passBasedKey = derivedKey;
|
||||||
|
|
||||||
// check if key exists
|
// check if key exists
|
||||||
if (!args.storedKeypair) {
|
if (!args.storedKeypair) {
|
||||||
// generate keys, encrypt and persist if none exists
|
// 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) {
|
function startWorker(script, args, callback, noWorker) {
|
||||||
// check for WebWorker support
|
// check for WebWorker support
|
||||||
if (window.Worker) {
|
if (window.Worker) {
|
||||||
|
@ -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)
|
* @param num [Number] The number of items to fetch (null means fetch all)
|
||||||
*/
|
*/
|
||||||
self.listItems = function(folderName, offset, num, callback) {
|
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)
|
// check if items are in memory already (account.folders model)
|
||||||
folder = self.account.get('folders').where({
|
folder = self.account.get('folders').where({
|
||||||
@ -99,45 +99,23 @@ define(['underscore', 'cryptoLib/util', 'js/crypto/crypto', 'js/dao/lawnchair-da
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// gather public key ids required to verify signatures
|
// decrypt list
|
||||||
encryptedList.forEach(function(i) {
|
crypto.decryptKeysAndList(encryptedList, function(err, decryptedList) {
|
||||||
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) {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
callback(err);
|
callback(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// decrypt list
|
// cache collection in folder memory
|
||||||
crypto.decryptListForUser(encryptedList, senderPubkeys, function(err, decryptedList) {
|
if (decryptedList.length > 0) {
|
||||||
if (err) {
|
folder = new app.model.Folder({
|
||||||
callback(err);
|
name: folderName
|
||||||
return;
|
});
|
||||||
}
|
folder.set('items', decryptedList);
|
||||||
|
self.account.get('folders').add(folder);
|
||||||
// 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);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
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'
|
* @param folderName [String] The name of the folder e.g. 'inbox'
|
||||||
*/
|
*/
|
||||||
self.syncFromCloud = function(folderName, callback) {
|
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
|
// return if an error occured
|
||||||
if (err) {
|
if (err) {
|
||||||
callback({
|
callback({
|
||||||
@ -164,19 +142,53 @@ define(['underscore', 'cryptoLib/util', 'js/crypto/crypto', 'js/dao/lawnchair-da
|
|||||||
}); // error
|
}); // error
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (encryptedList.length === 0) {
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: remove old folder items from devicestorage
|
// TODO: remove old folder items from devicestorage
|
||||||
|
|
||||||
// persist encrypted list in device storage
|
// gather public key ids required to verify signatures
|
||||||
devicestorage.storeEcryptedList(data, 'email_' + folderName, function() {
|
encryptedList.forEach(function(i) {
|
||||||
// remove cached folder in account model
|
already = null;
|
||||||
folder = self.account.get('folders').where({
|
already = _.findWhere(pubkeyIds, {
|
||||||
name: folderName
|
_id: i.senderPk
|
||||||
})[0];
|
});
|
||||||
if (folder) {
|
if (!already) {
|
||||||
self.account.get('folders').remove(folder);
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -17,7 +17,7 @@ define(['js/dao/email-dao', 'js/dao/keychain-dao', 'js/dao/lawnchair-dao',
|
|||||||
// init dependencies
|
// init dependencies
|
||||||
jsonDao.init(emaildaoTest.user);
|
jsonDao.init(emaildaoTest.user);
|
||||||
// cloud storage stub
|
// cloud storage stub
|
||||||
var cloudstorageStub = {
|
emaildaoTest.cloudstorageStub = {
|
||||||
putPublicKey: function(pk, callback) {
|
putPublicKey: function(pk, callback) {
|
||||||
callback();
|
callback();
|
||||||
},
|
},
|
||||||
@ -28,8 +28,8 @@ define(['js/dao/email-dao', 'js/dao/keychain-dao', 'js/dao/lawnchair-dao',
|
|||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
emaildaoTest.keychain = new KeychainDAO(cloudstorageStub);
|
emaildaoTest.keychain = new KeychainDAO(emaildaoTest.cloudstorageStub);
|
||||||
emaildaoTest.emailDao = new EmailDAO(cloudstorageStub, emaildaoTest.keychain);
|
emaildaoTest.emailDao = new EmailDAO(emaildaoTest.cloudstorageStub, emaildaoTest.keychain);
|
||||||
|
|
||||||
// generate test data
|
// generate test data
|
||||||
emaildaoTest.list = testData.getEmailCollection(100);
|
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) {
|
emaildaoTest.keychain.getUserKeyPair(emaildaoTest.user, function(err, keypair) {
|
||||||
ok(!err && keypair, 'Fetch keypair from keychain');
|
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');
|
encryptedList[i].sentDate = emaildaoTest.list.at(i).get('sentDate');
|
||||||
}
|
}
|
||||||
|
|
||||||
storage.storeEcryptedList(encryptedList, 'email_inbox', function() {
|
// set encrypted test list as return value for cloud storage stub
|
||||||
ok(true, 'Store encrypted list');
|
emaildaoTest.cloudstorageStub.listEncryptedItems = function(type, emailAddress, folderName, callback) {
|
||||||
|
callback(null, encryptedList);
|
||||||
|
};
|
||||||
|
|
||||||
|
emaildaoTest.emailDao.syncFromCloud('inbox', function() {
|
||||||
|
ok(true, 'Stored encrypted list');
|
||||||
|
|
||||||
start();
|
start();
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user