2013-04-02 09:02:57 -04:00
|
|
|
/**
|
|
|
|
* A high-level Data-Access Api for handling Email synchronization
|
|
|
|
* between the cloud service and the device's local storage
|
|
|
|
*/
|
2013-05-31 09:51:34 -04:00
|
|
|
app.dao.EmailDAO = function(_, crypto, devicestorage, cloudstorage, util, keychain) {
|
2013-04-01 18:12:15 -04:00
|
|
|
'use strict';
|
|
|
|
|
2013-03-13 11:58:46 -04:00
|
|
|
/**
|
2013-04-02 09:02:57 -04:00
|
|
|
* Inits all dependencies
|
2013-03-13 11:58:46 -04:00
|
|
|
*/
|
2013-04-02 09:02:57 -04:00
|
|
|
this.init = function(account, password, callback) {
|
|
|
|
this.account = account;
|
|
|
|
|
2013-05-31 09:51:34 -04:00
|
|
|
// call getUserKeyPair to read/sync keypair with devicestorage/cloud
|
|
|
|
keychain.getUserKeyPair(account.get('emailAddress'), function(err, storedKeypair) {
|
2013-04-02 09:02:57 -04:00
|
|
|
if (err) {
|
2013-05-31 09:51:34 -04:00
|
|
|
callback(err);
|
|
|
|
return;
|
2013-04-02 09:02:57 -04:00
|
|
|
}
|
|
|
|
// init crypto
|
2013-05-31 09:51:34 -04:00
|
|
|
initCrypto(storedKeypair);
|
2013-04-01 18:12:15 -04:00
|
|
|
|
2013-05-31 09:51:34 -04:00
|
|
|
}, function(err, keypairReplacement) {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// whipe local storage in case local keypair was replaced with cloud keypair
|
2013-04-02 09:02:57 -04:00
|
|
|
devicestorage.clear(function() {
|
2013-05-31 09:51:34 -04:00
|
|
|
// init crypto and generate new keypair
|
|
|
|
initCrypto(keypairReplacement);
|
2013-03-13 11:58:46 -04:00
|
|
|
});
|
2013-04-02 09:02:57 -04:00
|
|
|
});
|
2013-04-01 18:12:15 -04:00
|
|
|
|
2013-05-31 09:51:34 -04:00
|
|
|
function initCrypto(storedKeypair) {
|
2013-05-18 16:33:10 -04:00
|
|
|
crypto.init({
|
|
|
|
emailAddress: account.get('emailAddress'),
|
|
|
|
password: password,
|
2013-05-18 19:33:59 -04:00
|
|
|
keySize: account.get('symKeySize'),
|
2013-05-31 09:51:34 -04:00
|
|
|
rsaKeySize: account.get('asymKeySize'),
|
|
|
|
storedKeypair: storedKeypair
|
|
|
|
}, function(err, generatedKeypair) {
|
2013-05-07 09:10:51 -04:00
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2013-05-31 09:51:34 -04:00
|
|
|
if (generatedKeypair) {
|
|
|
|
// persist newly generated keypair
|
|
|
|
keychain.putUserKeyPair(generatedKeypair, callback);
|
|
|
|
} else {
|
|
|
|
callback();
|
|
|
|
}
|
2013-03-13 11:58:46 -04:00
|
|
|
});
|
2013-04-02 09:02:57 -04:00
|
|
|
}
|
|
|
|
};
|
2013-04-01 18:12:15 -04:00
|
|
|
|
2013-04-02 09:02:57 -04:00
|
|
|
/**
|
|
|
|
* Fetch an email with the following id
|
|
|
|
*/
|
|
|
|
this.getItem = function(folderName, itemId) {
|
|
|
|
var folder = this.account.get('folders').where({
|
|
|
|
name: folderName
|
|
|
|
})[0];
|
|
|
|
var mail = _.find(folder.get('items').models, function(email) {
|
|
|
|
return email.id + '' === itemId + '';
|
|
|
|
});
|
|
|
|
return mail;
|
|
|
|
};
|
2013-04-01 18:12:15 -04:00
|
|
|
|
2013-04-02 09:02:57 -04:00
|
|
|
/**
|
|
|
|
* Fetch a list of emails from the device's local storage
|
|
|
|
* @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)
|
|
|
|
*/
|
|
|
|
this.listItems = function(folderName, offset, num, callback) {
|
2013-05-31 09:51:34 -04:00
|
|
|
var collection, folder, already, pubkeyIds = [],
|
|
|
|
self = this;
|
2013-04-02 09:02:57 -04:00
|
|
|
|
|
|
|
// check if items are in memory already (account.folders model)
|
|
|
|
folder = this.account.get('folders').where({
|
|
|
|
name: folderName
|
|
|
|
})[0];
|
|
|
|
|
|
|
|
if (!folder) {
|
2013-05-31 09:51:34 -04:00
|
|
|
// get encrypted items from storage
|
|
|
|
devicestorage.listEncryptedItems('email_' + folderName, offset, num, function(err, encryptedList) {
|
2013-05-18 16:33:10 -04:00
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2013-05-31 09:51:34 -04:00
|
|
|
// 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) {
|
|
|
|
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);
|
2013-04-02 09:02:57 -04:00
|
|
|
});
|
|
|
|
|
2013-05-31 09:51:34 -04:00
|
|
|
});
|
2013-04-01 18:12:15 -04:00
|
|
|
});
|
|
|
|
|
2013-04-02 09:02:57 -04:00
|
|
|
} else {
|
|
|
|
// read items from memory
|
|
|
|
collection = folder.get('items');
|
2013-05-18 16:33:10 -04:00
|
|
|
callback(null, collection);
|
2013-04-02 09:02:57 -04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2013-04-19 13:13:27 -04:00
|
|
|
/**
|
|
|
|
* Checks the user virtual inbox containing end-2-end encrypted mail items
|
|
|
|
*/
|
|
|
|
this.checkVInbox = function(callback) {
|
|
|
|
var self = this;
|
|
|
|
|
|
|
|
cloudstorage.listEncryptedItems('email', this.account.get('emailAddress'), 'vinbox', function(err, data) {
|
|
|
|
// if virtual inbox is emtpy just callback
|
|
|
|
if (err || !data || data.status || data.length === 0) {
|
|
|
|
callback(); // error
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// asynchronously iterate over the encrypted items
|
|
|
|
var after = _.after(data.length, function() {
|
|
|
|
callback();
|
|
|
|
});
|
|
|
|
|
|
|
|
_.each(data, function(asymCt) {
|
|
|
|
// asymmetric decrypt
|
|
|
|
asymDecryptMail(asymCt, function(err, pt) {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// symmetric encrypt and push to cloud
|
|
|
|
symEncryptAndUpload(pt, function(err) {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2013-04-23 10:35:01 -04:00
|
|
|
// delete asymmetricall encrypted item from virtual inbox
|
|
|
|
deleteVinboxItem(asymCt, function(err) {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
after(); // asynchronously iterate through objects
|
|
|
|
});
|
2013-04-19 13:13:27 -04:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
function asymDecryptMail(m, callback) {
|
|
|
|
var pubKeyId = m.senderPk.split(';')[1];
|
|
|
|
// pull the sender's public key
|
|
|
|
cloudstorage.getPublicKey(pubKeyId, function(err, senderPk) {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// do authenticated decryption
|
|
|
|
naclCrypto.asymDecrypt(m.ciphertext, m.itemIV, senderPk.publicKey, keypair.boxSk, function(plaintext) {
|
|
|
|
callback(null, JSON.parse(plaintext));
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function symEncryptAndUpload(email, callback) {
|
|
|
|
var itemKey = util.random(self.account.get('symKeySize')),
|
|
|
|
itemIV = util.random(self.account.get('symIvSize')),
|
|
|
|
keyIV = util.random(self.account.get('symIvSize')),
|
|
|
|
json = JSON.stringify(email),
|
|
|
|
envelope, encryptedKey;
|
|
|
|
|
|
|
|
// symmetrically encrypt item
|
|
|
|
crypto.aesEncrypt(json, itemKey, itemIV, function(ct) {
|
|
|
|
|
|
|
|
// encrypt item key for user
|
|
|
|
encryptedKey = crypto.aesEncryptForUserSync(itemKey, keyIV);
|
|
|
|
envelope = {
|
|
|
|
id: email.id,
|
|
|
|
crypto: 'aes-128-ccm',
|
|
|
|
ciphertext: ct,
|
|
|
|
encryptedKey: encryptedKey,
|
|
|
|
keyIV: keyIV,
|
|
|
|
itemIV: itemIV
|
|
|
|
};
|
|
|
|
|
|
|
|
// push encrypted item to cloud
|
|
|
|
cloudstorage.putEncryptedItem(envelope, 'email', self.account.get('emailAddress'), 'inbox', function(err) {
|
|
|
|
callback(err);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
2013-04-23 10:35:01 -04:00
|
|
|
|
|
|
|
function deleteVinboxItem(email, callback) {
|
|
|
|
cloudstorage.deleteEncryptedItem(email.id, 'email', self.account.get('emailAddress'), 'vinbox', function(err) {
|
|
|
|
callback(err);
|
|
|
|
});
|
|
|
|
}
|
2013-04-19 13:13:27 -04:00
|
|
|
};
|
|
|
|
|
2013-04-02 09:02:57 -04:00
|
|
|
/**
|
|
|
|
* Synchronize a folder's items from the cloud to the device-storage
|
|
|
|
* @param folderName [String] The name of the folder e.g. 'inbox'
|
|
|
|
*/
|
|
|
|
this.syncFromCloud = function(folderName, callback) {
|
|
|
|
var folder, self = this;
|
|
|
|
|
2013-04-19 13:13:27 -04:00
|
|
|
cloudstorage.listEncryptedItems('email', this.account.get('emailAddress'), folderName, function(err, data) {
|
2013-04-02 09:02:57 -04:00
|
|
|
// return if an error occured or if fetched list from cloud storage is empty
|
2013-04-19 13:13:27 -04:00
|
|
|
if (err || !data || data.status || data.length === 0) {
|
2013-04-19 07:55:21 -04:00
|
|
|
callback({
|
|
|
|
error: err
|
|
|
|
}); // error
|
2013-04-02 09:02:57 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: remove old folder items from devicestorage
|
|
|
|
|
|
|
|
// persist encrypted list in device storage
|
2013-04-19 13:13:27 -04:00
|
|
|
devicestorage.storeEcryptedList(data, 'email_' + folderName, function() {
|
2013-04-02 09:02:57 -04:00
|
|
|
// remove cached folder in account model
|
|
|
|
folder = self.account.get('folders').where({
|
|
|
|
name: folderName
|
|
|
|
})[0];
|
|
|
|
if (folder) {
|
|
|
|
self.account.get('folders').remove(folder);
|
|
|
|
}
|
|
|
|
callback();
|
|
|
|
});
|
|
|
|
});
|
2013-03-13 11:58:46 -04:00
|
|
|
};
|
2013-04-01 18:12:15 -04:00
|
|
|
|
2013-05-02 12:49:22 -04:00
|
|
|
/**
|
|
|
|
* Send a plaintext Email to the user's outbox in the cloud
|
|
|
|
*/
|
|
|
|
this.sendEmail = function(email, callback) {
|
|
|
|
var userId = this.account.get('emailAddress');
|
|
|
|
|
2013-05-04 07:02:17 -04:00
|
|
|
// validate email addresses
|
2013-05-04 09:28:10 -04:00
|
|
|
var invalidRecipient;
|
2013-05-04 07:02:17 -04:00
|
|
|
_.each(email.get('to'), function(address) {
|
|
|
|
if (!validateEmail(address)) {
|
2013-05-04 09:28:10 -04:00
|
|
|
invalidRecipient = address;
|
2013-05-04 07:02:17 -04:00
|
|
|
}
|
|
|
|
});
|
2013-05-04 09:28:10 -04:00
|
|
|
if (invalidRecipient) {
|
|
|
|
callback({
|
|
|
|
errMsg: 'Invalid recipient: ' + invalidRecipient
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
2013-05-04 07:02:17 -04:00
|
|
|
if (!validateEmail(email.get('from'))) {
|
|
|
|
callback({
|
|
|
|
errMsg: 'Invalid sender: ' + email.from
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2013-05-03 10:09:13 -04:00
|
|
|
// generate a new UUID for the new email
|
|
|
|
email.set('id', util.UUID());
|
|
|
|
|
2013-05-04 07:02:17 -04:00
|
|
|
// send email to cloud service
|
2013-05-02 12:49:22 -04:00
|
|
|
cloudstorage.putEncryptedItem(email, 'email', userId, 'outbox', function(err) {
|
|
|
|
callback(err);
|
|
|
|
});
|
2013-05-04 07:02:17 -04:00
|
|
|
|
|
|
|
function validateEmail(email) {
|
|
|
|
var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
|
|
|
return re.test(email);
|
|
|
|
}
|
2013-05-02 12:49:22 -04:00
|
|
|
};
|
|
|
|
|
2013-04-02 09:02:57 -04:00
|
|
|
};
|