'use strict'; var MSG_PART_ATTR_CONTENT = 'content'; var MSG_PART_TYPE_TEXT = 'text'; var ngModule = angular.module('woServices'); ngModule.service('publickeyVerifier', PublickeyVerifier); module.exports = PublickeyVerifier; var ImapClient = require('imap-client'); function PublickeyVerifier(auth, appConfig, mailreader, keychain) { this._appConfig = appConfig; this._mailreader = mailreader; this._keychain = keychain; this._auth = auth; this._workerPath = appConfig.config.workerPath + '/tcp-socket-tls-worker.min.js'; this._keyServerUrl = this._appConfig.config.keyServerUrl; } // // Public API // PublickeyVerifier.prototype.configure = 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); }); }; PublickeyVerifier.prototype.uploadPublicKey = function() { if (this.keypair) { return this._keychain.uploadPublicKey(this.keypair.publicKey); } return new Promise(function(resolve) { resolve(); }); }; PublickeyVerifier.prototype.persistKeypair = function() { if (this.keypair) { return this._keychain.putUserKeyPair(this.keypair); } return new Promise(function(resolve) { resolve(); }); }; PublickeyVerifier.prototype.verify = function() { var self = this, verificationSuccessful = false; // have to wrap it in a promise to catch .onError of imap-client return new Promise(function(resolve, reject) { self._imap.onError = reject; // login self._imap.login().then(function() { // list folders return self._imap.listWellKnownFolders(); }).then(function(wellKnownFolders) { var paths = []; // gathers paths // extract the paths from the folder arrays for (var folderType in wellKnownFolders) { if (wellKnownFolders.hasOwnProperty(folderType) && Array.isArray(wellKnownFolders[folderType])) { paths = paths.concat(_.pluck(wellKnownFolders[folderType], 'path')); } } return paths; }).then(function(paths) { return self._searchAll(paths); // search }).then(function(candidates) { if (!candidates.length) { // nothing here to potentially verify verificationSuccessful = false; return; } // verify everything that looks like a verification mail return self._verifyAll(candidates).then(function(success) { verificationSuccessful = success; }); }).then(function() { // at this point, we don't care about errors anymore self._imap.onError = function() {}; self._imap.logout(); if (!verificationSuccessful) { // nothing unexpected went wrong, but no public key could be verified throw new Error(); } resolve(); // we're done }).catch(reject); }); }; PublickeyVerifier.prototype._searchAll = function(paths) { var self = this, candidates = []; // gather matching uids // async for-loop inside a then-able return new Promise(next); // search each path for the relevant email function next(resolve) { if (!paths.length) { resolve(candidates); return; } var path = paths.shift(); self._imap.search({ path: path, header: ['Subject', self._appConfig.string.verificationSubject] }).then(function(uids) { uids.forEach(function(uid) { candidates.push({ path: path, uid: uid }); }); next(resolve); // keep on searching }).catch(function() { next(resolve); // if there's an error, just search the next inbox }); } }; PublickeyVerifier.prototype._verifyAll = function(candidates) { var self = this; // async for-loop inside a then-able return new Promise(next); function next(resolve) { if (!candidates.length) { resolve(false); return; } var candidate = candidates.shift(); self._verify(candidate.path, candidate.uid).then(function(success) { if (success) { resolve(success); // we're done here } else { next(resolve); } }).catch(function() { next(resolve); // ignore }); } }; PublickeyVerifier.prototype._verify = function(path, uid) { var self = this, message; // get the metadata for the message return self._imap.listMessages({ path: path, firstUid: uid, lastUid: uid }).then(function(messages) { if (!messages.length) { // message has been deleted in the meantime throw new Error('Message has already been deleted'); } // remember in scope message = messages[0]; }).then(function() { // get the body for the message return self._imap.getBodyParts({ path: path, uid: uid, bodyParts: message.bodyParts }); }).then(function() { // parse the message return new Promise(function(resolve, reject) { self._mailreader.parse(message, function(err, root) { if (err) { reject(err); } else { resolve(root); } }); }); }).then(function(root) { // extract the nonce var body = _.pluck(filterBodyParts(root, MSG_PART_TYPE_TEXT), MSG_PART_ATTR_CONTENT).join('\n'), verificationUrlPrefix = self._keyServerUrl + self._appConfig.config.verificationUrl, uuid = body.split(verificationUrlPrefix).pop().substr(0, self._appConfig.config.verificationUuidLength), uuidRegex = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/; // there's no valid uuid in the message, so forget about it if (!uuidRegex.test(uuid)) { throw new Error('No public key verifier found!'); } // there's a valid uuid in the message, so try to verify it return self._keychain.verifyPublicKey(uuid).catch(function(err) { throw new Error('Verifying your public key failed: ' + err.message); }); }).then(function() { return self._imap.deleteMessage({ path: path, uid: uid }).catch(function() {}); // ignore error here }).then(function() { return true; }); }; /** * 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; }