diff --git a/src/service/pgp.js b/src/service/pgp.js index 8bb15d7..b6d1660 100644 --- a/src/service/pgp.js +++ b/src/service/pgp.js @@ -173,12 +173,59 @@ 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 compare with + * @return {String, newSigs} filterd 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: sourceSig}); + // do not add new signatures + source.splice(i, 1); + } + } + } + })); + })); + } + return {armored: srcKey.armor(), newSigs: newSigs}; + } /** * 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, newSigs} merged armored key block, list of new signatures + * @return {String} merged amored key block */ async updateKey(srcArmored, dstArmored) { const {keys: [srcKey], err: srcErr} = await openpgp.key.readArmored(srcArmored); @@ -190,37 +237,16 @@ 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'); - } - - // 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 {armored: dstKey.armor(), newSigs: newSigs}; + return dstKey.armor(); } /** * 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} + * @return {Object} The primary userId */ async getPrimaryUser(publicKeyArmored) { const {keys: [key], err: srcErr} = await openpgp.key.readArmored(publicKeyArmored); @@ -228,8 +254,8 @@ class PGP { 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}; + const primaryUser = await key.getPrimaryUser(); + return primaryUser; } /** diff --git a/src/service/public-key.js b/src/service/public-key.js index 019a02f..069ad17 100644 --- a/src/service/public-key.js +++ b/src/service/public-key.js @@ -88,18 +88,25 @@ class PublicKey { if (verified) { 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 and get new signatures - const {armored, newSigs} = await this._pgp.updateKey(verified.publicKeyArmored, filteredPublicKeyArmored); - key.publicKeyArmored = armored; + 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; + // 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(!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); - }))); + if(newSigs.length) { + await this._formatArrays(newSigs); + 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.signature === sourceSig.signature.signature; + }))); + } } + // send mails to verify all user ids await this._sendVerifyEmail(key, origin, ctx); // send mail to confirm all new signatures @@ -172,6 +179,21 @@ class PublicKey { _includeEmail(users, user) { return users.find(({email}) => email === user.email); } + + /** + * Convert all Uint8Array in every signatures to base64. + * @param {Array} signatures list of signatures to convert + * @return {Promise} + */ + async _formatArrays(signatures) { + signatures.map(function(sig) { + const signature = sig.signature; + signature.signatureData = util.base64EncArr(signature.signatureData); + signature.signedHashValue = util.base64EncArr(signature.signedHashValue); + signature.issuerFingerprint = util.base64EncArr(signature.issuerFingerprint); + signature.signature = util.base64EncArr(signature.signature); + }); + } /** * Send verification emails to the public keys user ids for verification. @@ -218,9 +240,10 @@ class PublicKey { * @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, keyId: key.keyId, data: {name: primaryUser.name, sigsNb: key.pendingSignatures.sigs.length, nonce: key.pendingSignatures.nonce}, origin, publicKeyArmored: key.publicKeyArmored}); + 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.confirmNewSigs.bind(null, ctx), userId, keyId: key.keyId, data: {name: userId.name, sigsNb: key.pendingSignatures.sigs.length, nonce: key.pendingSignatures.nonce}, origin, publicKeyArmored: key.publicKeyArmored}); } } @@ -297,7 +320,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).armored; + publicKeyArmored = await this._pgp.updateKey(key.publicKeyArmored, publicKeyArmored); } // flag the user id as verified @@ -386,6 +409,8 @@ class PublicKey { email: uid.email, verified: uid.verified })); + if(key.pendingSignatures) + delete key.pendingSignatures.nonce return key; } diff --git a/src/service/util.js b/src/service/util.js index 19dc93b..ea7ab8a 100644 --- a/src/service/util.js +++ b/src/service/util.js @@ -124,6 +124,99 @@ exports.equalsUint8Array = function (array1, array2) { return true; }; +/** + * Decode a base64 byte to uint6 + * @param {Integer} nChr base64 byte to decode + * @returns {Integer} the uint6 integer + * Comes from https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding + * All thanks to madmurphy + */ +function b64ToUint6 (nChr) { + return nChr > 64 && nChr < 91 ? + nChr - 65 + : nChr > 96 && nChr < 123 ? + nChr - 71 + : nChr > 47 && nChr < 58 ? + nChr + 4 + : nChr === 43 ? + 62 + : nChr === 47 ? + 63 + : + 0; +}; + +/** + * Decode a base64 String to Uint8Array + * @param {String} sBase64 base64 string to decode + * @returns {Uint8Array} decoded data + * Comes from https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding + * All thanks to madmurphy + */ +exports.base64DecToArr = function (sBase64) { + var sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), nInLen = sB64Enc.length, + nOutLen = nInLen * 3 + 1 >> 2, taBytes = new Uint8Array(nOutLen); + + for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) { + nMod4 = nInIdx & 3; + nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4; + if (nMod4 === 3 || nInLen - nInIdx === 1) { + for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) { + taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255; + } + nUint24 = 0; + } + } + return taBytes; +}; + +/** + * Encode a uint6 to base64 byte + * @param {Integer} nUint6 integer to encode + * @returns {Integer} the base64 byte + * Comes from https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding + * All thanks to madmurphy + */ +function uint6ToB64 (nUint6) { + return nUint6 < 26 ? + nUint6 + 65 + : nUint6 < 52 ? + nUint6 + 71 + : nUint6 < 62 ? + nUint6 - 4 + : nUint6 === 62 ? + 43 + : nUint6 === 63 ? + 47 + : + 65; +}; + + +/** + * Encode a Uint8Array to base64 + * @param {Uint8Array} aBytes array to encode + * @returns {String} base64 String + * Comes from https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding + * All thanks to madmurphy + */ +exports.base64EncArr = function (aBytes) { + var eqLen = (3 - (aBytes.length % 3)) % 3, sB64Enc = ""; + for (var nMod3, nLen = aBytes.length, nUint24 = 0, nIdx = 0; nIdx < nLen; nIdx++) { + nMod3 = nIdx % 3; + /* Uncomment the following line in order to split the output in lines 76-character long: */ + /* + if (nIdx > 0 && (nIdx * 4 / 3) % 76 === 0) { sB64Enc += "\r\n"; } + */ + nUint24 |= aBytes[nIdx] << (16 >>> nMod3 & 24); + if (nMod3 === 2 || aBytes.length - nIdx === 1) { + sB64Enc += String.fromCharCode(uint6ToB64(nUint24 >>> 18 & 63), uint6ToB64(nUint24 >>> 12 & 63), uint6ToB64(nUint24 >>> 6 & 63), uint6ToB64(nUint24 & 63)); + nUint24 = 0; + } + } + return eqLen === 0 ? sB64Enc : sB64Enc.substring(0, sB64Enc.length - eqLen) + (eqLen === 1 ? "=" : "=="); +}; + /** * Create an error with a custom status attribute e.g. for http codes.