fix(matrix): defer secret storage during forced repair

This commit is contained in:
Gustavo Madeira Santana
2026-04-15 11:32:34 -04:00
parent 28d2aac87b
commit 263507da3d
2 changed files with 55 additions and 5 deletions

View File

@@ -277,6 +277,49 @@ describe("MatrixCryptoBootstrapper", () => {
expectSecretStorageRepairRetry(deps, crypto, bootstrapCrossSigning);
});
it("does not mutate secret storage before forced repair fails on password UIA without a password", async () => {
const deps = createBootstrapperDeps();
deps.getPassword = vi.fn(() => undefined);
const bootstrapCrossSigning = vi.fn<
({
authUploadDeviceSigningKeys,
}: {
authUploadDeviceSigningKeys?: <T>(
makeRequest: (authData: Record<string, unknown> | null) => Promise<T>,
) => Promise<T>;
}) => Promise<void>
>(async ({ authUploadDeviceSigningKeys }) => {
await authUploadDeviceSigningKeys?.(async (authData) => {
if (authData === null) {
throw new Error("need auth");
}
if (authData.type === "m.login.dummy") {
throw new Error("dummy rejected");
}
return undefined;
});
});
const crypto = createCryptoApi({
bootstrapCrossSigning,
getDeviceVerificationStatus: vi.fn(async () => createVerifiedDeviceStatus()),
});
const bootstrapper = new MatrixCryptoBootstrapper(
deps as unknown as MatrixCryptoBootstrapperDeps<MatrixRawEvent>,
);
await expect(
bootstrapper.bootstrap(crypto, {
strict: true,
forceResetCrossSigning: true,
allowSecretStorageRecreateWithoutRecoveryKey: true,
}),
).rejects.toThrow(
"Matrix cross-signing key upload requires UIA; provide matrix.password for m.login.password fallback",
);
expect(deps.recoveryKeyStore.bootstrapSecretStorageWithRecoveryKey).not.toHaveBeenCalled();
});
it("fails in strict mode when cross-signing keys are still unpublished", async () => {
const deps = createBootstrapperDeps();
const crypto = createCryptoApi({

View File

@@ -47,14 +47,18 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
options: MatrixCryptoBootstrapOptions = {},
): Promise<MatrixCryptoBootstrapResult> {
const strict = options.strict === true;
const deferSecretStorageBootstrapUntilAfterCrossSigning =
options.forceResetCrossSigning === true;
// Register verification listeners before expensive bootstrap work so incoming requests
// are not missed during startup.
this.registerVerificationRequestHandler(crypto);
await this.bootstrapSecretStorage(crypto, {
strict,
allowSecretStorageRecreateWithoutRecoveryKey:
options.allowSecretStorageRecreateWithoutRecoveryKey === true,
});
if (!deferSecretStorageBootstrapUntilAfterCrossSigning) {
await this.bootstrapSecretStorage(crypto, {
strict,
allowSecretStorageRecreateWithoutRecoveryKey:
options.allowSecretStorageRecreateWithoutRecoveryKey === true,
});
}
const crossSigning = await this.bootstrapCrossSigning(crypto, {
forceResetCrossSigning: options.forceResetCrossSigning === true,
allowAutomaticCrossSigningReset: options.allowAutomaticCrossSigningReset !== false,
@@ -62,6 +66,9 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
options.allowSecretStorageRecreateWithoutRecoveryKey === true,
strict,
});
// Forced repair may need password UIA to upload new cross-signing keys. Delay any
// secret-storage repair/recreation until after that step succeeds so passwordless bots do
// not partially mutate SSSS on homeservers that require password-based UIA.
await this.bootstrapSecretStorage(crypto, {
strict,
allowSecretStorageRecreateWithoutRecoveryKey: