diff --git a/extension/js/common/core/msg-block-parser.ts b/extension/js/common/core/msg-block-parser.ts index 5a75a278885..b9cbe901feb 100644 --- a/extension/js/common/core/msg-block-parser.ts +++ b/extension/js/common/core/msg-block-parser.ts @@ -162,6 +162,34 @@ export class MsgBlockParser { ); } + /** + * Find an end marker that is NOT on a quoted line (lines starting with '>'). + * This is needed for signed messages that may contain quoted inner signatures. + */ + private static findNonQuotedEndMarker(text: string, startIndex: number, endMarker: string): number { + let searchFrom = startIndex; + // eslint-disable-next-line no-constant-condition + while (true) { + const endIndex = text.indexOf(endMarker, searchFrom); + if (endIndex === -1) { + return -1; // No end marker found + } + // Find the start of the line containing this end marker + const lastLf = text.lastIndexOf('\n', endIndex - 1); + const lastCr = text.lastIndexOf('\r', endIndex - 1); + const lineStart = Math.max(lastLf, lastCr) + 1; + const linePrefix = text.substring(lineStart, endIndex); + // Check if line prefix contains quote markers (ignoring whitespace) + // A quoted line has '>' as the first non-whitespace character or after other '>' + const isQuoted = /^\s*>/.test(linePrefix); + if (!isQuoted) { + return endIndex; // Found a non-quoted end marker + } + // Continue searching after this occurrence + searchFrom = endIndex + endMarker.length; + } + } + private static detectBlockNext(origText: string, startAt: number, completeOnly?: boolean) { const armorHdrTypes = Object.keys(PgpArmor.ARMOR_HEADER_DICT) as ReplaceableMsgBlockType[]; const result: { found: MsgBlock[]; continueAt?: number } = { found: [] as MsgBlock[] }; @@ -190,7 +218,13 @@ export class MsgBlockParser { let endIndex = -1; let foundBlockEndHeaderLength = 0; if (typeof blockHeaderDef.end === 'string') { - endIndex = origText.indexOf(blockHeaderDef.end, begin + blockHeaderDef.begin.length); + // For signed messages, we need to find the end marker that is NOT quoted + // (i.e., not on a line starting with '>'). This handles nested quoted signatures. + if (armorHdrType === 'signedMsg') { + endIndex = MsgBlockParser.findNonQuotedEndMarker(origText, begin + blockHeaderDef.begin.length, blockHeaderDef.end); + } else { + endIndex = origText.indexOf(blockHeaderDef.end, begin + blockHeaderDef.begin.length); + } foundBlockEndHeaderLength = blockHeaderDef.end.length; } else { // regexp diff --git a/test/source/tests/unit-node.ts b/test/source/tests/unit-node.ts index cb246849a14..ff84049154b 100644 --- a/test/source/tests/unit-node.ts +++ b/test/source/tests/unit-node.ts @@ -224,6 +224,41 @@ Something wrong with this key`), t.pass(); }); + test(`[unit][MsgBlockParser.detectBlocks] correctly handles nested quoted signed messages`, async t => { + // This tests the fix for the bug where a signed message containing a quoted signed message + // would cause "Misformed armored text" error. The parser should find the correct (outer, + // non-quoted) END PGP SIGNATURE marker instead of the quoted inner one. + const nestedSignedMessage = `-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +This is the outer message. + +On 2026-01-15, someone@example.com wrote: +> -----BEGIN PGP SIGNED MESSAGE----- +> Hash: SHA512 +> +> This is the quoted inner message. +> +> -----BEGIN PGP SIGNATURE----- +> quotedSignatureData +> -----END PGP SIGNATURE----- + +-----BEGIN PGP SIGNATURE----- +outerSignatureData +-----END PGP SIGNATURE-----`; + + const { blocks } = MsgBlockParser.detectBlocks(nestedSignedMessage); + expect(blocks).to.have.length(1); + expect(blocks[0].type).to.equal('signedMsg'); + // The block should contain the entire message including the quoted inner signature + expect(blocks[0].content).to.include('outerSignatureData'); + expect(blocks[0].content).to.include('quotedSignatureData'); + expect(blocks[0].content).to.include('-----BEGIN PGP SIGNED MESSAGE-----'); + // Verify it ends with the outer signature, not the quoted one + expect(blocks[0].content).to.match(/outerSignatureData\n-----END PGP SIGNATURE-----$/); + t.pass(); + }); + test(`[unit][PgpKey.usableForEncryptionButExpired] recognizes usable expired key`, async t => { const expiredKey = await KeyUtil.parse(testConstants.expiredPrv); expect(expiredKey.expiration).to.equal(1567605343000);