'use strict'; var ngModule = angular.module('woServices'); ngModule.service('privateKey', PrivateKey); module.exports = PrivateKey; var ImapClient = require('imap-client'); var util = require('crypto-lib').util; var IMAP_KEYS_FOLDER = 'openpgp_keys'; var MIME_TYPE = 'application/x.encrypted-pgp-key'; var MSG_PART_TYPE_ATTACHMENT = 'attachment'; function PrivateKey(auth, mailbuild, mailreader, appConfig, pgp, crypto, axe) { this._auth = auth; this._Mailbuild = mailbuild; this._mailreader = mailreader; this._appConfig = appConfig; this._pgp = pgp; this._crypto = crypto; this._axe = axe; } /** * Configure the local imap client used for key-sync with credentials from the auth module. */ PrivateKey.prototype.init = function() { var self = this; return self._auth.getCredentials().then(function(credentials) { // tls socket worker path for multithreaded tls in non-native tls environments credentials.imap.tlsWorkerPath = self._appConfig.config.workerPath + '/tcp-socket-tls-worker.min.js'; self._imap = new ImapClient(credentials.imap); self._imap.onError = self._axe.error; // login to the imap server return self._imap.login(); }); }; /** * Cleanup by logging out of the imap client. */ PrivateKey.prototype.destroy = function() { this._imap.login(); // don't wait for logout to complete return new Promise(function(resolve) { resolve(); }); }; /** * Encrypt and upload the private PGP key to the server. * @param {String} code The randomly generated or self selected code used to derive the key for the encryption of the private PGP key */ PrivateKey.prototype.encrypt = function(code) { var self = this, config = self._appConfig.config, keySize = config.symKeySize, encryptionKey, salt, iv, privkeyId; if (!code) { return new Promise(function() { throw new Error('Incomplete arguments!'); }); } // generate random salt and iv salt = util.random(keySize); iv = util.random(config.symIvSize); // derive key from the code using PBKDF2 return self._crypto.deriveKey(code, salt, keySize).then(function(key) { encryptionKey = key; // get private key from local storage return self._pgp.exportKeys(); }).then(function(keypair) { privkeyId = keypair.keyId; // encrypt the private key with the derived key return self._crypto.encrypt(keypair.privateKeyArmored, encryptionKey, iv); }).then(function(ct) { return { _id: privkeyId, encryptedPrivateKey: ct, salt: salt, iv: iv }; }); }; /** * Upload the encrypted private PGP key. * @param {String} options._id The hex encoded capital 16 char key id * @param {String} options.userId The user's email address * @param {String} options.encryptedPrivateKey The base64 encoded encrypted private PGP key */ PrivateKey.prototype.upload = function(options) { var self = this; return new Promise(function(resolve) { if (!options._id || !options.userId || !options.encryptedPrivateKey || !options.salt || !options.iv) { throw new Error('Incomplete arguments!'); } resolve(); }).then(function() { // create imap folder return self._imap.createFolder({ path: IMAP_KEYS_FOLDER }).then(function() { self._axe.debug('Successfully created imap folder ' + IMAP_KEYS_FOLDER); }).catch(function() { self._axe.debug('Creating imap folder ' + IMAP_KEYS_FOLDER + ' failed. Probably already available.'); }); }).then(createMessage).then(function(message) { // upload to imap folder return self._imap.uploadMessage({ path: IMAP_KEYS_FOLDER, message: message }); }); function createMessage() { var encryptedKeyBuf = util.binStr2Uint8Arr(util.base642Str(options.encryptedPrivateKey)); var saltBuf = util.binStr2Uint8Arr(util.base642Str(options.salt)); var ivBuf = util.binStr2Uint8Arr(util.base642Str(options.iv)); // allocate payload buffer for sync var payloadBuf = new Uint8Array(1 + saltBuf.length + ivBuf.length + encryptedKeyBuf.length); var offset = 0; // set version byte payloadBuf[offset] = 0x01; // version 1 of the key-sync protocol offset++; // copy salt bytes payloadBuf.set(saltBuf, offset); offset += saltBuf.length; // copy iv bytes payloadBuf.set(ivBuf, offset); offset += ivBuf.length; // copy encrypted key bytes payloadBuf.set(encryptedKeyBuf, offset); // create MIME tree var rootNode = options.rootNode || new self._Mailbuild(); rootNode.setHeader({ subject: options._id, from: options.userId, to: options.userId, 'content-type': MIME_TYPE + '; charset=us-ascii', 'content-transfer-encoding': 'base64' }); rootNode.setContent(payloadBuf); return rootNode.build(); } }; /** * Check if matching private key is stored in IMAP. */ PrivateKey.prototype.isSynced = function() { return this._fetchMessage({ userId: this._auth.emailAddress, keyId: this._pgp.getKeyId() }).then(function(msg) { return !!msg; }).catch(function() { return false; }); }; /** * Verify the download request for the private PGP key. * @param {String} options.userId The user's email address * @param {String} options.keyId The private key id * @return {Object} {_id:[hex encoded capital 16 char key id], encryptedPrivateKey:[base64 encoded], encryptedUserId: [base64 encoded]} */ PrivateKey.prototype.download = function(options) { var self = this, message; return self._fetchMessage(options).then(function(msg) { if (!msg) { throw new Error('Private key not synced!'); } message = msg; }).then(function() { // get the body for the message return self._imap.getBodyParts({ path: IMAP_KEYS_FOLDER, uid: message.uid, bodyParts: message.bodyParts }); }).then(function() { // parse the message return self._parse(message); }).then(function(root) { var payloadBuf = filterBodyParts(root, MSG_PART_TYPE_ATTACHMENT)[0].content; var offset = 0; var SALT_LEN = 32; var IV_LEN = 12; // check version var version = payloadBuf[offset]; offset++; if (version !== 1) { throw new Error('Unsupported key sync protocol version!'); } // salt var saltBuf = payloadBuf.subarray(offset, offset + SALT_LEN); offset += SALT_LEN; // iv var ivBuf = payloadBuf.subarray(offset, offset + IV_LEN); offset += IV_LEN; // encrypted private key var encryptedKeyBuf = payloadBuf.subarray(offset, payloadBuf.length); return { _id: options.keyId, userId: options.userId, encryptedPrivateKey: util.str2Base64(util.uint8Arr2BinStr(encryptedKeyBuf)), salt: util.str2Base64(util.uint8Arr2BinStr(saltBuf)), iv: util.str2Base64(util.uint8Arr2BinStr(ivBuf)) }; }); }; /** * 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 */ PrivateKey.prototype.decrypt = function(options) { var self = this, 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(options.code, options.salt, keySize).then(function(derivedKey) { // decrypt the private key with the derived key return self._crypto.decrypt(options.encryptedPrivateKey, derivedKey, options.iv).catch(function() { throw new Error('Invalid backup code!'); }); }).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!'); } return { _id: options._id, userId: options.userId, encryptedKey: privateKeyArmored }; }); }; PrivateKey.prototype._fetchMessage = function(options) { var self = this; if (!options.userId || !options.keyId) { return new Promise(function() { throw new Error('Incomplete arguments!'); }); } // get the metadata for the message return self._imap.listMessages({ path: IMAP_KEYS_FOLDER, }).then(function(messages) { if (!messages.length) { // message has been deleted in the meantime return; } // get matching private key if multiple keys uloaded return _.findWhere(messages, { subject: options.keyId }); }).catch(function() { throw new Error('Imap folder ' + IMAP_KEYS_FOLDER + ' does not exist for key sync!'); }); }; PrivateKey.prototype._parse = function(options) { var self = this; return new Promise(function(resolve, reject) { self._mailreader.parse(options, function(err, root) { if (err) { reject(err); } else { resolve(root); } }); }); }; /** * Helper function that recursively traverses the body parts tree. Looks for bodyParts that match the provided type and aggregates them * * @param {Array} bodyParts The bodyParts array * @param {String} type The type to look up * @param {undefined} result Leave undefined, only used for recursion */ function filterBodyParts(bodyParts, type, result) { result = result || []; bodyParts.forEach(function(part) { if (part.type === type) { result.push(part); } else if (Array.isArray(part.content)) { filterBodyParts(part.content, type, result); } }); return result; }