keyserver/src/service/public-key.js

287 lines
10 KiB
JavaScript
Raw Normal View History

/**
* Mailvelope - secure email with OpenPGP encryption for Webmail
* Copyright (C) 2016 Mailvelope GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License version 3
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
'use strict';
const util = require('./util');
const tpl = require('../email/templates.json');
2016-05-26 11:45:32 +00:00
/**
* Database documents have the format:
* {
* _id: ObjectId, // a randomly generated MongoDB document ID
* keyId: 'b8e4105cc9dedc77', // the 16 char key id in lowercase hex
* fingerprint: 'e3317db04d3958fd5f662c37b8e4105cc9dedc77', // the 40 char key fingerprint in lowercase hex
* userIds: [
* {
* name:'Jon Smith',
* email:'jon@smith.com',
* nonce: "6a314915c09368224b11df0feedbc53c", // random 32 char verifier used to prove ownership
* verified: true // if the user ID has been verified
* }
* ],
* created: Sat Oct 17 2015 12:17:03 GMT+0200 (CEST), // key creation time as JavaScript Date
* algorithm: 'rsa_encrypt_sign', // primary key alogrithm
* keySize: 4096, // key length in bits
* publicKeyArmored: '-----BEGIN PGP PUBLIC KEY BLOCK----- ... -----END PGP PUBLIC KEY BLOCK-----'
2016-05-26 11:45:32 +00:00
* }
*/
const DB_TYPE = 'publickey';
/**
2016-05-28 13:17:46 +00:00
* A service that handlers PGP public keys queries to the database
*/
class PublicKey {
/**
2016-05-28 13:17:46 +00:00
* Create an instance of the service
* @param {Object} pgp An instance of the OpenPGP.js wrapper
* @param {Object} mongo An instance of the MongoDB client
* @param {Object} email An instance of the Email Sender
*/
constructor(pgp, mongo, email) {
this._pgp = pgp;
this._mongo = mongo;
this._email = email;
}
/**
* Persist a new public key
* @param {String} publicKeyArmored The ascii armored pgp key block
* @param {String} primaryEmail (optional) The key's primary email address
* @param {Object} origin Required for links to the keyserver e.g. { protocol:'https', host:'openpgpkeys@example.com' }
* @yield {undefined}
*/
*put({ publicKeyArmored, primaryEmail, origin }) {
// parse key block
let key = this._pgp.parseKey(publicKeyArmored);
// check for existing verfied key by id or email addresses
let verified = yield this.getVerified(key);
if (verified) {
2016-06-01 10:28:37 +00:00
util.throw(304, 'Key for this user already exists');
}
2016-06-01 10:28:37 +00:00
// store key in database
yield this._persisKey(key);
2016-05-28 08:49:26 +00:00
// send mails to verify user ids (send only one if primary email is provided)
yield this._sendVerifyEmail(key, primaryEmail, origin);
2016-05-26 11:45:32 +00:00
}
2016-06-01 10:28:37 +00:00
/**
* Persist the public key and its user ids in the database.
* @param {Object} key public key parameters
* @yield {undefined} The persisted user id documents
2016-06-01 10:28:37 +00:00
*/
*_persisKey(key) {
// delete old/unverified key
yield this._mongo.remove({ keyId:key.keyId }, DB_TYPE);
// generate nonces for verification
for (let uid of key.userIds) {
uid.nonce = util.random();
}
2016-06-01 10:28:37 +00:00
// persist new key
let r = yield this._mongo.create(key, DB_TYPE);
2016-06-01 10:28:37 +00:00
if (r.insertedCount !== 1) {
util.throw(500, 'Failed to persist key');
}
}
/**
* 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)
2016-06-01 10:28:37 +00:00
* @yield {undefined}
*/
*_sendVerifyEmail(key, primaryEmail, origin) {
let userIds = key.userIds, keyId = key.keyId;
// check for primary email (send only one email)
2016-06-01 10:28:37 +00:00
let primaryUserId = userIds.find(uid => uid.email === primaryEmail);
if (primaryUserId) {
userIds = [primaryUserId];
}
// send emails
2016-06-01 10:28:37 +00:00
for (let userId of userIds) {
userId.publicKeyArmored = key.publicKeyArmored; // set key for encryption
yield this._email.send({ template:tpl.verifyKey, userId, keyId, origin });
2016-06-01 10:28:37 +00:00
}
}
/**
* 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}
*/
*verify({ keyId, nonce }) {
// look for verification nonce in database
let query = { keyId, 'userIds.nonce':nonce };
let key = yield this._mongo.get(query, DB_TYPE);
if (!key) {
util.throw(404, 'User id not found');
}
// check if user ids of this key have already been verified in another key
let verified = yield this.getVerified(key);
if (verified && verified.keyId !== keyId) {
util.throw(304, 'Key for this user already exists');
}
// flag the user id as verified
yield this._mongo.update(query, {
'userIds.$.verified': true,
'userIds.$.nonce': null
}, DB_TYPE);
2016-05-26 11:45:32 +00:00
}
/**
* Check if a verified key already exists either by fingerprint, 16 char key id,
* or email address. There can only be one verified user ID for an email address
* at any given time.
* @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
*/
*getVerified({ userIds, fingerprint, keyId }) {
let queries = [];
// query by fingerprint
if (fingerprint) {
queries.push({
fingerprint: fingerprint.toLowerCase(),
'userIds.verified': true
});
}
// query by key id (to prevent key id collision)
if (keyId) {
queries.push({
keyId: keyId.toLowerCase(),
'userIds.verified': true
});
}
// query by user id
if (userIds) {
queries = queries.concat(userIds.map(uid => ({
userIds: {
$elemMatch: {
'email': uid.email.toLowerCase(),
'verified': true
}
}
})));
}
return yield this._mongo.get({ $or:queries }, DB_TYPE);
}
/**
* Fetch a verified public key from the database. Either the key id or the
* email address muss be provided.
* @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
*/
*get({ fingerprint, keyId, email }) {
// look for verified key
let userIds = email ? [{ email:email }] : undefined;
let key = yield this.getVerified({ keyId, fingerprint, userIds });
if (!key) {
util.throw(404, 'Key not found');
}
// clean json return value (_id, nonce)
delete key._id;
key.userIds = key.userIds.map(uid => ({
name: uid.name,
email: uid.email,
verified: uid.verified
}));
return key;
}
/**
* Request removal of the public key by flagging all user ids and sending
* a verification email to the primary email address. Only one email
* needs to sent to a single user id to authenticate removal of all user ids
* that belong the a certain key id.
* @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}
*/
*requestRemove({ keyId, email, origin }) {
2016-06-10 17:58:26 +00:00
// flag user ids for removal
let key = yield this._flagForRemove(keyId, email);
if (!key) {
2016-06-01 10:28:37 +00:00
util.throw(404, 'User id not found');
}
2016-06-10 17:58:26 +00:00
// send verification mails
keyId = key.keyId; // get keyId in case request was by email
for (let userId of key.userIds) {
yield this._email.send({ template:tpl.verifyRemove, userId, keyId, origin });
}
}
/**
* Flag all user IDs of a key for removal by generating a new nonce and
* 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
*/
*_flagForRemove(keyId, email) {
let query = email ? { 'userIds.email':email } : { keyId };
let key = yield this._mongo.get(query, DB_TYPE);
if (!key) {
return;
}
2016-06-10 17:58:26 +00:00
// flag only the provided user id
if (email) {
let nonce = util.random();
yield this._mongo.update(query, { 'userIds.$.nonce':nonce }, DB_TYPE);
let uid = key.userIds.find(u => u.email === email);
uid.nonce = nonce;
return { userIds:[uid], keyId:key.keyId };
}
2016-06-10 17:58:26 +00:00
// flag all key user ids
if (keyId) {
for (let uid of key.userIds) {
let nonce = util.random();
yield this._mongo.update({ 'userIds.email':uid.email }, { 'userIds.$.nonce':nonce }, DB_TYPE);
uid.nonce = nonce;
}
return key;
}
2016-05-26 11:45:32 +00:00
}
/**
* Verify the removal of the user's key id by proving knowledge of the nonce.
* 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}
*/
*verifyRemove({ keyId, nonce }) {
2016-06-10 17:58:26 +00:00
// check if key exists in database
let flagged = yield this._mongo.get({ keyId, 'userIds.nonce':nonce }, DB_TYPE);
if (!flagged) {
util.throw(404, 'User id not found');
}
2016-06-10 17:58:26 +00:00
// delete the key
yield this._mongo.remove({ keyId }, DB_TYPE);
2016-05-26 11:45:32 +00:00
}
}
module.exports = PublicKey;