mail/src/js/dao/keychain-dao.js

541 lines
18 KiB
JavaScript
Raw Normal View History

2013-05-23 17:45:26 -04:00
/**
* A high-level Data-Access Api for handling Keypair synchronization
* between the cloud service and the device's local storage
*/
define(function(require) {
'use strict';
var _ = require('underscore');
var KeychainDAO = function(localDbDao, publicKeyDao, privateKeyDao, crypto) {
this._localDbDao = localDbDao;
this._publicKeyDao = publicKeyDao;
this._privateKeyDao = privateKeyDao;
this._crypto = crypto;
};
//
// Public key functions
//
/**
* Verifies the public key of a user o nthe public key store
* @param {String} uuid The uuid to verify the key
* @param {Function} callback(error) Callback with an optional error object when the verification is done. If the was an error, the error object contains the information for it.
*/
KeychainDAO.prototype.verifyPublicKey = function(uuid, callback) {
this._publicKeyDao.verify(uuid, callback);
};
/**
* Get an array of public keys by looking in local storage and
* fetching missing keys from the cloud service.
* @param ids [Array] the key ids as [{_id, userId}]
* @return [PublicKeyCollection] The requiested public keys
*/
KeychainDAO.prototype.getPublicKeys = function(ids, callback) {
var self = this,
after, already, pubkeys = [];
2013-10-04 11:43:55 -04:00
// return empty array if key ids are emtpy
if (ids.length < 1) {
callback(null, pubkeys);
return;
}
after = _.after(ids.length, function() {
callback(null, pubkeys);
});
_.each(ids, function(i) {
// lookup locally and in storage
self.lookupPublicKey(i._id, function(err, pubkey) {
if (err || !pubkey) {
callback({
errMsg: 'Error looking up public key!',
err: err
});
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
});
});
};
2014-05-23 04:52:34 -04:00
/**
* Checks for public key updates of a given user id
* @param {String} userId The user id (email address) for which to check the key
* @param {Function} callback(error, key) Invoked when the key has been updated or an error occurred
*/
KeychainDAO.prototype.refreshKeyForUserId = function(userId, callback) {
var self = this;
// get the public key corresponding to the userId
self.getReceiverPublicKey(userId, function(err, localKey) {
if (!localKey || !localKey._id) {
// there is no key available, no need to refresh
callback();
return;
}
// no need to refresh manually imported public keys
if (localKey.imported) {
callback(null, localKey);
return;
}
2014-05-23 04:52:34 -04:00
// check if the key id still exists on the key server
checkKeyExists(localKey);
});
// checks if the user's key has been revoked by looking up the key id
function checkKeyExists(localKey) {
self._publicKeyDao.get(localKey._id, function(err, cloudKey) {
if (err && err.code === 42) {
// we're offline, we're done checking the key
callback(null, localKey);
return;
}
if (err) {
// there was an error, exit and inform
callback(err);
return;
}
if (cloudKey && cloudKey._id === localKey._id) {
// the key is present on the server, all is well
callback(null, localKey);
return;
}
// the key has changed, update the key
updateKey(localKey);
});
}
function updateKey(localKey) {
// look for an updated key for the user id
self._publicKeyDao.getByUserId(userId, function(err, newKey) {
// offline?
if (err && err.code === 42) {
callback(null, localKey);
return;
}
if (err) {
callback(err);
return;
}
// the public key has changed, we need to ask for permission to update the key
self.requestPermissionForKeyUpdate({
userId: userId,
newKey: newKey
}, function(granted) {
if (!granted) {
// permission was not given to update the key, so don't overwrite the old one!
callback(null, localKey);
return;
}
// permission to update the key was given, so delete the old one and persist the new one
self.removeLocalPublicKey(localKey._id, function(err) {
if (err || !newKey) {
// error or no new key to save
callback(err);
return;
}
// persist the new key and return it
self.saveLocalPublicKey(newKey, function(err) {
callback(err, err ? undefined : newKey);
});
});
});
});
}
};
/**
* Look up a reveiver's public key by user id
* @param userId [String] the receiver's email address
*/
KeychainDAO.prototype.getReceiverPublicKey = function(userId, callback) {
var self = this;
// search local keyring for public key
self._localDbDao.list('publickey', 0, null, function(err, allPubkeys) {
if (err) {
callback(err);
return;
}
// query primary email address
var pubkey = _.findWhere(allPubkeys, {
userId: userId
});
// query mutliple userIds (for imported public keys)
if (!pubkey) {
for (var i = 0, match; i < allPubkeys.length; i++) {
match = _.findWhere(allPubkeys[i].userIds, {
emailAddress: userId
});
if (match) {
pubkey = allPubkeys[i];
break;
}
}
}
if (pubkey && pubkey._id) {
// that user's public key is already in local storage
callback(null, pubkey);
return;
}
// no public key by that user id in storage
// find from cloud by email address
2014-05-23 04:52:34 -04:00
self._publicKeyDao.getByUserId(userId, onKeyReceived);
});
2014-05-23 04:52:34 -04:00
function onKeyReceived(err, cloudPubkey) {
if (err && err.code === 42) {
// offline
callback();
return;
}
if (err) {
callback(err);
return;
}
if (!cloudPubkey) {
2014-05-23 04:52:34 -04:00
// public key has been deleted without replacement
callback();
return;
}
self.saveLocalPublicKey(cloudPubkey, function(err) {
if (err) {
callback(err);
return;
}
callback(null, cloudPubkey);
});
}
};
//
// Device registration functions
//
/**
* Set the device's memorable name e.g 'iPhone Work'
* @param {String} deviceName The device name
* @param {Function} callback(error)
*/
KeychainDAO.prototype.setDeviceName = function(deviceName, callback) {
callback(new Error('Not yet implemented!'));
};
/**
* Geneate a device specific key and secret to authenticate to the private key service.
* @param {Function} callback(error, deviceSecret:[base64 encoded string])
*/
KeychainDAO.prototype.getDeviceSecret = function(callback) {
// check if deviceName is already persisted in storage and if not return an error
// generate random deviceKeys or get from storage
// persist deviceKeys to local storage (in plaintext)
// encrypt: deviceSecret = Es(deviceKeys, deviceName) -> callback
callback(new Error('Not yet implemented!'));
};
/**
* Register the device on the private key server. This will give the device access to upload an encrypted private key.
* @param {String} userId The user's email address
* @param {String} deviceName The device's memorable name e.g 'iPhone Work'
* @param {[type]} deviceSecret The device specific secret derived from the device key and the device name.
* @param {Function} callback(error)
*/
KeychainDAO.prototype.registerDevice = function(userId, deviceName, deviceSecret, callback) {
callback(new Error('Not yet implemented!'));
};
//
// Private key functions
//
/**
* Authenticate to the private key server (required before private PGP key upload).
* @param {String} userId The user's email address
* @param {Function} callback(error)
*/
KeychainDAO.prototype.authenticateToPrivateKeyServer = function(userId, callback) {
callback(new Error('Not yet implemented!'));
};
/**
* Encrypt and upload the private PGP key to the server.
* @param {Object} privkey
* @param {String} code The randomly generated or self selected code used to derive the key for the encryption of the private PGP key
* @param {Function} callback
*/
KeychainDAO.prototype.uploadPrivateKeyToServer = function(privkey, code, callback) {
// generate random salt
// derive key from the code using PBKDF2
// encrypt the private key with the derived key (AES-GCM authenticated encryption)
// upload the 'privatekey' {salt:[base64 encoded string], encryptedPrivateKey:[base64 encoded string]}
callback(new Error('Not yet implemented!'));
};
/**
* Request downloading the user's encrypted private key. This will initiate the server to send the recovery token via email/sms to the user.
* @param {[type]} userId The user's email address
* @param {Function} callback(error)
*/
KeychainDAO.prototype.requestPrivateKeyDownload = function(userId, callback) {
callback(new Error('Not yet implemented!'));
};
/**
* Download the encrypted private PGP key from the server using the recovery token.
* @param {[type]} recoveryToken The recovery token acquired via email/sms from the key server
* @param {Function} callback(error, encryptedPrivateKey)
*/
KeychainDAO.prototype.downloadPrivateKeyFromServer = function(recoveryToken, callback) {
callback(new Error('Not yet implemented!'));
};
/**
* This is called after the encrypted private key has successfully been downloaded and it's ready to be decrypted and stored in localstorage.
* @param {[type]} code The randomly generated or self selected code used to derive the key for the decryption of the private PGP key
* @param {Object} encryptedPrivkey The encrypted private PGP key including the random salt {salt:[base64 encoded string], encryptedPrivateKey:[base64 encoded string]}
* @param {Function} callback(error, privateKey)
*/
KeychainDAO.prototype.decryptAndStorePrivateKeyLocally = function(code, encryptedPrivkey, callback) {
// derive key from the code and the salt using PBKDF2
// decrypt the private key with the derived key (AES-GCM authenticated decryption)
callback(new Error('Not yet implemented!'));
};
//
// Keypair functions
//
/**
* Gets the local user's key either from local storage
* or fetches it from the cloud. The private key is encrypted.
* If no key pair exists, null is returned.
* return [Object] The user's key pair {publicKey, privateKey}
*/
KeychainDAO.prototype.getUserKeyPair = function(userId, callback) {
var self = this;
// search for user's public key locally
self._localDbDao.list('publickey', 0, null, function(err, allPubkeys) {
if (err) {
callback(err);
return;
}
var pubkey = _.findWhere(allPubkeys, {
userId: userId
});
if (pubkey && pubkey._id) {
// that user's public key is already in local storage...
// sync keypair to the cloud
syncKeypair(pubkey._id);
return;
}
// no public key by that user id in storage
// find from cloud by email address
self._publicKeyDao.getByUserId(userId, function(err, cloudPubkey) {
if (err) {
callback(err);
return;
}
if (cloudPubkey && cloudPubkey._id) {
// there is a public key for that user already in the cloud...
// sync keypair to local storage
syncKeypair(cloudPubkey._id);
return;
}
// continue without keypair... generate in crypto.js
callback();
});
});
function syncKeypair(keypairId) {
// persist key pair in local storage
self.lookupPublicKey(keypairId, function(err, savedPubkey) {
if (err) {
callback(err);
return;
}
// persist private key in local storage
self.lookupPrivateKey(keypairId, function(err, savedPrivkey) {
2013-10-21 07:10:42 -04:00
var keys = {};
if (err) {
callback(err);
return;
}
2013-10-21 07:10:42 -04:00
if (savedPubkey && savedPubkey.publicKey) {
keys.publicKey = savedPubkey;
}
2013-10-21 07:10:42 -04:00
if (savedPrivkey && savedPrivkey.encryptedKey) {
keys.privateKey = savedPrivkey;
}
callback(null, keys);
});
});
}
};
/**
* Checks to see if the user's key pair is stored both
* locally and in the cloud and persist arccordingly
* @param [Object] The user's key pair {publicKey, privateKey}
*/
KeychainDAO.prototype.putUserKeyPair = function(keypair, callback) {
var self = this;
// validate input
if (!keypair || !keypair.publicKey || !keypair.privateKey || !keypair.publicKey.userId || keypair.publicKey.userId !== keypair.privateKey.userId) {
callback({
errMsg: 'Incorrect input!'
});
return;
}
// store public key locally
self.saveLocalPublicKey(keypair.publicKey, function(err) {
if (err) {
callback(err);
return;
}
// persist public key in cloud storage
self._publicKeyDao.put(keypair.publicKey, function(err) {
// validate result
if (err) {
callback(err);
return;
}
// store private key locally
2013-10-11 21:19:01 -04:00
self.saveLocalPrivateKey(keypair.privateKey, callback);
});
});
};
//
// Helper functions
//
KeychainDAO.prototype.lookupPublicKey = function(id, callback) {
var self = this;
if (!id) {
callback({
errMsg: 'ID must be set for public key query!'
});
return;
}
// lookup in local storage
self._localDbDao.read('publickey_' + id, function(err, pubkey) {
if (err) {
callback(err);
return;
}
if (pubkey) {
callback(null, pubkey);
return;
}
// fetch from cloud storage
self._publicKeyDao.get(id, function(err, cloudPubkey) {
if (err) {
callback(err);
return;
}
// cache public key in cache
self.saveLocalPublicKey(cloudPubkey, function(err) {
if (err) {
callback(err);
return;
}
callback(null, cloudPubkey);
});
});
});
};
/**
* List all the locally stored public keys
*/
KeychainDAO.prototype.listLocalPublicKeys = function(callback) {
// search local keyring for public key
this._localDbDao.list('publickey', 0, null, callback);
};
KeychainDAO.prototype.removeLocalPublicKey = function(id, callback) {
this._localDbDao.remove('publickey_' + id, callback);
};
KeychainDAO.prototype.lookupPrivateKey = function(id, callback) {
// lookup in local storage
this._localDbDao.read('privatekey_' + id, callback);
};
KeychainDAO.prototype.saveLocalPublicKey = function(pubkey, callback) {
// persist public key (email, _id)
var pkLookupKey = 'publickey_' + pubkey._id;
this._localDbDao.persist(pkLookupKey, pubkey, callback);
};
KeychainDAO.prototype.saveLocalPrivateKey = function(privkey, callback) {
// persist private key (email, _id)
var prkLookupKey = 'privatekey_' + privkey._id;
this._localDbDao.persist(prkLookupKey, privkey, callback);
};
return KeychainDAO;
2013-06-10 16:45:21 -04:00
});