2015-02-20 11:55:11 -05:00
|
|
|
'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);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2015-02-27 14:04:44 -05:00
|
|
|
PublickeyVerifier.prototype.uploadPublicKey = function() {
|
|
|
|
if (this.keypair) {
|
|
|
|
return this._keychain.uploadPublicKey(this.keypair.publicKey);
|
|
|
|
}
|
|
|
|
return new Promise(function(resolve) {
|
|
|
|
resolve();
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2015-02-20 11:55:11 -05:00
|
|
|
PublickeyVerifier.prototype.persistKeypair = function() {
|
2015-02-27 14:04:44 -05:00
|
|
|
if (this.keypair) {
|
|
|
|
return this._keychain.putUserKeyPair(this.keypair);
|
|
|
|
}
|
|
|
|
return new Promise(function(resolve) {
|
|
|
|
resolve();
|
|
|
|
});
|
2015-02-20 11:55:11 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
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
|
2015-04-14 12:46:08 -04:00
|
|
|
throw new Error();
|
2015-02-20 11:55:11 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|