diff --git a/src/service/public-key.js b/src/service/public-key.js index 862f8c3..cbdda76 100644 --- a/src/service/public-key.js +++ b/src/service/public-key.js @@ -40,13 +40,13 @@ class PublicKey { * @param {Object} openpgp An instance of OpenPGP.js * @param {Object} mongo An instance of the MongoDB client * @param {Object} email An instance of the Email Sender - * @param {Object} userid An instance of the UserId service + * @param {Object} userId An instance of the UserId service */ - constructor(openpgp, mongo, email, userid) { + constructor(openpgp, mongo, email, userId) { this._openpgp = openpgp; this._mongo = mongo; this._email = email; - this._userid = userid; + this._userId = userId; } /** @@ -59,39 +59,25 @@ class PublicKey { *put(options) { // parse key block let publicKeyArmored = options.publicKeyArmored, primaryEmail = options.primaryEmail, origin = options.origin; - let params = this.parseKey(publicKeyArmored); + publicKeyArmored = publicKeyArmored.trim(); // remove whitespace + let params = this._parseKey(publicKeyArmored); // check for existing verfied key by id or email addresses - let verified = yield this._userid.getVerfied(params); + let verified = yield this._userId.getVerfied(params); if (verified) { - util.throw(304, 'Key for this user already exists: ' + JSON.stringify(verified)); - } - // delete old/unverified key and user ids with the same key id - yield this.remove({ keyid:params.keyid }); - // persist new user ids - let userIds = yield this._userid.batch(params); - // persist new key - let r = yield this._mongo.create({ _id:params.keyid, publicKeyArmored }, DB_TYPE); - if (r.insertedCount !== 1) { - // rollback user ids - yield this.remove({ keyid:params.keyid }); - util.throw(500, 'Failed to persist key'); + util.throw(304, 'Key for this user already exists'); } + // store key in database + let userIds = yield this._persisKey(publicKeyArmored, params); // send mails to verify user ids (send only one if primary email is provided) - let primaryUserId = userIds.find(uid => uid.email === primaryEmail); - if (primaryUserId) { - userIds = [primaryUserId]; - } - for (let userId of userIds) { - yield this._email.send({ template:tpl.verifyKey, userId, origin }); - } + yield this._sendVerifyEmail(userIds, primaryEmail, origin); } /** * Parse an ascii armored pgp key block and get its parameters. - * @param {String} publicKeyArmored The ascii armored pgp key block - * @return {Object} The key's id and user ids + * @param {String} publicKeyArmored ascii armored pgp key block + * @return {Object} key's id and user ids */ - parseKey(publicKeyArmored) { + _parseKey(publicKeyArmored) { let keys, userIds = []; try { keys = this._openpgp.key.readArmored(publicKeyArmored).keys; @@ -109,6 +95,45 @@ class PublicKey { }; } + /** + * Persist the public key and its user ids in the database. + * @param {String} publicKeyArmored ascii armored pgp key block + * @param {Object} params public key parameters + * @yield {Array} The persisted user id documents + */ + *_persisKey(publicKeyArmored, params) { + // delete old/unverified key and user ids with the same key id + yield this.remove({ keyid:params.keyid }); + // persist new user ids + let userIds = yield this._userId.batch(params); + // persist new key + let r = yield this._mongo.create({ _id:params.keyid, publicKeyArmored }, DB_TYPE); + if (r.insertedCount !== 1) { + // rollback user ids + yield this.remove({ keyid:params.keyid }); + util.throw(500, 'Failed to persist key'); + } + return userIds; + } + + /** + * Send verification emails to the public keys user ids for verification. + * If a primary email address is provided only one email will be sent. + * @param {Array} userIds user id documents containg the verification nonces + * @param {string} primaryEmail the public key's primary email address + * @param {Object} origin the server's origin (required for email links) + * @yield {undefined} + */ + *_sendVerifyEmail(userIds, primaryEmail, origin) { + let primaryUserId = userIds.find(uid => uid.email === primaryEmail); + if (primaryUserId) { + userIds = [primaryUserId]; + } + for (let userId of userIds) { + yield this._email.send({ template:tpl.verifyKey, userId, origin }); + } + } + /** * Fetch a verified public key from the database. Either the key id or the * email address muss be provided. @@ -118,7 +143,7 @@ class PublicKey { */ *get(options) { let keyid = options.keyid, email = options.email; - let verified = yield this._userid.getVerfied({ + let verified = yield this._userId.getVerfied({ keyid: keyid ? keyid.toUpperCase() : undefined, userIds: email ? [{ email:email.toLowerCase() }] : undefined }); @@ -140,7 +165,10 @@ class PublicKey { */ *requestRemove(options) { let keyid = options.keyid, email = options.email, origin = options.origin; - let userIds = yield this._userid.flagForRemove({ keyid, email }, DB_TYPE); + let userIds = yield this._userId.flagForRemove({ keyid, email }, DB_TYPE); + if (!userIds.length) { + util.throw(404, 'User id not found'); + } for (let userId of userIds) { yield this._email.send({ template:tpl.verifyRemove, userId, origin }); } @@ -172,7 +200,7 @@ class PublicKey { // remove key document yield this._mongo.remove({ _id:keyid }, DB_TYPE); // remove matching user id documents - yield this._userid.remove({ keyid }); + yield this._userId.remove({ keyid }); } } diff --git a/test/integration/public-key-test.js b/test/integration/public-key-test.js new file mode 100644 index 0000000..ff4e86d --- /dev/null +++ b/test/integration/public-key-test.js @@ -0,0 +1,213 @@ +'use strict'; + +require('co-mocha')(require('mocha')); // monkey patch mocha for generators + +const log = require('npmlog'); +const openpgp = require('openpgp'); +const nodemailer = require('nodemailer'); +const Email = require('../../src/email/email'); +const Mongo = require('../../src/dao/mongo'); +const UserId = require('../../src/service/user-id'); +const PublicKey = require('../../src/service/public-key'); +const expect = require('chai').expect; +const sinon = require('sinon'); + +describe('Public Key Integration Tests', function() { + this.timeout(20000); + + let publicKey, email, mongo, userId, + sendEmailStub, publicKeyArmored, emailParams; + + const DB_TYPE_PUB_KEY = 'publickey'; + const DB_TYPE_USER_ID = 'userid'; + const primaryEmail = 'safewithme.testuser@gmail.com'; + const origin = { host:'localhost', protocol:'http' }; + + before(function *() { + publicKeyArmored = require('fs').readFileSync(__dirname + '/../key1.asc', 'utf8'); + let credentials; + try { + credentials = require('../../credentials.json'); + } catch(e) { + log.info('mongo-test', 'No credentials.json found ... using environment vars.'); + } + mongo = new Mongo({ + uri: process.env.MONGO_URI || credentials.mongo.uri, + user: process.env.MONGO_USER || credentials.mongo.user, + password: process.env.MONGO_PASS || credentials.mongo.pass + }); + yield mongo.connect(); + }); + + beforeEach(function *() { + yield mongo.clear(DB_TYPE_PUB_KEY); + yield mongo.clear(DB_TYPE_USER_ID); + emailParams = null; + sendEmailStub = sinon.stub().returns(Promise.resolve({ response:'250' })); + sendEmailStub.withArgs(sinon.match(recipient => { + return recipient.to.address === primaryEmail; + }), sinon.match(params => { + emailParams = params; + return !!params.nonce; + })); + sinon.stub(nodemailer, 'createTransport').returns({ + templateSender: () => { return sendEmailStub; } + }); + email = new Email(nodemailer); + email.init({ + host: 'localhost', + auth: { user:'user', pass:'pass' }, + sender: { name:'Foo Bar', email:'foo@bar.com' } + }); + userId = new UserId(mongo); + publicKey = new PublicKey(openpgp, mongo, email, userId); + }); + + afterEach(() => { + nodemailer.createTransport.restore(); + }); + + after(function *() { + yield mongo.clear(DB_TYPE_PUB_KEY); + yield mongo.clear(DB_TYPE_USER_ID); + yield mongo.disconnect(); + }); + + describe('put', () => { + it('should persist key and send verification email', function *() { + yield publicKey.put({ publicKeyArmored, primaryEmail, origin }); + expect(emailParams.nonce).to.exist; + }); + + it('should work twice if not yet verified', function *() { + yield publicKey.put({ publicKeyArmored, primaryEmail, origin }); + expect(emailParams.nonce).to.exist; + emailParams = null; + yield publicKey.put({ publicKeyArmored, primaryEmail, origin }); + expect(emailParams.nonce).to.exist; + }); + + it('should throw 304 if key already exists', function *() { + yield publicKey.put({ publicKeyArmored, primaryEmail, origin }); + yield userId.verify(emailParams); + try { + yield publicKey.put({ publicKeyArmored, primaryEmail, origin }); + expect(false).to.be.true; + } catch(e) { + expect(e.status).to.equal(304); + } + }); + }); + + describe('get', () => { + beforeEach(function *() { + yield publicKey.put({ publicKeyArmored, primaryEmail, origin }); + }); + + it('should return verified key by key id', function *() { + yield userId.verify(emailParams); + let key = yield publicKey.get({ keyid:emailParams.keyid }); + expect(key.publicKeyArmored).to.equal(publicKeyArmored); + }); + + it('should return verified key by email address', function *() { + yield userId.verify(emailParams); + let key = yield publicKey.get({ email:primaryEmail }); + expect(key.publicKeyArmored).to.equal(publicKeyArmored); + }); + + it('should throw 404 for unverified key', function *() { + try { + yield publicKey.get({ keyid:emailParams.keyid }); + expect(false).to.be.true; + } catch(e) { + expect(e.status).to.equal(404); + } + }); + }); + + describe('requestRemove', () => { + let keyid; + + beforeEach(function *() { + yield publicKey.put({ publicKeyArmored, primaryEmail, origin }); + keyid = emailParams.keyid; + }); + + it('should work for verified key', function *() { + yield userId.verify(emailParams); + emailParams = null; + yield publicKey.requestRemove({ keyid, origin }); + expect(emailParams.nonce).to.exist; + }); + + it('should work for unverified key', function *() { + emailParams = null; + yield publicKey.requestRemove({ keyid, origin }); + expect(emailParams.nonce).to.exist; + }); + + it('should work by email address', function *() { + emailParams = null; + yield publicKey.requestRemove({ email:primaryEmail, origin }); + expect(emailParams.nonce).to.exist; + }); + + it('should throw 404 for no key', function *() { + yield publicKey.remove({ keyid }); + try { + yield publicKey.requestRemove({ keyid, origin }); + expect(false).to.be.true; + } catch(e) { + expect(e.status).to.equal(404); + } + }); + }); + + describe('verifyRemove', () => { + let keyid; + + beforeEach(function *() { + yield publicKey.put({ publicKeyArmored, primaryEmail, origin }); + keyid = emailParams.keyid; + emailParams = null; + yield publicKey.requestRemove({ keyid, origin }); + }); + + it('should remove key', function *() { + yield publicKey.verifyRemove(emailParams); + let uid = yield mongo.get({ keyid }, DB_TYPE_USER_ID); + expect(uid).to.not.exist; + let key = yield mongo.get({ _id:keyid }, DB_TYPE_PUB_KEY); + expect(key).to.not.exist; + }); + + it('should throw 404 for no key', function *() { + yield publicKey.remove({ keyid }); + try { + yield publicKey.verifyRemove(emailParams); + expect(false).to.be.true; + } catch(e) { + expect(e.status).to.equal(404); + } + }); + }); + + describe('remove', () => { + let keyid; + + beforeEach(function *() { + yield publicKey.put({ publicKeyArmored, primaryEmail, origin }); + keyid = emailParams.keyid; + }); + + it('should remove key', function *() { + yield publicKey.remove({ keyid }); + let uid = yield mongo.get({ keyid }, DB_TYPE_USER_ID); + expect(uid).to.not.exist; + let key = yield mongo.get({ _id:keyid }, DB_TYPE_PUB_KEY); + expect(key).to.not.exist; + }); + }); + +}); \ No newline at end of file