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