From 1f574e0df73abbb76000bdbd6febad1a9262ac7d Mon Sep 17 00:00:00 2001 From: larabr <7375870+larabr@users.noreply.github.com> Date: Fri, 16 Aug 2024 13:43:46 +0200 Subject: [PATCH 1/4] Add support for parsing transferable private keys with a primary public key and public subkeys --- src/key/factory.js | 25 ++++++++++++++----------- src/key/key.js | 15 ++++++++++++--- src/key/private_key.js | 5 +---- src/key/public_key.js | 5 +---- 4 files changed, 28 insertions(+), 22 deletions(-) diff --git a/src/key/factory.js b/src/key/factory.js index e66eaa10..6326e2eb 100644 --- a/src/key/factory.js +++ b/src/key/factory.js @@ -51,15 +51,18 @@ const allowedKeyPackets = /*#__PURE__*/ util.constructAllowedPackets([ * @throws if no key packet was found */ function createKey(packetlist) { - for (const packet of packetlist) { - switch (packet.constructor.tag) { - case enums.packet.secretKey: - return new PrivateKey(packetlist); - case enums.packet.publicKey: - return new PublicKey(packetlist); + if (packetlist[0]?.constructor.tag === enums.packet.secretKey) { + return new PrivateKey(packetlist); + } else if (packetlist[0]?.constructor.tag === enums.packet.publicKey) { + if (packetlist.findPacket(enums.packet.secretSubkey)) { + return new PrivateKey(packetlist); + } else { + return new PublicKey(packetlist); } + } else { + throw new Error('No primary key packet found'); } - throw new Error('No key packet found'); + } @@ -384,10 +387,10 @@ export async function readPrivateKey({ armoredKey, binaryKey, config, ...rest }) const packetlist = await PacketList.fromBinary(input, allowedKeyPackets, config); const keyIndex = packetlist.indexOfTag(enums.packet.publicKey, enums.packet.secretKey); for (let i = 0; i < keyIndex.length; i++) { - if (packetlist[keyIndex[i]].constructor.tag === enums.packet.publicKey) { + const firstPrivateKeyList = packetlist.slice(keyIndex[i], keyIndex[i + 1]); + if (packetlist[keyIndex[i]].constructor.tag === enums.packet.publicKey && !firstPrivateKeyList.findPacket(enums.packet.secretSubkey)) { continue; } - const firstPrivateKeyList = packetlist.slice(keyIndex[i], keyIndex[i + 1]); return new PrivateKey(firstPrivateKeyList); } throw new Error('No secret key packet found'); @@ -471,10 +474,10 @@ export async function readPrivateKeys({ armoredKeys, binaryKeys, config }) { const packetlist = await PacketList.fromBinary(input, allowedKeyPackets, config); const keyIndex = packetlist.indexOfTag(enums.packet.publicKey, enums.packet.secretKey); for (let i = 0; i < keyIndex.length; i++) { - if (packetlist[keyIndex[i]].constructor.tag === enums.packet.publicKey) { + const oneKeyList = packetlist.slice(keyIndex[i], keyIndex[i + 1]); + if (packetlist[keyIndex[i]].constructor.tag === enums.packet.publicKey && !oneKeyList.findPacket(enums.packet.secretSubkey)) { continue; } - const oneKeyList = packetlist.slice(keyIndex[i], keyIndex[i + 1]); const newKey = new PrivateKey(oneKeyList); keys.push(newKey); } diff --git a/src/key/key.js b/src/key/key.js index 8e608572..b99d94ad 100644 --- a/src/key/key.js +++ b/src/key/key.js @@ -49,13 +49,14 @@ class Key { /** * Transforms packetlist to structured key data * @param {PacketList} packetlist - The packets that form a key - * @param {Set} disallowedPackets - disallowed packet tags + * @param {Boolean} expectPrivateKey - if a private key is expected, a SecretKeyPacket or SecretKeySubpacket must be present. */ - packetListToStructure(packetlist, disallowedPackets = new Set()) { + packetListToStructure(packetlist, expectPrivateKey) { let user; let primaryKeyID; let subkey; let ignoreUntil; + let isPrivateKey; for (const packet of packetlist) { @@ -78,7 +79,8 @@ class Key { if (!ignoreUntil.has(tag)) continue; ignoreUntil = null; } - if (disallowedPackets.has(tag)) { + isPrivateKey = isPrivateKey || tag === enums.packet.secretKey || tag === enums.packet.secretSubkey; + if (!expectPrivateKey && isPrivateKey) { throw new Error(`Unexpected packet type: ${tag}`); } switch (tag) { @@ -151,6 +153,13 @@ class Key { break; } } + + if (!this.keyPacket) { + throw new Error('Invalid key: missing primary key packet'); + } + if (expectPrivateKey && !isPrivateKey) { + throw new Error('No secret key packet found'); + } } /** diff --git a/src/key/private_key.js b/src/key/private_key.js index 37cd4588..a90f02b7 100644 --- a/src/key/private_key.js +++ b/src/key/private_key.js @@ -18,10 +18,7 @@ class PrivateKey extends PublicKey { */ constructor(packetlist) { super(); - this.packetListToStructure(packetlist, new Set([enums.packet.publicKey, enums.packet.publicSubkey])); - if (!this.keyPacket) { - throw new Error('Invalid key: missing private-key packet'); - } + this.packetListToStructure(packetlist, true); } /** diff --git a/src/key/public_key.js b/src/key/public_key.js index 7996a32c..964c7a2e 100644 --- a/src/key/public_key.js +++ b/src/key/public_key.js @@ -32,10 +32,7 @@ class PublicKey extends Key { this.users = []; this.subkeys = []; if (packetlist) { - this.packetListToStructure(packetlist, new Set([enums.packet.secretKey, enums.packet.secretSubkey])); - if (!this.keyPacket) { - throw new Error('Invalid key: missing public-key packet'); - } + this.packetListToStructure(packetlist, false); } } From 36308615adcc380d688c3866d59d40bebac2e7c7 Mon Sep 17 00:00:00 2001 From: larabr <7375870+larabr@users.noreply.github.com> Date: Fri, 16 Aug 2024 13:47:12 +0200 Subject: [PATCH 2/4] `Key.getSigningKey`: prefer private decrypted (sub)keys If dummy or public (sub)key packets are present alongside secret ones, the latter are now selected first, regardless of creation date. --- src/key/key.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/key/key.js b/src/key/key.js index b99d94ad..0fd87662 100644 --- a/src/key/key.js +++ b/src/key/key.js @@ -273,7 +273,13 @@ class Key { } catch (err) { throw util.wrapError('Could not verify primary key', err); } - const subkeys = this.subkeys.slice().sort((a, b) => b.keyPacket.created - a.keyPacket.created); + const subkeys = this.subkeys.slice().sort((a, b) => { + const aIsPrivate = a.isDecrypted() !== null && !a.isDummy(); + const bIsPrivate = b.isDecrypted() !== null && !b.isDummy(); + const diffIsPrivate = bIsPrivate - aIsPrivate; + // return non-dummy private (sub)keys first + return diffIsPrivate !== 0 ? diffIsPrivate : b.keyPacket.created - a.keyPacket.created; + }); let exception; for (const subkey of subkeys) { if (!keyID || subkey.getKeyID().equals(keyID)) { From 84d7d12c836ec8e16bc69f0a42a225b41dc1a3ac Mon Sep 17 00:00:00 2001 From: larabr <7375870+larabr@users.noreply.github.com> Date: Fri, 16 Aug 2024 16:42:53 +0200 Subject: [PATCH 3/4] `PrivateKey.getDecryptionKeys`: do not return public key packets --- openpgp.d.ts | 5 +++- src/key/helper.js | 2 ++ src/key/key.js | 4 +-- src/key/private_key.js | 8 +++--- test/general/key.js | 58 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 70 insertions(+), 7 deletions(-) diff --git a/openpgp.d.ts b/openpgp.d.ts index b4d54b5b..d5bd60ae 100644 --- a/openpgp.d.ts +++ b/openpgp.d.ts @@ -383,7 +383,7 @@ declare abstract class BasePublicKeyPacket extends BasePacket { public hasSameFingerprintAs(other: BasePublicKeyPacket): boolean; public getCreationTime(): Date; public getKeyID(): KeyID; - public isDecrypted(): boolean; + public isDecrypted(): boolean | null; public publicParams: object; // `isSubkey` is a dummy method to ensure that Subkey packets are not accepted as Key one, and vice versa. // The key class hierarchy is already modelled to cover this, but the concrete key packet classes @@ -394,11 +394,13 @@ declare abstract class BasePublicKeyPacket extends BasePacket { export class PublicKeyPacket extends BasePublicKeyPacket { static readonly tag: enums.packet.publicKey; protected isSubkey(): false; + public isDecrypted(): null; } export class PublicSubkeyPacket extends BasePublicKeyPacket { static readonly tag: enums.packet.publicSubkey; protected isSubkey(): true; + public isDecrypted(): null; } declare abstract class BaseSecretKeyPacket extends BasePublicKeyPacket { @@ -406,6 +408,7 @@ declare abstract class BaseSecretKeyPacket extends BasePublicKeyPacket { public encrypt(passphrase: string, config?: Config): Promise; // throws on error public decrypt(passphrase: string): Promise; // throws on error public validate(): Promise; // throws on error + public isDecrypted(): boolean; public isDummy(): boolean; public isMissingSecretKeyMaterial(): boolean; public makeDummy(config?: Config): void; diff --git a/src/key/helper.js b/src/key/helper.js index e803acab..fbc219b4 100644 --- a/src/key/helper.js +++ b/src/key/helper.js @@ -469,3 +469,5 @@ export function checkKeyRequirements(keyPacket, config) { break; } } + +export const isPublicOrDummyKeyPacket = keyPacket => keyPacket.isDecrypted() === null || keyPacket.isDummy(); diff --git a/src/key/key.js b/src/key/key.js index 0fd87662..318e4dfa 100644 --- a/src/key/key.js +++ b/src/key/key.js @@ -274,8 +274,8 @@ class Key { throw util.wrapError('Could not verify primary key', err); } const subkeys = this.subkeys.slice().sort((a, b) => { - const aIsPrivate = a.isDecrypted() !== null && !a.isDummy(); - const bIsPrivate = b.isDecrypted() !== null && !b.isDummy(); + const aIsPrivate = !helper.isPublicOrDummyKeyPacket(a.keyPacket); + const bIsPrivate = !helper.isPublicOrDummyKeyPacket(b.keyPacket); const diffIsPrivate = bIsPrivate - aIsPrivate; // return non-dummy private (sub)keys first return diffIsPrivate !== 0 ? diffIsPrivate : b.keyPacket.created - a.keyPacket.created; diff --git a/src/key/private_key.js b/src/key/private_key.js index a90f02b7..b255eb34 100644 --- a/src/key/private_key.js +++ b/src/key/private_key.js @@ -83,8 +83,8 @@ class PrivateKey extends PublicKey { let exception = null; for (let i = 0; i < this.subkeys.length; i++) { if (!keyID || this.subkeys[i].getKeyID().equals(keyID, true)) { - if (this.subkeys[i].keyPacket.isDummy()) { - exception = exception || new Error('Gnu-dummy key packets cannot be used for decryption'); + if (helper.isPublicOrDummyKeyPacket(this.subkeys[i].keyPacket)) { + exception = exception || new Error('Public or gnu-dummy key packets cannot be used for decryption'); continue; } @@ -103,8 +103,8 @@ class PrivateKey extends PublicKey { // evaluate primary key const selfCertification = await this.getPrimarySelfSignature(date, userID, config); if ((!keyID || primaryKey.getKeyID().equals(keyID, true)) && helper.validateDecryptionKeyPacket(primaryKey, selfCertification, config)) { - if (primaryKey.isDummy()) { - exception = exception || new Error('Gnu-dummy key packets cannot be used for decryption'); + if (helper.isPublicOrDummyKeyPacket(primaryKey)) { + exception = exception || new Error('Public or gnu-dummy key packets cannot be used for decryption'); } else { keys.push(this); } diff --git a/test/general/key.js b/test/general/key.js index 995c32b5..57b40d84 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -3019,6 +3019,64 @@ export default () => describe('Key', function() { expect(key.write()).to.deep.equal(expectedSerializedKey.data); }); + it('Parsing armored key with public primary key and private subkeys', async function() { + // public primary key with: + // - one signing private subkey + // - one signing public subkey + // - one encryption private subkey + // - one encryption public subkey + const privateKeyWithPublicKeyPackets = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xjMEZr9cyxYJKwYBBAHaRw8BAQdAlC7g7Q4UZmN5zNhFynWuEXYn1KV8Jfp4 +DMiKf8ZQ5oDNDzx0ZXN0QHRlc3QuY29tPsLAEwQTFgoAhQWCZr9cywMLCQcJ +kPhnPZ0LDe1wRRQAAAAAABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5v +cmeWobLLkXmgpg9Pet9xOLiKvUAGKchlrMxBubhvTpBGygUVCAoMDgQWAAIB +AhkBApsDAh4BFiEEYmvn2jb9uzAG41Ql+Gc9nQsN7XAAAHGyAP9URiF2uq/O +8PJkjoehixMx9h9a8L+lPd56ymbdxUb/swEA0tNWA9alk/m3rml0aTPNucuG +Aj/FLBixvbva+SWEkgrOMwRmv1zLFgkrBgEEAdpHDwEBB0DdyjGHiVCA0Cbr +5u5pcuZmYZylLDURujjhEWYcLKIg0MLAuwQYFgoBLQWCZr9cywmQ+Gc9nQsN +7XBFFAAAAAAAHAAgc2FsdEBub3RhdGlvbnMub3BlbnBncGpzLm9yZ5UtjGIA +N+2fOlsIv9vwi5MaTUxoXXc+Mw6ETjRvW3fUApsCvKAEGRYKAG0Fgma/XMsJ +kHC3NLlxFk7wRRQAAAAAABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5v +cme74M+JTJ/Clb0bApLqL/QGKOZbaXdL3Y/p02QX2o6AgRYhBFFraL50hRm3 +RC4PZHC3NLlxFk7wAABAgwEAhQ+EgvirBcabNlmHV6nEbqyTz85oBj8SKNM2 +d+tDQ3YBAN3jXoPGx22sVoy5rAMerwq1lhnlgCh4xfdTw1jtr2QEFiEEYmvn +2jb9uzAG41Ql+Gc9nQsN7XAAALGsAQCCDkhWJRgiCoHIvjWwRqVWU1BsVaj5 +dr6fxkK9yaOv6QEA+JjqAgPcRo/LGHpO0dZS9qi1Zpy4u5bT2FunXqvNDQ7O +MwRmv1zLFgkrBgEEAdpHDwEBB0A4CyxR5ZSvF8FrEhjzFO8W6paAtqm92p7k ++HQC7TRhKcLAuwQYFgoBLQWCZr9cywmQ+Gc9nQsN7XBFFAAAAAAAHAAgc2Fs +dEBub3RhdGlvbnMub3BlbnBncGpzLm9yZ8eA420LE1oqZm9UQ2mAl0VpYysv +9w9FhamakpSbum0jApsCvKAEGRYKAG0Fgma/XMsJkMrApZFFMT1MRRQAAAAA +ABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5vcmcyU0rvMw4GVCasfsGO +ke8VBnf9DX73KF4Fuw5HRwy65BYhBPmjv8j7/15Fu3bvL8rApZFFMT1MAADv +OgEAp2P/olg/8frAKhZNhBNNcnZ3mBPgw+jnVB2q3lJR3G4A/Az8kxPmyhmf +vaorTRswb2d7xes3ubunWLvHBcU+LSAIFiEEYmvn2jb9uzAG41Ql+Gc9nQsN +7XAAAO9CAP4y3wh7tKf5QkWhz18Lo2Zjqv4S+rCVJZWe9SDRFJq4rwD/eJTb +wFTiP1x1KHQY+bnZQUll10A9c/G9yMrOC0qR8gHHXQRmv1zLEgorBgEEAZdV +AQUBAQdAdWmLtTwhcOH7tlr+2FEysYY3Z6gPpP6EalQnbXEHKRsDAQgHAAD/ +ROS5zCtNP0O2t9U7T/Cs2+n1snTVW+RBTbESM+hjWfgTIcK+BBgWCgBwBYJm +v1zLCZD4Zz2dCw3tcEUUAAAAAAAcACBzYWx0QG5vdGF0aW9ucy5vcGVucGdw +anMub3JniqUd+21Sxr5oHb+ctdQKLEpRYCSR1I0Ap/Z5o3XfqRACmwwWIQRi +a+faNv27MAbjVCX4Zz2dCw3tcAAABPQBAK7S2Izm3D4dEkoJb869wOqtK4be +zdP4cdQMfO/4hsLRAPwMxCX51Okaj/SvwEzjI/RCPmfv9l441PmzQheQ4Gqr +BM44BGa/XMsSCisGAQQBl1UBBQEBB0CqHCGPBZg8ioGvjV4nKteCFs4hAzLn +nFHyv2RF+YdFJgMBCAfCvgQYFgoAcAWCZr9cywmQ+Gc9nQsN7XBFFAAAAAAA +HAAgc2FsdEBub3RhdGlvbnMub3BlbnBncGpzLm9yZw1hBEkI2dXfJXMVH/ha +uCz6Opp4tu616/VBZWGParIfApsMFiEEYmvn2jb9uzAG41Ql+Gc9nQsN7XAA +ABWvAQDZznAKuYpaWKklOVw2z8fgLP6sL9ai+zBjlWogKoswNgEAoXTZ1SOJ +bWGFbTrrvSBYui/idHvj8Tax0EDVVw6W1Qc= +=OKHa +-----END PGP PRIVATE KEY BLOCK-----` }); + + expect(privateKeyWithPublicKeyPackets.isDecrypted()).to.be.true; + expect(privateKeyWithPublicKeyPackets.isPrivate()).to.be.true; + const signingKey = await privateKeyWithPublicKeyPackets.getSigningKey(); + expect(signingKey.getKeyID().equals(privateKeyWithPublicKeyPackets.subkeys[0].getKeyID())).to.be.true; + const decryptedKeys = await privateKeyWithPublicKeyPackets.getDecryptionKeys(); + expect(decryptedKeys.length).to.equal(1); + expect(decryptedKeys[0].getKeyID().equals(privateKeyWithPublicKeyPackets.subkeys[2].getKeyID())).to.be.true; + }); + it('Parses V5 sample key', async function() { // sec ed25519 2019-03-20 [SC] // 19347BC9872464025F99DF3EC2E0000ED9884892E1F7B3EA4C94009159569B54 From 366d2c8eee9403cf0a64d3dd45dbb131bfdd4e22 Mon Sep 17 00:00:00 2001 From: larabr <7375870+larabr@users.noreply.github.com> Date: Fri, 16 Aug 2024 17:50:30 +0200 Subject: [PATCH 4/4] Add support for key encryption, decryption and validation --- src/key/private_key.js | 24 ++++---- src/openpgp.js | 14 +++-- test/general/key.js | 123 +++++++++++++++++++++++------------------ 3 files changed, 91 insertions(+), 70 deletions(-) diff --git a/src/key/private_key.js b/src/key/private_key.js index b255eb34..b85d2784 100644 --- a/src/key/private_key.js +++ b/src/key/private_key.js @@ -142,7 +142,7 @@ class PrivateKey extends PublicKey { } let signingKeyPacket; - if (!this.keyPacket.isDummy()) { + if (!helper.isPublicOrDummyKeyPacket(this.keyPacket)) { signingKeyPacket = this.keyPacket; } else { /** @@ -151,7 +151,7 @@ class PrivateKey extends PublicKey { */ const signingKey = await this.getSigningKey(null, null, undefined, { ...config, rejectPublicKeyAlgorithms: new Set(), minRSABits: 0 }); // This could again be a dummy key - if (signingKey && !signingKey.keyPacket.isDummy()) { + if (signingKey && !helper.isPublicOrDummyKeyPacket(signingKey.keyPacket)) { signingKeyPacket = signingKey.keyPacket; } } @@ -160,12 +160,12 @@ class PrivateKey extends PublicKey { return signingKeyPacket.validate(); } else { const keys = this.getKeys(); - const allDummies = keys.map(key => key.keyPacket.isDummy()).every(Boolean); - if (allDummies) { - throw new Error('Cannot validate an all-gnu-dummy key'); + const allPublicOrDummies = keys.map(key => helper.isPublicOrDummyKeyPacket(key.keyPacket)).every(Boolean); + if (allPublicOrDummies) { + throw new Error('Cannot validate key without secret key material'); } - return Promise.all(keys.map(async key => key.keyPacket.validate())); + return Promise.all(keys.map(async key => helper.isPublicOrDummyKeyPacket(key.keyPacket) || key.keyPacket.validate())); } } @@ -235,14 +235,14 @@ class PrivateKey extends PublicKey { if (options.rsaBits < config.minRSABits) { throw new Error(`rsaBits should be at least ${config.minRSABits}, got: ${options.rsaBits}`); } - const secretKeyPacket = this.keyPacket; - if (secretKeyPacket.isDummy()) { - throw new Error('Cannot add subkey to gnu-dummy primary key'); + const primaryKeyPacket = this.keyPacket; + if (helper.isPublicOrDummyKeyPacket(primaryKeyPacket)) { + throw new Error('Cannot add subkey to gnu-dummy or public primary key'); } - if (!secretKeyPacket.isDecrypted()) { + if (!primaryKeyPacket.isDecrypted()) { throw new Error('Key is not decrypted'); } - const defaultOptions = secretKeyPacket.getAlgorithmInfo(); + const defaultOptions = primaryKeyPacket.getAlgorithmInfo(); defaultOptions.type = getDefaultSubkeyType(defaultOptions.algorithm); defaultOptions.rsaBits = defaultOptions.bits || 4096; defaultOptions.curve = defaultOptions.curve || 'curve25519Legacy'; @@ -253,7 +253,7 @@ class PrivateKey extends PublicKey { // The config is always overwritten since we cannot tell if the defaultConfig was changed by the user. const keyPacket = await helper.generateSecretSubkey(options, { ...config, v6Keys: this.keyPacket.version === 6 }); helper.checkKeyRequirements(keyPacket, config); - const bindingSignature = await helper.createBindingSignature(keyPacket, secretKeyPacket, options, config); + const bindingSignature = await helper.createBindingSignature(keyPacket, primaryKeyPacket, options, config); const packetList = this.toPacketList(); packetList.push(keyPacket, bindingSignature); return new PrivateKey(packetList); diff --git a/src/openpgp.js b/src/openpgp.js index 7fd8a938..06a2c308 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -21,7 +21,7 @@ import { CleartextMessage } from './cleartext'; import { generate, reformat, getPreferredCompressionAlgo } from './key'; import defaultConfig from './config'; import util from './util'; -import { checkKeyRequirements } from './key/helper'; +import { checkKeyRequirements, isPublicOrDummyKeyPacket } from './key/helper'; ////////////////////// @@ -189,10 +189,13 @@ export async function decryptKey({ privateKey, passphrase, config, ...rest }) { const passphrases = util.isArray(passphrase) ? passphrase : [passphrase]; try { - await Promise.all(clonedPrivateKey.getKeys().map(key => ( + await Promise.all(clonedPrivateKey.getKeys().map(key => { + if (isPublicOrDummyKeyPacket(key.keyPacket)) { + return; + } // try to decrypt each key with any of the given passphrases - util.anyPromise(passphrases.map(passphrase => key.keyPacket.decrypt(passphrase))) - ))); + return util.anyPromise(passphrases.map(passphrase => key.keyPacket.decrypt(passphrase))); + })); await clonedPrivateKey.validate(config); return clonedPrivateKey; @@ -230,6 +233,9 @@ export async function encryptKey({ privateKey, passphrase, config, ...rest }) { try { await Promise.all(keys.map(async (key, i) => { const { keyPacket } = key; + if (isPublicOrDummyKeyPacket(keyPacket)) { + return; + } await keyPacket.encrypt(passphrases[i], config); keyPacket.clearPrivateParams(); })); diff --git a/test/general/key.js b/test/general/key.js index 57b40d84..1252be27 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -1857,6 +1857,54 @@ S2rSjQ4JF0Ktgdr9585haknpGwr31t486KxXOY4AEsiBmRyvTbaQegwKaQ+C -----END PGP PRIVATE KEY BLOCK----- `; +// public primary key with: +// - one signing private subkey +// - one signing public subkey +// - one encryption private subkey +// - one encryption public subkey +const privateKeyWithPublicKeyPackets = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xjMEZr9cyxYJKwYBBAHaRw8BAQdAlC7g7Q4UZmN5zNhFynWuEXYn1KV8Jfp4 +DMiKf8ZQ5oDNDzx0ZXN0QHRlc3QuY29tPsLAEwQTFgoAhQWCZr9cywMLCQcJ +kPhnPZ0LDe1wRRQAAAAAABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5v +cmeWobLLkXmgpg9Pet9xOLiKvUAGKchlrMxBubhvTpBGygUVCAoMDgQWAAIB +AhkBApsDAh4BFiEEYmvn2jb9uzAG41Ql+Gc9nQsN7XAAAHGyAP9URiF2uq/O +8PJkjoehixMx9h9a8L+lPd56ymbdxUb/swEA0tNWA9alk/m3rml0aTPNucuG +Aj/FLBixvbva+SWEkgrOMwRmv1zLFgkrBgEEAdpHDwEBB0DdyjGHiVCA0Cbr +5u5pcuZmYZylLDURujjhEWYcLKIg0MLAuwQYFgoBLQWCZr9cywmQ+Gc9nQsN +7XBFFAAAAAAAHAAgc2FsdEBub3RhdGlvbnMub3BlbnBncGpzLm9yZ5UtjGIA +N+2fOlsIv9vwi5MaTUxoXXc+Mw6ETjRvW3fUApsCvKAEGRYKAG0Fgma/XMsJ +kHC3NLlxFk7wRRQAAAAAABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5v +cme74M+JTJ/Clb0bApLqL/QGKOZbaXdL3Y/p02QX2o6AgRYhBFFraL50hRm3 +RC4PZHC3NLlxFk7wAABAgwEAhQ+EgvirBcabNlmHV6nEbqyTz85oBj8SKNM2 +d+tDQ3YBAN3jXoPGx22sVoy5rAMerwq1lhnlgCh4xfdTw1jtr2QEFiEEYmvn +2jb9uzAG41Ql+Gc9nQsN7XAAALGsAQCCDkhWJRgiCoHIvjWwRqVWU1BsVaj5 +dr6fxkK9yaOv6QEA+JjqAgPcRo/LGHpO0dZS9qi1Zpy4u5bT2FunXqvNDQ7O +MwRmv1zLFgkrBgEEAdpHDwEBB0A4CyxR5ZSvF8FrEhjzFO8W6paAtqm92p7k ++HQC7TRhKcLAuwQYFgoBLQWCZr9cywmQ+Gc9nQsN7XBFFAAAAAAAHAAgc2Fs +dEBub3RhdGlvbnMub3BlbnBncGpzLm9yZ8eA420LE1oqZm9UQ2mAl0VpYysv +9w9FhamakpSbum0jApsCvKAEGRYKAG0Fgma/XMsJkMrApZFFMT1MRRQAAAAA +ABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5vcmcyU0rvMw4GVCasfsGO +ke8VBnf9DX73KF4Fuw5HRwy65BYhBPmjv8j7/15Fu3bvL8rApZFFMT1MAADv +OgEAp2P/olg/8frAKhZNhBNNcnZ3mBPgw+jnVB2q3lJR3G4A/Az8kxPmyhmf +vaorTRswb2d7xes3ubunWLvHBcU+LSAIFiEEYmvn2jb9uzAG41Ql+Gc9nQsN +7XAAAO9CAP4y3wh7tKf5QkWhz18Lo2Zjqv4S+rCVJZWe9SDRFJq4rwD/eJTb +wFTiP1x1KHQY+bnZQUll10A9c/G9yMrOC0qR8gHHXQRmv1zLEgorBgEEAZdV +AQUBAQdAdWmLtTwhcOH7tlr+2FEysYY3Z6gPpP6EalQnbXEHKRsDAQgHAAD/ +ROS5zCtNP0O2t9U7T/Cs2+n1snTVW+RBTbESM+hjWfgTIcK+BBgWCgBwBYJm +v1zLCZD4Zz2dCw3tcEUUAAAAAAAcACBzYWx0QG5vdGF0aW9ucy5vcGVucGdw +anMub3JniqUd+21Sxr5oHb+ctdQKLEpRYCSR1I0Ap/Z5o3XfqRACmwwWIQRi +a+faNv27MAbjVCX4Zz2dCw3tcAAABPQBAK7S2Izm3D4dEkoJb869wOqtK4be +zdP4cdQMfO/4hsLRAPwMxCX51Okaj/SvwEzjI/RCPmfv9l441PmzQheQ4Gqr +BM44BGa/XMsSCisGAQQBl1UBBQEBB0CqHCGPBZg8ioGvjV4nKteCFs4hAzLn +nFHyv2RF+YdFJgMBCAfCvgQYFgoAcAWCZr9cywmQ+Gc9nQsN7XBFFAAAAAAA +HAAgc2FsdEBub3RhdGlvbnMub3BlbnBncGpzLm9yZw1hBEkI2dXfJXMVH/ha +uCz6Opp4tu616/VBZWGParIfApsMFiEEYmvn2jb9uzAG41Ql+Gc9nQsN7XAA +ABWvAQDZznAKuYpaWKklOVw2z8fgLP6sL9ai+zBjlWogKoswNgEAoXTZ1SOJ +bWGFbTrrvSBYui/idHvj8Tax0EDVVw6W1Qc= +=OKHa +-----END PGP PRIVATE KEY BLOCK-----`; + const eddsaKeyAsEcdsa = ` -----BEGIN PGP PRIVATE KEY BLOCK----- Version: OpenPGP.js VERSION @@ -3020,61 +3068,15 @@ export default () => describe('Key', function() { }); it('Parsing armored key with public primary key and private subkeys', async function() { - // public primary key with: - // - one signing private subkey - // - one signing public subkey - // - one encryption private subkey - // - one encryption public subkey - const privateKeyWithPublicKeyPackets = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- + const key = await openpgp.readKey({ armoredKey: privateKeyWithPublicKeyPackets }); -xjMEZr9cyxYJKwYBBAHaRw8BAQdAlC7g7Q4UZmN5zNhFynWuEXYn1KV8Jfp4 -DMiKf8ZQ5oDNDzx0ZXN0QHRlc3QuY29tPsLAEwQTFgoAhQWCZr9cywMLCQcJ -kPhnPZ0LDe1wRRQAAAAAABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5v -cmeWobLLkXmgpg9Pet9xOLiKvUAGKchlrMxBubhvTpBGygUVCAoMDgQWAAIB -AhkBApsDAh4BFiEEYmvn2jb9uzAG41Ql+Gc9nQsN7XAAAHGyAP9URiF2uq/O -8PJkjoehixMx9h9a8L+lPd56ymbdxUb/swEA0tNWA9alk/m3rml0aTPNucuG -Aj/FLBixvbva+SWEkgrOMwRmv1zLFgkrBgEEAdpHDwEBB0DdyjGHiVCA0Cbr -5u5pcuZmYZylLDURujjhEWYcLKIg0MLAuwQYFgoBLQWCZr9cywmQ+Gc9nQsN -7XBFFAAAAAAAHAAgc2FsdEBub3RhdGlvbnMub3BlbnBncGpzLm9yZ5UtjGIA -N+2fOlsIv9vwi5MaTUxoXXc+Mw6ETjRvW3fUApsCvKAEGRYKAG0Fgma/XMsJ -kHC3NLlxFk7wRRQAAAAAABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5v -cme74M+JTJ/Clb0bApLqL/QGKOZbaXdL3Y/p02QX2o6AgRYhBFFraL50hRm3 -RC4PZHC3NLlxFk7wAABAgwEAhQ+EgvirBcabNlmHV6nEbqyTz85oBj8SKNM2 -d+tDQ3YBAN3jXoPGx22sVoy5rAMerwq1lhnlgCh4xfdTw1jtr2QEFiEEYmvn -2jb9uzAG41Ql+Gc9nQsN7XAAALGsAQCCDkhWJRgiCoHIvjWwRqVWU1BsVaj5 -dr6fxkK9yaOv6QEA+JjqAgPcRo/LGHpO0dZS9qi1Zpy4u5bT2FunXqvNDQ7O -MwRmv1zLFgkrBgEEAdpHDwEBB0A4CyxR5ZSvF8FrEhjzFO8W6paAtqm92p7k -+HQC7TRhKcLAuwQYFgoBLQWCZr9cywmQ+Gc9nQsN7XBFFAAAAAAAHAAgc2Fs -dEBub3RhdGlvbnMub3BlbnBncGpzLm9yZ8eA420LE1oqZm9UQ2mAl0VpYysv -9w9FhamakpSbum0jApsCvKAEGRYKAG0Fgma/XMsJkMrApZFFMT1MRRQAAAAA -ABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5vcmcyU0rvMw4GVCasfsGO -ke8VBnf9DX73KF4Fuw5HRwy65BYhBPmjv8j7/15Fu3bvL8rApZFFMT1MAADv -OgEAp2P/olg/8frAKhZNhBNNcnZ3mBPgw+jnVB2q3lJR3G4A/Az8kxPmyhmf -vaorTRswb2d7xes3ubunWLvHBcU+LSAIFiEEYmvn2jb9uzAG41Ql+Gc9nQsN -7XAAAO9CAP4y3wh7tKf5QkWhz18Lo2Zjqv4S+rCVJZWe9SDRFJq4rwD/eJTb -wFTiP1x1KHQY+bnZQUll10A9c/G9yMrOC0qR8gHHXQRmv1zLEgorBgEEAZdV -AQUBAQdAdWmLtTwhcOH7tlr+2FEysYY3Z6gPpP6EalQnbXEHKRsDAQgHAAD/ -ROS5zCtNP0O2t9U7T/Cs2+n1snTVW+RBTbESM+hjWfgTIcK+BBgWCgBwBYJm -v1zLCZD4Zz2dCw3tcEUUAAAAAAAcACBzYWx0QG5vdGF0aW9ucy5vcGVucGdw -anMub3JniqUd+21Sxr5oHb+ctdQKLEpRYCSR1I0Ap/Z5o3XfqRACmwwWIQRi -a+faNv27MAbjVCX4Zz2dCw3tcAAABPQBAK7S2Izm3D4dEkoJb869wOqtK4be -zdP4cdQMfO/4hsLRAPwMxCX51Okaj/SvwEzjI/RCPmfv9l441PmzQheQ4Gqr -BM44BGa/XMsSCisGAQQBl1UBBQEBB0CqHCGPBZg8ioGvjV4nKteCFs4hAzLn -nFHyv2RF+YdFJgMBCAfCvgQYFgoAcAWCZr9cywmQ+Gc9nQsN7XBFFAAAAAAA -HAAgc2FsdEBub3RhdGlvbnMub3BlbnBncGpzLm9yZw1hBEkI2dXfJXMVH/ha -uCz6Opp4tu616/VBZWGParIfApsMFiEEYmvn2jb9uzAG41Ql+Gc9nQsN7XAA -ABWvAQDZznAKuYpaWKklOVw2z8fgLP6sL9ai+zBjlWogKoswNgEAoXTZ1SOJ -bWGFbTrrvSBYui/idHvj8Tax0EDVVw6W1Qc= -=OKHa ------END PGP PRIVATE KEY BLOCK-----` }); - - expect(privateKeyWithPublicKeyPackets.isDecrypted()).to.be.true; - expect(privateKeyWithPublicKeyPackets.isPrivate()).to.be.true; - const signingKey = await privateKeyWithPublicKeyPackets.getSigningKey(); - expect(signingKey.getKeyID().equals(privateKeyWithPublicKeyPackets.subkeys[0].getKeyID())).to.be.true; - const decryptedKeys = await privateKeyWithPublicKeyPackets.getDecryptionKeys(); + expect(key.isDecrypted()).to.be.true; + expect(key.isPrivate()).to.be.true; + const signingKey = await key.getSigningKey(); + expect(signingKey.getKeyID().equals(key.subkeys[0].getKeyID())).to.be.true; + const decryptedKeys = await key.getDecryptionKeys(); expect(decryptedKeys.length).to.equal(1); - expect(decryptedKeys[0].getKeyID().equals(privateKeyWithPublicKeyPackets.subkeys[2].getKeyID())).to.be.true; + expect(decryptedKeys[0].getKeyID().equals(key.subkeys[2].getKeyID())).to.be.true; }); it('Parses V5 sample key', async function() { @@ -3667,7 +3669,7 @@ PzIEeL7UH3trraFmi+Gq8u4kAA== it('validate() - throw if all-gnu-dummy key', async function() { const key = await openpgp.readKey({ armoredKey: gnuDummyKey }); - await expect(key.validate()).to.be.rejectedWith('Cannot validate an all-gnu-dummy key'); + await expect(key.validate()).to.be.rejectedWith('Cannot validate key without secret key material'); }); it('validate() - gnu-dummy primary key with signing subkey', async function() { @@ -3680,6 +3682,11 @@ PzIEeL7UH3trraFmi+Gq8u4kAA== await expect(key.validate()).to.not.be.rejected; }); + it('validate() - key with public key packets', async function() { + const key = await openpgp.readKey({ armoredKey: privateKeyWithPublicKeyPackets }); + await expect(key.validate()).to.not.be.rejected; + }); + it('validate() - curve ed25519 (eddsa) cannot be used for ecdsa', async function() { const key = await openpgp.readKey({ armoredKey: eddsaKeyAsEcdsa }); await expect(key.validate()).to.be.rejectedWith('Key is invalid'); @@ -4445,6 +4452,14 @@ VYGdb3eNlV8CfoEC await expect(openpgp.encryptKey({ privateKey: encryptedKey, passphrase })).to.be.eventually.rejectedWith(/Key packet is already encrypted/); }); + it('Should support encrypting a private key which includes public key packets', async function() { + const passphrase = 'hello world'; + const key = await openpgp.readKey({ armoredKey: privateKeyWithPublicKeyPackets }); + const encryptedKey = await openpgp.encryptKey({ privateKey: key, passphrase }); + const decryptedKey = await openpgp.decryptKey({ privateKey: encryptedKey, passphrase }); + await expect(decryptedKey.write()).to.deep.equal(key.write()); + }); + describe('addSubkey functionality testing', function() { const rsaBits = 1024; let minRSABits;