Compare commits
1 Commits
master
...
dependabot
Author | SHA1 | Date |
---|---|---|
|
8cbc4afc34 |
|
@ -41,9 +41,7 @@ module.exports = {
|
|||
},
|
||||
|
||||
publicKey: {
|
||||
purgeTimeInDays: process.env.PUBLIC_KEY_PURGE_TIME || 30,
|
||||
restrictUserOrigin: process.env.RESTRICT_USER_ORIGIN || false,
|
||||
restrictionRegEx: process.env.RESTRICTION_REGEX
|
||||
purgeTimeInDays: process.env.PUBLIC_KEY_PURGE_TIME || 30
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
|
||||
mongo: {
|
||||
uri: '127.0.0.1:27017/keyserver-test',
|
||||
user: 'keyserver-user',
|
||||
pass: 'trfepCpjhVrqgpXFWsEF'
|
||||
},
|
||||
|
||||
email: {
|
||||
host: 'smtp.gmail.com',
|
||||
port: 465,
|
||||
tls: true,
|
||||
starttls: true,
|
||||
pgp: true,
|
||||
auth: {
|
||||
user: 'user@gmail.com',
|
||||
pass: 'password'
|
||||
},
|
||||
sender: {
|
||||
name: 'OpenPGP Key Server',
|
||||
email: 'user@gmail.com'
|
||||
}
|
||||
},
|
||||
|
||||
publicKey: {
|
||||
restrictUserOrigin: true,
|
||||
restrictionRegEx: '^([a-z0-9\-.]+)@([a-z0-9.\-]*)esisar\.grenoble-inp\.fr$'
|
||||
}
|
||||
|
||||
};
|
|
@ -6,7 +6,5 @@
|
|||
"verify_success_link": "Your public OpenPGP key is now available at the following link:",
|
||||
"verify_removal_subject": "Verify key removal",
|
||||
"verify_removal_text": "Hello {0},\n\nplease verify removal of your email address {1} from our key server ({2}) by clicking on the following link:\n\n{3}\n\nGreetings from the Mailvelope Team",
|
||||
"removal_success": "Email address {0} removed from the key directory",
|
||||
"check_signatures_subject": "Confirm new signatures",
|
||||
"check_signatures_text": "Hello {0},\n\n{1} new signature(s) have been uploaded to your keys on our keyserver ({3}) ! Please select the ones you want to add by clicking on the following link:\n\n{2}\n\nGreetings from the Mailvelope Team"
|
||||
"removal_success": "Email address {0} removed from the key directory"
|
||||
}
|
||||
|
|
|
@ -719,12 +719,20 @@
|
|||
}
|
||||
},
|
||||
"eslint-utils": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.0.tgz",
|
||||
"integrity": "sha512-7ehnzPaP5IIEh1r1tkjuIrxqhNkzUJa9z3R92tLJdZIVdWaczEhr3EbhGtsMrVxi1KeR8qA7Off6SWc5WNQqyQ==",
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz",
|
||||
"integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"eslint-visitor-keys": "^1.0.0"
|
||||
"eslint-visitor-keys": "^1.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"eslint-visitor-keys": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz",
|
||||
"integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"eslint-visitor-keys": {
|
||||
|
|
|
@ -62,7 +62,7 @@ router.get('/manage.html', ctx => ctx.render('manage'));
|
|||
// HKP and REST api routes
|
||||
router.post('/pks/add', ctx => hkp.add(ctx));
|
||||
router.get('/pks/lookup', ctx => hkp.lookup(ctx));
|
||||
router.post('/api/v1/key', ctx => rest.postHandler(ctx));
|
||||
router.post('/api/v1/key', ctx => rest.create(ctx));
|
||||
router.get('/api/v1/key', ctx => rest.query(ctx));
|
||||
router.del('/api/v1/key', ctx => rest.remove(ctx));
|
||||
|
||||
|
|
|
@ -53,13 +53,12 @@ class Email {
|
|||
* @param {Object} template the email template function to use
|
||||
* @param {Object} userId recipient user id object: { name:'Jon Smith', email:'j@smith.com', publicKeyArmored:'...' }
|
||||
* @param {string} keyId key id of public key
|
||||
* @param {Object} data data used by template
|
||||
* @param {Object} origin origin of the server
|
||||
* @yield {Object} reponse object containing SMTP info
|
||||
*/
|
||||
async send({template, userId, keyId, data, origin, publicKeyArmored}) {
|
||||
async send({template, userId, keyId, origin, publicKeyArmored}) {
|
||||
const compiled = template({
|
||||
...data,
|
||||
...userId,
|
||||
origin,
|
||||
keyId
|
||||
});
|
||||
|
|
|
@ -18,12 +18,4 @@ function verifyRemove(ctx, {name, email, nonce, origin, keyId}) {
|
|||
};
|
||||
}
|
||||
|
||||
function checkNewSigs(ctx, {name, sigsNb, nonce, origin, keyId}) {
|
||||
const link = `${util.url(origin)}/api/v1/key?op=checkSignatures&keyId=${keyId}&nonce=${nonce}`;
|
||||
return {
|
||||
subject: ctx.__('check_signatures_subject'),
|
||||
text: ctx.__('check_signatures_text', [name, sigsNb, link, origin.host])
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {verifyKey, verifyRemove, checkNewSigs};
|
||||
module.exports = {verifyKey, verifyRemove};
|
||||
|
|
|
@ -24,7 +24,7 @@ const init = require('./app');
|
|||
(async () => {
|
||||
try {
|
||||
const app = await init();
|
||||
app.listen(config.server.port, "localhost");
|
||||
app.listen(config.server.port);
|
||||
log.info('app', `Listening on http://localhost:${config.server.port}`);
|
||||
} catch (err) {
|
||||
log.error('app', 'Initialization failed!', err);
|
||||
|
|
|
@ -136,9 +136,7 @@ class HKP {
|
|||
ctx.body = `info:${VERSION}:${COUNT}\npub:${fp}:${algo}:${key.keySize}:${created}::\n`;
|
||||
|
||||
for (const uid of key.userIds) {
|
||||
if(uid.verified) {
|
||||
ctx.body += `uid:${encodeURIComponent(`${uid.name} <${uid.email}>`)}:::\n`;
|
||||
}
|
||||
ctx.body += `uid:${encodeURIComponent(`${uid.name} <${uid.email}>`)}:::\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,26 +32,13 @@ class REST {
|
|||
constructor(publicKey) {
|
||||
this._publicKey = publicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* http POST handler
|
||||
* @param {Object} ctx The koa request/response context
|
||||
*/
|
||||
async postHandler(ctx) {
|
||||
const json = await parse.json(ctx, {limit: '1mb'});
|
||||
if(json.op === 'confirmSignatures')
|
||||
return this[json.op](ctx, json);//delegate operation
|
||||
|
||||
await this.create(ctx, json);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Public key / user ID upload via http POST
|
||||
* @param {Object} ctx The koa request/response context
|
||||
* @param {Object} json The json content of the request
|
||||
*/
|
||||
async create(ctx, json) {
|
||||
const {emails, publicKeyArmored} = json || await parse.json(ctx, {limit: '1mb'});
|
||||
async create(ctx) {
|
||||
const {emails, publicKeyArmored} = await parse.json(ctx, {limit: '1mb'});
|
||||
if (!publicKeyArmored) {
|
||||
ctx.throw(400, 'Invalid request!');
|
||||
}
|
||||
|
@ -67,8 +54,7 @@ class REST {
|
|||
*/
|
||||
async query(ctx) {
|
||||
const op = ctx.query.op;
|
||||
if (op === 'verify' || op === 'verifyRemove' || op === 'confirmSignatures' ||
|
||||
op === 'checkSignatures') {
|
||||
if (op === 'verify' || op === 'verifyRemove') {
|
||||
return this[op](ctx); // delegate operation
|
||||
}
|
||||
// do READ if no 'op' provided
|
||||
|
@ -88,43 +74,12 @@ class REST {
|
|||
if (!util.isKeyId(q.keyId) || !util.isString(q.nonce)) {
|
||||
ctx.throw(400, 'Invalid request!');
|
||||
}
|
||||
const {email} = await this._publicKey.verify(q, util.origin(ctx), ctx);
|
||||
const {email} = await this._publicKey.verify(q);
|
||||
// create link for sharing
|
||||
const link = util.url(util.origin(ctx), `/pks/lookup?op=get&search=${email}`);
|
||||
await ctx.render('verify-success', {email, link});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check public key's signatures via http GET
|
||||
* @param {Object} ctx The koa request/response context
|
||||
*/
|
||||
async checkSignatures(ctx) {
|
||||
const q = {keyId: ctx.query.keyId, nonce: ctx.query.nonce};
|
||||
if (!util.isKeyId(q.keyId) || !util.isString(q.nonce)) {
|
||||
ctx.throw(400, 'Invalid request!');
|
||||
}
|
||||
|
||||
const sigs = await this._publicKey.getPendingSignatures(q, ctx);
|
||||
// create link for confirmation
|
||||
const link = util.url(util.origin(ctx), `/api/v1/key`);
|
||||
await ctx.render('verify-certs', {keyId: q.keyId, link, nonce: q.nonce, sigs});
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm public key's signatures via http POST
|
||||
* @param {Object} ctx The koa request/response context
|
||||
* @param {Object} json The json content of the request
|
||||
*/
|
||||
async confirmSignatures(ctx, json) {
|
||||
const post = json || await parse.json(ctx, {limit: '1mb'});
|
||||
const q = {keyId: post.keyId, nonce: post.nonce, sigs: post.sig};
|
||||
const {email} = await this._publicKey.verifySignatures(q, util.origin(ctx), ctx);
|
||||
// create link for sharing
|
||||
const link = util.url(util.origin(ctx), `/pks/lookup?op=get&search=${email}`);
|
||||
ctx.body = `Update successful. You can find your key <a href="${link}" target="_blank">here</a>.`;
|
||||
ctx.status = 201;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Request public key removal via http DELETE
|
||||
* @param {Object} ctx The koa request/response context
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
|
||||
const log = require('winston');
|
||||
const util = require('./util');
|
||||
const config = require('config');
|
||||
const openpgp = require('openpgp');
|
||||
|
||||
const KEY_BEGIN = '-----BEGIN PGP PUBLIC KEY BLOCK-----';
|
||||
|
@ -68,14 +67,9 @@ class PGP {
|
|||
}
|
||||
|
||||
// check for at least one valid user id
|
||||
const {userIds, status} = await this.parseUserIds(key.users, primaryKey, verifyDate);
|
||||
const userIds = await this.parseUserIds(key.users, primaryKey, verifyDate);
|
||||
if (!userIds.length) {
|
||||
if (status == 1) {
|
||||
util.throw(400, 'Invalid PGP key: no user ID comes from a valid organisation');
|
||||
}
|
||||
else {
|
||||
util.throw(400, 'Invalid PGP key: invalid user IDs');
|
||||
}
|
||||
util.throw(400, 'Invalid PGP key: invalid user IDs');
|
||||
}
|
||||
|
||||
// get algorithm details from primary key
|
||||
|
@ -122,11 +116,10 @@ class PGP {
|
|||
|
||||
/**
|
||||
* Parse an array of user ids and verify signatures
|
||||
* @param {Array} users A list of openpgp.js user objects
|
||||
* @param {Array} users A list of openpgp.js user objects
|
||||
* @param {Object} primaryKey The primary key packet of the key
|
||||
* @param {Date} verifyDate Verify user IDs at this point in time
|
||||
* @return {Array, integer} An array of user id objects and a satus indicator
|
||||
* Values of status : 0 if no error, 1 if no address comes from a specific organisation.
|
||||
* @param {Date} verifyDate Verify user IDs at this point in time
|
||||
* @return {Array} An array of user id objects
|
||||
*/
|
||||
async parseUserIds(users, primaryKey, verifyDate = new Date()) {
|
||||
if (!users || !users.length) {
|
||||
|
@ -134,7 +127,6 @@ class PGP {
|
|||
}
|
||||
// at least one user id must be valid, revoked or expired
|
||||
const result = [];
|
||||
var isFromOrganisation = false;
|
||||
for (const user of users) {
|
||||
const userStatus = await user.verify(primaryKey, verifyDate);
|
||||
if (userStatus !== openpgp.enums.keyStatus.invalid && user.userId && user.userId.userid) {
|
||||
|
@ -148,18 +140,11 @@ class PGP {
|
|||
email: util.normalizeEmail(uid.email),
|
||||
verified: false
|
||||
});
|
||||
if(util.isFromOrganisation(util.normalizeEmail(uid.email)))
|
||||
isFromOrganisation = true;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
var status = 0;
|
||||
if(config.publicKey.restrictUserOrigin && !isFromOrganisation ){
|
||||
result.length = 0;
|
||||
status = 1;
|
||||
}
|
||||
return {userIds: result, status: status};
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -174,59 +159,12 @@ class PGP {
|
|||
key.users = key.users.filter(({userId}) => !userId || emails.includes(util.normalizeEmail(userId.email)));
|
||||
return key.armor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove signatures from source armored key which are not in compared armored key
|
||||
* @param {String} srcArmored armored key block to be filtered
|
||||
* @param {String} cmpArmored armored key block to be compared with
|
||||
* @return {String, newSigs} filtered {armored key block, list of new signatures}
|
||||
*/
|
||||
async filterKeyBySignatures(srcArmored, cmpArmored) {
|
||||
const {keys: [srcKey], err: srcErr} = await openpgp.key.readArmored(srcArmored);
|
||||
if (srcErr) {
|
||||
log.error('pgp', 'Failed to parse source PGP key:\n%s', srcArmored, srcErr);
|
||||
util.throw(500, 'Failed to parse PGP key');
|
||||
}
|
||||
const {keys: [cmpKey], err: cmpErr} = await openpgp.key.readArmored(cmpArmored);
|
||||
if (cmpErr) {
|
||||
log.error('pgp', 'Failed to parse destination PGP key:\n%s', cmpArmored, cmpErr);
|
||||
util.throw(500, 'Failed to parse PGP key');
|
||||
}
|
||||
|
||||
const newSigs=[];
|
||||
if(cmpKey.hasSameFingerprintAs(srcKey)) {
|
||||
await Promise.all(srcKey.users.map(async srcUser => {
|
||||
await Promise.all(cmpKey.users.map(async dstUser => {
|
||||
if ((srcUser.userId && dstUser.userId &&
|
||||
(srcUser.userId.userid === dstUser.userId.userid)) ||
|
||||
(srcUser.userAttribute && (srcUser.userAttribute.equals(dstUser.userAttribute)))) {
|
||||
const source = srcUser.otherCertifications;
|
||||
const dest = dstUser.otherCertifications;
|
||||
for(let i = source.length-1; i >= 0; i--) {
|
||||
const sourceSig = source[i];
|
||||
if (!sourceSig.isExpired() && !dest.some(function(destSig) {
|
||||
return util.equalsUint8Array(destSig.signature, sourceSig.signature);
|
||||
})) {
|
||||
// list new signatures
|
||||
let userId = (srcUser.userId) ? srcUser.userId.userid : null;
|
||||
let userAttribute = (srcUser.userAttribute) ? srcUser.userAttribute : null;
|
||||
newSigs.push({user: {userId: userId, userAttribute: userAttribute}, signature: Buffer.from(sourceSig.write()).toString('base64')});
|
||||
// do not add new signatures
|
||||
source.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
}));
|
||||
}
|
||||
return {armored: srcKey.armor(), newSigs: newSigs};
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge (update) armored key blocks without adding new signatures
|
||||
* Merge (update) armored key blocks
|
||||
* @param {String} srcArmored source amored key block
|
||||
* @param {String} dstArmored destination armored key block
|
||||
* @return {String} merged amored key block
|
||||
* @return {String} merged armored key block
|
||||
*/
|
||||
async updateKey(srcArmored, dstArmored) {
|
||||
const {keys: [srcKey], err: srcErr} = await openpgp.key.readArmored(srcArmored);
|
||||
|
@ -238,63 +176,11 @@ class PGP {
|
|||
if (dstErr) {
|
||||
log.error('pgp', 'Failed to parse destination PGP key for update:\n%s', dstArmored, dstErr);
|
||||
util.throw(500, 'Failed to parse PGP key');
|
||||
}
|
||||
}
|
||||
await dstKey.update(srcKey);
|
||||
return dstKey.armor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add new signature to key
|
||||
* @param {String} publicKeyArmored source amored key block
|
||||
* @param {Object} signature signature to add
|
||||
* @return {String} updated armored key block
|
||||
*/
|
||||
async addSignature(publicKeyArmored, {user, signature}) {
|
||||
const {keys: [key], err: srcErr} = await openpgp.key.readArmored(publicKeyArmored);
|
||||
const signaturePacket = await this.getSignatureFromBase64(signature);
|
||||
if (srcErr) {
|
||||
log.error('pgp', 'Failed to parse source PGP key for update:\n%s', publicKeyArmored, srcErr);
|
||||
util.throw(500, 'Failed to parse PGP key');
|
||||
}
|
||||
for(const srcUser of key.users) {
|
||||
if((srcUser.userId && user.userId === srcUser.userId.userid) ||
|
||||
(user.userAttribute && user.userAttribute === srcUser.userAttribute)) {
|
||||
if(!srcUser.otherCertifications.some(certSig => util.equalsUint8Array(certSig.signature, signaturePacket.signature))) {
|
||||
srcUser.otherCertifications.push(signaturePacket);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return key.armor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get openpgp.packet.Signature object from base64 encoded signature
|
||||
* @param {String} signature base64 encoded signature
|
||||
* @return {openpgp.packet.Signature} Signature object
|
||||
*/
|
||||
async getSignatureFromBase64(signature) {
|
||||
const signaturePacket = new openpgp.packet.Signature();
|
||||
signaturePacket.read(new Uint8Array(Buffer.from(signature, 'base64')));
|
||||
return signaturePacket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns primary user and most significant (latest valid) self signature
|
||||
* - if multiple primary users exist, returns the one with the latest self signature
|
||||
* - otherwise, returns the user with the latest self signature
|
||||
* @return {Object} The primary userId
|
||||
*/
|
||||
async getPrimaryUser(publicKeyArmored) {
|
||||
const {keys: [key], err: srcErr} = await openpgp.key.readArmored(publicKeyArmored);
|
||||
if (srcErr) {
|
||||
log.error('pgp', 'Failed to parse PGP key for getPrimaryUser:\n%s', publicKeyArmored, srcErr);
|
||||
util.throw(500, 'Failed to parse PGP key');
|
||||
}
|
||||
const primaryUser = await key.getPrimaryUser();
|
||||
return primaryUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove user ID from armored key block
|
||||
* @param {String} email email of user ID to be removed
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
const config = require('config');
|
||||
const util = require('./util');
|
||||
const tpl = require('../email/templates');
|
||||
const crypto = require('crypto');
|
||||
|
||||
/**
|
||||
* Database documents have the format:
|
||||
|
@ -89,30 +88,9 @@ class PublicKey {
|
|||
if (verified) {
|
||||
key.userIds = await this._mergeUsers(verified.userIds, key.userIds, key.publicKeyArmored);
|
||||
// reduce new key to verified user IDs
|
||||
let filteredPublicKeyArmored = await this._pgp.filterKeyByUserIds(key.userIds.filter(({verified}) => verified), key.publicKeyArmored);
|
||||
// reduce new key to verified signatures and get new signatures
|
||||
const {armored, newSigs} = await this._pgp.filterKeyBySignatures(filteredPublicKeyArmored, verified.publicKeyArmored);
|
||||
filteredPublicKeyArmored = armored;
|
||||
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);
|
||||
// store pending signatures in key and generate nounce for confirmation
|
||||
if(newSigs.length) {
|
||||
if(!verified.pendingSignatures)
|
||||
key.pendingSignatures = {sigs: newSigs, nonce: util.random()};
|
||||
else {
|
||||
key.pendingSignatures = verified.pendingSignatures;
|
||||
key.pendingSignatures.sigs = verified.pendingSignatures.sigs.concat(newSigs.filter(sourceSig => !verified.pendingSignatures.sigs.some(function(pendingSig) {
|
||||
return pendingSig.signature === sourceSig.signature;
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
// send mails to verify all user ids
|
||||
await this._sendVerifyEmail(key, origin, ctx);
|
||||
// send mail to confirm all new signatures
|
||||
await this._sendNewSigsEmail(key, origin, ctx);
|
||||
// store key in database
|
||||
await this._persistKey(key);
|
||||
} else {
|
||||
key.userIds = key.userIds.filter(userId => userId.status === KEY_STATUS_VALID);
|
||||
if (!key.userIds.length) {
|
||||
|
@ -121,19 +99,11 @@ class PublicKey {
|
|||
await this._addKeyArmored(key.userIds, key.publicKeyArmored);
|
||||
// new key, set armored to null
|
||||
key.publicKeyArmored = null;
|
||||
if(config.publicKey.restrictUserOrigin) {
|
||||
// send mails to verify organisation's user ids
|
||||
await this._sendVerifyOrganisationEmail(key, origin, ctx);
|
||||
// store key in database
|
||||
await this._persistKeyOrganisation(key);
|
||||
}
|
||||
else {
|
||||
// send mails to verify all user ids
|
||||
await this._sendVerifyEmail(key, origin, ctx);
|
||||
// store key in database
|
||||
await this._persistKey(key);
|
||||
}
|
||||
}
|
||||
// send mails to verify user ids
|
||||
await this._sendVerifyEmail(key, origin, ctx);
|
||||
// store key in database
|
||||
await this._persistKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -187,7 +157,7 @@ class PublicKey {
|
|||
_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.
|
||||
|
@ -201,54 +171,20 @@ class PublicKey {
|
|||
if (userId.notify && userId.notify === true) {
|
||||
// generate nonce for verification
|
||||
userId.nonce = util.random();
|
||||
await this._email.send({template: tpl.verifyKey.bind(null, ctx), userId, keyId, data: userId, origin, publicKeyArmored: userId.publicKeyArmored});
|
||||
await this._email.send({template: tpl.verifyKey.bind(null, ctx), userId, keyId, origin, publicKeyArmored: userId.publicKeyArmored});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send verification emails to the organisation's 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)
|
||||
* @param {Object} ctx Context
|
||||
* @return {Promise}
|
||||
*/
|
||||
async _sendVerifyOrganisationEmail({userIds, keyId}, origin, ctx) {
|
||||
for (const userId of userIds) {
|
||||
if (userId.notify && userId.notify === true && util.isFromOrganisation(userId.email)) {
|
||||
// generate nonce for verification
|
||||
userId.nonce = util.random();
|
||||
await this._email.send({template: tpl.verifyKey.bind(null, ctx), userId, keyId, data: userId, origin, publicKeyArmored: userId.publicKeyArmored});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send email to the public key's primary user ids for confirmation
|
||||
* of new signatures addition.
|
||||
* @param {Object} key key documents containg all the needed data
|
||||
* @param {Object} origin the server's origin (required for email links)
|
||||
* @param {Object} ctx Context
|
||||
* @return {Promise}
|
||||
*/
|
||||
async _sendNewSigsEmail(key, origin, ctx) {
|
||||
if(key.pendingSignatures && key.pendingSignatures.sigs.length){
|
||||
let primaryUser = await this._pgp.getPrimaryUser(key.publicKeyArmored);
|
||||
const userId = primaryUser.user.userId;
|
||||
await this._email.send({template: tpl.checkNewSigs.bind(null, ctx), userId, keyId: key.keyId, data: {name: userId.name, sigsNb: key.pendingSignatures.sigs.length, nonce: key.pendingSignatures.nonce}, origin, publicKeyArmored: key.publicKeyArmored});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the public key and its user ids in the database.
|
||||
* @param {Object} key public key parameters
|
||||
* @param {Object} key public key parameters
|
||||
* @return {Promise}
|
||||
*/
|
||||
async _persistKey(key) {
|
||||
// delete old/unverified key
|
||||
await this._mongo.remove({keyId: key.keyId}, DB_TYPE);
|
||||
|
||||
// generate nonces for verification
|
||||
for (const userId of key.userIds) {
|
||||
// remove status from user
|
||||
delete userId.status;
|
||||
|
@ -261,61 +197,26 @@ class PublicKey {
|
|||
util.throw(500, 'Failed to persist key');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the public key and its user ids in the database.
|
||||
* Mark all uids as unprocessed, except the ones with the organisation email.
|
||||
* @param {Object} key public key parameters
|
||||
* @return {Promise}
|
||||
*/
|
||||
async _persistKeyOrganisation(key) {
|
||||
// delete old/unverified key
|
||||
await this._mongo.remove({keyId: key.keyId}, DB_TYPE);
|
||||
|
||||
for (const userId of key.userIds) {
|
||||
if(util.isFromOrganisation(userId.email))
|
||||
{
|
||||
// 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);
|
||||
if (r.insertedCount !== 1) {
|
||||
util.throw(500, 'Failed to persist key');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param {Object} origin the server's origin (required for email links)
|
||||
* @param {Object} ctx Context
|
||||
* @return {Promise} The email that has been verified
|
||||
*/
|
||||
async verify({keyId, nonce}, origin, ctx) {
|
||||
async verify({keyId, nonce}) {
|
||||
// look for verification nonce in database
|
||||
const query = {keyId, 'userIds.nonce': nonce};
|
||||
const key = await this._mongo.get(query, DB_TYPE);
|
||||
if (!key) {
|
||||
util.throw(404, 'User ID not found');
|
||||
}
|
||||
|
||||
// send mails to verify all unnotified user ids
|
||||
await this._sendVerifyEmail(key, origin, ctx);
|
||||
// store key in database
|
||||
await this._persistKey(key);
|
||||
|
||||
await this._removeKeysWithSameEmail(key, nonce);
|
||||
let {publicKeyArmored, email} = 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,
|
||||
|
@ -325,46 +226,6 @@ class PublicKey {
|
|||
}, DB_TYPE);
|
||||
return {email};
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify signatures by proving knowledge of the nonce.
|
||||
* @param {string} keyId Correspronding public key id
|
||||
* @param {string} nonce The verification nonce proving email address ownership
|
||||
* @param {Array} sigs The list of signatures to verify
|
||||
* @param {Object} origin The server's origin (required for email links)
|
||||
* @param {Object} ctx Context
|
||||
* @return {Promise} The email that has been verified
|
||||
*/
|
||||
async verifySignatures({keyId, nonce, sigs}, origin, ctx) {
|
||||
// look for verification nonce in database
|
||||
const query = {keyId, 'pendingSignatures.nonce': nonce};
|
||||
const key = await this._mongo.get(query, DB_TYPE);
|
||||
if (!key) {
|
||||
util.throw(404, 'Signatures not found on key');
|
||||
}
|
||||
|
||||
let publicKeyArmored = key.publicKeyArmored;
|
||||
|
||||
for(const {user, signature} of key.pendingSignatures.sigs) {
|
||||
// update armored key
|
||||
let hash = crypto.createHash('md5');
|
||||
hash.update(signature, 'base64');
|
||||
hash = hash.digest('hex');
|
||||
if(sigs.includes(hash)) {
|
||||
publicKeyArmored = await this._pgp.addSignature(key.publicKeyArmored, {user, signature});
|
||||
publicKeyArmored = await this._pgp.updateKey(key.publicKeyArmored, publicKeyArmored);
|
||||
}
|
||||
}
|
||||
|
||||
key.pendingSignatures = null;
|
||||
|
||||
await this._mongo.update(query, {
|
||||
publicKeyArmored,
|
||||
'pendingSignatures': null
|
||||
}, DB_TYPE);
|
||||
const email = (await this._pgp.getPrimaryUser(publicKeyArmored)).user.userId.email;
|
||||
return {email};
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes keys with the same email address
|
||||
|
@ -442,62 +303,9 @@ class PublicKey {
|
|||
email: uid.email,
|
||||
verified: uid.verified
|
||||
}));
|
||||
if(key.pendingSignatures)
|
||||
delete key.pendingSignatures.nonce
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all pending signatures of a public key from the database. Either the
|
||||
* key fingerprint, id or the email address muss be provided.
|
||||
* @param {string} keyId Correspronding public key id
|
||||
* @param {string} nonce The verification nonce proving legitimity of the request
|
||||
* @param {Object} ctx Context
|
||||
* @return {Map} The list of userId and associated signatures
|
||||
*/
|
||||
async getPendingSignatures({fingerprint, keyId, email, nonce}, ctx) {
|
||||
// look for verified key
|
||||
const userIds = email ? [{email}] : undefined;
|
||||
const key = await this.getVerified({keyId, fingerprint, userIds});
|
||||
if (!key) {
|
||||
util.throw(404, ctx.__('key_not_found'));
|
||||
}
|
||||
if(!key.pendingSignatures)
|
||||
util.throw(404, "No pending signatures");
|
||||
if(key.pendingSignatures.nonce != nonce)
|
||||
util.throw(403, "Invalid nonce");
|
||||
|
||||
const signatures = new Map();
|
||||
|
||||
for(const {user, signature} of key.pendingSignatures.sigs) {
|
||||
const signedUserID = user.userId;
|
||||
|
||||
let hash = crypto.createHash('md5');
|
||||
hash.update(signature, 'base64');
|
||||
hash = hash.digest('hex')
|
||||
|
||||
const signaturePacket = await this._pgp.getSignatureFromBase64(signature);
|
||||
|
||||
const fingerprint = Buffer.from(signaturePacket.issuerFingerprint).toString('HEX');
|
||||
|
||||
const verified = await this.getVerified({fingerprint: fingerprint});
|
||||
|
||||
const issuerUID = (verified)? await this._pgp.getPrimaryUser(verified.publicKeyArmored): "[unknown identity]";
|
||||
|
||||
const sig = {issuerFingerprint: fingerprint,
|
||||
created: signaturePacket.created.toDateString(),
|
||||
userId: issuerUID,
|
||||
hash: hash
|
||||
};
|
||||
if(!signatures.has(signedUserID)) {
|
||||
signatures.set(signedUserID, []);
|
||||
}
|
||||
signatures.get(signedUserID).push(sig);
|
||||
}
|
||||
|
||||
return signatures;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request removal of the public key by flagging all user ids and sending
|
||||
* a verification email to the primary email address. Only one email
|
||||
|
@ -518,7 +326,7 @@ class PublicKey {
|
|||
// send verification mails
|
||||
keyId = key.keyId; // get keyId in case request was by email
|
||||
for (const userId of key.userIds) {
|
||||
await this._email.send({template: tpl.verifyRemove.bind(null, ctx), userId, keyId, data: userId, origin});
|
||||
await this._email.send({template: tpl.verifyRemove.bind(null, ctx), userId, keyId, origin});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
'use strict';
|
||||
|
||||
const crypto = require('crypto');
|
||||
const config = require('config');
|
||||
|
||||
/**
|
||||
* Checks for a valid string
|
||||
|
@ -79,19 +78,6 @@ exports.isEmail = function(data) {
|
|||
return re.test(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks for a valid specific organisation email address.
|
||||
* @param {string} data The email address
|
||||
* @return {boolean} Wether the email address comes from organisation
|
||||
*/
|
||||
exports.isFromOrganisation = function(data) {
|
||||
if (!this.isString(data)) {
|
||||
return false;
|
||||
}
|
||||
const re = new RegExp(config.publicKey.restrictionRegEx, 'g');
|
||||
return re.test(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalize email address to lowercase.
|
||||
* @param {string} email The email address
|
||||
|
@ -104,27 +90,6 @@ exports.normalizeEmail = function(email) {
|
|||
return email;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check Uint8Array equality
|
||||
* @param {Uint8Array} first array
|
||||
* @param {Uint8Array} second array
|
||||
* @returns {Boolean} equality
|
||||
*/
|
||||
exports.equalsUint8Array = function (array1, array2) {
|
||||
try {
|
||||
if (array1.length !== array2.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < array1.length; i++) {
|
||||
if (array1[i] !== array2[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (e) {return false;}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create an error with a custom status attribute e.g. for http codes.
|
||||
* @param {number} status The error's http status code
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,35 +0,0 @@
|
|||
/* eslint-disable */
|
||||
|
||||
;(function($) {
|
||||
'use strict';
|
||||
|
||||
$('#copy-button').tooltip({
|
||||
trigger: 'manual',
|
||||
placement: 'right',
|
||||
});
|
||||
|
||||
function setTooltip(message) {
|
||||
$('#copy-button').attr('data-original-title', message)
|
||||
.tooltip('show');
|
||||
}
|
||||
|
||||
function hideTooltip() {
|
||||
setTimeout(function() {
|
||||
$('#copy-button').tooltip('hide');
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
$('#copy-button').click(function(e) {
|
||||
const copyText = $('#publickey-block').text();
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.textContent = copyText;
|
||||
document.body.append(textArea);
|
||||
textArea.select();
|
||||
document.execCommand("copy");
|
||||
textArea.remove();
|
||||
|
||||
setTooltip('Key copied to clipboard!');
|
||||
hideTooltip();
|
||||
});
|
||||
|
||||
}(jQuery));
|
|
@ -49,54 +49,5 @@
|
|||
$('#' + region + ' .alert-' + outcome + ' span').text(text);
|
||||
$('#' + region + ' .alert-' + outcome).removeClass('hidden');
|
||||
}
|
||||
|
||||
$('#drop_zone').on('drop',
|
||||
function(ev) {
|
||||
// Prevent default behavior (Prevent file from being opened)
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
$('#addKey .alert').addClass('hidden');
|
||||
if(ev.originalEvent.dataTransfer.files[0].type != "text/plain") {
|
||||
alert('addKey', 'danger', 'You must import an ascii-armored key file!');
|
||||
return;
|
||||
}
|
||||
handleFiles(ev.originalEvent.dataTransfer.files);
|
||||
});
|
||||
$('#drop_zone').on('dragover',
|
||||
function(ev) {
|
||||
// Prevent default behavior (Prevent file from being opened)
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
ev.originalEvent.dataTransfer.dropEffect = 'copy';
|
||||
});
|
||||
$('#drop_zone').on('dragenter',
|
||||
function(ev) {
|
||||
// Prevent default behavior (Prevent file from being opened)
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
ev.originalEvent.dataTransfer.dropEffect = 'copy';
|
||||
});
|
||||
|
||||
$('#fileSelect').click(function() {
|
||||
$('#file-selector').click();
|
||||
});
|
||||
|
||||
$('#file-selector').change(function() {
|
||||
$('#addKey .alert').addClass('hidden');
|
||||
handleFiles(this.files);
|
||||
});
|
||||
|
||||
function handleFiles(files) {
|
||||
if(files.length > 1) {
|
||||
alert('addKey', 'danger', 'You must import a single file!');
|
||||
return;
|
||||
}
|
||||
const file = files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(){
|
||||
$('#addKey textarea').val(reader.result);
|
||||
}
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
}(jQuery));
|
||||
}(jQuery));
|
|
@ -1,47 +0,0 @@
|
|||
/* eslint-disable */
|
||||
|
||||
;(function($) {
|
||||
'use strict';
|
||||
|
||||
// POST signatures form
|
||||
$('#signatures form').submit(function(e) {
|
||||
e.preventDefault();
|
||||
$('#signatures .alert').addClass('hidden');
|
||||
var elements = $('#signatures form')[0];
|
||||
var obj = {sig: []};
|
||||
for(var elem of elements){
|
||||
switch(elem.name) {
|
||||
case "op":
|
||||
case "keyId":
|
||||
case "nonce":
|
||||
obj[elem.name] = elem.value;
|
||||
break;
|
||||
case "sig":
|
||||
if(elem.checked)
|
||||
obj["sig"].push(elem.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
$.ajax({
|
||||
method: 'POST',
|
||||
url: '/api/v1/key',
|
||||
data: JSON.stringify(obj),
|
||||
contentType: 'application/json',
|
||||
}).done(function(data, textStatus, xhr) {
|
||||
if (xhr.status === 304) {
|
||||
alert('signatures', 'danger', 'Key already exists!');
|
||||
} else {
|
||||
alert('signatures', 'success', xhr.responseText);
|
||||
}
|
||||
})
|
||||
.fail(function(xhr) {
|
||||
alert('signatures', 'danger', xhr.responseText);
|
||||
});
|
||||
});
|
||||
|
||||
function alert(region, outcome, text) {
|
||||
$('#' + region + ' .alert-' + outcome + ' span').html(text);
|
||||
$('#' + region + ' .alert-' + outcome).removeClass('hidden');
|
||||
}
|
||||
|
||||
}(jQuery));
|
|
@ -1,13 +1,3 @@
|
|||
<style type="text/css">
|
||||
|
||||
textarea {
|
||||
position: absolute;
|
||||
left: -100%;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<div class="header clearfix">
|
||||
|
@ -24,14 +14,10 @@
|
|||
<div class="row marketing">
|
||||
<div class="col-lg-12">
|
||||
<h4><%= query.email ? `Email: ${query.email}` : query.fingerprint ? `Fingerprint: ${query.fingerprint}` : `Key ID: ${query.keyId}` %></h4>
|
||||
<button id="copy-button" class="btn btn-primary">Copy key to clipboard</button>
|
||||
<pre id="publickey-block"><%= key.publicKeyArmored %></pre>
|
||||
<pre><%= key.publicKeyArmored %></pre>
|
||||
</div> <!-- /col-lg-12 -->
|
||||
</div>
|
||||
|
||||
<%- include('footer') %>
|
||||
|
||||
</div> <!-- /container -->
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/bootstrap-3.4.1.min.js"></script>
|
||||
<script src="/js/key-armored.js"></script>
|
||||
|
|
|
@ -24,9 +24,7 @@
|
|||
<div class="progress-bar progress-bar-striped active" role="progressbar"></div>
|
||||
</div>
|
||||
<form action="/pks/add" method="post">
|
||||
<p><textarea id="drop_zone" class="form-control ui-droppable" name="keytext" rows="5" spellcheck="false" placeholder="Drag a single ascii-armored key file here or paste PGP PUBLIC KEY BLOCK here ..." required></textarea></p>
|
||||
<input type="file" id="file-selector" accept="text/plain" style="display:none">
|
||||
<p>Or <button type="button" id="fileSelect">select a file</button> </p>
|
||||
<p><textarea class="form-control" name="keytext" rows="5" spellcheck="false" placeholder="Paste PGP PUBLIC KEY BLOCK here ..." required></textarea></p>
|
||||
<input class="btn btn-primary btn-lg" type="submit" value="Upload">
|
||||
</form>
|
||||
<hr>
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
<style type="text/css">
|
||||
|
||||
.uid { color: green; text-decoration: underline; }
|
||||
.warn { color: red; font-weight: bold; }
|
||||
|
||||
</style>
|
||||
<div class="container">
|
||||
|
||||
<div class="header clearfix">
|
||||
<nav>
|
||||
<ul class="nav nav-pills pull-right">
|
||||
<li role="presentation"><a href="/">Home</a></li>
|
||||
<li role="presentation"><a href="/manage.html">Manage Keys</a></li>
|
||||
<li role="presentation"><a href="https://github.com/mailvelope/keyserver" target="_blank">GitHub</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<h3 class="text-muted">Mailvelope Key Server</h3>
|
||||
</div>
|
||||
|
||||
<div class="row marketing">
|
||||
<div id="signatures" class="col-lg-12">
|
||||
<h2>Select the signatures you want to add to your key</h2>
|
||||
<div class="alert alert-success hidden" role="alert">
|
||||
<strong>Success!</strong> <span></span>
|
||||
</div>
|
||||
<div class="alert alert-danger hidden" role="alert">
|
||||
<strong>Error!</strong> <span></span>
|
||||
</div>
|
||||
<form id="sigForm" action="/api/v1/key" method="post"> <!-- <%= link %> -->
|
||||
<hr /><pre><strong>pub</strong> <a href="/pks/lookup?op=get&search=0x<%= keyId %>">0x<%= keyId %></a><input type="hidden" id="op" name="op" value="confirmSignatures"><input type="hidden" id="keyId" name="keyId" value="<%= keyId %>"><input type="hidden" id="nonce" name="nonce" value="<%= nonce %>">
|
||||
<% for(const [userid, signatures] of sigs) { %>
|
||||
<strong>uid</strong> <span class="uid"><%= userid %></span>
|
||||
<% for(sig of signatures) { %>
|
||||
 <input type="checkbox" id="<%= sig.hash %>" name="sig" value="<%= sig.hash %>"> <label for="<%= sig.hash %>"> <a href="/pks/lookup?op=get&search=0x<%= sig.issuerFingerprint %>">0x<%= sig.issuerFingerprint %></a> <%= sig.created %> <%= sig.userId %></label><br>
|
||||
<% } %>
|
||||
<% } %>
|
||||
</pre>
|
||||
<input class="btn btn-primary btn-lg" type="submit" value="Confirm selected signatures (and delete unselected ones)">
|
||||
</form>
|
||||
</div> <!-- /col-lg-12 -->
|
||||
</div> <!-- /row marketing -->
|
||||
|
||||
<%- include('footer') %>
|
||||
|
||||
</div> <!-- /container -->
|
||||
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/verify-certs.js"></script>
|
11
start-dev.sh
11
start-dev.sh
|
@ -1,11 +0,0 @@
|
|||
#:/bin/bash
|
||||
|
||||
systemctl start mongod
|
||||
systemctl status mongod
|
||||
|
||||
(echo 'use keyserver-test';echo 'db.createUser({ user:"keyserver-user", pwd:"trfepCpjhVrqgpXFWsEF", roles:[{ role:"readWrite",db:"keyserver-test" }] })') | mongo
|
||||
|
||||
|
||||
export NODE_ENV=development
|
||||
|
||||
npm start
|
|
@ -1,4 +0,0 @@
|
|||
#:/bin/bash
|
||||
|
||||
service mongod stop
|
||||
systemctl stop mongod
|
Loading…
Reference in New Issue