From 60f1ef89e75ec4d7b00dbf2fb55545c32ec128d8 Mon Sep 17 00:00:00 2001 From: Ioan Moldovan Date: Thu, 5 Mar 2026 01:01:52 -0800 Subject: [PATCH 1/2] feat: show correct message for sender and signer email mismatch --- extension/js/common/message-renderer.ts | 26 +++++++++++---- .../js/common/platform/store/contact-store.ts | 33 +++++++++++++++++++ test/source/tests/decrypt.ts | 25 ++++++++++++++ 3 files changed, 77 insertions(+), 7 deletions(-) diff --git a/extension/js/common/message-renderer.ts b/extension/js/common/message-renderer.ts index 1538757b886..e8602129fea 100644 --- a/extension/js/common/message-renderer.ts +++ b/extension/js/common/message-renderer.ts @@ -154,7 +154,7 @@ export class MessageRenderer { private static async renderPgpSignatureCheckResult( renderModule: RenderInterface, verifyRes: VerifyRes | undefined, - wasSignerEmailSupplied: boolean, + senderEmail: string | undefined, retryVerification?: () => Promise ) { if (verifyRes?.error) { @@ -188,25 +188,37 @@ export class MessageRenderer { retryVerificationAgain = retryVerification; } } - await MessageRenderer.renderPgpSignatureCheckResult(renderModule, verifyRes, wasSignerEmailSupplied, retryVerificationAgain); + await MessageRenderer.renderPgpSignatureCheckResult(renderModule, verifyRes, senderEmail, retryVerificationAgain); return; - } else if (!wasSignerEmailSupplied) { + } else if (!senderEmail) { // todo: unit-test this case? renderModule.renderSignatureStatus('could not verify signature: missing pubkey, missing sender info'); } else { - MessageRenderer.renderMissingPubkeyOrBadSignature(renderModule, verifyRes); + await MessageRenderer.renderMissingPubkeyOrBadSignature(renderModule, verifyRes, senderEmail); } } - private static renderMissingPubkeyOrBadSignature(renderModule: RenderInterfaceBase, verifyRes: VerifyRes): void { + private static async renderMissingPubkeyOrBadSignature(renderModule: RenderInterfaceBase, verifyRes: VerifyRes, senderEmail: string): Promise { // eslint-disable-next-line no-null/no-null if (verifyRes.match === null || !Value.arr.hasIntersection(verifyRes.signerLongids, verifyRes.suppliedLongids)) { - MessageRenderer.renderMissingPubkey(renderModule, verifyRes.signerLongids[0]); + const signerLongid = verifyRes.signerLongids[0]; + const signerEmails = await ContactStore.getEmailsByLongid(undefined, signerLongid); + const signerEmailNotMatchingSender = signerEmails.find(e => e !== senderEmail); + if (signerEmailNotMatchingSender) { + MessageRenderer.renderSignerSenderMismatch(renderModule, senderEmail, signerEmailNotMatchingSender); + } else { + MessageRenderer.renderMissingPubkey(renderModule, signerLongid); + } } else { MessageRenderer.renderBadSignature(renderModule); } } + private static renderSignerSenderMismatch(renderModule: RenderInterfaceBase, senderEmail: string, signerEmail: string) { + renderModule.renderSignatureStatus(`could not verify signature: signed by ${signerEmail} but message is from ${senderEmail}`); + renderModule.setFrameColor('red'); + } + private static renderMissingPubkey(renderModule: RenderInterfaceBase, signerLongid: string) { renderModule.renderSignatureStatus(`could not verify signature: missing pubkey ${signerLongid}`); } @@ -613,7 +625,7 @@ export class MessageRenderer { } decryptedContent = this.clipMessageIfLimitExceeds(decryptedContent); renderModule.separateQuotedContentAndRenderText(decryptedContent, isHtml, isChecksumInvalid); - await MessageRenderer.renderPgpSignatureCheckResult(renderModule, sigResult, Boolean(signerEmail), retryVerification); + await MessageRenderer.renderPgpSignatureCheckResult(renderModule, sigResult, signerEmail, retryVerification); if (renderableAttachments.length) { renderModule.renderInnerAttachments(renderableAttachments, isEncrypted); } diff --git a/extension/js/common/platform/store/contact-store.ts b/extension/js/common/platform/store/contact-store.ts index 5aa32228b7c..bbfb8d11e52 100644 --- a/extension/js/common/platform/store/contact-store.ts +++ b/extension/js/common/platform/store/contact-store.ts @@ -546,6 +546,39 @@ export class ContactStore extends AbstractStore { return (await ContactStore.extractPubkeys(db, fingerprints)).map(pubkey => pubkey.armoredKey).filter(Boolean); } + public static async getEmailsByLongid(db: IDBDatabase | undefined, longid: string): Promise { + if (!db) { + return (await BrowserMsg.retryOnBgNotReadyErr(() => BrowserMsg.send.bg.await.db({ f: 'getEmailsByLongid', args: [longid] }))) as string[]; + } + const tx = db.transaction(['pubkeys', 'emails'], 'readonly'); + const emails: string[] = []; + await new Promise((resolve, reject) => { + const req = tx.objectStore('pubkeys').index('index_longids').getAll(longid); + ContactStore.setReqPipe( + req, + (pubkeys: Pubkey[]) => { + if (!pubkeys.length) { + resolve(); + return; + } + ContactStore.setTxHandlers(tx, () => resolve(), reject); + for (const pubkey of pubkeys) { + const req2 = tx.objectStore('emails').index('index_fingerprints').getAll(pubkey.fingerprint); + ContactStore.setReqPipe(req2, (emailEntities: Email[]) => { + for (const entity of emailEntities) { + if (!emails.includes(entity.email)) { + emails.push(entity.email); + } + } + }); + } + }, + reject + ); + }); + return emails; + } + public static async getOneWithAllPubkeys(db: IDBDatabase | undefined, email: string): Promise { if (!db) { // relay op through background process diff --git a/test/source/tests/decrypt.ts b/test/source/tests/decrypt.ts index 5e5ca20d134..fd4c59a15a6 100644 --- a/test/source/tests/decrypt.ts +++ b/test/source/tests/decrypt.ts @@ -1535,6 +1535,31 @@ XZ8r4OC6sguP/yozWlkG+7dDxsgKQVBENeG6Lw== }) ); + test( + 'signature - shows sender/signer mismatch when signer key belongs to a different email', + testWithBrowser(async (t, browser) => { + const threadId = '1766644f13510f58'; + const { acctEmail, authHdr } = await BrowserRecipe.setupCommonAcctWithAttester(t, browser, 'ci.tests.gmail'); + const dbPage = await browser.newExtensionPage(t, 'chrome/dev/ci_unit_test.htm'); + await dbPage.page.evaluate(async (pubkey: string) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const key = await (window as any).KeyUtil.parse(pubkey); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await (window as any).ContactStore.update(undefined, 'actual.signer@example.com', { pubkey: key }); + }, testConstants.pubkey2864E326A5BE488A); + await dbPage.close(); + const gmailPage = await browser.newPage(t, `${t.context.urls?.mockGmailUrl()}/${threadId}`, undefined, authHdr); + await gmailPage.waitAll('iframe', { timeout: 2 }); + const pgpBlock = await gmailPage.getFrame(['/chrome/elements/pgp_block.htm'], { sleep: 10, timeout: 20 }); + const expectedMessage = { + content: ['How is my message signed?'], + encryption: 'not encrypted', + signature: 'could not verify signature: signed by actual.signer@example.com but message is from sender@example.com', + }; + await BrowserRecipe.pgpBlockCheck(t, pgpBlock, expectedMessage); + }) + ); + test( 'decrypt - protonmail - PGP/inline signed and encrypted message with pubkey - pubkey signature is ignored', testWithBrowser(async (t, browser) => { From 189f707f51a3987988163a0628f8609da770e5d2 Mon Sep 17 00:00:00 2001 From: Ioan Moldovan Date: Thu, 5 Mar 2026 02:55:37 -0800 Subject: [PATCH 2/2] fix: test --- test/source/tests/decrypt.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/source/tests/decrypt.ts b/test/source/tests/decrypt.ts index fd4c59a15a6..9615a82f1ba 100644 --- a/test/source/tests/decrypt.ts +++ b/test/source/tests/decrypt.ts @@ -177,7 +177,7 @@ export const defineDecryptTests = (testVariant: TestVariant, testWithBrowser: Te const { acctEmail, authHdr } = await BrowserRecipe.setupCommonAcctWithAttester(t, browser, 'compatibility'); const expectedMessage = { encryption: 'not encrypted', - signature: 'could not verify signature: missing pubkey ADAC279C95093207', + signature: 'could not verify signature: signed by flowcrypt.compatibility@gmail.com but message is from sender@domain.com', content: ['flowcrypt-browser issue #5029 test email'], }; const inboxPage = await browser.newExtensionPage(t, `chrome/settings/inbox/inbox.htm?acctEmail=${acctEmail}&threadId=${threadId}`); @@ -542,7 +542,7 @@ export const defineDecryptTests = (testVariant: TestVariant, testWithBrowser: Te { content: ['this was encrypted with gpg', 'gpg --sign --armor -r flowcrypt.compatibility@gmail.com ./text.txt'], encryption: 'not encrypted', - signature: 'could not verify signature: missing pubkey 7FDE685548AEA788', + signature: 'could not verify signature: signed by flowcrypt.compatibility@gmail.com but message is from sender@domain.com', quoted: false, }, authHdr @@ -1539,7 +1539,7 @@ XZ8r4OC6sguP/yozWlkG+7dDxsgKQVBENeG6Lw== 'signature - shows sender/signer mismatch when signer key belongs to a different email', testWithBrowser(async (t, browser) => { const threadId = '1766644f13510f58'; - const { acctEmail, authHdr } = await BrowserRecipe.setupCommonAcctWithAttester(t, browser, 'ci.tests.gmail'); + const { authHdr } = await BrowserRecipe.setupCommonAcctWithAttester(t, browser, 'ci.tests.gmail'); const dbPage = await browser.newExtensionPage(t, 'chrome/dev/ci_unit_test.htm'); await dbPage.page.evaluate(async (pubkey: string) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any