From ce0725539300329c7399d4fe19548b5cbb4e39d3 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Fri, 13 Mar 2026 02:03:36 +0000 Subject: [PATCH] Matrix: delay inbound SAS auto-confirm --- .../matrix/sdk/verification-manager.test.ts | 59 +++++++++++++++++-- .../src/matrix/sdk/verification-manager.ts | 49 +++++++++++---- 2 files changed, 91 insertions(+), 17 deletions(-) diff --git a/extensions/matrix/src/matrix/sdk/verification-manager.test.ts b/extensions/matrix/src/matrix/sdk/verification-manager.test.ts index f88f6441a4a..0b8eaa45897 100644 --- a/extensions/matrix/src/matrix/sdk/verification-manager.test.ts +++ b/extensions/matrix/src/matrix/sdk/verification-manager.test.ts @@ -165,7 +165,7 @@ describe("MatrixVerificationManager", () => { expect(sas.emoji?.length).toBe(3); await manager.confirmVerificationSas(tracked.id); - expect(confirm).toHaveBeenCalledTimes(2); + expect(confirm).toHaveBeenCalledTimes(1); manager.mismatchVerificationSas(tracked.id); expect(mismatch).toHaveBeenCalledTimes(1); @@ -256,7 +256,8 @@ describe("MatrixVerificationManager", () => { expect(manager.getVerificationSas(tracked.id).decimal).toEqual([1234, 5678, 9012]); }); - it("auto-confirms inbound SAS when callbacks are available", async () => { + it("auto-confirms inbound SAS after a short delay", async () => { + vi.useFakeTimers(); const confirm = vi.fn(async () => {}); const verifier = new MockVerifier( { @@ -280,12 +281,18 @@ describe("MatrixVerificationManager", () => { initiatedByMe: false, verifier, }); - const manager = new MatrixVerificationManager(); - manager.trackVerificationRequest(request); + try { + const manager = new MatrixVerificationManager(); + manager.trackVerificationRequest(request); - await vi.waitFor(() => { + await vi.advanceTimersByTimeAsync(1000); + expect(confirm).not.toHaveBeenCalled(); + + await vi.advanceTimersByTimeAsync(600); expect(confirm).toHaveBeenCalledTimes(1); - }); + } finally { + vi.useRealTimers(); + } }); it("does not auto-confirm SAS for verifications initiated by this device", async () => { @@ -324,6 +331,46 @@ describe("MatrixVerificationManager", () => { } }); + it("cancels a pending auto-confirm when SAS is explicitly mismatched", async () => { + vi.useFakeTimers(); + const confirm = vi.fn(async () => {}); + const mismatch = vi.fn(); + const verifier = new MockVerifier( + { + sas: { + decimal: [444, 555, 666], + emoji: [ + ["panda", "Panda"], + ["rocket", "Rocket"], + ["crown", "Crown"], + ], + }, + confirm, + mismatch, + cancel: vi.fn(), + }, + null, + async () => {}, + ); + const request = new MockVerificationRequest({ + transactionId: "txn-mismatch-cancels-auto-confirm", + initiatedByMe: false, + verifier, + }); + try { + const manager = new MatrixVerificationManager(); + const tracked = manager.trackVerificationRequest(request); + + manager.mismatchVerificationSas(tracked.id); + await vi.advanceTimersByTimeAsync(2000); + + expect(mismatch).toHaveBeenCalledTimes(1); + expect(confirm).not.toHaveBeenCalled(); + } finally { + vi.useRealTimers(); + } + }); + it("prunes stale terminal sessions during list operations", () => { const now = new Date("2026-02-08T15:00:00.000Z").getTime(); const nowSpy = vi.spyOn(Date, "now"); diff --git a/extensions/matrix/src/matrix/sdk/verification-manager.ts b/extensions/matrix/src/matrix/sdk/verification-manager.ts index 8cff8a07954..0239ab62720 100644 --- a/extensions/matrix/src/matrix/sdk/verification-manager.ts +++ b/extensions/matrix/src/matrix/sdk/verification-manager.ts @@ -102,12 +102,14 @@ type MatrixVerificationSession = { verifyStarted: boolean; startRequested: boolean; sasAutoConfirmStarted: boolean; + sasAutoConfirmTimer?: ReturnType; sasCallbacks?: MatrixShowSasCallbacks; reciprocateQrCallbacks?: MatrixShowQrCodeCallbacks; }; const MAX_TRACKED_VERIFICATION_SESSIONS = 256; const TERMINAL_SESSION_RETENTION_MS = 24 * 60 * 60 * 1000; +const SAS_AUTO_CONFIRM_DELAY_MS = 1500; export class MatrixVerificationManager { private readonly verificationSessions = new Map(); @@ -175,6 +177,14 @@ export class MatrixVerificationManager { session.updatedAtMs = Date.now(); } + private clearSasAutoConfirmTimer(session: MatrixVerificationSession): void { + if (!session.sasAutoConfirmTimer) { + return; + } + clearTimeout(session.sasAutoConfirmTimer); + session.sasAutoConfirmTimer = undefined; + } + private buildVerificationSummary(session: MatrixVerificationSession): MatrixVerificationSummary { const request = session.request; const phase = this.readRequestValue(request, () => request.phase, VerificationPhase.Requested); @@ -329,6 +339,7 @@ export class MatrixVerificationManager { this.touchVerificationSession(session); }); verifier.on(VerifierEvent.Cancel, (err) => { + this.clearSasAutoConfirmTimer(session); session.error = err instanceof Error ? err.message : String(err); this.touchVerificationSession(session); }); @@ -336,7 +347,7 @@ export class MatrixVerificationManager { } private maybeAutoConfirmSas(session: MatrixVerificationSession): void { - if (session.sasAutoConfirmStarted) { + if (session.sasAutoConfirmStarted || session.sasAutoConfirmTimer) { return; } if (this.readRequestValue(session.request, () => session.request.initiatedByMe, true)) { @@ -347,16 +358,29 @@ export class MatrixVerificationManager { return; } session.sasCallbacks = callbacks; - session.sasAutoConfirmStarted = true; - void callbacks - .confirm() - .then(() => { - this.touchVerificationSession(session); - }) - .catch((err) => { - session.error = err instanceof Error ? err.message : String(err); - this.touchVerificationSession(session); - }); + // Give the remote client a moment to surface the compare-emoji UI before + // we send our MAC and finish our side of the SAS flow. + session.sasAutoConfirmTimer = setTimeout(() => { + session.sasAutoConfirmTimer = undefined; + const phase = this.readRequestValue( + session.request, + () => session.request.phase, + VerificationPhase.Requested, + ); + if (phase >= VerificationPhase.Cancelled) { + return; + } + session.sasAutoConfirmStarted = true; + void callbacks + .confirm() + .then(() => { + this.touchVerificationSession(session); + }) + .catch((err) => { + session.error = err instanceof Error ? err.message : String(err); + this.touchVerificationSession(session); + }); + }, SAS_AUTO_CONFIRM_DELAY_MS); } private ensureVerificationStarted(session: MatrixVerificationSession): void { @@ -535,7 +559,9 @@ export class MatrixVerificationManager { if (!callbacks) { throw new Error("Matrix SAS confirmation is not available for this verification request"); } + this.clearSasAutoConfirmTimer(session); session.sasCallbacks = callbacks; + session.sasAutoConfirmStarted = true; await callbacks.confirm(); this.touchVerificationSession(session); return this.buildVerificationSummary(session); @@ -547,6 +573,7 @@ export class MatrixVerificationManager { if (!callbacks) { throw new Error("Matrix SAS mismatch is not available for this verification request"); } + this.clearSasAutoConfirmTimer(session); session.sasCallbacks = callbacks; callbacks.mismatch(); this.touchVerificationSession(session);