From 753b1fc6377c6e9a709d2ce59f1eb9121697d342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obernd=C3=B6rfer?= Date: Mon, 10 Feb 2014 18:57:17 +0100 Subject: [PATCH 1/8] Method getKeysForKeyId renamed and optimized, returns only single key. Deep optional parameter to search also in subkeys. Add method getKeyForLongId with same properties. Optimize access to keyid and fingerprint by using a buffer. --- src/keyring/keyring.js | 43 ++++++++++++++++++++++++++++++++++++---- src/packet/public_key.js | 36 ++++++++++++++++++++++++--------- 2 files changed, 66 insertions(+), 13 deletions(-) diff --git a/src/keyring/keyring.js b/src/keyring/keyring.js index 2c8388a2..4c414300 100644 --- a/src/keyring/keyring.js +++ b/src/keyring/keyring.js @@ -145,12 +145,47 @@ Keyring.prototype.getPrivateKeyForAddress = function (email) { }; /** - * Searches the keyring for public keys having the specified key id + * Searches the keyring for a key having the specified key id * @param {String} keyId provided as string of hex number (lowercase) - * @return {Array} public keys found + * @param {Boolean} deep if true search also in subkeys + * @return {module:key~Key|null} key found or null */ -Keyring.prototype.getKeysForKeyId = function (keyId) { - return checkForIdentityAndKeyTypeMatch(this.keys, idCheck, keyId, enums.packet.publicKey); +Keyring.prototype.getKeyForId = function (keyId, deep) { + for (var i = 0; i < this.keys.length; i++) { + if (this.keys[i].primaryKey.getKeyId().toHex() === keyId) { + return this.keys[i]; + } + if (deep) { + for (var j = 0; j < this.keys[i].subKeys.length; j++) { + if (this.keys[i].subKeys[j].getKeyId().toHex() === keyId) { + return this.keys[i]; + } + } + } + } + return null; +}; + +/** + * Searches the keyring for a key having the specified long key id (fingerprint) + * @param {String} longKeyId fingerprint in lowercase hex + * @param {Boolean} deep if true search also in subkeys + * @return {module:key~Key|null} key found or null + */ +Keyring.prototype.getKeyForLongId = function(longKeyId, deep) { + for (var i = 0; i < this.keys.length; i++) { + if (this.keys[i].primaryKey.getFingerprint() === longKeyId) { + return this.keys[i]; + } + if (deep) { + for (var j = 0; j < this.keys[i].subKeys.length; j++) { + if (this.keys[i].subKeys[j].getFingerprint() === longKeyId) { + return this.keys[i]; + } + } + } + } + return null; }; /** diff --git a/src/packet/public_key.js b/src/packet/public_key.js index 35965c47..770ad210 100644 --- a/src/packet/public_key.js +++ b/src/packet/public_key.js @@ -55,6 +55,16 @@ function PublicKey() { this.algorithm = 'rsa_sign'; // time in days (V3 only) this.expirationTimeV3 = 0; + /** + * Fingerprint in lowercase hex + * @type {String} + */ + this.fingerprint = null; + /** + * Keyid + * @type {module:type/keyid} + */ + this.keyid = null; } /** @@ -158,31 +168,39 @@ PublicKey.prototype.writeOld = function () { * @return {String} A 8 byte key id */ PublicKey.prototype.getKeyId = function () { - var keyid = new type_keyid(); - if (this.version == 4) { - keyid.read(this.getFingerprint().substr(12, 8)); - } else if (this.version == 3) { - keyid.read(this.mpi[0].write().substr(-8)); + if (this.keyid) { + return this.keyid; } - return keyid; + this.keyid = new type_keyid(); + if (this.version == 4) { + this.keyid.read(util.hex2bin(this.getFingerprint()).substr(12, 8)); + } else if (this.version == 3) { + this.keyid.read(this.mpi[0].write().substr(-8)); + } + return this.keyid; }; /** * Calculates the fingerprint of the key - * @return {String} A string containing the fingerprint + * @return {String} A string containing the fingerprint in lowercase hex */ PublicKey.prototype.getFingerprint = function () { + if (this.fingerprint) { + return this.fingerprint; + } var toHash = ''; if (this.version == 4) { toHash = this.writeOld(); - return crypto.hash.sha1(toHash); + this.fingerprint = crypto.hash.sha1(toHash); } else if (this.version == 3) { var mpicount = crypto.getPublicMpiCount(this.algorithm); for (var i = 0; i < mpicount; i++) { toHash += this.mpi[i].toBytes(); } - return crypto.hash.md5(toHash); + this.fingerprint = crypto.hash.md5(toHash); } + this.fingerprint = util.hexstrdump(this.fingerprint); + return this.fingerprint; }; /** 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 2/8] 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]; + }); +}; From efc384e71c116d50a77116034fce5d579e917cc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obernd=C3=B6rfer?= Date: Sun, 23 Feb 2014 20:48:38 +0100 Subject: [PATCH 3/8] Keyring: separate storage for public and private keys. ImportKey method supports update. --- src/keyring/keyring.js | 238 +++++++++++++++++++++----------------- src/keyring/localstore.js | 65 ++++++++--- 2 files changed, 181 insertions(+), 122 deletions(-) diff --git a/src/keyring/keyring.js b/src/keyring/keyring.js index 4c414300..fd923c9b 100644 --- a/src/keyring/keyring.js +++ b/src/keyring/keyring.js @@ -27,14 +27,6 @@ var enums = require('../enums.js'), keyModule = require('../key.js'), util = require('../util.js'); -/** - * Callback to check if a key matches the input - * @callback module:keyring/keyring.checkCallback - * @param {String} input input to search for - * @param {module:key~Key} key The key to be checked. - * @return {Boolean} True if the input matches the specified key - */ - module.exports = Keyring; /** @@ -45,21 +37,105 @@ module.exports = Keyring; */ function Keyring(storeHandler) { this.storeHandler = storeHandler || new (require('./localstore.js'))(); - this.keys = this.storeHandler.load(); -}; + this.publicKeys = new KeyArray(this.storeHandler.loadPublic()); + this.privateKeys = new KeyArray(this.storeHandler.loadPrivate()); +} /** * Calls the storeHandler to save the keys */ Keyring.prototype.store = function () { - this.storeHandler.store(this.keys); + this.storeHandler.storePublic(this.publicKeys.keys); + this.storeHandler.storePrivate(this.privateKeys.keys); }; /** * Clear the keyring - erase all the keys */ Keyring.prototype.clear = function() { - this.keys = []; + this.publicKeys.keys = []; + this.privateKeys.keys = []; +}; + +/** + * Searches the keyring for keys having the specified key id + * @param {String} keyId provided as string of hex number (lowercase) + * @param {Boolean} deep if true search also in subkeys + * @return {Array|null} keys found or null + */ +Keyring.prototype.getKeysForId = function (keyId, deep) { + var result = []; + result = result.concat(this.publicKeys.getForId(keyId, deep) || []); + result = result.concat(this.privateKeys.getForId(keyId, deep) || []); + return result.length ? result : null; +}; + +/** + * Removes keys having the specified key id from the keyring + * @param {String} keyId provided as string of hex number (lowercase) + * @return {Array|null} keys found or null + */ +Keyring.prototype.removeKeysForId = function (keyId) { + var result = []; + result = result.concat(this.publicKeys.removeForId(keyId) || []); + result = result.concat(this.privateKeys.removeForId(keyId) || []); + return result.length ? result : null; +}; + +/** + * Searches the keyring for keys having the specified long key id (fingerprint) + * @param {String} longKeyId fingerprint in lowercase hex + * @param {Boolean} deep if true search also in subkeys + * @return {Array|null} keys found or null + */ +Keyring.prototype.getKeysForLongId = function (longKeyId, deep) { + var result = []; + result = result.concat(this.publicKeys.getForLongId(longKeyId, deep) || []); + result = result.concat(this.privateKeys.getForLongId(longKeyId, deep) || []); + return result.length ? result : null; +}; + +/** + * Removes keys having the specified long key id (fingerprint) from the keyring + * @param {String} longKeyId fingerprint in lowercase hex + * @return {Array|null} keys found or null + */ +Keyring.prototype.removeKeysForLongId = function (longKeyId) { + var result = []; + result = result.concat(this.publicKeys.removeForLongId(longKeyId) || []); + result = result.concat(this.privateKeys.removeForLongId(longKeyId) || []); + return result.length ? result : null; +}; + +/** + * Get all public and private keys + * @return {Array} all keys + */ +Keyring.prototype.getAllKeys = function () { + return this.publicKeys.keys.concat(this.privateKeys.keys); +}; + +/** + * Array of keys + * @param {Array} keys The keys to store in this array + */ +function KeyArray(keys) { + this.keys = keys; +} + +/** + * Searches all keys in the KeyArray matching the address or address part of the user ids + * @param {String} email email address to search for + * @return {Array} The public keys associated with provided email address. + */ +KeyArray.prototype.getForAddress = function(email) { + var results = []; + for (var i = 0; i < this.keys.length; i++) { + if (emailCheck(email, this.keys[i])) { + results.push(this.keys[i]); + } + } + return results; }; /** @@ -82,82 +158,19 @@ function emailCheck(email, key) { } /** - * Checks a key to see if it matches the specified keyid - * @param {String} id hex string keyid to search for - * @param {module:key~Key} key the key to be checked. - * @return {Boolean} true if the email address is defined in the specified key - * @inner - */ -function idCheck(id, key) { - var keyids = key.getKeyIds(); - for (var i = 0; i < keyids.length; i++) { - if (util.hexstrdump(keyids[i].write()) == id) { - return true; - } - } - return false; -} - -/** - * searches all public keys in the keyring matching the address or address part of the user ids - * @param {Array} keys array of keys to search - * @param {module:keyring/keyring.checkCallback} identityFunction callback function which checks for a match - * @param {String} identityInput input to check against - * @param {module:enums.packet} keyType packet types of keys to check - * @return {Array} array of keys which match - */ -function checkForIdentityAndKeyTypeMatch(keys, identityFunction, identityInput, keyType) { - var results = []; - for (var p = 0; p < keys.length; p++) { - var key = keys[p]; - switch (keyType) { - case enums.packet.publicKey: - if (key.isPublic() && identityFunction(identityInput, key)) { - results.push(key); - } - break; - case enums.packet.secretKey: - if (key.isPrivate() && identityFunction(identityInput, key)) { - results.push(key); - } - break; - } - } - return results; -} - -/** - * searches all public keys in the keyring matching the address or address part of the user ids - * @param {String} email email address to search for - * @return {Array} The public keys associated with provided email address. - */ -Keyring.prototype.getPublicKeyForAddress = function (email) { - return checkForIdentityAndKeyTypeMatch(this.keys, emailCheck, email, enums.packet.publicKey); -}; - -/** - * Searches the keyring for a private key containing the specified email address - * @param {String} email email address to search for - * @return {Array} private keys found - */ -Keyring.prototype.getPrivateKeyForAddress = function (email) { - return checkForIdentityAndKeyTypeMatch(this.keys, emailCheck, email, enums.packet.secretKey); -}; - -/** - * Searches the keyring for a key having the specified key id + * Searches the KeyArray for a key having the specified key id * @param {String} keyId provided as string of hex number (lowercase) * @param {Boolean} deep if true search also in subkeys * @return {module:key~Key|null} key found or null */ -Keyring.prototype.getKeyForId = function (keyId, deep) { +KeyArray.prototype.getForId = function (keyId, deep) { for (var i = 0; i < this.keys.length; i++) { if (this.keys[i].primaryKey.getKeyId().toHex() === keyId) { return this.keys[i]; } - if (deep) { + if (deep && this.keys[i].subKeys) { for (var j = 0; j < this.keys[i].subKeys.length; j++) { - if (this.keys[i].subKeys[j].getKeyId().toHex() === keyId) { + if (this.keys[i].subKeys[j].subKey.getKeyId().toHex() === keyId) { return this.keys[i]; } } @@ -172,14 +185,14 @@ Keyring.prototype.getKeyForId = function (keyId, deep) { * @param {Boolean} deep if true search also in subkeys * @return {module:key~Key|null} key found or null */ -Keyring.prototype.getKeyForLongId = function(longKeyId, deep) { +KeyArray.prototype.getForLongId = function(longKeyId, deep) { for (var i = 0; i < this.keys.length; i++) { if (this.keys[i].primaryKey.getFingerprint() === longKeyId) { return this.keys[i]; } - if (deep) { + if (deep && this.keys[i].subKeys) { for (var j = 0; j < this.keys[i].subKeys.length; j++) { - if (this.keys[i].subKeys[j].getFingerprint() === longKeyId) { + if (this.keys[i].subKeys[j].subKey.getFingerprint() === longKeyId) { return this.keys[i]; } } @@ -191,38 +204,57 @@ Keyring.prototype.getKeyForLongId = function(longKeyId, deep) { /** * Imports a key from an ascii armored message * @param {String} armored message to read the keys/key from + * @return {Array|null} array of error objects or null */ -Keyring.prototype.importKey = function (armored) { - this.keys = this.keys.concat(keyModule.readArmored(armored).keys); - - return true; +KeyArray.prototype.importKey = function (armored) { + var imported = keyModule.readArmored(armored); + var that = this; + imported.keys.forEach(function(key) { + // check if key already in key array + var keyidHex = key.primaryKey.getKeyId().toHex(); + var keyFound = that.getForId(keyidHex); + if (keyFound) { + keyFound.update(key); + } else { + that.push(key); + } + }); + return imported.err ? imported.err : null; }; /** - * returns the armored message representation of the key at key ring index - * @param {Integer} index the index of the key within the array - * @return {String} armored message representing the key object + * Add key to KeyArray + * @param {module:key~Key} key The key that will be added to the keyring + * @return {Number} The new length of the KeyArray */ -Keyring.prototype.exportKey = function (index) { - return this.keys[index].armor(); +KeyArray.prototype.push = function (key) { + return this.keys.push(key); }; /** - * Removes a public key from the public key keyring at the specified index - * @param {Integer} index the index of the public key within the publicKeys array - * @return {module:key~Key} The public key object which has been removed + * Removes a key with the specified keyid from the keyring + * @param {String} keyId provided as string of hex number (lowercase) + * @return {module:key~Key|null} The key object which has been removed or null */ -Keyring.prototype.removeKey = function (index) { - var removed = this.keys.splice(index, 1); - - return removed; +KeyArray.prototype.removeForId = function (keyId) { + for (var i = 0; i < this.keys.length; i++) { + if (this.keys[i].primaryKey.getKeyId().toHex() === keyId) { + return this.keys.splice(i, 1)[0]; + } + } + return null; }; /** - * returns the armored message representation of the public key portion of the key at key ring index - * @param {Integer} index the index of the key within the array - * @return {String} armored message representing the public key object + * Removes a key with the specified long key id (fingerprint) from the keyring + * @param {String} longKeyId fingerprint in lowercase hex + * @return {module:key~Key|null} The key object which has been removed or null */ -Keyring.prototype.exportPublicKey = function (index) { - return this.keys[index].toPublic().armor(); +KeyArray.prototype.removeForLongId = function (longKeyId) { + for (var i = 0; i < this.keys.length; i++) { + if (this.keys[i].primaryKey.getFingerprint() === longKeyId) { + return this.keys.splice(i, 1)[0]; + } + } + return null; }; diff --git a/src/keyring/localstore.js b/src/keyring/localstore.js index 4d169646..f9fa91c7 100644 --- a/src/keyring/localstore.js +++ b/src/keyring/localstore.js @@ -17,56 +17,83 @@ /** * The class that deals with storage of the keyring. Currently the only option is to use HTML5 local storage. - * @requires openpgp + * @requires config * @module keyring/localstore - * @param {String} item itemname in localstore + * @param {String} prefix prefix for itemnames in localstore */ module.exports = LocalStore; -var openpgp = require('../'); +var config = require('../config'), + keyModule = require('../key.js'); -function LocalStore(item) { +function LocalStore(prefix) { + prefix = prefix || 'openpgp-'; + this.publicKeysItem = prefix + this.publicKeysItem; + this.privateKeysItem = prefix + this.privateKeysItem; if (typeof window != 'undefined' && window.localStorage) { this.storage = window.localStorage; } else { - this.storage = new (require('node-localstorage').LocalStorage)(openpgp.config.node_store); - } - if(typeof item == 'string') { - this.item = item; + this.storage = new (require('node-localstorage').LocalStorage)(config.node_store); } } /* - * Declare the localstore itemname + * Declare the localstore itemnames */ -LocalStore.prototype.item = 'armoredKeys'; +LocalStore.prototype.publicKeysItem = 'public-keys'; +LocalStore.prototype.privateKeysItem = 'private-keys'; /** - * Load the keyring from HTML5 local storage and initializes this instance. + * Load the public keys from HTML5 local storage. * @return {Array} array of keys retrieved from localstore */ -LocalStore.prototype.load = function () { - var armoredKeys = JSON.parse(this.storage.getItem(this.item)); +LocalStore.prototype.loadPublic = function () { + return loadKeys(this.storage, this.publicKeysItem); +}; + +/** + * Load the private keys from HTML5 local storage. + * @return {Array} array of keys retrieved from localstore + */ +LocalStore.prototype.loadPrivate = function () { + return loadKeys(this.storage, this.privateKeysItem); +}; + +function loadKeys(storage, itemname) { + var armoredKeys = JSON.parse(storage.getItem(itemname)); var keys = []; if (armoredKeys !== null && armoredKeys.length !== 0) { var key; for (var i = 0; i < armoredKeys.length; i++) { - key = openpgp.key.readArmored(armoredKeys[i]).keys[0]; + key = keyModule.readArmored(armoredKeys[i]).keys[0]; keys.push(key); } } return keys; +} + +/** + * Saves the current state of the public keys to HTML5 local storage. + * The key array gets stringified using JSON + * @param {Array} keys array of keys to save in localstore + */ +LocalStore.prototype.storePublic = function (keys) { + storeKeys(this.storage, this.publicKeysItem, keys); }; /** - * Saves the current state of the keyring to HTML5 local storage. - * The privateKeys array and publicKeys array gets Stringified using JSON + * Saves the current state of the private keys to HTML5 local storage. + * The key array gets stringified using JSON * @param {Array} keys array of keys to save in localstore */ -LocalStore.prototype.store = function (keys) { +LocalStore.prototype.storePrivate = function (keys) { + storeKeys(this.storage, this.privateKeysItem, keys); +}; + +function storeKeys(storage, itemname, keys) { var armoredKeys = []; for (var i = 0; i < keys.length; i++) { armoredKeys.push(keys[i].armor()); } - this.storage.setItem(this.item, JSON.stringify(armoredKeys)); -}; + storage.setItem(itemname, JSON.stringify(armoredKeys)); +} From 5d4d3f5ba18c9e57a555a8d74a971b5617c89484 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obernd=C3=B6rfer?= Date: Sun, 23 Feb 2014 20:49:51 +0100 Subject: [PATCH 4/8] Fix structure cloning after keyid buffering change --- src/packet/public_key.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/packet/public_key.js b/src/packet/public_key.js index 770ad210..5010351c 100644 --- a/src/packet/public_key.js +++ b/src/packet/public_key.js @@ -218,4 +218,7 @@ PublicKey.prototype.postCloneTypeFix = function() { for (var i = 0; i < this.mpi.length; i++) { this.mpi[i] = type_mpi.fromClone(this.mpi[i]); } + if (this.keyid) { + this.keyid = type_keyid.fromClone(this.keyid); + } }; From 151694ff07592de3ecf3336bee902fd807f7c249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obernd=C3=B6rfer?= Date: Sun, 23 Feb 2014 20:51:02 +0100 Subject: [PATCH 5/8] Write unhashed subpackets. Fix #178. --- src/packet/signature.js | 8 ++++++-- test/general/signature.js | 8 ++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/packet/signature.js b/src/packet/signature.js index 0f9f7f8b..607a0528 100644 --- a/src/packet/signature.js +++ b/src/packet/signature.js @@ -51,6 +51,7 @@ function Signature() { this.publicKeyAlgorithm = null; this.signatureData = null; + this.unhashedSubpackets = null; this.signedHashValue = null; this.created = new Date(); @@ -166,9 +167,11 @@ Signature.prototype.read = function (bytes) { // hash algorithm, the hashed subpacket length, and the hashed // subpacket body. this.signatureData = bytes.substr(0, i); + var sigDataLength = i; // unhashed subpackets i += subpackets.call(this, bytes.substr(i), false); + this.unhashedSubpackets = bytes.substr(sigDataLength, i - sigDataLength); break; default: @@ -184,7 +187,8 @@ Signature.prototype.read = function (bytes) { Signature.prototype.write = function () { return this.signatureData + - util.writeNumber(0, 2) + // Number of unsigned subpackets. + // unhashed subpackets or two octets for zero + (this.unhashedSubpackets ? this.unhashedSubpackets : util.writeNumber(0, 2)) + this.signedHashValue + this.signature; }; @@ -559,7 +563,7 @@ Signature.prototype.toSign = function (type, data) { case t.key: if (data.key === undefined) - throw new Error('Key packet is required for this sigtature.'); + throw new Error('Key packet is required for this signature.'); return data.key.writeOld(); diff --git a/test/general/signature.js b/test/general/signature.js index df69d0e3..8c0b29a3 100644 --- a/test/general/signature.js +++ b/test/general/signature.js @@ -571,4 +571,12 @@ describe("Signature", function() { expect(verified).to.be.true; done(); }); + + it('Write unhashed subpackets', function() { + var pubKey = openpgp.key.readArmored(pub_key_arm2).keys[0]; + expect(pubKey.users[0].selfCertifications).to.exist; + pubKey = openpgp.key.readArmored(pubKey.armor()).keys[0] + expect(pubKey.users[0].selfCertifications).to.exist; + }); + }); From e71a897d8a7a47457a9d30421835adbf55ac4d4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obernd=C3=B6rfer?= Date: Sun, 23 Feb 2014 20:52:03 +0100 Subject: [PATCH 6/8] Add unit tests for key update --- test/general/key.js | 136 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 134 insertions(+), 2 deletions(-) diff --git a/test/general/key.js b/test/general/key.js index 151b847a..0f377d69 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -221,6 +221,48 @@ describe('Key', function() { '=e8xo', '-----END PGP PUBLIC KEY BLOCK-----'].join('\n'); + var priv_key_rsa = + ['-----BEGIN PGP PRIVATE KEY BLOCK-----', + 'Version: GnuPG v2.0.19 (GNU/Linux)', + 'Type: RSA/RSA 1024', + 'Pwd: hello world', + '', + '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', + '=lw5e', + '-----END PGP PRIVATE KEY BLOCK-----'].join('\n'); + it('Parsing armored text with two keys', function(done) { var pubKeys = openpgp.key.readArmored(twoKeys); expect(pubKeys).to.exist; @@ -250,9 +292,9 @@ describe('Key', function() { expect(pubKeyV3).to.exist; expect(pubKeyV4.getKeyPacket().getKeyId().toHex()).to.equal('4a63613a4d6e4094'); - expect(openpgp.util.hexstrdump(pubKeyV4.getKeyPacket().getFingerprint())).to.equal('f470e50dcb1ad5f1e64e08644a63613a4d6e4094'); + expect(pubKeyV4.getKeyPacket().getFingerprint()).to.equal('f470e50dcb1ad5f1e64e08644a63613a4d6e4094'); expect(pubKeyV3.getKeyPacket().getKeyId().toHex()).to.equal('e5b7a014a237ba9d'); - expect(openpgp.util.hexstrdump(pubKeyV3.getKeyPacket().getFingerprint())).to.equal('a44fcee620436a443bc4913640ab3e49'); + expect(pubKeyV3.getKeyPacket().getFingerprint()).to.equal('a44fcee620436a443bc4913640ab3e49'); done(); }); @@ -322,5 +364,95 @@ describe('Key', function() { expect(pubKey.subKeys[0].getExpirationTime().toISOString()).to.be.equal('2018-11-26T10:58:29.000Z'); }); + it('update() - throw error if fingerprints not equal', function() { + var keys = openpgp.key.readArmored(twoKeys).keys; + expect(keys[0].update.bind(keys[0], keys[1])).to.throw('Key update method: fingerprints of keys not equal'); + }); + + it('update() - merge revocation signature', function() { + var source = openpgp.key.readArmored(pub_revoked).keys[0]; + var dest = openpgp.key.readArmored(pub_revoked).keys[0]; + expect(source.revocationSignature).to.exist; + dest.revocationSignature = null; + dest.update(source); + expect(dest.revocationSignature).to.exist.and.be.an.instanceof(openpgp.packet.Signature); + }); + + it('update() - merge user', function() { + var source = openpgp.key.readArmored(pub_sig_test).keys[0]; + var dest = openpgp.key.readArmored(pub_sig_test).keys[0]; + expect(source.users[1]).to.exist; + dest.users.pop(); + dest.update(source); + expect(dest.users[1]).to.exist; + expect(dest.users[1].userId).to.equal(source.users[1].userId); + }); + + it('update() - merge user - other and revocation certification', function() { + var source = openpgp.key.readArmored(pub_sig_test).keys[0]; + var dest = openpgp.key.readArmored(pub_sig_test).keys[0]; + expect(source.users[1].otherCertifications).to.exist; + expect(source.users[1].revocationCertifications).to.exist; + dest.users[1].otherCertifications = null; + dest.users[1].revocationCertifications.pop(); + dest.update(source); + expect(dest.users[1].otherCertifications).to.exist.and.to.have.length(1); + expect(dest.users[1].otherCertifications[0].signature).to.equal(source.users[1].otherCertifications[0].signature); + expect(dest.users[1].revocationCertifications).to.exist.and.to.have.length(2); + expect(dest.users[1].revocationCertifications[1].signature).to.equal(source.users[1].revocationCertifications[1].signature); + }); + + it('update() - merge subkey', function() { + var source = openpgp.key.readArmored(pub_sig_test).keys[0]; + var dest = openpgp.key.readArmored(pub_sig_test).keys[0]; + expect(source.subKeys[1]).to.exist; + dest.subKeys.pop(); + dest.update(source); + expect(dest.subKeys[1]).to.exist; + expect(dest.subKeys[1].subKey.getKeyId().toHex()).to.equal(source.subKeys[1].subKey.getKeyId().toHex()); + }); + + it('update() - merge subkey - revocation signature', function() { + var source = openpgp.key.readArmored(pub_sig_test).keys[0]; + var dest = openpgp.key.readArmored(pub_sig_test).keys[0]; + expect(source.subKeys[0].revocationSignature).to.exist; + dest.subKeys[0].revocationSignature = null; + dest.update(source); + expect(dest.subKeys[0].revocationSignature).to.exist; + expect(dest.subKeys[0].revocationSignature.signature).to.equal(dest.subKeys[0].revocationSignature.signature); + }); + + it('update() - merge private key into public key', function() { + var source = openpgp.key.readArmored(priv_key_rsa).keys[0]; + var dest = openpgp.key.readArmored(twoKeys).keys[0]; + expect(dest.isPublic()).to.be.true; + dest.update(source); + expect(dest.isPrivate()).to.be.true; + expect(source.verifyPrimaryKey()).to.equal(dest.verifyPrimaryKey()); + expect(source.users[0].verify(source.primaryKey)).to.equal(dest.users[0].verify(dest.primaryKey)); + expect(source.subKeys[0].verify(source.primaryKey)).to.equal(dest.subKeys[0].verify(dest.primaryKey)); + }); + + it('update() - merge private key into public key - no subkeys', function() { + var source = openpgp.key.readArmored(priv_key_rsa).keys[0]; + var dest = openpgp.key.readArmored(twoKeys).keys[0]; + source.subKeys = null; + dest.subKeys = null; + expect(dest.isPublic()).to.be.true; + dest.update(source); + expect(dest.isPrivate()).to.be.true; + expect(source.verifyPrimaryKey()).to.equal(dest.verifyPrimaryKey()); + expect(source.users[0].verify(source.primaryKey)).to.equal(dest.users[0].verify(dest.primaryKey)); + }); + + it('update() - merge private key into public key - mismatch throws error', function() { + var source = openpgp.key.readArmored(priv_key_rsa).keys[0]; + var dest = openpgp.key.readArmored(twoKeys).keys[0]; + source.subKeys = null; + expect(dest.subKeys).to.exist; + expect(dest.isPublic()).to.be.true; + expect(dest.update.bind(dest, source)).to.throw('Cannot update public key with private key if subkey mismatch'); + }); + }); From 7bdbb58266d9e7231cb14c4e61f06f49e94a8c76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obernd=C3=B6rfer?= Date: Sun, 23 Feb 2014 20:52:52 +0100 Subject: [PATCH 7/8] Update keyring unit tests --- test/general/keyring.js | 259 +++++++++++++++++++++++++++++++++------- 1 file changed, 219 insertions(+), 40 deletions(-) diff --git a/test/general/keyring.js b/test/general/keyring.js index 6bf24fae..2dcff4ec 100644 --- a/test/general/keyring.js +++ b/test/general/keyring.js @@ -10,7 +10,8 @@ describe("Keyring", function() { var user = 'whiteout.test@t-online.de', passphrase = 'asdf', keySize = 512, - keyId = 'F6F60E9B42CDFF4C', + keyId = 'f6f60e9b42cdff4c', + keyFingerP = '5856cef789c3a307e8a1b976f6f60e9b42cdff4c', pubkey = '-----BEGIN PGP PUBLIC KEY BLOCK-----\n' + 'Version: OpenPGP.js v.1.20131011\n' + 'Comment: http://openpgpjs.org\n' + @@ -37,65 +38,243 @@ describe("Keyring", function() { 'Af9C+kYW1AvNWmivdtr0M0iYCUjM9DNOQH1fcvXqIiN602mWrkd8jcEzLsW5\n' + 'IUNzVPLhrFIuKyBDTpLnC07Loce1\n' + '=ULta\n' + - '-----END PGP PRIVATE KEY BLOCK-----'; + '-----END PGP PRIVATE KEY BLOCK-----', + keyId2 = 'ba993fc2aee18a3a', + keyFingerP2 = '560b7a7f3f9ab516b233b299ba993fc2aee18a3a', + subkeyId2 = 'f47c5210a8cc2740', + subkeyFingerP2 = '2a20c371141e000833848d85f47c5210a8cc2740', + pubkey2 = + ['-----BEGIN PGP PUBLIC KEY BLOCK-----', + 'Version: GnuPG v2.0.22 (GNU/Linux)', + '', + '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', + 'og2umGfGng==', + '=v3+L', + '-----END PGP PUBLIC KEY BLOCK-----'].join('\n'); - it('Import key pair', function(done) { + it('Import key pair', function() { // clear any keys already in the keychain keyring.clear(); - keyring.importKey(privkey); - keyring.importKey(pubkey); - done(); + keyring.store(); + keyring.publicKeys.importKey(pubkey); + keyring.publicKeys.importKey(pubkey2); + keyring.privateKeys.importKey(privkey); }); - it('getPublicKeyForAddress() - unknown address', function(done) { - var key = keyring.getPublicKeyForAddress('nobody@example.com'); - expect(key).to.be.empty; - done(); + it('getKeysForId() - unknown id', function() { + var keys = keyring.getKeysForId('000102030405060708'); + expect(keys).to.be.null; }); - it('getPublicKeyForAddress() - valid address', function(done) { - var key = keyring.getPublicKeyForAddress(user); - expect(key).to.exist.and.have.length(1); - done(); + + it('getKeysForId() - valid id', function() { + var keys = keyring.getKeysForId(keyId); + // we get public and private key + expect(keys).to.exist.and.have.length(2); + expect(keys[0].primaryKey.getKeyId().toHex()).equals(keyId); }); - it('getPrivateKeyForAddress() - unknown address', function(done) { - var key = keyring.getPrivateKeyForAddress('nobody@example.com'); - expect(key).to.be.empty; - done(); + + it('publicKeys.getForId() - unknown id', function() { + var key = keyring.publicKeys.getForId('000102030405060708'); + expect(key).to.be.null; }); - it('getPrivateKeyForAddress() - valid address', function(done) { - var key = keyring.getPrivateKeyForAddress(user); - expect(key).to.exist.and.have.length(1); - done(); + + it('publicKeys.getForId() - valid id', function() { + var key = keyring.publicKeys.getForId(keyId); + expect(key).to.exist.and.be.an.instanceof(openpgp.key.Key); + expect(key.primaryKey.getKeyId().toHex()).equals(keyId); }); - it('getKeysForKeyId() - unknown id', function(done) { - var keys = keyring.getKeysForKeyId('000102030405060708'); + + it('privateKeys.getForId() - unknown id', function() { + var key = keyring.privateKeys.getForId('000102030405060708'); + expect(key).to.be.null; + }); + + it('privateKeys.getForId() - valid id', function() { + var key = keyring.privateKeys.getForId(keyId); + expect(key).to.exist.and.be.an.instanceof(openpgp.key.Key); + expect(key.primaryKey.getKeyId().toHex()).equals(keyId); + }); + + it('publicKeys.getForId() - subkey id', function() { + var key = keyring.publicKeys.getForId(subkeyId2); + expect(key).to.be.null; + }); + + it('publicKeys.getForId() - deep, including subkeys - subkey id', function() { + var key = keyring.publicKeys.getForId(subkeyId2, true); + expect(key).to.exist.and.be.an.instanceof(openpgp.key.Key); + expect(key.primaryKey.getKeyId().toHex()).equals(keyId2); + }); + + it('getKeysForLongId() - unknown long id', function() { + var keys = keyring.getKeysForLongId('000102030405060708'); + expect(keys).to.be.null; + }); + + it('getKeysForLongId() - valid long id', function() { + var keys = keyring.getKeysForLongId(keyFingerP2); + expect(keys).to.exist.and.have.length(1); + expect(keys[0].primaryKey.getKeyId().toHex()).equals(keyId2); + }); + + it('publicKeys.getForLongId() - unknown long id', function() { + var key = keyring.publicKeys.getForLongId('000102030405060708'); + expect(key).to.be.null; + }); + + it('publicKeys.getForLongId() - valid long id', function() { + var key = keyring.publicKeys.getForLongId(keyFingerP2); + expect(key).to.exist.and.be.an.instanceof(openpgp.key.Key); + expect(key.primaryKey.getKeyId().toHex()).equals(keyId2); + }); + + it('publicKeys.getForLongId() - subkey long id', function() { + var key = keyring.publicKeys.getForLongId(subkeyFingerP2); + expect(key).to.be.null; + }); + + it('publicKeys.getForLongId() - deep, including subkeys - subkey long id', function() { + var key = keyring.publicKeys.getForLongId(subkeyFingerP2, true); + expect(key).to.exist.and.be.an.instanceof(openpgp.key.Key); + expect(key.primaryKey.getKeyId().toHex()).equals(keyId2); + }); + + it('publicKeys.getForAddress() - unknown address', function() { + var keys = keyring.publicKeys.getForAddress('nobody@example.com'); expect(keys).to.be.empty; - done(); }); - it('getKeysForKeyId() - valid id', function(done) { - var keys = keyring.getKeysForKeyId(keyId.toLowerCase()); + + it('publicKeys.getForAddress() - valid address', function() { + var keys = keyring.publicKeys.getForAddress(user); expect(keys).to.exist.and.have.length(1); - done(); }); - it('store keys in localstorage', function(done){ + + it('privateKeys.getForAddress() - unknown address', function() { + var key = keyring.privateKeys.getForAddress('nobody@example.com'); + expect(key).to.be.empty; + }); + + it('privateKeys.getForAddress() - valid address', function() { + var key = keyring.privateKeys.getForAddress(user); + expect(key).to.exist.and.have.length(1); + }); + + it('store keys in localstorage', function(){ keyring.store(); - done(); }); - it('after loading from localstorage: getKeysForKeyId() - valid id', function(done) { + + it('after loading from localstorage: getKeysForKeyId() - valid id', function() { var keyring = new openpgp.Keyring(), - keys = keyring.getKeysForKeyId(keyId.toLowerCase()); - expect(keys).to.exist.and.have.length(1); - done(); + keys = keyring.getKeysForId(keyId); + // we expect public and private key + expect(keys).to.exist.and.have.length(2); }); + + it('publicKeys.removeForId() - unknown id', function() { + var key = keyring.publicKeys.removeForId('000102030405060708'); + expect(key).to.be.null; + }); + + it('publicKeys.removeForId() - valid id', function() { + var key = keyring.publicKeys.removeForId(keyId); + expect(key).to.exist.and.be.an.instanceof(openpgp.key.Key); + expect(key.primaryKey.getKeyId().toHex()).equals(keyId); + expect(keyring.publicKeys.keys).to.exist.and.have.length(1); + }); + + it('publicKeys.removeForLongId() - unknown id', function() { + var key = keyring.publicKeys.removeForLongId('000102030405060708'); + expect(key).to.be.null; + expect(keyring.publicKeys.keys).to.exist.and.have.length(1); + }); + + it('publicKeys.removeForLongId() - valid id', function() { + var key = keyring.publicKeys.removeForLongId(keyFingerP2); + expect(key).to.exist.and.be.an.instanceof(openpgp.key.Key); + expect(key.primaryKey.getKeyId().toHex()).equals(keyId2); + expect(keyring.publicKeys.keys).to.be.empty; + }); + it('customize localstorage itemname', function() { - var localstore1 = new openpgp.Keyring.localstore('my-custom-name'); - var localstore2 = new openpgp.Keyring.localstore('my-custom-name'); + var localstore1 = new openpgp.Keyring.localstore('my-custom-prefix-'); + var localstore2 = new openpgp.Keyring.localstore('my-custom-prefix-'); var localstore3 = new openpgp.Keyring.localstore(); - localstore3.store([]); + localstore3.storePublic([]); var key = openpgp.key.readArmored(pubkey).keys[0]; - localstore1.store([key]); - expect(localstore2.load()[0].primaryKey.getKeyId().equals(key.primaryKey.getKeyId())).to.be.true; - expect(localstore3.load()).to.have.length(0); + localstore1.storePublic([key]); + expect(localstore2.loadPublic()[0].primaryKey.getKeyId().equals(key.primaryKey.getKeyId())).to.be.true; + expect(localstore3.loadPublic()).to.have.length(0); }); + + it('removeKeysForId() - unknown id', function() { + keyring.publicKeys.importKey(pubkey); + keyring.publicKeys.importKey(pubkey2); + keyring.privateKeys.importKey(privkey); + expect(keyring.publicKeys.keys).to.have.length(2); + expect(keyring.privateKeys.keys).to.have.length(1); + var keys = keyring.removeKeysForId('000102030405060708'); + expect(keys).to.be.null; + expect(keyring.publicKeys.keys).to.have.length(2); + expect(keyring.privateKeys.keys).to.have.length(1); + }); + + it('removeKeysForId() - valid id', function() { + var keys = keyring.removeKeysForId(keyId); + expect(keys).to.have.length(2); + expect(keyring.publicKeys.keys).to.have.length(1); + expect(keyring.privateKeys.keys).to.have.length(0); + }); + + it('removeKeysForLongId() - unknown id', function() { + keyring.publicKeys.importKey(pubkey); + keyring.publicKeys.importKey(pubkey2); + keyring.privateKeys.importKey(privkey); + expect(keyring.publicKeys.keys).to.have.length(2); + expect(keyring.privateKeys.keys).to.have.length(1); + var keys = keyring.removeKeysForLongId('000102030405060708'); + expect(keys).to.be.null; + expect(keyring.publicKeys.keys).to.have.length(2); + expect(keyring.privateKeys.keys).to.have.length(1); + }); + + it('removeKeysForLongId() - valid id', function() { + var keys = keyring.removeKeysForLongId(keyFingerP); + expect(keys).to.have.length(2); + expect(keyring.publicKeys.keys).to.have.length(1); + expect(keyring.privateKeys.keys).to.have.length(0); + }); + }); From 1dfdfb62cb3505732f546057a88e719bd765e7fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Obernd=C3=B6rfer?= Date: Wed, 26 Feb 2014 11:45:03 +0100 Subject: [PATCH 8/8] Keyring: simplify API, accept 16 char hex or fingerprint as keyid. --- src/keyring/keyring.js | 96 ++++++++++++----------------------------- test/general/keyring.js | 50 ++++++++++----------- 2 files changed, 53 insertions(+), 93 deletions(-) diff --git a/src/keyring/keyring.js b/src/keyring/keyring.js index fd923c9b..c991a763 100644 --- a/src/keyring/keyring.js +++ b/src/keyring/keyring.js @@ -59,7 +59,8 @@ Keyring.prototype.clear = function() { /** * Searches the keyring for keys having the specified key id - * @param {String} keyId provided as string of hex number (lowercase) + * @param {String} keyId provided as string of lowercase hex number + * withouth 0x prefix (can be 16-character key ID or fingerprint) * @param {Boolean} deep if true search also in subkeys * @return {Array|null} keys found or null */ @@ -72,7 +73,8 @@ Keyring.prototype.getKeysForId = function (keyId, deep) { /** * Removes keys having the specified key id from the keyring - * @param {String} keyId provided as string of hex number (lowercase) + * @param {String} keyId provided as string of lowercase hex number + * withouth 0x prefix (can be 16-character key ID or fingerprint) * @return {Array|null} keys found or null */ Keyring.prototype.removeKeysForId = function (keyId) { @@ -82,31 +84,6 @@ Keyring.prototype.removeKeysForId = function (keyId) { return result.length ? result : null; }; -/** - * Searches the keyring for keys having the specified long key id (fingerprint) - * @param {String} longKeyId fingerprint in lowercase hex - * @param {Boolean} deep if true search also in subkeys - * @return {Array|null} keys found or null - */ -Keyring.prototype.getKeysForLongId = function (longKeyId, deep) { - var result = []; - result = result.concat(this.publicKeys.getForLongId(longKeyId, deep) || []); - result = result.concat(this.privateKeys.getForLongId(longKeyId, deep) || []); - return result.length ? result : null; -}; - -/** - * Removes keys having the specified long key id (fingerprint) from the keyring - * @param {String} longKeyId fingerprint in lowercase hex - * @return {Array|null} keys found or null - */ -Keyring.prototype.removeKeysForLongId = function (longKeyId) { - var result = []; - result = result.concat(this.publicKeys.removeForLongId(longKeyId) || []); - result = result.concat(this.privateKeys.removeForLongId(longKeyId) || []); - return result.length ? result : null; -}; - /** * Get all public and private keys * @return {Array} all keys @@ -140,6 +117,7 @@ KeyArray.prototype.getForAddress = function(email) { /** * Checks a key to see if it matches the specified email address + * @private * @param {String} email email address to search for * @param {module:key~Key} key The key to be checked. * @return {Boolean} True if the email address is defined in the specified key @@ -157,42 +135,37 @@ function emailCheck(email, key) { return false; } +/** + * Checks a key to see if it matches the specified keyid + * @private + * @param {String} keyId provided as string of lowercase hex number + * withouth 0x prefix (can be 16-character key ID or fingerprint) + * @param {module:packet/secret_key|public_key|public_subkey|secret_subkey} keypacket The keypacket to be checked + * @return {Boolean} True if keypacket has the specified keyid + */ +function keyIdCheck(keyId, keypacket) { + if (keyId.length === 16) { + return keyId === keypacket.getKeyId().toHex(); + } else { + return keyId === keypacket.getFingerprint(); + } +} + /** * Searches the KeyArray for a key having the specified key id - * @param {String} keyId provided as string of hex number (lowercase) + * @param {String} keyId provided as string of lowercase hex number + * withouth 0x prefix (can be 16-character key ID or fingerprint) * @param {Boolean} deep if true search also in subkeys * @return {module:key~Key|null} key found or null */ KeyArray.prototype.getForId = function (keyId, deep) { for (var i = 0; i < this.keys.length; i++) { - if (this.keys[i].primaryKey.getKeyId().toHex() === keyId) { + if (keyIdCheck(keyId, this.keys[i].primaryKey)) { return this.keys[i]; } if (deep && this.keys[i].subKeys) { for (var j = 0; j < this.keys[i].subKeys.length; j++) { - if (this.keys[i].subKeys[j].subKey.getKeyId().toHex() === keyId) { - return this.keys[i]; - } - } - } - } - return null; -}; - -/** - * Searches the keyring for a key having the specified long key id (fingerprint) - * @param {String} longKeyId fingerprint in lowercase hex - * @param {Boolean} deep if true search also in subkeys - * @return {module:key~Key|null} key found or null - */ -KeyArray.prototype.getForLongId = function(longKeyId, deep) { - for (var i = 0; i < this.keys.length; i++) { - if (this.keys[i].primaryKey.getFingerprint() === longKeyId) { - return this.keys[i]; - } - if (deep && this.keys[i].subKeys) { - for (var j = 0; j < this.keys[i].subKeys.length; j++) { - if (this.keys[i].subKeys[j].subKey.getFingerprint() === longKeyId) { + if (keyIdCheck(keyId, this.keys[i].subKeys[j].subKey)) { return this.keys[i]; } } @@ -233,26 +206,13 @@ KeyArray.prototype.push = function (key) { /** * Removes a key with the specified keyid from the keyring - * @param {String} keyId provided as string of hex number (lowercase) + * @param {String} keyId provided as string of lowercase hex number + * withouth 0x prefix (can be 16-character key ID or fingerprint) * @return {module:key~Key|null} The key object which has been removed or null */ KeyArray.prototype.removeForId = function (keyId) { for (var i = 0; i < this.keys.length; i++) { - if (this.keys[i].primaryKey.getKeyId().toHex() === keyId) { - return this.keys.splice(i, 1)[0]; - } - } - return null; -}; - -/** - * Removes a key with the specified long key id (fingerprint) from the keyring - * @param {String} longKeyId fingerprint in lowercase hex - * @return {module:key~Key|null} The key object which has been removed or null - */ -KeyArray.prototype.removeForLongId = function (longKeyId) { - for (var i = 0; i < this.keys.length; i++) { - if (this.keys[i].primaryKey.getFingerprint() === longKeyId) { + if (keyIdCheck(keyId, this.keys[i].primaryKey)) { return this.keys.splice(i, 1)[0]; } } diff --git a/test/general/keyring.js b/test/general/keyring.js index 2dcff4ec..4e77dcaf 100644 --- a/test/general/keyring.js +++ b/test/general/keyring.js @@ -94,7 +94,7 @@ describe("Keyring", function() { }); it('getKeysForId() - unknown id', function() { - var keys = keyring.getKeysForId('000102030405060708'); + var keys = keyring.getKeysForId('01234567890123456'); expect(keys).to.be.null; }); @@ -106,7 +106,7 @@ describe("Keyring", function() { }); it('publicKeys.getForId() - unknown id', function() { - var key = keyring.publicKeys.getForId('000102030405060708'); + var key = keyring.publicKeys.getForId('01234567890123456'); expect(key).to.be.null; }); @@ -117,7 +117,7 @@ describe("Keyring", function() { }); it('privateKeys.getForId() - unknown id', function() { - var key = keyring.privateKeys.getForId('000102030405060708'); + var key = keyring.privateKeys.getForId('01234567890123456'); expect(key).to.be.null; }); @@ -138,35 +138,35 @@ describe("Keyring", function() { expect(key.primaryKey.getKeyId().toHex()).equals(keyId2); }); - it('getKeysForLongId() - unknown long id', function() { - var keys = keyring.getKeysForLongId('000102030405060708'); + it('getKeysForId() - unknown fingerprint', function() { + var keys = keyring.getKeysForId('71130e8383bef9526e062600d5e9f93acbbc7275'); expect(keys).to.be.null; }); - it('getKeysForLongId() - valid long id', function() { - var keys = keyring.getKeysForLongId(keyFingerP2); + it('getKeysForId() - valid fingerprint', function() { + var keys = keyring.getKeysForId(keyFingerP2); expect(keys).to.exist.and.have.length(1); expect(keys[0].primaryKey.getKeyId().toHex()).equals(keyId2); }); - it('publicKeys.getForLongId() - unknown long id', function() { - var key = keyring.publicKeys.getForLongId('000102030405060708'); + it('publicKeys.getForId() - unknown fingerprint', function() { + var key = keyring.publicKeys.getForId('71130e8383bef9526e062600d5e9f93acbbc7275'); expect(key).to.be.null; }); - it('publicKeys.getForLongId() - valid long id', function() { - var key = keyring.publicKeys.getForLongId(keyFingerP2); + it('publicKeys.getForId() - valid fingerprint', function() { + var key = keyring.publicKeys.getForId(keyFingerP2); expect(key).to.exist.and.be.an.instanceof(openpgp.key.Key); expect(key.primaryKey.getKeyId().toHex()).equals(keyId2); }); - it('publicKeys.getForLongId() - subkey long id', function() { - var key = keyring.publicKeys.getForLongId(subkeyFingerP2); + it('publicKeys.getForId() - subkey fingerprint', function() { + var key = keyring.publicKeys.getForId(subkeyFingerP2); expect(key).to.be.null; }); - it('publicKeys.getForLongId() - deep, including subkeys - subkey long id', function() { - var key = keyring.publicKeys.getForLongId(subkeyFingerP2, true); + it('publicKeys.getForId() - deep, including subkeys - subkey fingerprint', function() { + var key = keyring.publicKeys.getForId(subkeyFingerP2, true); expect(key).to.exist.and.be.an.instanceof(openpgp.key.Key); expect(key.primaryKey.getKeyId().toHex()).equals(keyId2); }); @@ -203,7 +203,7 @@ describe("Keyring", function() { }); it('publicKeys.removeForId() - unknown id', function() { - var key = keyring.publicKeys.removeForId('000102030405060708'); + var key = keyring.publicKeys.removeForId('01234567890123456'); expect(key).to.be.null; }); @@ -214,14 +214,14 @@ describe("Keyring", function() { expect(keyring.publicKeys.keys).to.exist.and.have.length(1); }); - it('publicKeys.removeForLongId() - unknown id', function() { - var key = keyring.publicKeys.removeForLongId('000102030405060708'); + it('publicKeys.removeForId() - unknown fingerprint', function() { + var key = keyring.publicKeys.removeForId('71130e8383bef9526e062600d5e9f93acbbc7275'); expect(key).to.be.null; expect(keyring.publicKeys.keys).to.exist.and.have.length(1); }); - it('publicKeys.removeForLongId() - valid id', function() { - var key = keyring.publicKeys.removeForLongId(keyFingerP2); + it('publicKeys.removeForId() - valid fingerprint', function() { + var key = keyring.publicKeys.removeForId(keyFingerP2); expect(key).to.exist.and.be.an.instanceof(openpgp.key.Key); expect(key.primaryKey.getKeyId().toHex()).equals(keyId2); expect(keyring.publicKeys.keys).to.be.empty; @@ -244,7 +244,7 @@ describe("Keyring", function() { keyring.privateKeys.importKey(privkey); expect(keyring.publicKeys.keys).to.have.length(2); expect(keyring.privateKeys.keys).to.have.length(1); - var keys = keyring.removeKeysForId('000102030405060708'); + var keys = keyring.removeKeysForId('01234567890123456'); expect(keys).to.be.null; expect(keyring.publicKeys.keys).to.have.length(2); expect(keyring.privateKeys.keys).to.have.length(1); @@ -257,20 +257,20 @@ describe("Keyring", function() { expect(keyring.privateKeys.keys).to.have.length(0); }); - it('removeKeysForLongId() - unknown id', function() { + it('removeKeysForId() - unknown fingerprint', function() { keyring.publicKeys.importKey(pubkey); keyring.publicKeys.importKey(pubkey2); keyring.privateKeys.importKey(privkey); expect(keyring.publicKeys.keys).to.have.length(2); expect(keyring.privateKeys.keys).to.have.length(1); - var keys = keyring.removeKeysForLongId('000102030405060708'); + var keys = keyring.removeKeysForId('71130e8383bef9526e062600d5e9f93acbbc7275'); expect(keys).to.be.null; expect(keyring.publicKeys.keys).to.have.length(2); expect(keyring.privateKeys.keys).to.have.length(1); }); - it('removeKeysForLongId() - valid id', function() { - var keys = keyring.removeKeysForLongId(keyFingerP); + it('removeKeysForId() - valid fingerprint', function() { + var keys = keyring.removeKeysForId(keyFingerP); expect(keys).to.have.length(2); expect(keyring.publicKeys.keys).to.have.length(1); expect(keyring.privateKeys.keys).to.have.length(0);