finished refactoring email dao for unit tests

This commit is contained in:
Tankred Hase 2013-05-31 15:51:34 +02:00
parent 0fb0e7c1e7
commit dca3b252ce
6 changed files with 175 additions and 106 deletions

View File

@ -16,21 +16,22 @@ app.dao.DeviceStorage = function(util, crypto, jsonDao, sqlcipherDao) {
var i, date, key, items = []; var i, date, key, items = [];
// format items for batch storing in dao // format items for batch storing in dao
for (i = 0; i < list.length; i++) { list.forEach(function(i) {
// put date in key if available... for easy querying // put date in key if available... for easy querying
if (list[i].sentDate) { if (i.sentDate) {
date = util.parseDate(list[i].sentDate); date = util.parseDate(i.sentDate);
key = crypto.emailAddress + '_' + type + '_' + date.getTime() + '_' + list[i].id; key = crypto.emailAddress + '_' + type + '_' + date.getTime() + '_' + i.id;
} else { } else {
key = crypto.emailAddress + '_' + type + '_' + list[i].id; key = crypto.emailAddress + '_' + type + '_' + i.id;
} }
items.push({ items.push({
key: key, key: key,
object: list[i] object: i
}); });
}
});
jsonDao.batch(items, function() { jsonDao.batch(items, function() {
callback(); callback();
@ -38,20 +39,16 @@ app.dao.DeviceStorage = function(util, crypto, jsonDao, sqlcipherDao) {
}; };
/** /**
* Decrypts the stored items of a given type and returns them * List stored items of a given type
* @param type [String] The type of item e.g. 'email' * @param type [String] The type of item e.g. 'email'
* @param offset [Number] The offset of items to fetch (0 is the last stored item) * @param offset [Number] The offset of items to fetch (0 is the last stored item)
* @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)
*/ */
this.listItems = function(type, offset, num, senderPubkeys, callback) { this.listEncryptedItems = function(type, offset, num, callback) {
// fetch all items of a certain type from the data-store // fetch all items of a certain type from the data-store
jsonDao.list(crypto.emailAddress + '_' + type, offset, num, function(encryptedList) { jsonDao.list(crypto.emailAddress + '_' + type, offset, num, function(encryptedList) {
// decrypt list callback(null, encryptedList);
crypto.decryptListForUser(encryptedList, senderPubkeys, function(err, decryptedList) {
callback(err, decryptedList);
});
}); });
}; };

View File

@ -2,7 +2,7 @@
* A high-level Data-Access Api for handling Email synchronization * A high-level Data-Access Api for handling Email synchronization
* between the cloud service and the device's local storage * between the cloud service and the device's local storage
*/ */
app.dao.EmailDAO = function(_, crypto, devicestorage, cloudstorage, util) { app.dao.EmailDAO = function(_, crypto, devicestorage, cloudstorage, util, keychain) {
'use strict'; 'use strict';
/** /**
@ -11,54 +11,46 @@ app.dao.EmailDAO = function(_, crypto, devicestorage, cloudstorage, util) {
this.init = function(account, password, callback) { this.init = function(account, password, callback) {
this.account = account; this.account = account;
// TODO: call getUserKeyPair to read/sync keypair with devicestorage/cloud // call getUserKeyPair to read/sync keypair with devicestorage/cloud
keychain.getUserKeyPair(account.get('emailAddress'), function(err, storedKeypair) {
// sync user's cloud key with local storage
var storedKey = crypto.getEncryptedPrivateKey(account.get('emailAddress'));
cloudstorage.syncPrivateKey(account.get('emailAddress'), storedKey, function(err) {
if (err) { if (err) {
console.log('Error syncing private key to cloud: ' + err); callback(err);
return;
} }
// init crypto // init crypto
initCrypto(); initCrypto(storedKeypair);
}, function(fetchedKey) { }, function(err, keypairReplacement) {
// replace local key with cloud key if (err) {
crypto.putEncryptedPrivateKey(fetchedKey); callback(err);
// whipe local storage return;
}
// whipe local storage in case local keypair was replaced with cloud keypair
devicestorage.clear(function() { devicestorage.clear(function() {
initCrypto(); // init crypto and generate new keypair
initCrypto(keypairReplacement);
}); });
}); });
function initCrypto() { function initCrypto(storedKeypair) {
// TODO: passed fetched keypair from keychain dao
crypto.init({ crypto.init({
emailAddress: account.get('emailAddress'), emailAddress: account.get('emailAddress'),
password: password, password: password,
keySize: account.get('symKeySize'), keySize: account.get('symKeySize'),
rsaKeySize: account.get('asymKeySize') rsaKeySize: account.get('asymKeySize'),
}, function(err) { storedKeypair: storedKeypair
}, function(err, generatedKeypair) {
if (err) { if (err) {
callback(err); callback(err);
return; return;
} }
publishPublicKey(); if (generatedKeypair) {
}); // persist newly generated keypair
} keychain.putUserKeyPair(generatedKeypair, callback);
} else {
// TODO: refactor to be part of sync in getUserKeypair callback();
}
function publishPublicKey() {
// get public key from crypto
var pubkey = crypto.getPublicKey();
//publish public key to cloud service
cloudstorage.putPublicKey(pubkey, function(err) {
callback(err);
}); });
} }
}; };
@ -82,7 +74,8 @@ app.dao.EmailDAO = function(_, crypto, devicestorage, cloudstorage, util) {
* @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)
*/ */
this.listItems = function(folderName, offset, num, callback) { this.listItems = function(folderName, offset, num, callback) {
var collection, folder, self = this; var collection, folder, already, pubkeyIds = [],
self = this;
// check if items are in memory already (account.folders model) // check if items are in memory already (account.folders model)
folder = this.account.get('folders').where({ folder = this.account.get('folders').where({
@ -90,26 +83,56 @@ app.dao.EmailDAO = function(_, crypto, devicestorage, cloudstorage, util) {
})[0]; })[0];
if (!folder) { if (!folder) {
// get items from storage // get encrypted items from storage
devicestorage.listItems('email_' + folderName, offset, num, null, function(err, decryptedList) { devicestorage.listEncryptedItems('email_' + folderName, offset, num, function(err, encryptedList) {
if (err) { if (err) {
callback(err); callback(err);
return; return;
} }
// parse to backbone model collection // gather public key ids required to verify signatures
collection = new app.model.EmailCollection(decryptedList); encryptedList.forEach(function(i) {
already = null;
// cache collection in folder memory already = _.findWhere(pubkeyIds, {
if (decryptedList.length > 0) { _id: i.senderPk
folder = new app.model.Folder({
name: folderName
}); });
folder.set('items', collection); if (!already) {
self.account.get('folders').add(folder); pubkeyIds.push({
} _id: i.senderPk
});
}
});
callback(null, collection); // fetch public keys from keychain
keychain.getPublicKeys(pubkeyIds, function(err, senderPubkeys) {
if (err) {
callback(err);
return;
}
// decrypt list
crypto.decryptListForUser(encryptedList, senderPubkeys, function(err, decryptedList) {
if (err) {
callback(err);
return;
}
// parse to backbone model collection
collection = new app.model.EmailCollection(decryptedList);
// cache collection in folder memory
if (decryptedList.length > 0) {
folder = new app.model.Folder({
name: folderName
});
folder.set('items', collection);
self.account.get('folders').add(folder);
}
callback(null, collection);
});
});
}); });
} else { } else {

View File

@ -12,32 +12,59 @@ app.dao.KeychainDAO = function(jsonDao, cloudstorage) {
* @return [PublicKeyCollection] The requiested public keys * @return [PublicKeyCollection] The requiested public keys
*/ */
this.getPublicKeys = function(ids, callback) { this.getPublicKeys = function(ids, callback) {
var already, pubkeys = [];
var after = _.after(ids.length, function() {
callback(null, pubkeys);
});
_.each(ids, function(i) {
// try to read public key from local storage
jsonDao.read('publickey_' + i._id, function(pubkey) {
if (!pubkey) {
// TODO: fetch from cloud storage
callback({
errMsg: 'Not implemented yet!'
});
return;
}
// check if public key with that id has already been fetched
already = null;
already = _.findWhere(pubkeys, {
_id: i._id
});
if (!already) {
pubkeys.push(pubkey);
}
after(); // asynchronously iterate through objects
});
});
}; };
/** /**
* Gets the local user's key either from local storage * Gets the local user's key either from local storage
* or syncronizes from the cloud. The private key is encrypted. * or fetches it from the cloud. The private key is encrypted.
* If no key pair exists, null is returned. * If no key pair exists, null is returned.
* return [Object] The user's key pair {publicKey, privateKey} * return [Object] The user's key pair {publicKey, privateKey}
*/ */
this.getUserKeyPair = function(userId, callback) { this.getUserKeyPair = function(userId, callback) {
// loojup public key id // lookup public key id
jsonDao.read('publickey_' + userId, function(pubkeyId) { jsonDao.read('publickey_' + userId, function(pubkeyId) {
if (!pubkeyId || !pubkeyId._id) {
// no public key in storage
// TODO: fetch from cloud
// TODO: persist in local storage
callback();
return;
}
// try to read public key from local storage // try to read public key from local storage
jsonDao.read('publickey_' + pubkeyId._id, function(pubkey) { jsonDao.read('publickey_' + pubkeyId._id, function(pubkey) {
if (!pubkey) { // public key found
// no public key in storage // get corresponding private key
// TODO: fetch from cloud fetchEncryptedPrivateKey(pubkey);
// TODO: persist in local storage
callback({
errMsg: 'Not implemented yet!'
});
} else {
// public key found
// get corresponding private key
fetchEncryptedPrivateKey(pubkey);
}
}); });
}); });
@ -76,6 +103,8 @@ app.dao.KeychainDAO = function(jsonDao, cloudstorage) {
return; return;
} }
// TODO: persist in the cloud
// persist public key (email, _id) // persist public key (email, _id)
var pkLookupKey = 'publickey_' + keypair.publicKey.userId; var pkLookupKey = 'publickey_' + keypair.publicKey.userId;
jsonDao.persist(pkLookupKey, { jsonDao.persist(pkLookupKey, {

View File

@ -59,28 +59,33 @@ asyncTest("Store encrypted list", 1, function() {
}); });
}); });
asyncTest("List items", 3, function() { asyncTest("List items", 4, function() {
var senderPubkeys = [devicestorage_test.generatedKeypair.publicKey]; var senderPubkeys = [devicestorage_test.generatedKeypair.publicKey];
var offset = 2, var offset = 2,
num = 6; num = 6;
// list items from storage (decrypted) // list encrypted items from storage
devicestorage_test.storage.listItems('email_inbox_5', offset, num, senderPubkeys, function(err, decryptedList) { devicestorage_test.storage.listEncryptedItems('email_inbox_5', offset, num, function(err, encryptedList) {
ok(!err); ok(!err);
equal(decryptedList.length, num, 'Found ' + decryptedList.length + ' items in store (and decrypted)');
var decrypted, orig = devicestorage_test.list[54]; // decrypt list
devicestorage_test.crypto.decryptListForUser(encryptedList, senderPubkeys, function(err, decryptedList) {
ok(!err);
equal(decryptedList.length, num, 'Found ' + decryptedList.length + ' items in store (and decrypted)');
// check ids var decrypted, orig = devicestorage_test.list[54];
for (var i = 0; i < decryptedList.length; i++) {
if (decryptedList[i].id === orig.id && decryptedList[i].from === orig.from) { // check ids
deepEqual(decryptedList[i], orig, 'Messages decrypted correctly'); for (var i = 0; i < decryptedList.length; i++) {
break; if (decryptedList[i].id === orig.id && decryptedList[i].from === orig.from) {
deepEqual(decryptedList[i], orig, 'Messages decrypted correctly');
break;
}
} }
}
start(); start();
});
}); });
}); });

View File

@ -23,7 +23,8 @@ asyncTest("Init", 3, function() {
callback(); callback();
} }
}; };
emaildao_test.emailDao = new app.dao.EmailDAO(_, emaildao_test.crypto, emaildao_test.storage, cloudstorageStub, util); emaildao_test.keychain = new app.dao.KeychainDAO(jsonDao, cloudstorageStub);
emaildao_test.emailDao = new app.dao.EmailDAO(_, emaildao_test.crypto, emaildao_test.storage, cloudstorageStub, util, emaildao_test.keychain);
// generate test data // generate test data
emaildao_test.list = new TestData().getEmailCollection(100); emaildao_test.list = new TestData().getEmailCollection(100);
@ -35,33 +36,39 @@ asyncTest("Init", 3, function() {
asymKeySize: emaildao_test.rsaKeySize asymKeySize: emaildao_test.rsaKeySize
}); });
emaildao_test.emailDao.init(account, emaildao_test.password, function(err) { // clear db before tests
ok(!err); jsonDao.clear(function(err) {
equal(emaildao_test.emailDao.account.get('emailAddress'), emaildao_test.user, 'Email DAO Account'); ok(!err, 'DB cleared. Error status: ' + err);
// clear db before tests emaildao_test.emailDao.init(account, emaildao_test.password, function(err) {
jsonDao.clear(function(err) { ok(!err);
ok(!err, 'DB cleared. Error status: ' + err); equal(emaildao_test.emailDao.account.get('emailAddress'), emaildao_test.user, 'Email DAO Account');
start(); start();
}); });
}); });
}); });
asyncTest("Persist test emails", 3, function() { asyncTest("Persist test emails", 4, function() {
emaildao_test.crypto.encryptListForUser(emaildao_test.list.toJSON(), null, function(err, encryptedList) { emaildao_test.keychain.getUserKeyPair(emaildao_test.user, function(err, keypair) {
ok(!err); ok(!err && keypair, 'Fetch keypair from keychain');
equal(encryptedList.length, emaildao_test.list.length, 'Encrypt list');
// add sent date to encrypted items var receiverPubkeys = [keypair.publicKey];
for (var i = 0; i < encryptedList.length; i++) {
encryptedList[i].sentDate = emaildao_test.list.at(i).get('sentDate');
}
emaildao_test.storage.storeEcryptedList(encryptedList, 'email_inbox', function() { emaildao_test.crypto.encryptListForUser(emaildao_test.list.toJSON(), receiverPubkeys, function(err, encryptedList) {
ok(true, 'Store encrypted list'); ok(!err);
equal(encryptedList.length, emaildao_test.list.length, 'Encrypt list');
start(); // add sent date to encrypted items
for (var i = 0; i < encryptedList.length; i++) {
encryptedList[i].sentDate = emaildao_test.list.at(i).get('sentDate');
}
emaildao_test.storage.storeEcryptedList(encryptedList, 'email_inbox', function() {
ok(true, 'Store encrypted list');
start();
});
}); });
}); });
}); });

View File

@ -66,6 +66,14 @@ asyncTest("Get User Keypair", 2, function() {
}); });
}); });
// asyncTest("Get Public Keys", 1, function() { asyncTest("Get Public Keys", 2, function() {
var pubkeyIds = [{
_id: keychaindao_test.keypair.publicKey._id
}];
keychaindao_test.keychainDao.getPublicKeys(pubkeyIds, function(err, pubkeys) {
ok(!err);
deepEqual(pubkeys[0], keychaindao_test.keypair.publicKey, "Fetch public key");
// }); start();
});
});