mail/src/js/service/publickey-verifier.js

233 lines
7.2 KiB
JavaScript

'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.persistKeypair = function() {
return this._keychain.putUserKeyPair(this.keypair);
};
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('Could not verify public key');
}
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;
}