Restrict addition of signatures
Sending an email to the primary userID to select which signatures to add when new signatures are uploaded.esisar-restrictions
parent
d0083a4f57
commit
5a05cecdb8
|
@ -6,5 +6,7 @@
|
|||
"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"
|
||||
"removal_success": "Email address {0} removed from the key directory",
|
||||
"confirm_signatures_subject": "Confirm new signatures",
|
||||
"confirm_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"
|
||||
}
|
||||
|
|
|
@ -53,12 +53,13 @@ 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, origin, publicKeyArmored}) {
|
||||
async send({template, userId, keyId, data, origin, publicKeyArmored}) {
|
||||
const compiled = template({
|
||||
...userId,
|
||||
...data,
|
||||
origin,
|
||||
keyId
|
||||
});
|
||||
|
|
|
@ -18,4 +18,12 @@ function verifyRemove(ctx, {name, email, nonce, origin, keyId}) {
|
|||
};
|
||||
}
|
||||
|
||||
module.exports = {verifyKey, verifyRemove};
|
||||
function confirmNewSigs(ctx, {name, sigsNb, nonce, origin, keyId}) {
|
||||
const link = `${util.url(origin)}/api/v1/key?op=confirmSignatures&keyId=${keyId}&nonce=${nonce}`;
|
||||
return {
|
||||
subject: ctx.__('confirm_signatures_subject'),
|
||||
text: ctx.__('confirm_signatures_text', [name, sigsNb, link, origin.host])
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {verifyKey, verifyRemove, confirmNewSigs};
|
||||
|
|
|
@ -124,7 +124,7 @@ class PGP {
|
|||
* @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.
|
||||
* @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.
|
||||
*/
|
||||
async parseUserIds(users, primaryKey, verifyDate = new Date()) {
|
||||
|
@ -175,10 +175,10 @@ class PGP {
|
|||
}
|
||||
|
||||
/**
|
||||
* Merge (update) armored key blocks
|
||||
* Merge (update) armored key blocks without adding new signatures
|
||||
* @param {String} srcArmored source amored key block
|
||||
* @param {String} dstArmored destination armored key block
|
||||
* @return {String} merged armored key block
|
||||
* @return {String, newSigs} merged armored key block, list of new signatures
|
||||
*/
|
||||
async updateKey(srcArmored, dstArmored) {
|
||||
const {keys: [srcKey], err: srcErr} = await openpgp.key.readArmored(srcArmored);
|
||||
|
@ -191,10 +191,47 @@ class PGP {
|
|||
log.error('pgp', 'Failed to parse destination PGP key for update:\n%s', dstArmored, dstErr);
|
||||
util.throw(500, 'Failed to parse PGP key');
|
||||
}
|
||||
|
||||
// list new signatures
|
||||
const newSigs=[];
|
||||
if(dstKey.hasSameFingerprintAs(srcKey)) {
|
||||
const source = srcKey.directSignatures;
|
||||
const dest = dstKey.directSignatures;
|
||||
if(source) {
|
||||
for(const sourceSig of source) {
|
||||
if (!sourceSig.isExpired() && !dest.some(function(destSig) {
|
||||
return util.equalsUint8Array(destSig.signature, sourceSig.signature);
|
||||
})) {
|
||||
newSigs.push(sourceSig);
|
||||
}
|
||||
}
|
||||
}
|
||||
// do not add new signatures
|
||||
source = source.filter(sourceSig => !newSigs.some(function(sig) {
|
||||
return util.equalsUint8Array(sig.signature, sourceSig.signature);
|
||||
}));
|
||||
}
|
||||
|
||||
await dstKey.update(srcKey);
|
||||
return dstKey.armor();
|
||||
return {armored: dstKey.armor(), newSigs: newSigs};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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 object: { name, email}
|
||||
*/
|
||||
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 = key.getPrimaryUser();
|
||||
return {name: primaryUser.name, email: primaryUser.email};
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove user ID from armored key block
|
||||
* @param {String} email email of user ID to be removed
|
||||
|
|
|
@ -89,10 +89,21 @@ class PublicKey {
|
|||
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);
|
||||
// update verified key with new key and get new signatures
|
||||
let newSigs = [];
|
||||
{armored: key.publicKeyArmored, newSigs: newSigs} = await this._pgp.updateKey(verified.publicKeyArmored, filteredPublicKeyArmored);
|
||||
// store pending signatures in key and generate nounce for confirmation
|
||||
if(!key.pendingSignatures)
|
||||
key.pendingSignatures = {sigs: newSigs, nonce: util.random()};
|
||||
else {
|
||||
key.pendingSignatures = key.pendingSignatures.concat(newSigs.filter(sourceSig => !key.pendingSignatures.some(function(sig) {
|
||||
return util.equalsUint8Array(sig.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 {
|
||||
|
@ -175,7 +186,7 @@ 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, origin, publicKeyArmored: userId.publicKeyArmored});
|
||||
await this._email.send({template: tpl.verifyKey.bind(null, ctx), userId, keyId, userId, origin, publicKeyArmored: userId.publicKeyArmored});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -193,14 +204,29 @@ class PublicKey {
|
|||
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, origin, publicKeyArmored: userId.publicKeyArmored});
|
||||
await this._email.send({template: tpl.verifyKey.bind(null, ctx), userId, keyId, userId, origin, publicKeyArmored: userId.publicKeyArmored});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send confirmation email to the public keys 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.sigs.length){
|
||||
const primaryUser = this._pgp.getPrimaryUser(key.publicKeyArmored);
|
||||
await this._email.send({template: tpl.confirmNewSigs.bind(null, ctx), primaryUser, key.keyId, {name: primaryUser.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) {
|
||||
|
@ -271,7 +297,7 @@ class PublicKey {
|
|||
let {publicKeyArmored, email} = key.userIds.find(userId => userId.nonce === nonce);
|
||||
// update armored key
|
||||
if (key.publicKeyArmored) {
|
||||
publicKeyArmored = await this._pgp.updateKey(key.publicKeyArmored, publicKeyArmored);
|
||||
publicKeyArmored = await this._pgp.updateKey(key.publicKeyArmored, publicKeyArmored).armored;
|
||||
}
|
||||
|
||||
// flag the user id as verified
|
||||
|
@ -383,7 +409,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, origin});
|
||||
await this._email.send({template: tpl.verifyRemove.bind(null, ctx), userId, keyId, userId, origin});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,29 @@ exports.isString = function(data) {
|
|||
exports.isTrue = function(data) {
|
||||
if (this.isString(data)) {
|
||||
return data === 'true';
|
||||
} else {
|
||||
} else /**
|
||||
* Check Uint8Array equality
|
||||
* @param {Uint8Array} first array
|
||||
* @param {Uint8Array} second array
|
||||
* @returns {Boolean} equality
|
||||
*/
|
||||
equalsUint8Array: function (array1, array2) {
|
||||
if (!util.isUint8Array(array1) || !util.isUint8Array(array2)) {
|
||||
throw new Error('Data must be in the form of a Uint8Array');
|
||||
}
|
||||
|
||||
if (array1.length !== array2.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < array1.length; i++) {
|
||||
if (array1[i] !== array2[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
return Boolean(data);
|
||||
}
|
||||
};
|
||||
|
@ -103,6 +125,28 @@ 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
|
||||
|
|
Loading…
Reference in New Issue