From 02adaad9394eec6ae8dd59dc1a451d748b02d893 Mon Sep 17 00:00:00 2001 From: Martin Hauck Date: Thu, 14 Feb 2019 18:11:37 +0100 Subject: [PATCH] Add upload, update and removal for single user IDs (emails) --- src/dao/papertrail.js | 30 +++--- src/email/email.js | 6 +- src/email/templates.js | 4 +- src/route/hkp.js | 2 +- src/route/rest.js | 10 +- src/service/pgp.js | 53 ++++++++-- src/service/public-key.js | 158 ++++++++++++++++++++++------ test/integration/app-test.js | 8 +- test/integration/public-key-test.js | 98 ++++++++++++----- 9 files changed, 274 insertions(+), 95 deletions(-) diff --git a/src/dao/papertrail.js b/src/dao/papertrail.js index 7fc18cb..ed576cb 100644 --- a/src/dao/papertrail.js +++ b/src/dao/papertrail.js @@ -33,21 +33,21 @@ const formatLogs = log.format(info => { }); exports.init = function({host, port}) { - if (!host || !port) { - if (process.env.NODE_ENV !== 'production') { - log.add(new log.transports.Console({ - format: log.format.combine( - formatLogs(), - log.format.simple() - ) - })); - } + if (host && port) { + log.add(new log.transports.Papertrail({ + format: formatLogs(), + level: config.log.level, + host, + port + })); return; } - log.add(new log.transports.Papertrail({ - format: formatLogs(), - level: config.log.level, - host, - port - })); + if (process.env.NODE_ENV !== 'production') { + log.add(new log.transports.Console({ + format: log.format.combine( + formatLogs(), + log.format.simple() + ) + })); + } }; diff --git a/src/email/email.js b/src/email/email.js index b169529..7fab760 100644 --- a/src/email/email.js +++ b/src/email/email.js @@ -56,15 +56,15 @@ class Email { * @param {Object} origin origin of the server * @yield {Object} reponse object containing SMTP info */ - async send({template, userId, keyId, origin}) { + async send({template, userId, keyId, origin, publicKeyArmored}) { const compiled = template({ name: userId.name, baseUrl: util.url(origin), keyId, nonce: userId.nonce }); - if (this._usePGPEncryption && userId.publicKeyArmored) { - compiled.text = await this._pgpEncrypt(compiled.text, userId.publicKeyArmored); + if (this._usePGPEncryption && publicKeyArmored) { + compiled.text = await this._pgpEncrypt(compiled.text, publicKeyArmored); } const sendOptions = { from: {name: this._sender.name, address: this._sender.email}, diff --git a/src/email/templates.js b/src/email/templates.js index 002ec55..806aaa2 100644 --- a/src/email/templates.js +++ b/src/email/templates.js @@ -2,10 +2,10 @@ exports.verifyKey = ({name, baseUrl, keyId, nonce}) => ({ subject: `Verify Your Key`, - text: `Hello ${name},\n\nplease click here to verify your key:\n\n${baseUrl}/api/v1/key?op=verify&keyId=${keyId}&nonce=${nonce}`, + text: `Hello ${name},\n\nplease click here to verify your email address:\n\n${baseUrl}/api/v1/key?op=verify&keyId=${keyId}&nonce=${nonce}`, }); exports.verifyRemove = ({name, baseUrl, keyId, nonce}) => ({ subject: `Verify Key Removal`, - text: `Hello ${name},\n\nplease click here to verify the removal of your key:\n\n${baseUrl}/api/v1/key?op=verifyRemove&keyId=${keyId}&nonce=${nonce}`, + text: `Hello ${name},\n\nplease click here to verify the removal of your email address:\n\n${baseUrl}/api/v1/key?op=verifyRemove&keyId=${keyId}&nonce=${nonce}`, }); diff --git a/src/route/hkp.js b/src/route/hkp.js index be26616..4fed747 100644 --- a/src/route/hkp.js +++ b/src/route/hkp.js @@ -43,7 +43,7 @@ class HKP { ctx.throw(400, 'Invalid request!'); } const origin = util.origin(ctx); - await this._publicKey.put({publicKeyArmored, origin}); + await this._publicKey.put({emails: [], publicKeyArmored, origin}); ctx.body = 'Upload successful. Check your inbox to verify your email address.'; ctx.status = 201; } diff --git a/src/route/rest.js b/src/route/rest.js index 847edd3..89b6307 100644 --- a/src/route/rest.js +++ b/src/route/rest.js @@ -34,16 +34,16 @@ class REST { } /** - * Public key upload via http POST + * Public key / user ID upload via http POST * @param {Object} ctx The koa request/response context */ async create(ctx) { - const {publicKeyArmored} = await parse.json(ctx, {limit: '1mb'}); + const {emails, publicKeyArmored} = await parse.json(ctx, {limit: '1mb'}); if (!publicKeyArmored) { ctx.throw(400, 'Invalid request!'); } const origin = util.origin(ctx); - await this._publicKey.put({publicKeyArmored, origin}); + await this._publicKey.put({emails: emails ? emails : [], publicKeyArmored, origin}); ctx.body = 'Upload successful. Check your inbox to verify your email address.'; ctx.status = 201; } @@ -91,7 +91,7 @@ class REST { ctx.throw(400, 'Invalid request!'); } await this._publicKey.requestRemove(q); - ctx.body = 'Check your inbox to verify the removal of your key.'; + ctx.body = 'Check your inbox to verify the removal of your email address.'; ctx.status = 202; } @@ -105,7 +105,7 @@ class REST { ctx.throw(400, 'Invalid request!'); } await this._publicKey.verifyRemove(q); - ctx.body = 'Key successfully removed!'; + ctx.body = 'Email address successfully removed!'; } } diff --git a/src/service/pgp.js b/src/service/pgp.js index 25f1436..69ba6cc 100644 --- a/src/service/pgp.js +++ b/src/service/pgp.js @@ -124,16 +124,55 @@ class PGP { if (userStatus !== openpgp.enums.keyStatus.invalid && user.userId && user.userId.userid) { const uid = addressparser(user.userId.userid)[0]; if (util.isEmail(uid.address)) { - result.push(uid); + // map to local user id object format + result.push({ + status: userStatus, + name: uid.name, + email: uid.address.toLowerCase(), + verified: false + }); } } } - // map to local user id object format - return result.map(uid => ({ - name: uid.name, - email: uid.address.toLowerCase(), - verified: false - })); + return result; + } + + /** + * Remove user IDs from armored key block which are not in array of user IDs + * @param {Array} userIds user IDs to be kept + * @param {String} armored armored key block to be filtered + * @return {String} filtered amored key block + */ + async filterKeyByUserIds(userIds, armored) { + const emails = userIds.map(({email}) => email); + const {keys: [key]} = await openpgp.key.readArmored(armored); + key.users = key.users.filter(({userId: {email}}) => emails.includes(email)); + return key.armor(); + } + + /** + * Merge (update) armored key blocks + * @param {String} srcArmored source amored key block + * @param {String} dstArmored destination armored key block + * @return {String} merged armored key block + */ + async updateKey(srcArmored, dstArmored) { + const {keys: [srcKey]} = await openpgp.key.readArmored(srcArmored); + const {keys: [dstKey]} = await openpgp.key.readArmored(dstArmored); + await dstKey.update(srcKey); + return dstKey.armor(); + } + + /** + * Remove user ID from armored key block + * @param {String} email email of user ID to be removed + * @param {String} publicKeyArmored amored key block to be filtered + * @return {String} filtered armored key block + */ + async removeUserId(email, publicKeyArmored) { + const {keys: [key]} = await openpgp.key.readArmored(publicKeyArmored); + key.users = key.users.filter(({userId}) => userId.email !== email); + return key.armor(); } } diff --git a/src/service/public-key.js b/src/service/public-key.js index ce91ca6..3c2203d 100644 --- a/src/service/public-key.js +++ b/src/service/public-key.js @@ -43,6 +43,7 @@ const tpl = require('../email/templates'); * } */ const DB_TYPE = 'publickey'; +const KEY_STATUS_VALID = 3; /** * A service that handlers PGP public keys queries to the database @@ -62,29 +63,47 @@ class PublicKey { /** * Persist a new public key + * @param {Array} emails (optional) The emails to upload/update * @param {String} publicKeyArmored The ascii armored pgp key block * @param {Object} origin Required for links to the keyserver e.g. { protocol:'https', host:'openpgpkeys@example.com' } - * @yield {undefined} + * @return {Promise} */ - async put({publicKeyArmored, origin}) { + async put({emails, publicKeyArmored, origin}) { // lazily purge old/unverified keys on every key upload await this._purgeOldUnverified(); // parse key block const key = await this._pgp.parseKey(publicKeyArmored); + // if emails array is empty, all userIds of the key will be submitted + if (emails.length) { + // keep submitted user IDs only + key.userIds = key.userIds.filter(({email}) => emails.includes(email)); + if (key.userIds.length !== emails.length) { + util.throw(400, 'Provided email address does not match a valid user ID of the key'); + } + } // check for existing verified key with same id const verified = await this.getVerified({keyId: key.keyId}); if (verified) { - util.throw(304, 'Key with this key id already exists'); + key.userIds = await this._mergeUsers(verified.userIds, key.userIds, key.publicKeyArmored); + // reduce new key to verified user IDs + const filteredPublicKeyArmored = await this._pgp.filterKeyByUserIds(key.userIds.filter(({verified}) => verified), key.publicKeyArmored); + // update verified key with new key + key.publicKeyArmored = await this._pgp.updateKey(verified.publicKeyArmored, filteredPublicKeyArmored); + } else { + key.userIds = key.userIds.filter(userId => userId.status === KEY_STATUS_VALID); + await this._addKeyArmored(key.userIds, key.publicKeyArmored); + // new key, set armored to null + key.publicKeyArmored = null; } - // store key in database - await this._persisKey(key); - // send mails to verify user ids (send only one if primary email is provided) + // send mails to verify user ids await this._sendVerifyEmail(key, origin); + // store key in database + await this._persistKey(key); } /** * Delete all keys where no user id has been verified after x days. - * @yield {undefined} + * @return {Promise} */ async _purgeOldUnverified() { // create date in the past to compare with @@ -97,17 +116,74 @@ class PublicKey { }, DB_TYPE); } + /** + * Merge existing and new user IDs + * @param {Array} existingUsers source user IDs + * @param {Array} newUsers new user IDs + * @param {String} publicKeyArmored armored key block of new user IDs + * @return {Array} merged user IDs + */ + async _mergeUsers(existingUsers, newUsers, publicKeyArmored) { + const result = []; + // existing verified valid or revoked users + const verifiedUsers = existingUsers.filter(userId => userId.verified); + // valid new users which are not yet verified + const validUsers = newUsers.filter(userId => userId.status === KEY_STATUS_VALID && !this._includeEmail(verifiedUsers, userId)); + // pending users are not verified, not newly submitted + const pendingUsers = existingUsers.filter(userId => !userId.verified && !this._includeEmail(validUsers, userId)); + await this._addKeyArmored(validUsers, publicKeyArmored); + result.push(...validUsers, ...pendingUsers, ...verifiedUsers); + return result; + } + + /** + * Create amored key block which contains the corresponding user ID only and add it to the user ID object + * @param {Array} userIds user IDs to be extended + * @param {String} PublicKeyArmored armored key block to be filtered + * @return {Promise} + */ + async _addKeyArmored(userIds, publicKeyArmored) { + for (const userId of userIds) { + userId.publicKeyArmored = await this._pgp.filterKeyByUserIds([userId], publicKeyArmored); + userId.notify = true; + } + } + + _includeEmail(users, user) { + return users.find(({email}) => email === user.email); + } + + /** + * 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 {Object} origin the server's origin (required for email links) + * @return {Promise} + */ + async _sendVerifyEmail({userIds, keyId}, origin) { + for (const userId of userIds) { + if (userId.notify && userId.notify === true) { + // generate nonce for verification + userId.nonce = util.random(); + await this._email.send({template: tpl.verifyKey, userId, keyId, origin, publicKeyArmored: userId.publicKeyArmored}); + } + } + } + /** * Persist the public key and its user ids in the database. * @param {Object} key public key parameters - * @yield {undefined} The persisted user id documents + * @return {Promise} */ - async _persisKey(key) { + async _persistKey(key) { // delete old/unverified key await this._mongo.remove({keyId: key.keyId}, DB_TYPE); // generate nonces for verification - for (const uid of key.userIds) { - uid.nonce = util.random(); + for (const userId of key.userIds) { + // remove status from user + delete userId.status; + // remove notify flag from user + delete userId.notify; } // persist new key const r = await this._mongo.create(key, DB_TYPE); @@ -116,25 +192,11 @@ class PublicKey { } } - /** - * 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 {Object} origin the server's origin (required for email links) - * @yield {undefined} - */ - async _sendVerifyEmail({userIds, keyId, publicKeyArmored}, origin) { - for (const userId of userIds) { - userId.publicKeyArmored = publicKeyArmored; // set key for encryption - await this._email.send({template: tpl.verifyKey, userId, keyId, origin}); - } - } - /** * Verify a user id by proving knowledge of the nonce. * @param {string} keyId Correspronding public key id * @param {string} nonce The verification nonce proving email address ownership - * @yield {undefined} + * @return {Promise} */ async verify({keyId, nonce}) { // look for verification nonce in database @@ -144,13 +206,27 @@ class PublicKey { util.throw(404, 'User id not found'); } await this._removeKeysWithSameEmail(key, nonce); + let {publicKeyArmored} = key.userIds.find(userId => userId.nonce === nonce); + // update armored key + if (key.publicKeyArmored) { + publicKeyArmored = await this._pgp.updateKey(key.publicKeyArmored, publicKeyArmored); + } // flag the user id as verified await this._mongo.update(query, { + publicKeyArmored, 'userIds.$.verified': true, - 'userIds.$.nonce': null + 'userIds.$.nonce': null, + 'userIds.$.publicKeyArmored': null }, DB_TYPE); } + /** + * Removes keys with the same email address + * @param {String} options.keyId source key ID + * @param {Array} options.userIds user IDs of source key + * @param {Array} nonce relevant nonce + * @return {Promise} + */ async _removeKeysWithSameEmail({keyId, userIds}, nonce) { return this._mongo.remove({ keyId: {$ne: keyId}, @@ -165,7 +241,7 @@ class PublicKey { * @param {Array} userIds A list of user ids to check * @param {string} fingerprint The public key fingerprint * @param {string} keyId (optional) The public key id - * @yield {Object} The verified key document + * @return {Object} The verified key document */ async getVerified({userIds, fingerprint, keyId}) { let queries = []; @@ -203,7 +279,7 @@ class PublicKey { * @param {string} fingerprint (optional) The public key fingerprint * @param {string} keyId (optional) The public key id * @param {String} email (optional) The user's email address - * @yield {Object} The public key document + * @return {Object} The public key document */ async get({fingerprint, keyId, email}) { // look for verified key @@ -230,7 +306,7 @@ class PublicKey { * @param {String} keyId (optional) The public key id * @param {String} email (optional) The user's email address * @param {Object} origin Required for links to the keyserver e.g. { protocol:'https', host:'openpgpkeys@example.com' } - * @yield {undefined} + * @return {Promise} */ async requestRemove({keyId, email, origin}) { // flag user ids for removal @@ -250,7 +326,7 @@ class PublicKey { * saving it. Either a key id or email address must be provided * @param {String} keyId (optional) The public key id * @param {String} email (optional) The user's email address - * @yield {Array} A list of user ids with nonces + * @return {Array} A list of user ids with nonces */ async _flagForRemove(keyId, email) { const query = email ? {'userIds.email': email} : {keyId}; @@ -282,7 +358,7 @@ class PublicKey { * Also deletes all user id documents of that key id. * @param {string} keyId public key id * @param {string} nonce The verification nonce proving email address ownership - * @yield {undefined} + * @return {Promise} */ async verifyRemove({keyId, nonce}) { // check if key exists in database @@ -290,8 +366,22 @@ class PublicKey { if (!flagged) { util.throw(404, 'User id not found'); } - // delete the key - await this._mongo.remove({keyId}, DB_TYPE); + if (flagged.userIds.length === 1) { + // delete the key + return this._mongo.remove({keyId}, DB_TYPE); + } + // update the key + const rmIdx = flagged.userIds.findIndex(userId => userId.nonce === nonce); + const rmUserId = flagged.userIds[rmIdx]; + if (rmUserId.verified) { + if (flagged.userIds.filter(({verified}) => verified).length > 1) { + flagged.publicKeyArmored = await this._pgp.removeUserId(rmUserId.email, flagged.publicKeyArmored); + } else { + flagged.publicKeyArmored = null; + } + } + flagged.userIds.splice(rmIdx, 1); + await this._mongo.update({keyId}, flagged, DB_TYPE); } } diff --git a/test/integration/app-test.js b/test/integration/app-test.js index 419f646..825e009 100644 --- a/test/integration/app-test.js +++ b/test/integration/app-test.js @@ -301,21 +301,21 @@ describe('Koa App (HTTP Server) Integration Tests', function() { it('should return 200 for key id', done => { request(app.listen()) .get(`/pks/lookup?op=get&search=0x${emailParams.keyId}`) - .expect(200, publicKeyArmored) + .expect(200) .end(done); }); it('should return 200 for fingerprint', done => { request(app.listen()) .get(`/pks/lookup?op=get&search=0x${fingerprint}`) - .expect(200, publicKeyArmored) + .expect(200) .end(done); }); it('should return 200 for correct email address', done => { request(app.listen()) .get(`/pks/lookup?op=get&search=${primaryEmail}`) - .expect(200, publicKeyArmored) + .expect(200) .end(done); }); @@ -324,7 +324,7 @@ describe('Koa App (HTTP Server) Integration Tests', function() { .get(`/pks/lookup?op=get&options=mr&search=${primaryEmail}`) .expect('Content-Type', 'application/pgp-keys; charset=utf-8') .expect('Content-Disposition', 'attachment; filename=openpgpkey.asc') - .expect(200, publicKeyArmored) + .expect(200) .end(done); }); diff --git a/test/integration/public-key-test.js b/test/integration/public-key-test.js index 9846c2e..8b62425 100644 --- a/test/integration/public-key-test.js +++ b/test/integration/public-key-test.js @@ -59,7 +59,7 @@ describe('Public Key Integration Tests', function() { email.init({ host: 'localhost', auth: {user: 'user', pass: 'pass'}, - sender: {name: 'Foo Bar', email: 'foo@bar.com'} + sender: {name: 'Foo Bar', emails: 'foo@bar.com'} }); pgp = new PGP(); publicKey = new PublicKey(pgp, mongo, email); @@ -77,22 +77,22 @@ describe('Public Key Integration Tests', function() { describe('put', () => { it('should persist key and send verification email', async () => { - await publicKey.put({publicKeyArmored, origin}); + await publicKey.put({emails: [], publicKeyArmored, origin}); expect(mailsSent.length).to.equal(4); }); it('should work twice if not yet verified', async () => { - await publicKey.put({publicKeyArmored, origin}); + await publicKey.put({emails: [], publicKeyArmored, origin}); expect(mailsSent.length).to.equal(4); - await publicKey.put({publicKeyArmored, origin}); + await publicKey.put({emails: [], publicKeyArmored, origin}); expect(mailsSent.length).to.equal(8); }); - it('should throw 304 if key already exists', async () => { - await publicKey.put({publicKeyArmored, origin}); + it.skip('should throw 304 if key already exists', async () => { + await publicKey.put({emails: [], publicKeyArmored, origin}); await publicKey.verify(mailsSent[0].params); try { - await publicKey.put({publicKeyArmored, origin}); + await publicKey.put({emails: [], publicKeyArmored, origin}); expect(false).to.be.true; } catch (e) { expect(e.status).to.equal(304); @@ -100,9 +100,9 @@ describe('Public Key Integration Tests', function() { }); it('should work for a key with an existing/verified email address to allow key update without an extra delete step in between', async () => { - await publicKey.put({publicKeyArmored, origin}); + await publicKey.put({emails: [], publicKeyArmored, origin}); await publicKey.verify(mailsSent[1].params); - await publicKey.put({publicKeyArmored: publicKeyArmored2, origin}); + await publicKey.put({emails: [], publicKeyArmored: publicKeyArmored2, origin}); expect(mailsSent.length).to.equal(5); }); }); @@ -120,14 +120,14 @@ describe('Public Key Integration Tests', function() { }); it('should not remove a current unverified key', async () => { - await publicKey._persisKey(key); + await publicKey._persistKey(key); const r = await publicKey._purgeOldUnverified(); expect(r.deletedCount).to.equal(0); }); it('should not remove a current verified key', async () => { key.userIds[0].verified = true; - await publicKey._persisKey(key); + await publicKey._persistKey(key); const r = await publicKey._purgeOldUnverified(); expect(r.deletedCount).to.equal(0); }); @@ -135,14 +135,14 @@ describe('Public Key Integration Tests', function() { it('should not remove an old verified key', async () => { key.uploaded.setDate(key.uploaded.getDate() - 31); key.userIds[0].verified = true; - await publicKey._persisKey(key); + await publicKey._persistKey(key); const r = await publicKey._purgeOldUnverified(); expect(r.deletedCount).to.equal(0); }); it('should remove an old unverified key', async () => { key.uploaded.setDate(key.uploaded.getDate() - 31); - await publicKey._persisKey(key); + await publicKey._persistKey(key); const r = await publicKey._purgeOldUnverified(); expect(r.deletedCount).to.equal(1); }); @@ -150,7 +150,7 @@ describe('Public Key Integration Tests', function() { describe('verify', () => { it('should update the document', async () => { - await publicKey.put({publicKeyArmored, origin}); + await publicKey.put({emails: [], publicKeyArmored, origin}); const emailParams = mailsSent[0].params; await publicKey.verify(emailParams); const gotten = await mongo.get({keyId: emailParams.keyId}, DB_TYPE); @@ -161,7 +161,7 @@ describe('Public Key Integration Tests', function() { }); it('should not find the document', async () => { - await publicKey.put({publicKeyArmored, origin}); + await publicKey.put({emails: [], publicKeyArmored, origin}); const emailParams = mailsSent[0].params; try { await publicKey.verify({keyId: emailParams.keyId, nonce: 'fake_nonce'}); @@ -177,11 +177,11 @@ describe('Public Key Integration Tests', function() { }); it('should verify a second key for an already verified user id and delete the old key', async () => { - await publicKey.put({publicKeyArmored, origin}); + await publicKey.put({emails: [], publicKeyArmored, origin}); await publicKey.verify(mailsSent[1].params); let firstKey = await publicKey.getVerified({keyId: mailsSent[1].params.keyId}); expect(firstKey).to.exist; - await publicKey.put({publicKeyArmored: publicKeyArmored2, origin}); + await publicKey.put({emails: [], publicKeyArmored: publicKeyArmored2, origin}); await publicKey.verify(mailsSent[4].params); firstKey = await publicKey.getVerified({keyId: mailsSent[1].params.keyId}); expect(firstKey).to.not.exist; @@ -190,8 +190,8 @@ describe('Public Key Integration Tests', function() { }); it('should delete other keys with the same user id when verifying', async () => { - await publicKey.put({publicKeyArmored, origin}); - await publicKey.put({publicKeyArmored: publicKeyArmored2, origin}); + await publicKey.put({emails: [], publicKeyArmored, origin}); + await publicKey.put({emails: [], publicKeyArmored: publicKeyArmored2, origin}); expect(mailsSent[1].to).to.equal(mailsSent[4].to); await publicKey.verify(mailsSent[1].params); const firstKey = await publicKey.getVerified({keyId: mailsSent[1].params.keyId}); @@ -201,7 +201,7 @@ describe('Public Key Integration Tests', function() { }); it('should be able to verify multiple user ids', async () => { - await publicKey.put({publicKeyArmored, origin}); + await publicKey.put({emails: [], publicKeyArmored, origin}); expect(mailsSent.length).to.equal(4); await publicKey.verify(mailsSent[0].params); await publicKey.verify(mailsSent[1].params); @@ -221,7 +221,7 @@ describe('Public Key Integration Tests', function() { describe('should find a verified key', () => { beforeEach(async () => { key = await pgp.parseKey(publicKeyArmored); - await publicKey.put({publicKeyArmored, origin}); + await publicKey.put({emails: [], publicKeyArmored, origin}); await publicKey.verify(mailsSent[0].params); }); @@ -289,7 +289,7 @@ describe('Public Key Integration Tests', function() { let emailParams; beforeEach(async () => { - await publicKey.put({publicKeyArmored, origin}); + await publicKey.put({emails: [], publicKeyArmored, origin}); emailParams = mailsSent[0].params; }); @@ -345,7 +345,7 @@ describe('Public Key Integration Tests', function() { let keyId; beforeEach(async () => { - await publicKey.put({publicKeyArmored, origin}); + await publicKey.put({emails: [], publicKeyArmored, origin}); keyId = mailsSent[0].params.keyId; }); @@ -380,13 +380,63 @@ describe('Public Key Integration Tests', function() { let keyId; beforeEach(async () => { - await publicKey.put({publicKeyArmored, origin}); + await publicKey.put({emails: [], publicKeyArmored, origin}); keyId = mailsSent[0].params.keyId; + }); + + afterEach(() => { + mailsSent = []; + }); + + it('should remove unverified user ID', async () => { await publicKey.requestRemove({keyId, origin}); + const key = await mongo.get({keyId}, DB_TYPE); + expect(key.userIds[0].verified).to.be.false; + expect(key.userIds[0].email).to.equal(primaryEmail); + await publicKey.verifyRemove(mailsSent[4].params); + const modifiedKey = await mongo.get({keyId}, DB_TYPE); + expect(modifiedKey.userIds[0].email).to.not.equal(primaryEmail); + }); + + it('should remove single verfied user ID', async () => { + await publicKey.verify(mailsSent[0].params); + const key = await mongo.get({keyId}, DB_TYPE); + expect(key.userIds[0].verified).to.be.true; + expect(key.userIds[0].email).to.equal(primaryEmail); + const keyFromArmored = await pgp.parseKey(key.publicKeyArmored); + expect(keyFromArmored.userIds.find(userId => userId.email === primaryEmail)).not.to.be.undefined; + await publicKey.requestRemove({keyId, origin}); + await publicKey.verifyRemove(mailsSent[4].params); + const modifiedKey = await mongo.get({keyId}, DB_TYPE); + expect(modifiedKey.userIds[0].email).to.not.equal(primaryEmail); + expect(modifiedKey.publicKeyArmored).to.be.null; + }); + + it('should remove verfied user ID', async () => { + await publicKey.verify(mailsSent[0].params); + await publicKey.verify(mailsSent[1].params); + const key = await mongo.get({keyId}, DB_TYPE); + expect(key.userIds[0].verified).to.be.true; + expect(key.userIds[1].verified).to.be.true; + const emails = [key.userIds[0].email, key.userIds[1].email]; + const keyFromArmored = await pgp.parseKey(key.publicKeyArmored); + expect(keyFromArmored.userIds.filter(userId => emails.includes(userId.email)).length).to.equal(2); + await publicKey.requestRemove({keyId, origin}); + await publicKey.verifyRemove(mailsSent[5].params); + const modifiedKey = await mongo.get({keyId}, DB_TYPE); + expect(modifiedKey.userIds[0].email).to.equal(emails[0]); + expect(modifiedKey.userIds[1].email).to.not.equal(emails[1]); + expect(modifiedKey.publicKeyArmored).not.to.be.null; + const keyFromModifiedArmored = await pgp.parseKey(modifiedKey.publicKeyArmored); + expect(keyFromModifiedArmored.userIds.filter(userId => emails.includes(userId.email)).length).to.equal(1); }); it('should remove key', async () => { + await publicKey.requestRemove({keyId, origin}); await publicKey.verifyRemove(mailsSent[4].params); + await publicKey.verifyRemove(mailsSent[5].params); + await publicKey.verifyRemove(mailsSent[6].params); + await publicKey.verifyRemove(mailsSent[7].params); const key = await mongo.get({keyId}, DB_TYPE); expect(key).to.not.exist; });