Matrix: delay inbound SAS auto-confirm

This commit is contained in:
Gustavo Madeira Santana
2026-03-13 02:03:36 +00:00
parent 5ede08d168
commit ce07255393
2 changed files with 91 additions and 17 deletions

View File

@@ -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");

View File

@@ -102,12 +102,14 @@ type MatrixVerificationSession = {
verifyStarted: boolean;
startRequested: boolean;
sasAutoConfirmStarted: boolean;
sasAutoConfirmTimer?: ReturnType<typeof setTimeout>;
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<string, MatrixVerificationSession>();
@@ -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);