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
|
|
|
|
*/
|
2013-10-29 07:19:27 -04:00
|
|
|
define(function(require) {
|
2013-08-16 15:21:24 -04:00
|
|
|
'use strict';
|
|
|
|
|
2014-06-05 11:14:46 -04:00
|
|
|
var _ = require('underscore'),
|
2014-06-06 12:36:23 -04:00
|
|
|
util = require('js/crypto/util'),
|
|
|
|
config = require('js/app-config').config;
|
2014-06-05 11:14:46 -04:00
|
|
|
|
|
|
|
var DB_PUBLICKEY = 'publickey',
|
|
|
|
DB_PRIVATEKEY = 'privatekey',
|
|
|
|
DB_DEVICENAME = 'devicename',
|
2014-06-06 12:36:23 -04:00
|
|
|
DB_DEVICE_SECRET = 'devicesecret';
|
2013-08-16 15:21:24 -04:00
|
|
|
|
2014-06-06 12:36:23 -04:00
|
|
|
var KeychainDAO = function(localDbDao, publicKeyDao, privateKeyDao, crypto, pgp) {
|
2013-10-29 07:19:27 -04:00
|
|
|
this._localDbDao = localDbDao;
|
|
|
|
this._publicKeyDao = publicKeyDao;
|
2014-06-03 14:31:15 -04:00
|
|
|
this._privateKeyDao = privateKeyDao;
|
|
|
|
this._crypto = crypto;
|
2014-06-06 12:36:23 -04:00
|
|
|
this._pgp = pgp;
|
2013-08-16 15:21:24 -04:00
|
|
|
};
|
|
|
|
|
2014-06-03 14:31:15 -04:00
|
|
|
//
|
|
|
|
// Public key functions
|
|
|
|
//
|
|
|
|
|
2013-11-08 03:29:04 -05:00
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
};
|
|
|
|
|
2013-08-16 15:21:24 -04:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
|
|
|
|
2013-08-16 15:21:24 -04:00
|
|
|
after = _.after(ids.length, function() {
|
|
|
|
callback(null, pubkeys);
|
|
|
|
});
|
|
|
|
|
|
|
|
_.each(ids, function(i) {
|
2014-06-18 04:02:33 -04:00
|
|
|
// lookup locally and in storage
|
2013-08-16 15:21:24 -04:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2014-06-18 04:02:33 -04:00
|
|
|
// 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);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2013-08-16 15:21:24 -04:00
|
|
|
/**
|
|
|
|
* Look up a reveiver's public key by user id
|
|
|
|
* @param userId [String] the receiver's email address
|
|
|
|
*/
|
2013-10-29 07:19:27 -04:00
|
|
|
KeychainDAO.prototype.getReceiverPublicKey = function(userId, callback) {
|
2013-08-16 15:21:24 -04:00
|
|
|
var self = this;
|
|
|
|
|
|
|
|
// search local keyring for public key
|
2014-06-05 11:14:46 -04:00
|
|
|
self._localDbDao.list(DB_PUBLICKEY, 0, null, function(err, allPubkeys) {
|
2013-10-29 07:19:27 -04:00
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-06-18 04:02:33 -04:00
|
|
|
// query primary email address
|
2013-08-16 15:21:24 -04:00
|
|
|
var pubkey = _.findWhere(allPubkeys, {
|
|
|
|
userId: userId
|
|
|
|
});
|
|
|
|
|
2014-06-18 04:02:33 -04:00
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-10-29 07:19:27 -04:00
|
|
|
if (pubkey && pubkey._id) {
|
2013-08-16 15:21:24 -04:00
|
|
|
// that user's public key is already in local storage
|
|
|
|
callback(null, pubkey);
|
2013-10-29 07:19:27 -04:00
|
|
|
return;
|
2013-08-16 15:21:24 -04:00
|
|
|
}
|
2013-10-29 07:19:27 -04:00
|
|
|
|
|
|
|
// 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);
|
2013-08-16 15:21:24 -04:00
|
|
|
});
|
2013-10-29 07:19:27 -04:00
|
|
|
|
2014-05-23 04:52:34 -04:00
|
|
|
function onKeyReceived(err, cloudPubkey) {
|
|
|
|
if (err && err.code === 42) {
|
|
|
|
// offline
|
|
|
|
callback();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2013-10-29 07:19:27 -04:00
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!cloudPubkey) {
|
2014-05-23 04:52:34 -04:00
|
|
|
// public key has been deleted without replacement
|
2013-10-29 07:19:27 -04:00
|
|
|
callback();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
self.saveLocalPublicKey(cloudPubkey, function(err) {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
callback(null, cloudPubkey);
|
|
|
|
});
|
|
|
|
}
|
2013-08-16 15:21:24 -04:00
|
|
|
};
|
|
|
|
|
2014-06-03 14:31:15 -04:00
|
|
|
//
|
|
|
|
// 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) {
|
2014-06-13 06:33:30 -04:00
|
|
|
if (!deviceName) {
|
|
|
|
callback(new Error('Please set a device name!'));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-06-05 11:14:46 -04:00
|
|
|
this._localDbDao.persist(DB_DEVICENAME, deviceName, callback);
|
2014-06-03 14:31:15 -04:00
|
|
|
};
|
|
|
|
|
2014-06-06 12:36:23 -04:00
|
|
|
/**
|
|
|
|
* Get the device' memorable name from local storage. Throws an error if not set
|
|
|
|
* @param {Function} callback(error, deviceName)
|
|
|
|
* @return {String} The device name
|
|
|
|
*/
|
|
|
|
KeychainDAO.prototype.getDeviceName = function(callback) {
|
|
|
|
// check if deviceName is already persisted in storage
|
|
|
|
this._localDbDao.read(DB_DEVICENAME, function(err, deviceName) {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!deviceName) {
|
|
|
|
callback(new Error('Device name not set!'));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
callback(null, deviceName);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2014-06-03 14:31:15 -04:00
|
|
|
/**
|
|
|
|
* 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) {
|
2014-06-05 11:14:46 -04:00
|
|
|
var self = this;
|
|
|
|
|
2014-06-06 12:36:23 -04:00
|
|
|
// generate random deviceSecret or get from storage
|
|
|
|
self._localDbDao.read(DB_DEVICE_SECRET, function(err, storedDevSecret) {
|
2014-06-05 11:14:46 -04:00
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-06-06 12:36:23 -04:00
|
|
|
if (storedDevSecret) {
|
|
|
|
// a device key is already available locally
|
|
|
|
callback(null, storedDevSecret);
|
2014-06-05 11:14:46 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-06-06 12:36:23 -04:00
|
|
|
// generate random deviceSecret
|
|
|
|
var deviceSecret = util.random(config.symKeySize);
|
|
|
|
// persist deviceSecret to local storage (in plaintext)
|
|
|
|
self._localDbDao.persist(DB_DEVICE_SECRET, deviceSecret, function(err) {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
callback(null, deviceSecret);
|
2014-06-05 11:14:46 -04:00
|
|
|
});
|
|
|
|
});
|
2014-06-06 12:36:23 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Register the device on the private key server. This will give the device access to upload an encrypted private key.
|
|
|
|
* @param {String} options.userId The user's email address
|
|
|
|
* @param {Function} callback(error)
|
|
|
|
*/
|
|
|
|
KeychainDAO.prototype.registerDevice = function(options, callback) {
|
2014-06-13 06:33:30 -04:00
|
|
|
var self = this,
|
|
|
|
devName;
|
2014-06-06 12:36:23 -04:00
|
|
|
|
|
|
|
// check if deviceName is already persisted in storage
|
|
|
|
self.getDeviceName(function(err, deviceName) {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
requestDeviceRegistration(deviceName);
|
|
|
|
});
|
2014-06-05 11:14:46 -04:00
|
|
|
|
2014-06-06 12:36:23 -04:00
|
|
|
function requestDeviceRegistration(deviceName) {
|
2014-06-13 06:33:30 -04:00
|
|
|
devName = deviceName;
|
|
|
|
|
2014-06-06 12:36:23 -04:00
|
|
|
// request device registration session key
|
|
|
|
self._privateKeyDao.requestDeviceRegistration({
|
|
|
|
userId: options.userId,
|
|
|
|
deviceName: deviceName
|
|
|
|
}, function(err, regSessionKey) {
|
2014-06-05 11:14:46 -04:00
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
2014-06-03 14:31:15 -04:00
|
|
|
|
2014-06-06 12:36:23 -04:00
|
|
|
if (!regSessionKey.encryptedRegSessionKey) {
|
|
|
|
callback(new Error('Invalid format for session key!'));
|
2014-06-05 11:14:46 -04:00
|
|
|
return;
|
|
|
|
}
|
2014-06-03 14:31:15 -04:00
|
|
|
|
2014-06-06 12:36:23 -04:00
|
|
|
decryptSessionKey(regSessionKey);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function decryptSessionKey(regSessionKey) {
|
2014-06-13 06:33:30 -04:00
|
|
|
self.lookupPublicKey(config.serverPrivateKeyId, function(err, serverPubkey) {
|
2014-06-06 12:36:23 -04:00
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-06-13 06:33:30 -04:00
|
|
|
if (!serverPubkey || !serverPubkey.publicKey) {
|
|
|
|
callback(new Error('Server public key for device registration not found!'));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-06-06 12:36:23 -04:00
|
|
|
// decrypt the session key
|
|
|
|
var ct = regSessionKey.encryptedRegSessionKey;
|
2014-06-30 13:59:02 -04:00
|
|
|
self._pgp.decrypt(ct, serverPubkey.publicKey, function(err, decrypedSessionKey, signaturesValid) {
|
|
|
|
if (err || !signaturesValid) {
|
2014-06-27 10:19:30 -04:00
|
|
|
return callback(err || new Error('Verifying PGP signature failed!'));
|
2014-06-05 11:14:46 -04:00
|
|
|
}
|
2014-06-03 14:31:15 -04:00
|
|
|
|
2014-06-06 12:36:23 -04:00
|
|
|
uploadDeviceSecret(decrypedSessionKey);
|
2014-06-05 11:14:46 -04:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
2014-06-03 14:31:15 -04:00
|
|
|
|
2014-06-06 12:36:23 -04:00
|
|
|
function uploadDeviceSecret(regSessionKey) {
|
|
|
|
// read device secret from local storage
|
|
|
|
self.getDeviceSecret(function(err, deviceSecret) {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
2014-06-03 14:31:15 -04:00
|
|
|
|
2014-06-13 06:33:30 -04:00
|
|
|
// generate iv
|
|
|
|
var iv = util.random(config.symIvSize);
|
2014-06-06 12:36:23 -04:00
|
|
|
// encrypt deviceSecret
|
2014-06-13 06:33:30 -04:00
|
|
|
self._crypto.encrypt(deviceSecret, regSessionKey, iv, function(err, encryptedDeviceSecret) {
|
2014-06-06 12:36:23 -04:00
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// upload encryptedDeviceSecret
|
|
|
|
self._privateKeyDao.uploadDeviceSecret({
|
|
|
|
userId: options.userId,
|
2014-06-13 06:33:30 -04:00
|
|
|
deviceName: devName,
|
2014-06-06 12:36:23 -04:00
|
|
|
encryptedDeviceSecret: encryptedDeviceSecret,
|
2014-06-13 06:33:30 -04:00
|
|
|
iv: iv
|
2014-06-06 12:36:23 -04:00
|
|
|
}, callback);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
2014-06-03 14:31:15 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
//
|
|
|
|
// Private key functions
|
|
|
|
//
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Authenticate to the private key server (required before private PGP key upload).
|
|
|
|
* @param {String} userId The user's email address
|
2014-06-06 12:36:23 -04:00
|
|
|
* @param {Function} callback(error, authSessionKey)
|
|
|
|
* @return {Object} {sessionId:String, sessionKey:[base64 encoded]}
|
2014-06-03 14:31:15 -04:00
|
|
|
*/
|
2014-06-06 12:36:23 -04:00
|
|
|
KeychainDAO.prototype._authenticateToPrivateKeyServer = function(userId, callback) {
|
|
|
|
var self = this,
|
|
|
|
sessionId;
|
|
|
|
|
|
|
|
// request auth session key required for upload
|
2014-06-13 06:33:30 -04:00
|
|
|
self._privateKeyDao.requestAuthSessionKey({
|
2014-06-06 12:36:23 -04:00
|
|
|
userId: userId
|
|
|
|
}, function(err, authSessionKey) {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!authSessionKey.encryptedAuthSessionKey || !authSessionKey.encryptedChallenge || !authSessionKey.sessionId) {
|
|
|
|
callback(new Error('Invalid format for session key!'));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// remember session id for verification
|
|
|
|
sessionId = authSessionKey.sessionId;
|
|
|
|
|
|
|
|
decryptSessionKey(authSessionKey);
|
|
|
|
});
|
|
|
|
|
|
|
|
function decryptSessionKey(authSessionKey) {
|
2014-06-13 06:33:30 -04:00
|
|
|
self.lookupPublicKey(config.serverPrivateKeyId, function(err, serverPubkey) {
|
2014-06-06 12:36:23 -04:00
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-06-13 06:33:30 -04:00
|
|
|
if (!serverPubkey || !serverPubkey.publicKey) {
|
|
|
|
callback(new Error('Server public key for authentication not found!'));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-06-06 12:36:23 -04:00
|
|
|
// decrypt the session key
|
|
|
|
var ct1 = authSessionKey.encryptedAuthSessionKey;
|
2014-06-30 13:59:02 -04:00
|
|
|
self._pgp.decrypt(ct1, serverPubkey.publicKey, function(err, decryptedSessionKey, signaturesValid) {
|
|
|
|
if (err || !signaturesValid) {
|
2014-06-27 10:19:30 -04:00
|
|
|
return callback(err || new Error('Verifying PGP signature failed!'));
|
2014-06-06 12:36:23 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// decrypt the challenge
|
2014-06-13 06:33:30 -04:00
|
|
|
var ct2 = authSessionKey.encryptedChallenge;
|
2014-06-30 13:59:02 -04:00
|
|
|
self._pgp.decrypt(ct2, serverPubkey.publicKey, function(err, decryptedChallenge, signaturesValid) {
|
|
|
|
if (err || !signaturesValid) {
|
2014-06-27 10:19:30 -04:00
|
|
|
return callback(err || new Error('Verifying PGP signature failed!'));
|
2014-06-06 12:36:23 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
encryptChallenge(decryptedSessionKey, decryptedChallenge);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function encryptChallenge(sessionKey, challenge) {
|
|
|
|
// get device secret
|
|
|
|
self.getDeviceSecret(function(err, deviceSecret) {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var iv = util.random(config.symIvSize);
|
|
|
|
// encrypt the challenge
|
|
|
|
self._crypto.encrypt(challenge, sessionKey, iv, function(err, encryptedChallenge) {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// encrypt the device secret
|
|
|
|
self._crypto.encrypt(deviceSecret, sessionKey, iv, function(err, encryptedDeviceSecret) {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
replyChallenge({
|
|
|
|
encryptedChallenge: encryptedChallenge,
|
|
|
|
encryptedDeviceSecret: encryptedDeviceSecret,
|
|
|
|
iv: iv
|
|
|
|
}, sessionKey);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2014-06-13 06:33:30 -04:00
|
|
|
function replyChallenge(response, sessionKey) {
|
2014-06-06 12:36:23 -04:00
|
|
|
// respond to challenge by uploading the with the session key encrypted challenge
|
|
|
|
self._privateKeyDao.verifyAuthentication({
|
|
|
|
userId: userId,
|
|
|
|
sessionId: sessionId,
|
2014-06-13 06:33:30 -04:00
|
|
|
encryptedChallenge: response.encryptedChallenge,
|
|
|
|
encryptedDeviceSecret: response.encryptedDeviceSecret,
|
|
|
|
iv: response.iv
|
2014-06-06 12:36:23 -04:00
|
|
|
}, function(err) {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
callback(null, {
|
|
|
|
sessionId: sessionId,
|
|
|
|
sessionKey: sessionKey
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
2014-06-03 14:31:15 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Encrypt and upload the private PGP key to the server.
|
2014-06-06 12:36:23 -04:00
|
|
|
* @param {String} options.userId The user's email address
|
|
|
|
* @param {String} options.code The randomly generated or self selected code used to derive the key for the encryption of the private PGP key
|
|
|
|
* @param {Function} callback(error)
|
2014-06-03 14:31:15 -04:00
|
|
|
*/
|
2014-06-06 12:36:23 -04:00
|
|
|
KeychainDAO.prototype.uploadPrivateKey = function(options, callback) {
|
|
|
|
var self = this,
|
|
|
|
keySize = config.symKeySize,
|
|
|
|
salt;
|
|
|
|
|
|
|
|
if (!options.userId || !options.code) {
|
|
|
|
callback(new Error('Incomplete arguments!'));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
deriveKey(options.code);
|
|
|
|
|
|
|
|
function deriveKey(code) {
|
|
|
|
// generate random salt
|
|
|
|
salt = util.random(keySize);
|
|
|
|
// derive key from the code using PBKDF2
|
|
|
|
self._crypto.deriveKey(code, salt, keySize, function(err, key) {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
encryptPrivateKey(key);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function encryptPrivateKey(encryptionKey) {
|
|
|
|
// get private key from local storage
|
|
|
|
self.getUserKeyPair(options.userId, function(err, keypair) {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var privkeyId = keypair.privateKey._id,
|
|
|
|
pgpBlock = keypair.privateKey.encryptedKey;
|
|
|
|
|
2014-06-13 06:33:30 -04:00
|
|
|
// encrypt the private key with the derived key
|
2014-06-06 12:36:23 -04:00
|
|
|
var iv = util.random(config.symIvSize);
|
|
|
|
self._crypto.encrypt(pgpBlock, encryptionKey, iv, function(err, ct) {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
2014-06-03 14:31:15 -04:00
|
|
|
|
2014-06-06 12:36:23 -04:00
|
|
|
var payload = {
|
|
|
|
_id: privkeyId,
|
2014-06-13 06:33:30 -04:00
|
|
|
userId: options.userId,
|
2014-06-06 12:36:23 -04:00
|
|
|
encryptedPrivateKey: ct,
|
|
|
|
salt: salt,
|
|
|
|
iv: iv
|
|
|
|
};
|
2014-06-03 14:31:15 -04:00
|
|
|
|
2014-06-06 12:36:23 -04:00
|
|
|
uploadPrivateKey(payload);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function uploadPrivateKey(payload) {
|
|
|
|
// authenticate to server for upload
|
|
|
|
self._authenticateToPrivateKeyServer(options.userId, function(err, authSessionKey) {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// encrypt encryptedPrivateKey again using authSessionKey
|
|
|
|
var pt = payload.encryptedPrivateKey,
|
|
|
|
iv = payload.iv,
|
|
|
|
key = authSessionKey.sessionKey;
|
|
|
|
self._crypto.encrypt(pt, key, iv, function(err, ct) {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
2014-06-03 14:31:15 -04:00
|
|
|
|
2014-06-06 12:36:23 -04:00
|
|
|
// replace the encryptedPrivateKey with the double wrapped ciphertext
|
|
|
|
payload.encryptedPrivateKey = ct;
|
|
|
|
// set sessionId
|
|
|
|
payload.sessionId = authSessionKey.sessionId;
|
2014-06-03 14:31:15 -04:00
|
|
|
|
2014-06-06 12:36:23 -04:00
|
|
|
// upload the encrypted priavet key
|
|
|
|
self._privateKeyDao.upload(payload, callback);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
2014-06-03 14:31:15 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Request downloading the user's encrypted private key. This will initiate the server to send the recovery token via email/sms to the user.
|
2014-06-13 06:33:30 -04:00
|
|
|
* @param {String} options.userId The user's email address
|
|
|
|
* @param {String} options.keyId The private PGP key id
|
2014-06-03 14:31:15 -04:00
|
|
|
* @param {Function} callback(error)
|
|
|
|
*/
|
2014-06-13 06:33:30 -04:00
|
|
|
KeychainDAO.prototype.requestPrivateKeyDownload = function(options, callback) {
|
|
|
|
this._privateKeyDao.requestDownload(options, callback);
|
2014-06-03 14:31:15 -04:00
|
|
|
};
|
|
|
|
|
2014-08-28 12:17:34 -04:00
|
|
|
/**
|
|
|
|
* Query if an encrypted private PGP key exists on the server without initializing the recovery procedure
|
|
|
|
* @param {String} options.userId The user's email address
|
|
|
|
* @param {String} options.keyId The private PGP key id
|
|
|
|
* @param {Function} callback(error)
|
|
|
|
*/
|
|
|
|
KeychainDAO.prototype.hasPrivateKey = function(options, callback) {
|
|
|
|
this._privateKeyDao.hasPrivateKey(options, callback);
|
|
|
|
};
|
|
|
|
|
2014-06-03 14:31:15 -04:00
|
|
|
/**
|
|
|
|
* Download the encrypted private PGP key from the server using the recovery token.
|
2014-06-06 12:36:23 -04:00
|
|
|
* @param {String} options.userId The user's email address
|
|
|
|
* @param {String} options.keyId The user's email address
|
|
|
|
* @param {String} options.recoveryToken The recovery token acquired via email/sms from the key server
|
2014-06-03 14:31:15 -04:00
|
|
|
* @param {Function} callback(error, encryptedPrivateKey)
|
|
|
|
*/
|
2014-06-06 12:36:23 -04:00
|
|
|
KeychainDAO.prototype.downloadPrivateKey = function(options, callback) {
|
|
|
|
this._privateKeyDao.download(options, callback);
|
2014-06-03 14:31:15 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This is called after the encrypted private key has successfully been downloaded and it's ready to be decrypted and stored in localstorage.
|
2014-06-13 06:33:30 -04:00
|
|
|
* @param {String} options._id The private PGP key id
|
2014-06-06 12:36:23 -04:00
|
|
|
* @param {String} options.userId The user's email address
|
|
|
|
* @param {String} options.code The randomly generated or self selected code used to derive the key for the decryption of the private PGP key
|
2014-06-13 06:33:30 -04:00
|
|
|
* @param {String} options.encryptedPrivateKey The encrypted private PGP key
|
2014-06-06 12:36:23 -04:00
|
|
|
* @param {String} options.salt The salt required to derive the code derived key
|
|
|
|
* @param {String} options.iv The iv used to encrypt the private PGP key
|
2014-06-13 06:33:30 -04:00
|
|
|
* @param {Function} callback(error, keyObject)
|
2014-06-03 14:31:15 -04:00
|
|
|
*/
|
2014-06-06 12:36:23 -04:00
|
|
|
KeychainDAO.prototype.decryptAndStorePrivateKeyLocally = function(options, callback) {
|
|
|
|
var self = this,
|
|
|
|
code = options.code,
|
|
|
|
salt = options.salt,
|
2014-06-13 06:33:30 -04:00
|
|
|
keySize = config.symKeySize;
|
2014-06-06 12:36:23 -04:00
|
|
|
|
2014-06-13 06:33:30 -04:00
|
|
|
if (!options._id || !options.userId || !options.code || !options.salt || !options.encryptedPrivateKey || !options.iv) {
|
2014-06-06 12:36:23 -04:00
|
|
|
callback(new Error('Incomplete arguments!'));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-06-03 14:31:15 -04:00
|
|
|
// derive key from the code and the salt using PBKDF2
|
2014-06-06 12:36:23 -04:00
|
|
|
self._crypto.deriveKey(code, salt, keySize, function(err, key) {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
decryptAndStore(key);
|
|
|
|
});
|
2014-06-03 14:31:15 -04:00
|
|
|
|
2014-06-06 12:36:23 -04:00
|
|
|
function decryptAndStore(derivedKey) {
|
|
|
|
// decrypt the private key with the derived key
|
2014-06-13 06:33:30 -04:00
|
|
|
var ct = options.encryptedPrivateKey,
|
2014-06-06 12:36:23 -04:00
|
|
|
iv = options.iv;
|
2014-06-03 14:31:15 -04:00
|
|
|
|
2014-06-13 06:33:30 -04:00
|
|
|
self._crypto.decrypt(ct, derivedKey, iv, function(err, privateKeyArmored) {
|
2014-06-06 12:36:23 -04:00
|
|
|
if (err) {
|
2014-06-13 06:33:30 -04:00
|
|
|
callback(new Error('Invalid keychain code!'));
|
2014-06-06 12:36:23 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-06-13 06:33:30 -04:00
|
|
|
// validate pgp key
|
|
|
|
var keyParams;
|
|
|
|
try {
|
|
|
|
keyParams = self._pgp.getKeyParams(privateKeyArmored);
|
|
|
|
} catch (e) {
|
|
|
|
callback(new Error('Error parsing private PGP key!'));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (keyParams._id !== options._id || keyParams.userId !== options.userId) {
|
|
|
|
callback(new Error('Private key parameters don\'t match with public key\'s!'));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var keyObject = {
|
|
|
|
_id: options._id,
|
2014-06-06 12:36:23 -04:00
|
|
|
userId: options.userId,
|
2014-06-13 06:33:30 -04:00
|
|
|
encryptedKey: privateKeyArmored
|
|
|
|
};
|
|
|
|
|
|
|
|
// store private key locally
|
|
|
|
self.saveLocalPrivateKey(keyObject, function(err) {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
callback(null, keyObject);
|
|
|
|
});
|
2014-06-06 12:36:23 -04:00
|
|
|
});
|
|
|
|
}
|
2014-06-03 14:31:15 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
//
|
|
|
|
// Keypair functions
|
|
|
|
//
|
|
|
|
|
2013-08-16 15:21:24 -04:00
|
|
|
/**
|
|
|
|
* 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
|
2014-06-05 11:14:46 -04:00
|
|
|
self._localDbDao.list(DB_PUBLICKEY, 0, null, function(err, allPubkeys) {
|
2013-10-29 07:19:27 -04:00
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2013-08-16 15:21:24 -04:00
|
|
|
var pubkey = _.findWhere(allPubkeys, {
|
|
|
|
userId: userId
|
|
|
|
});
|
|
|
|
|
2013-10-29 07:19:27 -04:00
|
|
|
if (pubkey && pubkey._id) {
|
2013-08-16 15:21:24 -04:00
|
|
|
// that user's public key is already in local storage...
|
|
|
|
// sync keypair to the cloud
|
|
|
|
syncKeypair(pubkey._id);
|
2013-10-29 07:19:27 -04:00
|
|
|
return;
|
2013-08-16 15:21:24 -04:00
|
|
|
}
|
2013-10-29 07:19:27 -04:00
|
|
|
|
|
|
|
// 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();
|
|
|
|
});
|
2013-08-16 15:21:24 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
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 = {};
|
|
|
|
|
2013-08-16 15:21:24 -04:00
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2013-10-21 07:10:42 -04:00
|
|
|
if (savedPubkey && savedPubkey.publicKey) {
|
|
|
|
keys.publicKey = savedPubkey;
|
2013-08-16 15:21:24 -04:00
|
|
|
}
|
2013-10-29 07:19:27 -04:00
|
|
|
|
2013-10-21 07:10:42 -04:00
|
|
|
if (savedPrivkey && savedPrivkey.encryptedKey) {
|
|
|
|
keys.privateKey = savedPrivkey;
|
|
|
|
}
|
|
|
|
|
|
|
|
callback(null, keys);
|
2013-08-16 15:21:24 -04:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
|
|
|
|
2014-09-02 08:38:17 -04:00
|
|
|
// don't check the user's own public key for deletion in refreshKeyForUserId
|
|
|
|
keypair.publicKey.imported = true;
|
|
|
|
|
2013-08-16 15:21:24 -04:00
|
|
|
// store public key locally
|
|
|
|
self.saveLocalPublicKey(keypair.publicKey, function(err) {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// persist public key in cloud storage
|
2013-10-29 07:19:27 -04:00
|
|
|
self._publicKeyDao.put(keypair.publicKey, function(err) {
|
2013-08-16 15:21:24 -04:00
|
|
|
// validate result
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// store private key locally
|
2013-10-11 21:19:01 -04:00
|
|
|
self.saveLocalPrivateKey(keypair.privateKey, callback);
|
2013-08-16 15:21:24 -04:00
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
//
|
|
|
|
// Helper functions
|
|
|
|
//
|
|
|
|
|
|
|
|
KeychainDAO.prototype.lookupPublicKey = function(id, callback) {
|
|
|
|
var self = this;
|
|
|
|
|
2013-10-29 07:19:27 -04:00
|
|
|
if (!id) {
|
|
|
|
callback({
|
|
|
|
errMsg: 'ID must be set for public key query!'
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2013-08-16 15:21:24 -04:00
|
|
|
// lookup in local storage
|
2014-06-05 11:14:46 -04:00
|
|
|
self._localDbDao.read(DB_PUBLICKEY + '_' + id, function(err, pubkey) {
|
2013-10-29 07:19:27 -04:00
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-03-06 12:19:51 -05:00
|
|
|
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) {
|
2013-08-16 15:21:24 -04:00
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-03-06 12:19:51 -05:00
|
|
|
callback(null, cloudPubkey);
|
2013-08-16 15:21:24 -04:00
|
|
|
});
|
2014-03-06 12:19:51 -05:00
|
|
|
});
|
2013-08-16 15:21:24 -04:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2014-03-06 12:19:51 -05:00
|
|
|
/**
|
|
|
|
* List all the locally stored public keys
|
|
|
|
*/
|
|
|
|
KeychainDAO.prototype.listLocalPublicKeys = function(callback) {
|
|
|
|
// search local keyring for public key
|
2014-06-05 11:14:46 -04:00
|
|
|
this._localDbDao.list(DB_PUBLICKEY, 0, null, callback);
|
2014-03-06 12:19:51 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
KeychainDAO.prototype.removeLocalPublicKey = function(id, callback) {
|
2014-06-05 11:14:46 -04:00
|
|
|
this._localDbDao.remove(DB_PUBLICKEY + '_' + id, callback);
|
2014-03-06 12:19:51 -05:00
|
|
|
};
|
|
|
|
|
2013-08-16 15:21:24 -04:00
|
|
|
KeychainDAO.prototype.lookupPrivateKey = function(id, callback) {
|
|
|
|
// lookup in local storage
|
2014-06-05 11:14:46 -04:00
|
|
|
this._localDbDao.read(DB_PRIVATEKEY + '_' + id, callback);
|
2013-08-16 15:21:24 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
KeychainDAO.prototype.saveLocalPublicKey = function(pubkey, callback) {
|
|
|
|
// persist public key (email, _id)
|
2014-06-05 11:14:46 -04:00
|
|
|
var pkLookupKey = DB_PUBLICKEY + '_' + pubkey._id;
|
2013-10-29 07:19:27 -04:00
|
|
|
this._localDbDao.persist(pkLookupKey, pubkey, callback);
|
2013-08-16 15:21:24 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
KeychainDAO.prototype.saveLocalPrivateKey = function(privkey, callback) {
|
|
|
|
// persist private key (email, _id)
|
2014-06-05 11:14:46 -04:00
|
|
|
var prkLookupKey = DB_PRIVATEKEY + '_' + privkey._id;
|
2013-10-29 07:19:27 -04:00
|
|
|
this._localDbDao.persist(prkLookupKey, privkey, callback);
|
2013-08-16 15:21:24 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
return KeychainDAO;
|
2014-06-27 10:19:30 -04:00
|
|
|
});
|