From d6e4e3c028e9967a23dc90d4b81b73a2d79f51ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obernd=C3=B6rfer?= Date: Sun, 23 Feb 2014 20:46:52 +0100 Subject: [PATCH] Add key update method. Fix getPrimaryUser: evaluation of most significant self signature. --- src/key.js | 148 +++++++++++++++++++++++++++++++++-- src/packet/user_attribute.js | 14 ++++ 2 files changed, 157 insertions(+), 5 deletions(-) diff --git a/src/key.js b/src/key.js index 00d2ba03..4bc4b58a 100644 --- a/src/key.js +++ b/src/key.js @@ -474,13 +474,13 @@ function getExpirationTime(keyPacket, selfCertificate) { return new Date(keyPacket.created.getTime() + selfCertificate.keyExpirationTime*1000); } return null; -}; +} /** * Returns primary user and most significant (latest valid) self signature * - if multiple users are marked as primary users returns the one with the latest self signature * - if no primary user is found returns the user with the latest self signature - * @return {{user: Array, selfCertificate: Array}} The primary user and the self signature + * @return {{user: Array, selfCertificate: Array}|null} The primary user and the self signature */ Key.prototype.getPrimaryUser = function() { var user = null; @@ -493,9 +493,9 @@ Key.prototype.getPrimaryUser = function() { if (!selfCert) { continue; } - if (!user || - !userSelfCert.isPrimaryUserID && selfCert.isPrimaryUserID || - userSelfCert.created < selfCert.created) { + if (!user || + (!userSelfCert.isPrimaryUserID || selfCert.isPrimaryUserID) && + userSelfCert.created > selfCert.created) { user = this.users[i]; userSelfCert = selfCert; } @@ -503,6 +503,102 @@ Key.prototype.getPrimaryUser = function() { return user ? {user: user, selfCertificate: userSelfCert} : null; }; +/** + * Update key with new components from specified key with same key ID: + * users, subkeys, certificates are merged into the destination key, + * duplicates are ignored. + * If the specified key is a private key and the destination key is public, + * the destination key is tranformed to a private key. + * @param {module:key~Key} key source key to merge + */ +Key.prototype.update = function(key) { + var that = this; + if (key.verifyPrimaryKey() === enums.keyStatus.invalid) { + return; + } + if (this.primaryKey.getFingerprint() !== key.primaryKey.getFingerprint()) { + throw new Error('Key update method: fingerprints of keys not equal'); + } + if (this.isPublic() && key.isPrivate()) { + // check for equal subkey packets + var equal = ((this.subKeys && this.subKeys.length) === (key.subKeys && key.subKeys.length)) && + (!this.subKeys || this.subKeys.every(function(destSubKey) { + return key.subKeys.some(function(srcSubKey) { + return destSubKey.subKey.getFingerprint() === srcSubKey.subKey.getFingerprint(); + }); + })); + if (!equal) { + throw new Error('Cannot update public key with private key if subkey mismatch'); + } + this.primaryKey = key.primaryKey; + } + // revocation signature + if (!this.revocationSignature && key.revocationSignature && !key.revocationSignature.isExpired() && + (key.revocationSignature.verified || + key.revocationSignature.verify(key.primaryKey, {key: key.primaryKey}))) { + this.revocationSignature = key.revocationSignature; + } + // direct signatures + mergeSignatures(key, this, 'directSignatures'); + // users + key.users.forEach(function(srcUser) { + var found = false; + for (var i = 0; i < that.users.length; i++) { + if (srcUser.userId && (srcUser.userId.userid === that.users[i].userId.userid) || + srcUser.userAttribute && (srcUser.userAttribute.equals(that.users[i].userAttribute))) { + that.users[i].update(srcUser, that.primaryKey); + found = true; + break; + } + } + if (!found) { + that.users.push(srcUser); + } + }); + // subkeys + if (key.subKeys) { + key.subKeys.forEach(function(srcSubKey) { + var found = false; + for (var i = 0; i < that.subKeys.length; i++) { + if (srcSubKey.subKey.getFingerprint() === that.subKeys[i].subKey.getFingerprint()) { + that.subKeys[i].update(srcSubKey, that.primaryKey); + found = true; + break; + } + } + if (!found) { + that.subKeys.push(srcSubKey); + } + }); + } +}; + +/** + * Merges signatures from source[attr] to dest[attr] + * @private + * @param {Object} source + * @param {Object} dest + * @param {String} attr + * @param {Function} checkFn optional, signature only merged if true + */ +function mergeSignatures(source, dest, attr, checkFn) { + source = source[attr]; + if (source) { + if (!dest[attr]) { + dest[attr] = source; + } else { + source.forEach(function(sourceSig) { + if (!sourceSig.isExpired() && (!checkFn || checkFn(sourceSig)) && + !dest[attr].some(function(destSig) { + return destSig.signature === sourceSig.signature; + })) { + dest[attr].push(sourceSig); + } + }); + } + } +} + // TODO Key.prototype.revoke = function() { @@ -616,6 +712,24 @@ User.prototype.verify = function(primaryKey) { return status; }; +/** + * Update user with new components from specified user + * @param {module:key~User} user source user to merge + * @param {module:packet/signature} primaryKey primary key used for validation + */ +User.prototype.update = function(user, primaryKey) { + var that = this; + // self signatures + mergeSignatures(user, this, 'selfCertifications', function(srcSelfSig) { + return srcSelfSig.verified || + srcSelfSig.verify(primaryKey, {userid: that.userId || that.userAttribute, key: primaryKey}); + }); + // other signatures + mergeSignatures(user, this, 'otherCertifications'); + // revocation signatures + mergeSignatures(user, this, 'revocationCertifications'); +}; + /** * @class * @classdesc Class that represents a subkey packet and the relevant signatures. @@ -706,6 +820,30 @@ SubKey.prototype.getExpirationTime = function() { return getExpirationTime(this.subKey, this.bindingSignature); }; +/** + * Update subkey with new components from specified subkey + * @param {module:key~SubKey} subKey source subkey to merge + * @param {module:packet/signature} primaryKey primary key used for validation + */ +SubKey.prototype.update = function(subKey, primaryKey) { + if (this.verify(primaryKey) === enums.keyStatus.invalid) { + return; + } + if (this.subKey.getFingerprint() !== subKey.subKey.getFingerprint()) { + throw new Error('SubKey update method: fingerprints of subkeys not equal'); + } + if (this.subKey.tag === enums.packet.publicSubkey && + subKey.subKey.tag === enums.packet.secretSubkey) { + this.subKey = subKey.subKey; + } + // revocation signature + if (!this.revocationSignature && subKey.revocationSignature && !subKey.revocationSignature.isExpired() && + (subKey.revocationSignature.verified || + subKey.revocationSignature.verify(primaryKey, {key: primaryKey, bind: this.subKey}))) { + this.revocationSignature = subKey.revocationSignature; + } +}; + /** * Reads an OpenPGP armored text and returns one or multiple key objects * @param {String} armoredText text to be parsed diff --git a/src/packet/user_attribute.js b/src/packet/user_attribute.js index d14727d3..161738be 100644 --- a/src/packet/user_attribute.js +++ b/src/packet/user_attribute.js @@ -64,3 +64,17 @@ UserAttribute.prototype.read = function(bytes) { i += len.len; } }; + +/** + * Compare for equality + * @param {module:user_attribute~UserAttribute} usrAttr + * @return {Boolean} true if equal + */ +UserAttribute.prototype.equals = function(usrAttr) { + if (!usrAttr || !(usrAttr instanceof UserAttribute)) { + return false; + } + return this.attributes.every(function(attr, index) { + return attr === usrAttr.attributes[index]; + }); +};