|
|
|
@ -4,12 +4,8 @@ var ngModule = angular.module('woServices');
|
|
|
|
|
ngModule.service('keychain', Keychain);
|
|
|
|
|
module.exports = Keychain;
|
|
|
|
|
|
|
|
|
|
var util = require('crypto-lib').util;
|
|
|
|
|
|
|
|
|
|
var DB_PUBLICKEY = 'publickey',
|
|
|
|
|
DB_PRIVATEKEY = 'privatekey',
|
|
|
|
|
DB_DEVICENAME = 'devicename',
|
|
|
|
|
DB_DEVICE_SECRET = 'devicesecret';
|
|
|
|
|
DB_PRIVATEKEY = 'privatekey';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A high-level Data-Access Api for handling Keypair synchronization
|
|
|
|
@ -199,390 +195,6 @@ Keychain.prototype.getReceiverPublicKey = function(userId) {
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
|
// Device registration functions
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set the device's memorable name e.g 'iPhone Work'
|
|
|
|
|
* @param {String} deviceName The device name
|
|
|
|
|
*/
|
|
|
|
|
Keychain.prototype.setDeviceName = function(deviceName) {
|
|
|
|
|
if (!deviceName) {
|
|
|
|
|
return new Promise(function() {
|
|
|
|
|
throw new Error('Please set a device name!');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return this._lawnchairDAO.persist(DB_DEVICENAME, deviceName);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the device' memorable name from local storage. Throws an error if not set
|
|
|
|
|
* @return {String} The device name
|
|
|
|
|
*/
|
|
|
|
|
Keychain.prototype.getDeviceName = function() {
|
|
|
|
|
// check if deviceName is already persisted in storage
|
|
|
|
|
return this._lawnchairDAO.read(DB_DEVICENAME).then(function(deviceName) {
|
|
|
|
|
if (!deviceName) {
|
|
|
|
|
throw new Error('Device name not set!');
|
|
|
|
|
}
|
|
|
|
|
return deviceName;
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Geneate a device specific key and secret to authenticate to the private key service.
|
|
|
|
|
*/
|
|
|
|
|
Keychain.prototype.getDeviceSecret = function() {
|
|
|
|
|
var self = this,
|
|
|
|
|
config = self._appConfig.config;
|
|
|
|
|
|
|
|
|
|
// generate random deviceSecret or get from storage
|
|
|
|
|
return self._lawnchairDAO.read(DB_DEVICE_SECRET).then(function(storedDevSecret) {
|
|
|
|
|
if (storedDevSecret) {
|
|
|
|
|
// a device key is already available locally
|
|
|
|
|
return storedDevSecret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// generate random deviceSecret
|
|
|
|
|
var deviceSecret = util.random(config.symKeySize);
|
|
|
|
|
// persist deviceSecret to local storage (in plaintext)
|
|
|
|
|
return self._lawnchairDAO.persist(DB_DEVICE_SECRET, deviceSecret).then(function() {
|
|
|
|
|
return deviceSecret;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
*/
|
|
|
|
|
Keychain.prototype.registerDevice = function(options) {
|
|
|
|
|
var self = this,
|
|
|
|
|
devName,
|
|
|
|
|
config = self._appConfig.config;
|
|
|
|
|
|
|
|
|
|
// check if deviceName is already persisted in storage
|
|
|
|
|
return self.getDeviceName().then(function(deviceName) {
|
|
|
|
|
return requestDeviceRegistration(deviceName);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function requestDeviceRegistration(deviceName) {
|
|
|
|
|
devName = deviceName;
|
|
|
|
|
|
|
|
|
|
// request device registration session key
|
|
|
|
|
return self._privateKeyDao.requestDeviceRegistration({
|
|
|
|
|
userId: options.userId,
|
|
|
|
|
deviceName: deviceName
|
|
|
|
|
}).then(function(regSessionKey) {
|
|
|
|
|
if (!regSessionKey.encryptedRegSessionKey) {
|
|
|
|
|
throw new Error('Invalid format for session key!');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return decryptSessionKey(regSessionKey);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function decryptSessionKey(regSessionKey) {
|
|
|
|
|
return self.lookupPublicKey(config.serverPrivateKeyId).then(function(serverPubkey) {
|
|
|
|
|
if (!serverPubkey || !serverPubkey.publicKey) {
|
|
|
|
|
throw new Error('Server public key for device registration not found!');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// decrypt the session key
|
|
|
|
|
var ct = regSessionKey.encryptedRegSessionKey;
|
|
|
|
|
return self._pgp.decrypt(ct, serverPubkey.publicKey).then(function(pt) {
|
|
|
|
|
if (!pt.signaturesValid) {
|
|
|
|
|
throw new Error('Verifying PGP signature failed!');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return uploadDeviceSecret(pt.decrypted);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function uploadDeviceSecret(regSessionKey) {
|
|
|
|
|
// generate iv
|
|
|
|
|
var iv = util.random(config.symIvSize);
|
|
|
|
|
// read device secret from local storage
|
|
|
|
|
return self.getDeviceSecret().then(function(deviceSecret) {
|
|
|
|
|
// encrypt deviceSecret
|
|
|
|
|
return self._crypto.encrypt(deviceSecret, regSessionKey, iv);
|
|
|
|
|
|
|
|
|
|
}).then(function(encryptedDeviceSecret) {
|
|
|
|
|
// upload encryptedDeviceSecret
|
|
|
|
|
return self._privateKeyDao.uploadDeviceSecret({
|
|
|
|
|
userId: options.userId,
|
|
|
|
|
deviceName: devName,
|
|
|
|
|
encryptedDeviceSecret: encryptedDeviceSecret,
|
|
|
|
|
iv: iv
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
|
// Private key functions
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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]}
|
|
|
|
|
*/
|
|
|
|
|
Keychain.prototype._authenticateToPrivateKeyServer = function(userId) {
|
|
|
|
|
var self = this,
|
|
|
|
|
sessionId,
|
|
|
|
|
config = self._appConfig.config;
|
|
|
|
|
|
|
|
|
|
// request auth session key required for upload
|
|
|
|
|
return self._privateKeyDao.requestAuthSessionKey({
|
|
|
|
|
userId: userId
|
|
|
|
|
}).then(function(authSessionKey) {
|
|
|
|
|
if (!authSessionKey.encryptedAuthSessionKey || !authSessionKey.encryptedChallenge || !authSessionKey.sessionId) {
|
|
|
|
|
throw new Error('Invalid format for session key!');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// remember session id for verification
|
|
|
|
|
sessionId = authSessionKey.sessionId;
|
|
|
|
|
|
|
|
|
|
return decryptSessionKey(authSessionKey);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function decryptSessionKey(authSessionKey) {
|
|
|
|
|
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!');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
serverPubkey = pubkey;
|
|
|
|
|
// decrypt the session key
|
|
|
|
|
var ct1 = authSessionKey.encryptedAuthSessionKey;
|
|
|
|
|
return self._pgp.decrypt(ct1, serverPubkey.publicKey);
|
|
|
|
|
|
|
|
|
|
}).then(function(pt) {
|
|
|
|
|
if (!pt.signaturesValid) {
|
|
|
|
|
throw new Error('Verifying PGP signature failed!');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function encryptChallenge(sessionKey, challenge) {
|
|
|
|
|
var deviceSecret, encryptedChallenge;
|
|
|
|
|
var iv = util.random(config.symIvSize);
|
|
|
|
|
// get device secret
|
|
|
|
|
return self.getDeviceSecret().then(function(secret) {
|
|
|
|
|
deviceSecret = secret;
|
|
|
|
|
// encrypt the challenge
|
|
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function replyChallenge(response, sessionKey) {
|
|
|
|
|
// respond to challenge by uploading the with the session key encrypted challenge
|
|
|
|
|
return self._privateKeyDao.verifyAuthentication({
|
|
|
|
|
userId: userId,
|
|
|
|
|
sessionId: sessionId,
|
|
|
|
|
encryptedChallenge: response.encryptedChallenge,
|
|
|
|
|
encryptedDeviceSecret: response.encryptedDeviceSecret,
|
|
|
|
|
iv: response.iv
|
|
|
|
|
}).then(function() {
|
|
|
|
|
return {
|
|
|
|
|
sessionId: sessionId,
|
|
|
|
|
sessionKey: sessionKey
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
*/
|
|
|
|
|
Keychain.prototype.uploadPrivateKey = function(options) {
|
|
|
|
|
var self = this,
|
|
|
|
|
config = self._appConfig.config,
|
|
|
|
|
keySize = config.symKeySize,
|
|
|
|
|
salt;
|
|
|
|
|
|
|
|
|
|
if (!options.userId || !options.code) {
|
|
|
|
|
return new Promise(function() {
|
|
|
|
|
throw 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
|
|
|
|
|
return self._crypto.deriveKey(code, salt, keySize).then(function(key) {
|
|
|
|
|
return encryptPrivateKey(key);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function encryptPrivateKey(encryptionKey) {
|
|
|
|
|
var privkeyId, pgpBlock,
|
|
|
|
|
iv = util.random(config.symIvSize);
|
|
|
|
|
|
|
|
|
|
// get private key from local storage
|
|
|
|
|
return self.getUserKeyPair(options.userId).then(function(keypair) {
|
|
|
|
|
privkeyId = keypair.privateKey._id;
|
|
|
|
|
pgpBlock = keypair.privateKey.encryptedKey;
|
|
|
|
|
|
|
|
|
|
// encrypt the private key with the derived key
|
|
|
|
|
return self._crypto.encrypt(pgpBlock, encryptionKey, iv);
|
|
|
|
|
|
|
|
|
|
}).then(function(ct) {
|
|
|
|
|
return uploadPrivateKey({
|
|
|
|
|
_id: privkeyId,
|
|
|
|
|
userId: options.userId,
|
|
|
|
|
encryptedPrivateKey: ct,
|
|
|
|
|
salt: salt,
|
|
|
|
|
iv: iv
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function uploadPrivateKey(payload) {
|
|
|
|
|
var pt = payload.encryptedPrivateKey,
|
|
|
|
|
iv = payload.iv;
|
|
|
|
|
|
|
|
|
|
// authenticate to server for upload
|
|
|
|
|
return self._authenticateToPrivateKeyServer(options.userId).then(function(authSessionKey) {
|
|
|
|
|
// set sessionId
|
|
|
|
|
payload.sessionId = authSessionKey.sessionId;
|
|
|
|
|
// encrypt encryptedPrivateKey again using authSessionKey
|
|
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
*/
|
|
|
|
|
Keychain.prototype.requestPrivateKeyDownload = function(options) {
|
|
|
|
|
return this._privateKeyDao.requestDownload(options);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
*/
|
|
|
|
|
Keychain.prototype.hasPrivateKey = function(options) {
|
|
|
|
|
return this._privateKeyDao.hasPrivateKey(options);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
*/
|
|
|
|
|
Keychain.prototype.downloadPrivateKey = function(options) {
|
|
|
|
|
return this._privateKeyDao.download(options);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
*/
|
|
|
|
|
Keychain.prototype.decryptAndStorePrivateKeyLocally = function(options) {
|
|
|
|
|
var self = this,
|
|
|
|
|
code = options.code,
|
|
|
|
|
salt = options.salt,
|
|
|
|
|
config = self._appConfig.config,
|
|
|
|
|
keySize = config.symKeySize;
|
|
|
|
|
|
|
|
|
|
if (!options._id || !options.userId || !options.code || !options.salt || !options.encryptedPrivateKey || !options.iv) {
|
|
|
|
|
return new Promise(function() {
|
|
|
|
|
throw new Error('Incomplete arguments!');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// derive key from the code and the salt using PBKDF2
|
|
|
|
|
return self._crypto.deriveKey(code, salt, keySize).then(function(key) {
|
|
|
|
|
return decryptAndStore(key);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function decryptAndStore(derivedKey) {
|
|
|
|
|
// decrypt the private key with the derived key
|
|
|
|
|
var ct = options.encryptedPrivateKey,
|
|
|
|
|
iv = options.iv;
|
|
|
|
|
|
|
|
|
|
return self._crypto.decrypt(ct, derivedKey, iv).then(function(privateKeyArmored) {
|
|
|
|
|
// validate pgp key
|
|
|
|
|
var keyParams;
|
|
|
|
|
try {
|
|
|
|
|
keyParams = self._pgp.getKeyParams(privateKeyArmored);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
throw new Error('Error parsing private PGP key!');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (keyParams._id !== options._id || keyParams.userId !== options.userId) {
|
|
|
|
|
throw new Error('Private key parameters don\'t match with public key\'s!');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var keyObject = {
|
|
|
|
|
_id: options._id,
|
|
|
|
|
userId: options.userId,
|
|
|
|
|
encryptedKey: privateKeyArmored
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// store private key locally
|
|
|
|
|
return self.saveLocalPrivateKey(keyObject).then(function() {
|
|
|
|
|
return keyObject;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
}).catch(function() {
|
|
|
|
|
throw new Error('Invalid keychain code!');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
|
// Keypair functions
|
|
|
|
|
//
|
|
|
|
|