mail/src/js/service/keychain.js

777 lines
26 KiB
JavaScript
Raw Normal View History

2014-10-02 16:05:44 -04:00
'use strict';
var ngModule = angular.module('woServices');
ngModule.service('keychain', Keychain);
module.exports = Keychain;
var util = require('crypto-lib').util;
2014-10-02 16:05:44 -04:00
var DB_PUBLICKEY = 'publickey',
DB_PRIVATEKEY = 'privatekey',
DB_DEVICENAME = 'devicename',
DB_DEVICE_SECRET = 'devicesecret';
/**
* A high-level Data-Access Api for handling Keypair synchronization
* between the cloud service and the device's local storage
*/
function Keychain(accountLawnchair, publicKey, privateKey, crypto, pgp, dialog, appConfig) {
this._lawnchairDAO = accountLawnchair;
this._publicKeyDao = publicKey;
this._privateKeyDao = privateKey;
2014-10-02 16:05:44 -04:00
this._crypto = crypto;
this._pgp = pgp;
this._dialog = dialog;
this._appConfig = appConfig;
}
2014-10-02 16:05:44 -04:00
//
// Public key functions
//
/**
* Display confirmation dialog to request a public key update
* @param {Object} params.newKey The user's updated public key object
* @param {String} params.userId The user's email address
*/
Keychain.prototype.requestPermissionForKeyUpdate = function(params, callback) {
var str = this._appConfig.string;
var message = params.newKey ? str.updatePublicKeyMsgNewKey : str.updatePublicKeyMsgRemovedKey;
message = message.replace('{0}', params.userId);
this._dialog.confirm({
title: str.updatePublicKeyTitle,
message: message,
positiveBtnStr: str.updatePublicKeyPosBtn,
negativeBtnStr: str.updatePublicKeyNegBtn,
showNegativeBtn: true,
callback: callback
});
};
2014-10-02 16:05:44 -04:00
/**
* Verifies the public key of a user o nthe public key store
* @param {String} uuid The uuid to verify the key
*/
2014-12-11 14:43:51 -05:00
Keychain.prototype.verifyPublicKey = function(uuid) {
return this._publicKeyDao.verify(uuid);
2014-10-02 16:05:44 -04:00
};
2014-10-02 16:05:44 -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
*/
2014-12-12 12:02:13 -05:00
Keychain.prototype.getPublicKeys = function(ids) {
2014-12-18 10:50:11 -05:00
var self = this,
jobs = [],
pubkeys = [];
2014-10-02 16:05:44 -04:00
2014-12-18 10:50:11 -05:00
ids.forEach(function(i) {
// lookup locally and in storage
var promise = self.lookupPublicKey(i._id).then(function(pubkey) {
if (!pubkey) {
throw new Error('Error looking up public key!');
}
2014-10-02 16:05:44 -04:00
2014-12-18 10:50:11 -05:00
// check if public key with that id has already been fetched
var already = _.findWhere(pubkeys, {
_id: i._id
});
if (!already) {
pubkeys.push(pubkey);
}
2014-12-12 12:02:13 -05:00
});
2014-12-18 10:50:11 -05:00
jobs.push(promise);
});
2014-10-02 16:05:44 -04:00
2014-12-18 10:50:11 -05:00
return Promise.all(jobs).then(function() {
return pubkeys;
2014-10-02 16:05:44 -04:00
});
};
/**
* Checks for public key updates of a given user id
* @param {String} options.userId The user id (email address) for which to check the key
* @param {String} options.overridePermission (optional) Indicates if the update should happen automatically (true) or with the user being queried (false). Defaults to false
2014-10-02 16:05:44 -04:00
*/
2014-12-12 12:02:13 -05:00
Keychain.prototype.refreshKeyForUserId = function(options) {
var self = this,
userId = options.userId,
overridePermission = options.overridePermission;
2014-10-02 16:05:44 -04:00
// get the public key corresponding to the userId
2014-12-12 12:02:13 -05:00
return self.getReceiverPublicKey(userId).then(function(localKey) {
2014-10-02 16:05:44 -04:00
if (!localKey || !localKey._id) {
// there is no key available, no need to refresh
return;
}
// no need to refresh manually imported public keys
if (localKey.imported) {
2014-12-12 12:02:13 -05:00
return localKey;
2014-10-02 16:05:44 -04:00
}
// check if the key id still exists on the key server
2014-12-12 12:02:13 -05:00
return checkKeyExists(localKey);
2014-10-02 16:05:44 -04:00
});
// checks if the user's key has been revoked by looking up the key id
function checkKeyExists(localKey) {
2014-12-12 12:02:13 -05:00
return self._publicKeyDao.get(localKey._id).then(function(cloudKey) {
2014-10-02 16:05:44 -04:00
if (cloudKey && cloudKey._id === localKey._id) {
// the key is present on the server, all is well
2014-12-12 12:02:13 -05:00
return localKey;
}
2014-10-02 16:05:44 -04:00
// the key has changed, update the key
2014-12-12 12:02:13 -05:00
return updateKey(localKey);
}).catch(function(err) {
if (err && err.code === 42) {
// we're offline, we're done checking the key
return localKey;
}
throw err;
2014-05-23 04:52:34 -04:00
});
2014-10-02 16:05:44 -04:00
}
2014-05-23 04:52:34 -04:00
2014-10-02 16:05:44 -04:00
function updateKey(localKey) {
// look for an updated key for the user id
2014-12-12 12:02:13 -05:00
return self._publicKeyDao.getByUserId(userId).then(function(newKey) {
2014-10-02 16:05:44 -04:00
// the public key has changed, we need to ask for permission to update the key
if (overridePermission) {
// don't query the user, update the public key right away
2014-12-12 12:02:13 -05:00
return permissionGranted(localKey, newKey);
} else {
2014-12-12 12:02:13 -05:00
return requestPermission(localKey, newKey);
}
2014-12-12 12:02:13 -05:00
}).catch(function(err) {
// offline?
if (err && err.code === 42) {
return localKey;
}
throw err;
});
}
function requestPermission(localKey, newKey) {
return new Promise(function(resolve, reject) {
// query the user if the public key should be updated
self.requestPermissionForKeyUpdate({
userId: userId,
newKey: newKey
}, function(granted) {
2014-10-02 16:05:44 -04:00
if (!granted) {
// permission was not given to update the key, so don't overwrite the old one!
2014-12-12 12:02:13 -05:00
resolve(localKey);
2014-05-23 04:52:34 -04:00
return;
}
2014-12-12 12:02:13 -05:00
// permission was granted by the user
permissionGranted(localKey, newKey).then(resolve).catch(reject);
});
});
}
2014-05-23 04:52:34 -04:00
2014-12-12 12:02:13 -05:00
function permissionGranted(localKey, newKey) {
// permission to update the key was given, so delete the old one and persist the new one
return self.removeLocalPublicKey(localKey._id).then(function() {
if (!newKey) {
// error or no new key to save
return;
}
2014-12-12 12:02:13 -05:00
// persist the new key and return it
return self.saveLocalPublicKey(newKey).then(function() {
return newKey;
});
2014-10-02 16:05:44 -04:00
});
}
};
/**
* Look up a reveiver's public key by user id
* @param userId [String] the receiver's email address
*/
2014-12-12 12:02:13 -05:00
Keychain.prototype.getReceiverPublicKey = function(userId) {
2014-10-02 16:05:44 -04:00
var self = this;
// search local keyring for public key
2014-12-12 12:02:13 -05:00
return self._lawnchairDAO.list(DB_PUBLICKEY, 0, null).then(function(allPubkeys) {
2014-10-02 16:05:44 -04:00
// 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;
2014-05-23 04:52:34 -04:00
}
2014-10-02 16:05:44 -04:00
}
}
2014-12-12 12:02:13 -05:00
// that user's public key is already in local storage
2014-10-02 16:05:44 -04:00
if (pubkey && pubkey._id) {
2014-12-12 12:02:13 -05:00
return pubkey;
2014-10-02 16:05:44 -04:00
}
// no public key by that user id in storage
// find from cloud by email address
2014-12-12 12:02:13 -05:00
return self._publicKeyDao.getByUserId(userId).then(onKeyReceived).catch(onError);
2014-10-02 16:05:44 -04:00
});
2014-05-23 04:52:34 -04:00
2014-12-12 12:02:13 -05:00
function onKeyReceived(cloudPubkey) {
2014-10-02 16:05:44 -04:00
if (!cloudPubkey) {
// public key has been deleted without replacement
return;
}
2014-12-12 12:02:13 -05:00
// persist and return cloud key
return self.saveLocalPublicKey(cloudPubkey).then(function() {
return cloudPubkey;
2014-10-02 16:05:44 -04:00
});
}
2014-12-12 12:02:13 -05:00
function onError(err) {
if (err && err.code === 42) {
// offline
return;
}
throw err;
}
2014-10-02 16:05:44 -04:00
};
2014-10-02 16:05:44 -04:00
//
// Device registration functions
//
2014-10-02 16:05:44 -04:00
/**
* Set the device's memorable name e.g 'iPhone Work'
* @param {String} deviceName The device name
*/
2014-12-12 12:02:13 -05:00
Keychain.prototype.setDeviceName = function(deviceName) {
2014-10-02 16:05:44 -04:00
if (!deviceName) {
2014-12-12 12:02:13 -05:00
return new Promise(function() {
throw new Error('Please set a device name!');
});
2014-10-02 16:05:44 -04:00
}
2014-12-12 12:02:13 -05:00
return this._lawnchairDAO.persist(DB_DEVICENAME, deviceName);
2014-10-02 16:05:44 -04:00
};
2014-10-02 16:05:44 -04:00
/**
* Get the device' memorable name from local storage. Throws an error if not set
* @return {String} The device name
*/
2014-12-12 12:02:13 -05:00
Keychain.prototype.getDeviceName = function() {
2014-10-02 16:05:44 -04:00
// check if deviceName is already persisted in storage
2014-12-12 12:02:13 -05:00
return this._lawnchairDAO.read(DB_DEVICENAME).then(function(deviceName) {
2014-10-02 16:05:44 -04:00
if (!deviceName) {
2014-12-12 12:02:13 -05:00
throw new Error('Device name not set!');
2014-10-02 16:05:44 -04:00
}
2014-12-12 12:02:13 -05:00
return deviceName;
2014-10-02 16:05:44 -04:00
});
};
/**
* Geneate a device specific key and secret to authenticate to the private key service.
*/
2014-12-12 12:02:13 -05:00
Keychain.prototype.getDeviceSecret = function() {
var self = this,
config = self._appConfig.config;
2014-10-02 16:05:44 -04:00
// generate random deviceSecret or get from storage
2014-12-12 12:02:13 -05:00
return self._lawnchairDAO.read(DB_DEVICE_SECRET).then(function(storedDevSecret) {
2014-10-02 16:05:44 -04:00
if (storedDevSecret) {
// a device key is already available locally
2014-12-12 12:02:13 -05:00
return storedDevSecret;
2014-10-02 16:05:44 -04:00
}
2014-05-23 04:52:34 -04:00
2014-10-02 16:05:44 -04:00
// generate random deviceSecret
var deviceSecret = util.random(config.symKeySize);
// persist deviceSecret to local storage (in plaintext)
2014-12-12 12:02:13 -05:00
return self._lawnchairDAO.persist(DB_DEVICE_SECRET, deviceSecret).then(function() {
return deviceSecret;
2014-10-02 16:05:44 -04:00
});
});
};
2014-10-02 16:05:44 -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
*/
2014-12-12 12:02:13 -05:00
Keychain.prototype.registerDevice = function(options) {
2014-10-02 16:05:44 -04:00
var self = this,
devName,
config = self._appConfig.config;
2014-10-02 16:05:44 -04:00
// check if deviceName is already persisted in storage
2014-12-12 12:02:13 -05:00
return self.getDeviceName().then(function(deviceName) {
return requestDeviceRegistration(deviceName);
2014-10-02 16:05:44 -04:00
});
function requestDeviceRegistration(deviceName) {
devName = deviceName;
// request device registration session key
2014-12-12 12:02:13 -05:00
return self._privateKeyDao.requestDeviceRegistration({
2014-10-02 16:05:44 -04:00
userId: options.userId,
deviceName: deviceName
2014-12-12 12:02:13 -05:00
}).then(function(regSessionKey) {
2014-10-02 16:05:44 -04:00
if (!regSessionKey.encryptedRegSessionKey) {
2014-12-12 12:02:13 -05:00
throw new Error('Invalid format for session key!');
2014-06-06 12:36:23 -04:00
}
2014-12-12 12:02:13 -05:00
return decryptSessionKey(regSessionKey);
2014-06-06 12:36:23 -04:00
});
2014-10-02 16:05:44 -04:00
}
2014-06-06 12:36:23 -04:00
2014-10-02 16:05:44 -04:00
function decryptSessionKey(regSessionKey) {
2014-12-12 12:02:13 -05:00
return self.lookupPublicKey(config.serverPrivateKeyId).then(function(serverPubkey) {
2014-10-02 16:05:44 -04:00
if (!serverPubkey || !serverPubkey.publicKey) {
2014-12-12 12:02:13 -05:00
throw new Error('Server public key for device registration not found!');
}
2014-10-02 16:05:44 -04:00
// decrypt the session key
var ct = regSessionKey.encryptedRegSessionKey;
2014-12-12 12:02:13 -05:00
return self._pgp.decrypt(ct, serverPubkey.publicKey).then(function(pt) {
if (!pt.signaturesValid) {
throw new Error('Verifying PGP signature failed!');
2014-06-06 12:36:23 -04:00
}
2014-12-12 12:02:13 -05:00
return uploadDeviceSecret(pt.decrypted);
});
});
2014-10-02 16:05:44 -04:00
}
function uploadDeviceSecret(regSessionKey) {
2014-12-12 12:02:13 -05:00
// generate iv
var iv = util.random(config.symIvSize);
2014-10-02 16:05:44 -04:00
// read device secret from local storage
2014-12-12 12:02:13 -05:00
return self.getDeviceSecret().then(function(deviceSecret) {
2014-10-02 16:05:44 -04:00
// encrypt deviceSecret
2014-12-12 12:02:13 -05:00
return self._crypto.encrypt(deviceSecret, regSessionKey, iv);
2014-12-12 12:02:13 -05:00
}).then(function(encryptedDeviceSecret) {
// upload encryptedDeviceSecret
return self._privateKeyDao.uploadDeviceSecret({
userId: options.userId,
deviceName: devName,
encryptedDeviceSecret: encryptedDeviceSecret,
iv: iv
2014-06-06 12:36:23 -04:00
});
2014-10-02 16:05:44 -04:00
});
}
};
2014-06-06 12:36:23 -04:00
2014-10-02 16:05:44 -04:00
//
// Private key functions
//
2014-10-02 16:05:44 -04:00
/**
* Authenticate to the private key server (required before private PGP key upload).
* @param {String} userId The user's email address
* @return {Object} {sessionId:String, sessionKey:[base64 encoded]}
*/
2014-12-12 12:02:13 -05:00
Keychain.prototype._authenticateToPrivateKeyServer = function(userId) {
2014-10-02 16:05:44 -04:00
var self = this,
sessionId,
config = self._appConfig.config;
2014-10-02 16:05:44 -04:00
// request auth session key required for upload
2014-12-12 12:02:13 -05:00
return self._privateKeyDao.requestAuthSessionKey({
2014-10-02 16:05:44 -04:00
userId: userId
2014-12-12 12:02:13 -05:00
}).then(function(authSessionKey) {
2014-10-02 16:05:44 -04:00
if (!authSessionKey.encryptedAuthSessionKey || !authSessionKey.encryptedChallenge || !authSessionKey.sessionId) {
2014-12-12 12:02:13 -05:00
throw new Error('Invalid format for session key!');
}
2014-10-02 16:05:44 -04:00
// remember session id for verification
sessionId = authSessionKey.sessionId;
2014-12-12 12:02:13 -05:00
return decryptSessionKey(authSessionKey);
2014-10-02 16:05:44 -04:00
});
2014-06-06 12:36:23 -04:00
2014-10-02 16:05:44 -04:00
function decryptSessionKey(authSessionKey) {
2014-12-12 12:02:13 -05:00
var ptSessionKey, ptChallenge, serverPubkey;
return self.lookupPublicKey(config.serverPrivateKeyId).then(function(pubkey) {
if (!pubkey || !pubkey.publicKey) {
throw new Error('Server public key for authentication not found!');
2014-06-06 12:36:23 -04:00
}
2014-12-12 12:02:13 -05:00
serverPubkey = pubkey;
2014-10-02 16:05:44 -04:00
// decrypt the session key
var ct1 = authSessionKey.encryptedAuthSessionKey;
2014-12-12 12:02:13 -05:00
return self._pgp.decrypt(ct1, serverPubkey.publicKey);
2014-12-12 12:02:13 -05:00
}).then(function(pt) {
if (!pt.signaturesValid) {
throw new Error('Verifying PGP signature failed!');
}
2014-06-06 12:36:23 -04:00
2014-12-12 12:02:13 -05:00
ptSessionKey = pt.decrypted;
// decrypt the challenge
var ct2 = authSessionKey.encryptedChallenge;
return self._pgp.decrypt(ct2, serverPubkey.publicKey);
}).then(function(pt) {
if (!pt.signaturesValid) {
throw new Error('Verifying PGP signature failed!');
}
ptChallenge = pt.decrypted;
return encryptChallenge(ptSessionKey, ptChallenge);
2014-10-02 16:05:44 -04:00
});
}
function encryptChallenge(sessionKey, challenge) {
2014-12-12 12:02:13 -05:00
var deviceSecret, encryptedChallenge;
var iv = util.random(config.symIvSize);
2014-10-02 16:05:44 -04:00
// get device secret
2014-12-12 12:02:13 -05:00
return self.getDeviceSecret().then(function(secret) {
deviceSecret = secret;
2014-10-02 16:05:44 -04:00
// encrypt the challenge
2014-12-12 12:02:13 -05:00
return self._crypto.encrypt(challenge, sessionKey, iv);
}).then(function(ct) {
encryptedChallenge = ct;
// encrypt the device secret
return self._crypto.encrypt(deviceSecret, sessionKey, iv);
}).then(function(encryptedDeviceSecret) {
return replyChallenge({
encryptedChallenge: encryptedChallenge,
encryptedDeviceSecret: encryptedDeviceSecret,
iv: iv
}, sessionKey);
2014-10-02 16:05:44 -04:00
});
}
function replyChallenge(response, sessionKey) {
// respond to challenge by uploading the with the session key encrypted challenge
2014-12-12 12:02:13 -05:00
return self._privateKeyDao.verifyAuthentication({
2014-10-02 16:05:44 -04:00
userId: userId,
sessionId: sessionId,
encryptedChallenge: response.encryptedChallenge,
encryptedDeviceSecret: response.encryptedDeviceSecret,
iv: response.iv
2014-12-12 12:02:13 -05:00
}).then(function() {
return {
2014-06-06 12:36:23 -04:00
sessionId: sessionId,
2014-10-02 16:05:44 -04:00
sessionKey: sessionKey
2014-12-12 12:02:13 -05:00
};
2014-10-02 16:05:44 -04:00
});
}
};
2014-06-06 12:36:23 -04:00
2014-10-02 16:05:44 -04:00
/**
* Encrypt and upload the private PGP key to the server.
* @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
*/
2014-12-12 12:02:13 -05:00
Keychain.prototype.uploadPrivateKey = function(options) {
2014-10-02 16:05:44 -04:00
var self = this,
config = self._appConfig.config,
2014-10-02 16:05:44 -04:00
keySize = config.symKeySize,
salt;
if (!options.userId || !options.code) {
2014-12-12 12:02:13 -05:00
return new Promise(function() {
throw new Error('Incomplete arguments!');
});
2014-10-02 16:05:44 -04:00
}
2014-12-12 12:02:13 -05:00
return deriveKey(options.code);
2014-10-02 16:05:44 -04:00
function deriveKey(code) {
// generate random salt
salt = util.random(keySize);
// derive key from the code using PBKDF2
2014-12-12 12:02:13 -05:00
return self._crypto.deriveKey(code, salt, keySize).then(function(key) {
return encryptPrivateKey(key);
2014-10-02 16:05:44 -04:00
});
}
function encryptPrivateKey(encryptionKey) {
2014-12-12 12:02:13 -05:00
var privkeyId, pgpBlock,
iv = util.random(config.symIvSize);
2014-10-02 16:05:44 -04:00
2014-12-12 12:02:13 -05:00
// get private key from local storage
return self.getUserKeyPair(options.userId).then(function(keypair) {
privkeyId = keypair.privateKey._id;
pgpBlock = keypair.privateKey.encryptedKey;
2014-10-02 16:05:44 -04:00
// encrypt the private key with the derived key
2014-12-12 12:02:13 -05:00
return self._crypto.encrypt(pgpBlock, encryptionKey, iv);
2014-10-02 16:05:44 -04:00
2014-12-12 12:02:13 -05:00
}).then(function(ct) {
return uploadPrivateKey({
_id: privkeyId,
userId: options.userId,
encryptedPrivateKey: ct,
salt: salt,
iv: iv
2014-06-06 12:36:23 -04:00
});
2014-10-02 16:05:44 -04:00
});
}
2014-06-06 12:36:23 -04:00
2014-10-02 16:05:44 -04:00
function uploadPrivateKey(payload) {
2014-12-12 12:02:13 -05:00
var pt = payload.encryptedPrivateKey,
iv = payload.iv;
2014-10-02 16:05:44 -04:00
2014-12-12 12:02:13 -05:00
// authenticate to server for upload
return self._authenticateToPrivateKeyServer(options.userId).then(function(authSessionKey) {
// set sessionId
payload.sessionId = authSessionKey.sessionId;
2014-10-02 16:05:44 -04:00
// encrypt encryptedPrivateKey again using authSessionKey
2014-12-12 12:02:13 -05:00
var key = authSessionKey.sessionKey;
return self._crypto.encrypt(pt, key, iv);
}).then(function(ct) {
// replace the encryptedPrivateKey with the double wrapped ciphertext
payload.encryptedPrivateKey = ct;
// upload the encrypted priavet key
return self._privateKeyDao.upload(payload);
2014-10-02 16:05:44 -04:00
});
}
};
2014-06-06 12:36:23 -04:00
2014-10-02 16:05:44 -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.
* @param {String} options.userId The user's email address
* @param {String} options.keyId The private PGP key id
*/
2014-12-12 12:02:13 -05:00
Keychain.prototype.requestPrivateKeyDownload = function(options) {
return this._privateKeyDao.requestDownload(options);
2014-10-02 16:05:44 -04:00
};
2014-06-06 12:36:23 -04:00
2014-10-02 16:05:44 -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
*/
2014-12-12 12:02:13 -05:00
Keychain.prototype.hasPrivateKey = function(options) {
return this._privateKeyDao.hasPrivateKey(options);
2014-10-02 16:05:44 -04:00
};
2014-10-02 16:05:44 -04:00
/**
* Download the encrypted private PGP key from the server using the recovery token.
* @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-12-12 12:02:13 -05:00
Keychain.prototype.downloadPrivateKey = function(options) {
return this._privateKeyDao.download(options);
2014-10-02 16:05:44 -04:00
};
2014-10-02 16:05:44 -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.
* @param {String} options._id The private PGP key id
* @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
* @param {String} options.encryptedPrivateKey The encrypted private PGP key
* @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-12-12 12:02:13 -05:00
Keychain.prototype.decryptAndStorePrivateKeyLocally = function(options) {
2014-10-02 16:05:44 -04:00
var self = this,
code = options.code,
salt = options.salt,
config = self._appConfig.config,
2014-10-02 16:05:44 -04:00
keySize = config.symKeySize;
if (!options._id || !options.userId || !options.code || !options.salt || !options.encryptedPrivateKey || !options.iv) {
2014-12-12 12:02:13 -05:00
return new Promise(function() {
throw new Error('Incomplete arguments!');
});
2014-10-02 16:05:44 -04:00
}
// derive key from the code and the salt using PBKDF2
2014-12-12 12:02:13 -05:00
return self._crypto.deriveKey(code, salt, keySize).then(function(key) {
return decryptAndStore(key);
2014-10-02 16:05:44 -04:00
});
function decryptAndStore(derivedKey) {
// decrypt the private key with the derived key
var ct = options.encryptedPrivateKey,
iv = options.iv;
2014-12-12 12:02:13 -05:00
return self._crypto.decrypt(ct, derivedKey, iv).then(function(privateKeyArmored) {
2014-10-02 16:05:44 -04:00
// validate pgp key
var keyParams;
try {
keyParams = self._pgp.getKeyParams(privateKeyArmored);
} catch (e) {
2014-12-12 12:02:13 -05:00
throw new Error('Error parsing private PGP key!');
2014-10-02 16:05:44 -04:00
}
if (keyParams._id !== options._id || keyParams.userId !== options.userId) {
2014-12-12 12:02:13 -05:00
throw new Error('Private key parameters don\'t match with public key\'s!');
2014-10-02 16:05:44 -04:00
}
2014-10-02 16:05:44 -04:00
var keyObject = {
_id: options._id,
userId: options.userId,
encryptedKey: privateKeyArmored
};
2014-10-02 16:05:44 -04:00
// store private key locally
2014-12-12 12:02:13 -05:00
return self.saveLocalPrivateKey(keyObject).then(function() {
return keyObject;
2014-10-02 16:05:44 -04:00
});
2014-12-12 12:02:13 -05:00
}).catch(function() {
throw new Error('Invalid keychain code!');
2014-10-02 16:05:44 -04:00
});
}
};
2014-10-02 16:05:44 -04:00
//
// Keypair functions
//
2014-10-02 16:05:44 -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}
*/
2014-12-12 12:02:13 -05:00
Keychain.prototype.getUserKeyPair = function(userId) {
2014-10-02 16:05:44 -04:00
var self = this;
2014-10-02 16:05:44 -04:00
// search for user's public key locally
2014-12-12 12:02:13 -05:00
return self._lawnchairDAO.list(DB_PUBLICKEY, 0, null).then(function(allPubkeys) {
2014-10-02 16:05:44 -04:00
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
2014-12-12 12:02:13 -05:00
return syncKeypair(pubkey._id);
2014-06-06 12:36:23 -04:00
}
2014-10-02 16:05:44 -04:00
// no public key by that user id in storage
// find from cloud by email address
2014-12-12 12:02:13 -05:00
return self._publicKeyDao.getByUserId(userId).then(function(cloudPubkey) {
2014-10-02 16:05:44 -04:00
if (cloudPubkey && cloudPubkey._id) {
// there is a public key for that user already in the cloud...
// sync keypair to local storage
2014-12-12 12:02:13 -05:00
return syncKeypair(cloudPubkey._id);
2014-10-02 16:05:44 -04:00
}
2014-10-02 16:05:44 -04:00
// continue without keypair... generate in crypto.js
});
});
function syncKeypair(keypairId) {
2014-12-12 12:02:13 -05:00
var savedPubkey, savedPrivkey;
2014-10-02 16:05:44 -04:00
// persist key pair in local storage
2014-12-12 12:02:13 -05:00
return self.lookupPublicKey(keypairId).then(function(pub) {
savedPubkey = pub;
2014-10-02 16:05:44 -04:00
// persist private key in local storage
2014-12-12 12:02:13 -05:00
return self.lookupPrivateKey(keypairId).then(function(priv) {
savedPrivkey = priv;
});
2014-12-12 12:02:13 -05:00
}).then(function() {
var keys = {};
2014-12-12 12:02:13 -05:00
if (savedPubkey && savedPubkey.publicKey) {
keys.publicKey = savedPubkey;
}
if (savedPrivkey && savedPrivkey.encryptedKey) {
keys.privateKey = savedPrivkey;
}
2014-12-12 12:02:13 -05:00
return keys;
2014-10-02 16:05:44 -04:00
});
}
};
2013-10-21 07:10:42 -04:00
2014-10-02 16:05:44 -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}
*/
2014-12-12 12:02:13 -05:00
Keychain.prototype.putUserKeyPair = function(keypair) {
2014-10-02 16:05:44 -04:00
var self = this;
2014-10-02 16:05:44 -04:00
// validate input
if (!keypair || !keypair.publicKey || !keypair.privateKey || !keypair.publicKey.userId || keypair.publicKey.userId !== keypair.privateKey.userId) {
2014-12-12 12:02:13 -05:00
return new Promise(function() {
throw new Error('Incorrect input!');
2014-10-02 16:05:44 -04:00
});
}
2014-10-02 16:05:44 -04:00
// don't check the user's own public key for deletion in refreshKeyForUserId
keypair.publicKey.imported = true;
2013-10-21 07:10:42 -04:00
2014-10-02 16:05:44 -04:00
// store public key locally
2014-12-12 12:02:13 -05:00
return self.saveLocalPublicKey(keypair.publicKey).then(function() {
2014-10-02 16:05:44 -04:00
// persist public key in cloud storage
2014-12-12 12:02:13 -05:00
return self._publicKeyDao.put(keypair.publicKey);
}).then(function() {
// store private key locally
return self.saveLocalPrivateKey(keypair.privateKey);
2014-10-02 16:05:44 -04:00
});
};
2014-10-02 16:05:44 -04:00
//
// Helper functions
//
2014-12-11 14:43:51 -05:00
Keychain.prototype.lookupPublicKey = function(id) {
2014-12-12 12:02:13 -05:00
var self = this,
cloudPubkey;
2014-10-02 16:05:44 -04:00
if (!id) {
2014-12-11 14:43:51 -05:00
return new Promise(function() {
throw new Error('ID must be set for public key query!');
2014-10-02 16:05:44 -04:00
});
}
// lookup in local storage
2014-12-11 14:43:51 -05:00
return self._lawnchairDAO.read(DB_PUBLICKEY + '_' + id).then(function(pubkey) {
2014-10-02 16:05:44 -04:00
if (pubkey) {
2014-12-11 14:43:51 -05:00
return pubkey;
2014-10-02 16:05:44 -04:00
}
// fetch from cloud storage
2014-12-12 12:02:13 -05:00
return self._publicKeyDao.get(id).then(function(pub) {
cloudPubkey = pub;
2014-10-02 16:05:44 -04:00
// cache public key in cache
2014-12-12 12:02:13 -05:00
return self.saveLocalPublicKey(cloudPubkey);
}).then(function() {
return cloudPubkey;
});
2014-10-02 16:05:44 -04:00
});
};
/**
* List all the locally stored public keys
*/
2014-12-11 14:43:51 -05:00
Keychain.prototype.listLocalPublicKeys = function() {
2014-10-02 16:05:44 -04:00
// search local keyring for public key
2014-12-11 14:43:51 -05:00
return this._lawnchairDAO.list(DB_PUBLICKEY, 0, null);
2014-10-02 16:05:44 -04:00
};
2014-12-11 14:43:51 -05:00
Keychain.prototype.removeLocalPublicKey = function(id) {
return this._lawnchairDAO.remove(DB_PUBLICKEY + '_' + id);
2014-10-02 16:05:44 -04:00
};
2014-12-11 14:43:51 -05:00
Keychain.prototype.lookupPrivateKey = function(id) {
2014-10-02 16:05:44 -04:00
// lookup in local storage
2014-12-11 14:43:51 -05:00
return this._lawnchairDAO.read(DB_PRIVATEKEY + '_' + id);
2014-10-02 16:05:44 -04:00
};
2014-12-11 14:43:51 -05:00
Keychain.prototype.saveLocalPublicKey = function(pubkey) {
2014-10-02 16:05:44 -04:00
// persist public key (email, _id)
var pkLookupKey = DB_PUBLICKEY + '_' + pubkey._id;
2014-12-11 14:43:51 -05:00
return this._lawnchairDAO.persist(pkLookupKey, pubkey);
2014-10-02 16:05:44 -04:00
};
2014-12-11 14:43:51 -05:00
Keychain.prototype.saveLocalPrivateKey = function(privkey) {
2014-10-02 16:05:44 -04:00
// persist private key (email, _id)
var prkLookupKey = DB_PRIVATEKEY + '_' + privkey._id;
2014-12-11 14:43:51 -05:00
return this._lawnchairDAO.persist(prkLookupKey, privkey);
};