Add upload, update and removal for single user IDs (emails)
This commit is contained in:
parent
1651571d36
commit
02adaad939
@ -33,21 +33,21 @@ const formatLogs = log.format(info => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
exports.init = function({host, port}) {
|
exports.init = function({host, port}) {
|
||||||
if (!host || !port) {
|
if (host && port) {
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
log.add(new log.transports.Papertrail({
|
||||||
log.add(new log.transports.Console({
|
format: formatLogs(),
|
||||||
format: log.format.combine(
|
level: config.log.level,
|
||||||
formatLogs(),
|
host,
|
||||||
log.format.simple()
|
port
|
||||||
)
|
}));
|
||||||
}));
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log.add(new log.transports.Papertrail({
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
format: formatLogs(),
|
log.add(new log.transports.Console({
|
||||||
level: config.log.level,
|
format: log.format.combine(
|
||||||
host,
|
formatLogs(),
|
||||||
port
|
log.format.simple()
|
||||||
}));
|
)
|
||||||
|
}));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@ -56,15 +56,15 @@ class Email {
|
|||||||
* @param {Object} origin origin of the server
|
* @param {Object} origin origin of the server
|
||||||
* @yield {Object} reponse object containing SMTP info
|
* @yield {Object} reponse object containing SMTP info
|
||||||
*/
|
*/
|
||||||
async send({template, userId, keyId, origin}) {
|
async send({template, userId, keyId, origin, publicKeyArmored}) {
|
||||||
const compiled = template({
|
const compiled = template({
|
||||||
name: userId.name,
|
name: userId.name,
|
||||||
baseUrl: util.url(origin),
|
baseUrl: util.url(origin),
|
||||||
keyId,
|
keyId,
|
||||||
nonce: userId.nonce
|
nonce: userId.nonce
|
||||||
});
|
});
|
||||||
if (this._usePGPEncryption && userId.publicKeyArmored) {
|
if (this._usePGPEncryption && publicKeyArmored) {
|
||||||
compiled.text = await this._pgpEncrypt(compiled.text, userId.publicKeyArmored);
|
compiled.text = await this._pgpEncrypt(compiled.text, publicKeyArmored);
|
||||||
}
|
}
|
||||||
const sendOptions = {
|
const sendOptions = {
|
||||||
from: {name: this._sender.name, address: this._sender.email},
|
from: {name: this._sender.name, address: this._sender.email},
|
||||||
|
@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
exports.verifyKey = ({name, baseUrl, keyId, nonce}) => ({
|
exports.verifyKey = ({name, baseUrl, keyId, nonce}) => ({
|
||||||
subject: `Verify Your Key`,
|
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}) => ({
|
exports.verifyRemove = ({name, baseUrl, keyId, nonce}) => ({
|
||||||
subject: `Verify Key Removal`,
|
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}`,
|
||||||
});
|
});
|
||||||
|
@ -43,7 +43,7 @@ class HKP {
|
|||||||
ctx.throw(400, 'Invalid request!');
|
ctx.throw(400, 'Invalid request!');
|
||||||
}
|
}
|
||||||
const origin = util.origin(ctx);
|
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.body = 'Upload successful. Check your inbox to verify your email address.';
|
||||||
ctx.status = 201;
|
ctx.status = 201;
|
||||||
}
|
}
|
||||||
|
@ -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
|
* @param {Object} ctx The koa request/response context
|
||||||
*/
|
*/
|
||||||
async create(ctx) {
|
async create(ctx) {
|
||||||
const {publicKeyArmored} = await parse.json(ctx, {limit: '1mb'});
|
const {emails, publicKeyArmored} = await parse.json(ctx, {limit: '1mb'});
|
||||||
if (!publicKeyArmored) {
|
if (!publicKeyArmored) {
|
||||||
ctx.throw(400, 'Invalid request!');
|
ctx.throw(400, 'Invalid request!');
|
||||||
}
|
}
|
||||||
const origin = util.origin(ctx);
|
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.body = 'Upload successful. Check your inbox to verify your email address.';
|
||||||
ctx.status = 201;
|
ctx.status = 201;
|
||||||
}
|
}
|
||||||
@ -91,7 +91,7 @@ class REST {
|
|||||||
ctx.throw(400, 'Invalid request!');
|
ctx.throw(400, 'Invalid request!');
|
||||||
}
|
}
|
||||||
await this._publicKey.requestRemove(q);
|
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;
|
ctx.status = 202;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,7 +105,7 @@ class REST {
|
|||||||
ctx.throw(400, 'Invalid request!');
|
ctx.throw(400, 'Invalid request!');
|
||||||
}
|
}
|
||||||
await this._publicKey.verifyRemove(q);
|
await this._publicKey.verifyRemove(q);
|
||||||
ctx.body = 'Key successfully removed!';
|
ctx.body = 'Email address successfully removed!';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,16 +124,55 @@ class PGP {
|
|||||||
if (userStatus !== openpgp.enums.keyStatus.invalid && user.userId && user.userId.userid) {
|
if (userStatus !== openpgp.enums.keyStatus.invalid && user.userId && user.userId.userid) {
|
||||||
const uid = addressparser(user.userId.userid)[0];
|
const uid = addressparser(user.userId.userid)[0];
|
||||||
if (util.isEmail(uid.address)) {
|
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;
|
||||||
return result.map(uid => ({
|
}
|
||||||
name: uid.name,
|
|
||||||
email: uid.address.toLowerCase(),
|
/**
|
||||||
verified: false
|
* 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,6 +43,7 @@ const tpl = require('../email/templates');
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
const DB_TYPE = 'publickey';
|
const DB_TYPE = 'publickey';
|
||||||
|
const KEY_STATUS_VALID = 3;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service that handlers PGP public keys queries to the database
|
* A service that handlers PGP public keys queries to the database
|
||||||
@ -62,29 +63,47 @@ class PublicKey {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Persist a new public key
|
* Persist a new public key
|
||||||
|
* @param {Array} emails (optional) The emails to upload/update
|
||||||
* @param {String} publicKeyArmored The ascii armored pgp key block
|
* @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' }
|
* @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
|
// lazily purge old/unverified keys on every key upload
|
||||||
await this._purgeOldUnverified();
|
await this._purgeOldUnverified();
|
||||||
// parse key block
|
// parse key block
|
||||||
const key = await this._pgp.parseKey(publicKeyArmored);
|
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
|
// check for existing verified key with same id
|
||||||
const verified = await this.getVerified({keyId: key.keyId});
|
const verified = await this.getVerified({keyId: key.keyId});
|
||||||
if (verified) {
|
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
|
// send mails to verify user ids
|
||||||
await this._persisKey(key);
|
|
||||||
// send mails to verify user ids (send only one if primary email is provided)
|
|
||||||
await this._sendVerifyEmail(key, origin);
|
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.
|
* Delete all keys where no user id has been verified after x days.
|
||||||
* @yield {undefined}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
async _purgeOldUnverified() {
|
async _purgeOldUnverified() {
|
||||||
// create date in the past to compare with
|
// create date in the past to compare with
|
||||||
@ -97,17 +116,74 @@ class PublicKey {
|
|||||||
}, DB_TYPE);
|
}, 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.
|
* Persist the public key and its user ids in the database.
|
||||||
* @param {Object} key public key parameters
|
* @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
|
// delete old/unverified key
|
||||||
await this._mongo.remove({keyId: key.keyId}, DB_TYPE);
|
await this._mongo.remove({keyId: key.keyId}, DB_TYPE);
|
||||||
// generate nonces for verification
|
// generate nonces for verification
|
||||||
for (const uid of key.userIds) {
|
for (const userId of key.userIds) {
|
||||||
uid.nonce = util.random();
|
// remove status from user
|
||||||
|
delete userId.status;
|
||||||
|
// remove notify flag from user
|
||||||
|
delete userId.notify;
|
||||||
}
|
}
|
||||||
// persist new key
|
// persist new key
|
||||||
const r = await this._mongo.create(key, DB_TYPE);
|
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.
|
* Verify a user id by proving knowledge of the nonce.
|
||||||
* @param {string} keyId Correspronding public key id
|
* @param {string} keyId Correspronding public key id
|
||||||
* @param {string} nonce The verification nonce proving email address ownership
|
* @param {string} nonce The verification nonce proving email address ownership
|
||||||
* @yield {undefined}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
async verify({keyId, nonce}) {
|
async verify({keyId, nonce}) {
|
||||||
// look for verification nonce in database
|
// look for verification nonce in database
|
||||||
@ -144,13 +206,27 @@ class PublicKey {
|
|||||||
util.throw(404, 'User id not found');
|
util.throw(404, 'User id not found');
|
||||||
}
|
}
|
||||||
await this._removeKeysWithSameEmail(key, nonce);
|
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
|
// flag the user id as verified
|
||||||
await this._mongo.update(query, {
|
await this._mongo.update(query, {
|
||||||
|
publicKeyArmored,
|
||||||
'userIds.$.verified': true,
|
'userIds.$.verified': true,
|
||||||
'userIds.$.nonce': null
|
'userIds.$.nonce': null,
|
||||||
|
'userIds.$.publicKeyArmored': null
|
||||||
}, DB_TYPE);
|
}, 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) {
|
async _removeKeysWithSameEmail({keyId, userIds}, nonce) {
|
||||||
return this._mongo.remove({
|
return this._mongo.remove({
|
||||||
keyId: {$ne: keyId},
|
keyId: {$ne: keyId},
|
||||||
@ -165,7 +241,7 @@ class PublicKey {
|
|||||||
* @param {Array} userIds A list of user ids to check
|
* @param {Array} userIds A list of user ids to check
|
||||||
* @param {string} fingerprint The public key fingerprint
|
* @param {string} fingerprint The public key fingerprint
|
||||||
* @param {string} keyId (optional) The public key id
|
* @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}) {
|
async getVerified({userIds, fingerprint, keyId}) {
|
||||||
let queries = [];
|
let queries = [];
|
||||||
@ -203,7 +279,7 @@ class PublicKey {
|
|||||||
* @param {string} fingerprint (optional) The public key fingerprint
|
* @param {string} fingerprint (optional) The public key fingerprint
|
||||||
* @param {string} keyId (optional) The public key id
|
* @param {string} keyId (optional) The public key id
|
||||||
* @param {String} email (optional) The user's email address
|
* @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}) {
|
async get({fingerprint, keyId, email}) {
|
||||||
// look for verified key
|
// look for verified key
|
||||||
@ -230,7 +306,7 @@ class PublicKey {
|
|||||||
* @param {String} keyId (optional) The public key id
|
* @param {String} keyId (optional) The public key id
|
||||||
* @param {String} email (optional) The user's email address
|
* @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' }
|
* @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}) {
|
async requestRemove({keyId, email, origin}) {
|
||||||
// flag user ids for removal
|
// flag user ids for removal
|
||||||
@ -250,7 +326,7 @@ class PublicKey {
|
|||||||
* saving it. Either a key id or email address must be provided
|
* saving it. Either a key id or email address must be provided
|
||||||
* @param {String} keyId (optional) The public key id
|
* @param {String} keyId (optional) The public key id
|
||||||
* @param {String} email (optional) The user's email address
|
* @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) {
|
async _flagForRemove(keyId, email) {
|
||||||
const query = email ? {'userIds.email': email} : {keyId};
|
const query = email ? {'userIds.email': email} : {keyId};
|
||||||
@ -282,7 +358,7 @@ class PublicKey {
|
|||||||
* Also deletes all user id documents of that key id.
|
* Also deletes all user id documents of that key id.
|
||||||
* @param {string} keyId public key id
|
* @param {string} keyId public key id
|
||||||
* @param {string} nonce The verification nonce proving email address ownership
|
* @param {string} nonce The verification nonce proving email address ownership
|
||||||
* @yield {undefined}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
async verifyRemove({keyId, nonce}) {
|
async verifyRemove({keyId, nonce}) {
|
||||||
// check if key exists in database
|
// check if key exists in database
|
||||||
@ -290,8 +366,22 @@ class PublicKey {
|
|||||||
if (!flagged) {
|
if (!flagged) {
|
||||||
util.throw(404, 'User id not found');
|
util.throw(404, 'User id not found');
|
||||||
}
|
}
|
||||||
// delete the key
|
if (flagged.userIds.length === 1) {
|
||||||
await this._mongo.remove({keyId}, DB_TYPE);
|
// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -301,21 +301,21 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
|
|||||||
it('should return 200 for key id', done => {
|
it('should return 200 for key id', done => {
|
||||||
request(app.listen())
|
request(app.listen())
|
||||||
.get(`/pks/lookup?op=get&search=0x${emailParams.keyId}`)
|
.get(`/pks/lookup?op=get&search=0x${emailParams.keyId}`)
|
||||||
.expect(200, publicKeyArmored)
|
.expect(200)
|
||||||
.end(done);
|
.end(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 200 for fingerprint', done => {
|
it('should return 200 for fingerprint', done => {
|
||||||
request(app.listen())
|
request(app.listen())
|
||||||
.get(`/pks/lookup?op=get&search=0x${fingerprint}`)
|
.get(`/pks/lookup?op=get&search=0x${fingerprint}`)
|
||||||
.expect(200, publicKeyArmored)
|
.expect(200)
|
||||||
.end(done);
|
.end(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 200 for correct email address', done => {
|
it('should return 200 for correct email address', done => {
|
||||||
request(app.listen())
|
request(app.listen())
|
||||||
.get(`/pks/lookup?op=get&search=${primaryEmail}`)
|
.get(`/pks/lookup?op=get&search=${primaryEmail}`)
|
||||||
.expect(200, publicKeyArmored)
|
.expect(200)
|
||||||
.end(done);
|
.end(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -324,7 +324,7 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
|
|||||||
.get(`/pks/lookup?op=get&options=mr&search=${primaryEmail}`)
|
.get(`/pks/lookup?op=get&options=mr&search=${primaryEmail}`)
|
||||||
.expect('Content-Type', 'application/pgp-keys; charset=utf-8')
|
.expect('Content-Type', 'application/pgp-keys; charset=utf-8')
|
||||||
.expect('Content-Disposition', 'attachment; filename=openpgpkey.asc')
|
.expect('Content-Disposition', 'attachment; filename=openpgpkey.asc')
|
||||||
.expect(200, publicKeyArmored)
|
.expect(200)
|
||||||
.end(done);
|
.end(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ describe('Public Key Integration Tests', function() {
|
|||||||
email.init({
|
email.init({
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
auth: {user: 'user', pass: 'pass'},
|
auth: {user: 'user', pass: 'pass'},
|
||||||
sender: {name: 'Foo Bar', email: 'foo@bar.com'}
|
sender: {name: 'Foo Bar', emails: 'foo@bar.com'}
|
||||||
});
|
});
|
||||||
pgp = new PGP();
|
pgp = new PGP();
|
||||||
publicKey = new PublicKey(pgp, mongo, email);
|
publicKey = new PublicKey(pgp, mongo, email);
|
||||||
@ -77,22 +77,22 @@ describe('Public Key Integration Tests', function() {
|
|||||||
|
|
||||||
describe('put', () => {
|
describe('put', () => {
|
||||||
it('should persist key and send verification email', async () => {
|
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);
|
expect(mailsSent.length).to.equal(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work twice if not yet verified', async () => {
|
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);
|
expect(mailsSent.length).to.equal(4);
|
||||||
await publicKey.put({publicKeyArmored, origin});
|
await publicKey.put({emails: [], publicKeyArmored, origin});
|
||||||
expect(mailsSent.length).to.equal(8);
|
expect(mailsSent.length).to.equal(8);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw 304 if key already exists', async () => {
|
it.skip('should throw 304 if key already exists', async () => {
|
||||||
await publicKey.put({publicKeyArmored, origin});
|
await publicKey.put({emails: [], publicKeyArmored, origin});
|
||||||
await publicKey.verify(mailsSent[0].params);
|
await publicKey.verify(mailsSent[0].params);
|
||||||
try {
|
try {
|
||||||
await publicKey.put({publicKeyArmored, origin});
|
await publicKey.put({emails: [], publicKeyArmored, origin});
|
||||||
expect(false).to.be.true;
|
expect(false).to.be.true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e.status).to.equal(304);
|
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 () => {
|
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.verify(mailsSent[1].params);
|
||||||
await publicKey.put({publicKeyArmored: publicKeyArmored2, origin});
|
await publicKey.put({emails: [], publicKeyArmored: publicKeyArmored2, origin});
|
||||||
expect(mailsSent.length).to.equal(5);
|
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 () => {
|
it('should not remove a current unverified key', async () => {
|
||||||
await publicKey._persisKey(key);
|
await publicKey._persistKey(key);
|
||||||
const r = await publicKey._purgeOldUnverified();
|
const r = await publicKey._purgeOldUnverified();
|
||||||
expect(r.deletedCount).to.equal(0);
|
expect(r.deletedCount).to.equal(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not remove a current verified key', async () => {
|
it('should not remove a current verified key', async () => {
|
||||||
key.userIds[0].verified = true;
|
key.userIds[0].verified = true;
|
||||||
await publicKey._persisKey(key);
|
await publicKey._persistKey(key);
|
||||||
const r = await publicKey._purgeOldUnverified();
|
const r = await publicKey._purgeOldUnverified();
|
||||||
expect(r.deletedCount).to.equal(0);
|
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 () => {
|
it('should not remove an old verified key', async () => {
|
||||||
key.uploaded.setDate(key.uploaded.getDate() - 31);
|
key.uploaded.setDate(key.uploaded.getDate() - 31);
|
||||||
key.userIds[0].verified = true;
|
key.userIds[0].verified = true;
|
||||||
await publicKey._persisKey(key);
|
await publicKey._persistKey(key);
|
||||||
const r = await publicKey._purgeOldUnverified();
|
const r = await publicKey._purgeOldUnverified();
|
||||||
expect(r.deletedCount).to.equal(0);
|
expect(r.deletedCount).to.equal(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove an old unverified key', async () => {
|
it('should remove an old unverified key', async () => {
|
||||||
key.uploaded.setDate(key.uploaded.getDate() - 31);
|
key.uploaded.setDate(key.uploaded.getDate() - 31);
|
||||||
await publicKey._persisKey(key);
|
await publicKey._persistKey(key);
|
||||||
const r = await publicKey._purgeOldUnverified();
|
const r = await publicKey._purgeOldUnverified();
|
||||||
expect(r.deletedCount).to.equal(1);
|
expect(r.deletedCount).to.equal(1);
|
||||||
});
|
});
|
||||||
@ -150,7 +150,7 @@ describe('Public Key Integration Tests', function() {
|
|||||||
|
|
||||||
describe('verify', () => {
|
describe('verify', () => {
|
||||||
it('should update the document', async () => {
|
it('should update the document', async () => {
|
||||||
await publicKey.put({publicKeyArmored, origin});
|
await publicKey.put({emails: [], publicKeyArmored, origin});
|
||||||
const emailParams = mailsSent[0].params;
|
const emailParams = mailsSent[0].params;
|
||||||
await publicKey.verify(emailParams);
|
await publicKey.verify(emailParams);
|
||||||
const gotten = await mongo.get({keyId: emailParams.keyId}, DB_TYPE);
|
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 () => {
|
it('should not find the document', async () => {
|
||||||
await publicKey.put({publicKeyArmored, origin});
|
await publicKey.put({emails: [], publicKeyArmored, origin});
|
||||||
const emailParams = mailsSent[0].params;
|
const emailParams = mailsSent[0].params;
|
||||||
try {
|
try {
|
||||||
await publicKey.verify({keyId: emailParams.keyId, nonce: 'fake_nonce'});
|
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 () => {
|
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);
|
await publicKey.verify(mailsSent[1].params);
|
||||||
let firstKey = await publicKey.getVerified({keyId: mailsSent[1].params.keyId});
|
let firstKey = await publicKey.getVerified({keyId: mailsSent[1].params.keyId});
|
||||||
expect(firstKey).to.exist;
|
expect(firstKey).to.exist;
|
||||||
await publicKey.put({publicKeyArmored: publicKeyArmored2, origin});
|
await publicKey.put({emails: [], publicKeyArmored: publicKeyArmored2, origin});
|
||||||
await publicKey.verify(mailsSent[4].params);
|
await publicKey.verify(mailsSent[4].params);
|
||||||
firstKey = await publicKey.getVerified({keyId: mailsSent[1].params.keyId});
|
firstKey = await publicKey.getVerified({keyId: mailsSent[1].params.keyId});
|
||||||
expect(firstKey).to.not.exist;
|
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 () => {
|
it('should delete other keys with the same user id when verifying', async () => {
|
||||||
await publicKey.put({publicKeyArmored, origin});
|
await publicKey.put({emails: [], publicKeyArmored, origin});
|
||||||
await publicKey.put({publicKeyArmored: publicKeyArmored2, origin});
|
await publicKey.put({emails: [], publicKeyArmored: publicKeyArmored2, origin});
|
||||||
expect(mailsSent[1].to).to.equal(mailsSent[4].to);
|
expect(mailsSent[1].to).to.equal(mailsSent[4].to);
|
||||||
await publicKey.verify(mailsSent[1].params);
|
await publicKey.verify(mailsSent[1].params);
|
||||||
const firstKey = await publicKey.getVerified({keyId: mailsSent[1].params.keyId});
|
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 () => {
|
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);
|
expect(mailsSent.length).to.equal(4);
|
||||||
await publicKey.verify(mailsSent[0].params);
|
await publicKey.verify(mailsSent[0].params);
|
||||||
await publicKey.verify(mailsSent[1].params);
|
await publicKey.verify(mailsSent[1].params);
|
||||||
@ -221,7 +221,7 @@ describe('Public Key Integration Tests', function() {
|
|||||||
describe('should find a verified key', () => {
|
describe('should find a verified key', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
key = await pgp.parseKey(publicKeyArmored);
|
key = await pgp.parseKey(publicKeyArmored);
|
||||||
await publicKey.put({publicKeyArmored, origin});
|
await publicKey.put({emails: [], publicKeyArmored, origin});
|
||||||
await publicKey.verify(mailsSent[0].params);
|
await publicKey.verify(mailsSent[0].params);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -289,7 +289,7 @@ describe('Public Key Integration Tests', function() {
|
|||||||
let emailParams;
|
let emailParams;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await publicKey.put({publicKeyArmored, origin});
|
await publicKey.put({emails: [], publicKeyArmored, origin});
|
||||||
emailParams = mailsSent[0].params;
|
emailParams = mailsSent[0].params;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -345,7 +345,7 @@ describe('Public Key Integration Tests', function() {
|
|||||||
let keyId;
|
let keyId;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await publicKey.put({publicKeyArmored, origin});
|
await publicKey.put({emails: [], publicKeyArmored, origin});
|
||||||
keyId = mailsSent[0].params.keyId;
|
keyId = mailsSent[0].params.keyId;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -380,13 +380,63 @@ describe('Public Key Integration Tests', function() {
|
|||||||
let keyId;
|
let keyId;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await publicKey.put({publicKeyArmored, origin});
|
await publicKey.put({emails: [], publicKeyArmored, origin});
|
||||||
keyId = mailsSent[0].params.keyId;
|
keyId = mailsSent[0].params.keyId;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mailsSent = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove unverified user ID', async () => {
|
||||||
await publicKey.requestRemove({keyId, origin});
|
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 () => {
|
it('should remove key', async () => {
|
||||||
|
await publicKey.requestRemove({keyId, origin});
|
||||||
await publicKey.verifyRemove(mailsSent[4].params);
|
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);
|
const key = await mongo.get({keyId}, DB_TYPE);
|
||||||
expect(key).to.not.exist;
|
expect(key).to.not.exist;
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user