Matrix-js: harden rust verification request handling

This commit is contained in:
Gustavo Madeira Santana
2026-02-23 00:55:44 -05:00
parent 0bbd8ef439
commit c17fae4a11
4 changed files with 112 additions and 22 deletions

View File

@@ -258,6 +258,41 @@ describe("MatrixCryptoBootstrapper", () => {
expect(verificationRequest.accept).toHaveBeenCalledTimes(1);
});
it("still auto-accepts verification when tracking summary throws", async () => {
const deps = createBootstrapperDeps();
deps.verificationManager.trackVerificationRequest = vi.fn(() => {
throw new Error("summary failure");
});
const listeners = new Map<string, (...args: unknown[]) => void>();
const crypto = createCryptoApi({
getDeviceVerificationStatus: vi.fn(async () => ({
isVerified: () => true,
})),
on: vi.fn((eventName: string, listener: (...args: unknown[]) => void) => {
listeners.set(eventName, listener);
}),
});
const bootstrapper = new MatrixCryptoBootstrapper(
deps as unknown as MatrixCryptoBootstrapperDeps<MatrixRawEvent>,
);
await bootstrapper.bootstrap(crypto);
const verificationRequest = {
otherUserId: "@alice:example.org",
isSelfVerification: false,
initiatedByMe: false,
accept: vi.fn(async () => {}),
};
const listener = Array.from(listeners.entries()).find(([eventName]) =>
eventName.toLowerCase().includes("verificationrequest"),
)?.[1];
expect(listener).toBeTypeOf("function");
await listener?.(verificationRequest);
expect(verificationRequest.accept).toHaveBeenCalledTimes(1);
});
it("registers verification listeners only once across repeated bootstrap calls", async () => {
const deps = createBootstrapperDeps();
const crypto = createCryptoApi({

View File

@@ -221,7 +221,15 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
// Auto-accept incoming verification requests from other users/devices.
crypto.on(CryptoEvent.VerificationRequestReceived, async (request) => {
const verificationRequest = request as MatrixVerificationRequestLike;
this.deps.verificationManager.trackVerificationRequest(verificationRequest);
try {
this.deps.verificationManager.trackVerificationRequest(verificationRequest);
} catch (err) {
LogService.warn(
"MatrixClientLite",
`Failed to track verification request from ${verificationRequest.otherUserId}:`,
err,
);
}
const otherUserId = verificationRequest.otherUserId;
const isSelfVerification = verificationRequest.isSelfVerification;
const initiatedByMe = verificationRequest.initiatedByMe;

View File

@@ -84,6 +84,25 @@ class MockVerificationRequest extends EventEmitter implements MatrixVerification
}
describe("MatrixVerificationManager", () => {
it("handles rust verification requests whose methods getter throws", () => {
const manager = new MatrixVerificationManager();
const request = new MockVerificationRequest({
transactionId: "txn-rust-methods",
phase: VerificationPhase.Requested,
});
Object.defineProperty(request, "methods", {
get() {
throw new Error("not implemented");
},
});
const summary = manager.trackVerificationRequest(request);
expect(summary.id).toBeTruthy();
expect(summary.methods).toEqual([]);
expect(summary.phase).toBe(VerificationPhase.Requested);
});
it("reuses the same tracked id for repeated transaction IDs", () => {
const manager = new MatrixVerificationManager();
const first = new MockVerificationRequest({

View File

@@ -109,9 +109,21 @@ export class MatrixVerificationManager {
private readonly trackedVerificationRequests = new WeakSet<object>();
private readonly trackedVerificationVerifiers = new WeakSet<object>();
private readRequestValue<T>(
request: MatrixVerificationRequestLike,
reader: () => T,
fallback: T,
): T {
try {
return reader();
} catch {
return fallback;
}
}
private pruneVerificationSessions(nowMs: number): void {
for (const [id, session] of this.verificationSessions) {
const phase = session.request.phase;
const phase = this.readRequestValue(session.request, () => session.request.phase, -1);
const isTerminal = phase === VerificationPhase.Done || phase === VerificationPhase.Cancelled;
if (isTerminal && nowMs - session.updatedAtMs > TERMINAL_SESSION_RETENTION_MS) {
this.verificationSessions.delete(id);
@@ -159,21 +171,28 @@ export class MatrixVerificationManager {
private buildVerificationSummary(session: MatrixVerificationSession): MatrixVerificationSummary {
const request = session.request;
const phase = request.phase;
const canAccept = phase < VerificationPhase.Ready && !request.accepting && !request.declining;
const phase = this.readRequestValue(request, () => request.phase, VerificationPhase.Requested);
const accepting = this.readRequestValue(request, () => request.accepting, false);
const declining = this.readRequestValue(request, () => request.declining, false);
const pending = this.readRequestValue(request, () => request.pending, false);
const methodsRaw = this.readRequestValue<unknown>(request, () => request.methods, []);
const methods = Array.isArray(methodsRaw)
? methodsRaw.filter((entry): entry is string => typeof entry === "string")
: [];
const canAccept = phase < VerificationPhase.Ready && !accepting && !declining;
return {
id: session.id,
transactionId: request.transactionId,
roomId: request.roomId,
otherUserId: request.otherUserId,
otherDeviceId: request.otherDeviceId,
isSelfVerification: request.isSelfVerification,
initiatedByMe: request.initiatedByMe,
transactionId: this.readRequestValue(request, () => request.transactionId, undefined),
roomId: this.readRequestValue(request, () => request.roomId, undefined),
otherUserId: this.readRequestValue(request, () => request.otherUserId, "unknown"),
otherDeviceId: this.readRequestValue(request, () => request.otherDeviceId, undefined),
isSelfVerification: this.readRequestValue(request, () => request.isSelfVerification, false),
initiatedByMe: this.readRequestValue(request, () => request.initiatedByMe, false),
phase,
phaseName: this.getVerificationPhaseName(phase),
pending: request.pending,
methods: Array.isArray(request.methods) ? request.methods : [],
chosenMethod: request.chosenMethod ?? null,
pending,
methods,
chosenMethod: this.readRequestValue(request, () => request.chosenMethod ?? null, null),
canAccept,
hasSas: Boolean(session.sasCallbacks),
hasReciprocateQr: Boolean(session.reciprocateQrCallbacks),
@@ -190,7 +209,8 @@ export class MatrixVerificationManager {
return direct;
}
for (const session of this.verificationSessions.values()) {
if (session.request.transactionId === id) {
const txId = this.readRequestValue(session.request, () => session.request.transactionId, "");
if (txId === id) {
return session;
}
}
@@ -205,8 +225,9 @@ export class MatrixVerificationManager {
this.trackedVerificationRequests.add(requestObj);
session.request.on(VerificationRequestEvent.Change, () => {
this.touchVerificationSession(session);
if (session.request.verifier) {
this.attachVerifierToVerificationSession(session, session.request.verifier);
const verifier = this.readRequestValue(session.request, () => session.request.verifier, null);
if (verifier) {
this.attachVerifierToVerificationSession(session, verifier);
}
});
}
@@ -266,14 +287,20 @@ export class MatrixVerificationManager {
trackVerificationRequest(request: MatrixVerificationRequestLike): MatrixVerificationSummary {
this.pruneVerificationSessions(Date.now());
const txId = request.transactionId?.trim();
const txId = this.readRequestValue(request, () => request.transactionId?.trim(), "");
if (txId) {
for (const existing of this.verificationSessions.values()) {
if (existing.request.transactionId === txId) {
const existingTxId = this.readRequestValue(
existing.request,
() => existing.request.transactionId,
"",
);
if (existingTxId === txId) {
existing.request = request;
this.ensureVerificationRequestTracked(existing);
if (request.verifier) {
this.attachVerifierToVerificationSession(existing, request.verifier);
const verifier = this.readRequestValue(request, () => request.verifier, null);
if (verifier) {
this.attachVerifierToVerificationSession(existing, verifier);
}
this.touchVerificationSession(existing);
return this.buildVerificationSummary(existing);
@@ -292,8 +319,9 @@ export class MatrixVerificationManager {
};
this.verificationSessions.set(session.id, session);
this.ensureVerificationRequestTracked(session);
if (request.verifier) {
this.attachVerifierToVerificationSession(session, request.verifier);
const verifier = this.readRequestValue(request, () => request.verifier, null);
if (verifier) {
this.attachVerifierToVerificationSession(session, verifier);
}
return this.buildVerificationSummary(session);
}