Matrix: refresh crypto bootstrap state

Refresh published cross-signing keys before bootstrap imports secret-storage keys, add sync-filter plumbing for QA E2EE clients, and document the remaining upstream key-backup cache noise without suppressing SDK logs.
This commit is contained in:
Gustavo Madeira Santana
2026-04-16 21:23:32 -04:00
parent bb7e9823a8
commit 94081d8863
5 changed files with 63 additions and 19 deletions

View File

@@ -1,12 +1,8 @@
import { logger as matrixJsSdkRootLogger } from "matrix-js-sdk/lib/logger.js";
import { ConsoleLogger, LogService, setMatrixConsoleLogging } from "../sdk/logger.js";
let matrixSdkLoggingConfigured = false;
let matrixSdkLogMode: "default" | "quiet" = "default";
const matrixSdkBaseLogger = new ConsoleLogger();
const matrixSdkSilentMethodFactory = () => () => {};
let matrixSdkRootMethodFactory: unknown;
let matrixSdkRootLoggerInitialized = false;
type MatrixJsSdkLogger = {
trace: (...messageOrObject: unknown[]) => void;
@@ -52,22 +48,7 @@ export function createMatrixJsSdkClientLogger(prefix = "matrix"): MatrixJsSdkLog
return createMatrixJsSdkLoggerInstance(prefix);
}
function applyMatrixJsSdkRootLoggerMode(): void {
const rootLogger = matrixJsSdkRootLogger as {
methodFactory?: unknown;
rebuild?: () => void;
};
if (!matrixSdkRootLoggerInitialized) {
matrixSdkRootMethodFactory = rootLogger.methodFactory;
matrixSdkRootLoggerInitialized = true;
}
rootLogger.methodFactory =
matrixSdkLogMode === "quiet" ? matrixSdkSilentMethodFactory : matrixSdkRootMethodFactory;
rootLogger.rebuild?.();
}
function applyMatrixSdkLogger(): void {
applyMatrixJsSdkRootLoggerMode();
if (matrixSdkLogMode === "quiet") {
LogService.setLogger({
trace: () => {},

View File

@@ -1404,6 +1404,26 @@ describe("MatrixClient crypto bootstrapping", () => {
expect(logger?.getChild).toBeTypeOf("function");
});
it("passes a custom sync filter to matrix-js-sdk startup", async () => {
const client = new MatrixClient("https://matrix.example.org", "token", {
userId: "@bot:example.org",
syncFilter: { room: { ephemeral: { not_types: ["m.receipt"] } } },
});
await client.start();
const startOpts = matrixJsClient.startClient.mock.calls[0]?.[0] as
| { filter?: { getDefinition?: () => unknown } }
| undefined;
expect(startOpts?.filter?.getDefinition?.()).toEqual({
room: {
ephemeral: {
not_types: ["m.receipt"],
},
},
});
});
it("schedules periodic crypto snapshot persistence with fake timers", async () => {
vi.useFakeTimers();
const databasesSpy = vi.spyOn(indexedDB, "databases").mockResolvedValue([]);

View File

@@ -1,9 +1,11 @@
import { EventEmitter } from "node:events";
import {
ClientEvent,
Filter,
MatrixEventEvent,
Preset,
createClient as createMatrixJsClient,
type IFilterDefinition,
type MatrixClient as MatrixJsClient,
type MatrixEvent,
} from "matrix-js-sdk/lib/matrix.js";
@@ -216,6 +218,7 @@ export class MatrixClient {
private readonly httpClient: MatrixAuthedHttpClient;
private readonly localTimeoutMs: number;
private readonly initialSyncLimit?: number;
private readonly syncFilter?: IFilterDefinition;
private readonly encryptionEnabled: boolean;
private readonly password?: string;
private readonly syncStore?: FileBackedMatrixSyncStore;
@@ -258,6 +261,7 @@ export class MatrixClient {
localTimeoutMs?: number;
encryption?: boolean;
initialSyncLimit?: number;
syncFilter?: IFilterDefinition;
storagePath?: string;
recoveryKeyPath?: string;
idbSnapshotPath?: string;
@@ -275,6 +279,7 @@ export class MatrixClient {
});
this.localTimeoutMs = Math.max(1, opts.localTimeoutMs ?? 60_000);
this.initialSyncLimit = opts.initialSyncLimit;
this.syncFilter = opts.syncFilter;
this.encryptionEnabled = opts.encryption === true;
this.password = opts.password;
this.syncStore = opts.storagePath ? new FileBackedMatrixSyncStore(opts.storagePath) : undefined;
@@ -496,6 +501,7 @@ export class MatrixClient {
await this.client.startClient({
initialSyncLimit: this.initialSyncLimit,
...(this.syncFilter ? { filter: Filter.fromJson(this.selfUserId, "", this.syncFilter) } : {}),
});
await this.waitForInitialSyncReady({
abortSignal: opts.abortSignal,
@@ -1674,6 +1680,12 @@ export class MatrixClient {
"MatrixClientLite",
"No room key backup version found on server, creating one via secret storage bootstrap",
);
// matrix-js-sdk 41.3.0 can log a transient PerSessionKeyBackupDownloader
// "current backup version ... undefined" warning while setupNewKeyBackup creates
// the backup: resetKeyBackup emits key-backup cache events before its async
// checkKeyBackupAndEnable pass has populated active backup state. Keep the
// explicit server re-check below and do not hide the SDK logs; if this needs
// fixing in code, upstream a minimal Matrix SDK repro instead of patching here.
await this.recoveryKeyStore.bootstrapSecretStorageWithRecoveryKey(crypto, {
setupNewKeyBackup: true,
});

View File

@@ -222,6 +222,26 @@ describe("MatrixCryptoBootstrapper", () => {
);
});
it("refreshes published cross-signing keys before importing private keys from secret storage", async () => {
const bootstrapCrossSigning = vi.fn(async () => {});
const userHasCrossSigningKeys = vi.fn(async () => true);
const { bootstrapper, crypto } = createBootstrapperHarness({
bootstrapCrossSigning,
getDeviceVerificationStatus: vi.fn(async () => createVerifiedDeviceStatus()),
isCrossSigningReady: vi.fn(async () => true),
userHasCrossSigningKeys,
});
await bootstrapper.bootstrap(crypto, {
allowAutomaticCrossSigningReset: false,
});
expect(userHasCrossSigningKeys).toHaveBeenCalledWith("@bot:example.org", true);
expect(userHasCrossSigningKeys.mock.invocationCallOrder[0]).toBeLessThan(
bootstrapCrossSigning.mock.invocationCallOrder[0],
);
});
it("passes explicit secret-storage repair allowance only when requested", async () => {
const deps = createBootstrapperDeps();
const crypto = createCryptoApi({

View File

@@ -142,6 +142,16 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
return false;
}
};
const refreshPublishedCrossSigningKeys = async (): Promise<void> => {
if (typeof crypto.userHasCrossSigningKeys !== "function") {
return;
}
try {
await crypto.userHasCrossSigningKeys(userId, true);
} catch {
// The normal bootstrap flow below handles missing or unavailable keys.
}
};
const isCrossSigningReady = async (): Promise<boolean> => {
if (typeof crypto.isCrossSigningReady !== "function") {
return true;
@@ -212,6 +222,7 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
// First pass: preserve existing cross-signing identity and ensure public keys are uploaded.
try {
await refreshPublishedCrossSigningKeys();
await crypto.bootstrapCrossSigning({
authUploadDeviceSigningKeys,
});