Matrix: retry cross-signing after secret storage repair

This commit is contained in:
Gustavo Madeira Santana
2026-03-09 02:17:29 -04:00
parent 5b98d8e5aa
commit 897e79a4a2
3 changed files with 88 additions and 17 deletions

View File

@@ -159,6 +159,55 @@ describe("MatrixCryptoBootstrapper", () => {
);
});
it("recreates secret storage and retries cross-signing when explicit bootstrap hits a stale server key", async () => {
const deps = createBootstrapperDeps();
const bootstrapCrossSigning = vi
.fn<() => Promise<void>>()
.mockRejectedValueOnce(new Error("getSecretStorageKey callback returned falsey"))
.mockResolvedValueOnce(undefined);
const crypto = createCryptoApi({
bootstrapCrossSigning,
isCrossSigningReady: vi.fn(async () => true),
userHasCrossSigningKeys: vi.fn(async () => true),
getDeviceVerificationStatus: vi.fn(async () => ({
isVerified: () => true,
localVerified: true,
crossSigningVerified: true,
signedByOwner: true,
})),
});
const bootstrapper = new MatrixCryptoBootstrapper(
deps as unknown as MatrixCryptoBootstrapperDeps<MatrixRawEvent>,
);
await bootstrapper.bootstrap(crypto, {
strict: true,
allowSecretStorageRecreateWithoutRecoveryKey: true,
allowAutomaticCrossSigningReset: false,
});
expect(deps.recoveryKeyStore.bootstrapSecretStorageWithRecoveryKey).toHaveBeenCalledWith(
crypto,
{
allowSecretStorageRecreateWithoutRecoveryKey: true,
forceNewSecretStorage: true,
},
);
expect(bootstrapCrossSigning).toHaveBeenCalledTimes(2);
expect(bootstrapCrossSigning).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
authUploadDeviceSigningKeys: expect.any(Function),
}),
);
expect(bootstrapCrossSigning).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
authUploadDeviceSigningKeys: expect.any(Function),
}),
);
});
it("fails in strict mode when cross-signing keys are still unpublished", async () => {
const deps = createBootstrapperDeps();
const crypto = createCryptoApi({

View File

@@ -58,6 +58,8 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
const crossSigning = await this.bootstrapCrossSigning(crypto, {
forceResetCrossSigning: options.forceResetCrossSigning === true,
allowAutomaticCrossSigningReset: options.allowAutomaticCrossSigningReset !== false,
allowSecretStorageRecreateWithoutRecoveryKey:
options.allowSecretStorageRecreateWithoutRecoveryKey === true,
strict,
});
await this.bootstrapSecretStorage(crypto, {
@@ -105,6 +107,7 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
options: {
forceResetCrossSigning: boolean;
allowAutomaticCrossSigningReset: boolean;
allowSecretStorageRecreateWithoutRecoveryKey: boolean;
strict: boolean;
},
): Promise<{ ready: boolean; published: boolean }> {
@@ -171,30 +174,47 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
authUploadDeviceSigningKeys,
});
} catch (err) {
if (!options.allowAutomaticCrossSigningReset) {
const shouldRepairSecretStorage =
options.allowSecretStorageRecreateWithoutRecoveryKey &&
err instanceof Error &&
err.message.includes("getSecretStorageKey callback returned falsey");
if (shouldRepairSecretStorage) {
LogService.warn(
"MatrixClientLite",
"Cross-signing bootstrap could not access secret storage; recreating secret storage during explicit bootstrap and retrying.",
);
await this.deps.recoveryKeyStore.bootstrapSecretStorageWithRecoveryKey(crypto, {
allowSecretStorageRecreateWithoutRecoveryKey: true,
forceNewSecretStorage: true,
});
await crypto.bootstrapCrossSigning({
authUploadDeviceSigningKeys,
});
} else if (!options.allowAutomaticCrossSigningReset) {
LogService.warn(
"MatrixClientLite",
"Initial cross-signing bootstrap failed and automatic reset is disabled:",
err,
);
return { ready: false, published: false };
}
LogService.warn(
"MatrixClientLite",
"Initial cross-signing bootstrap failed, trying reset:",
err,
);
try {
await crypto.bootstrapCrossSigning({
setupNewCrossSigning: true,
authUploadDeviceSigningKeys,
});
} catch (resetErr) {
LogService.warn("MatrixClientLite", "Failed to bootstrap cross-signing:", resetErr);
if (options.strict) {
throw resetErr instanceof Error ? resetErr : new Error(String(resetErr));
} else {
LogService.warn(
"MatrixClientLite",
"Initial cross-signing bootstrap failed, trying reset:",
err,
);
try {
await crypto.bootstrapCrossSigning({
setupNewCrossSigning: true,
authUploadDeviceSigningKeys,
});
} catch (resetErr) {
LogService.warn("MatrixClientLite", "Failed to bootstrap cross-signing:", resetErr);
if (options.strict) {
throw resetErr instanceof Error ? resetErr : new Error(String(resetErr));
}
return { ready: false, published: false };
}
return { ready: false, published: false };
}
}

View File

@@ -131,6 +131,7 @@ export class MatrixRecoveryKeyStore {
options: {
setupNewKeyBackup?: boolean;
allowSecretStorageRecreateWithoutRecoveryKey?: boolean;
forceNewSecretStorage?: boolean;
} = {},
): Promise<void> {
let status: MatrixSecretStorageStatus | null = null;
@@ -185,6 +186,7 @@ export class MatrixRecoveryKeyStore {
};
const shouldRecreateSecretStorage =
options.forceNewSecretStorage === true ||
!hasDefaultSecretStorageKey ||
(!recoveryKey && status?.ready === false) ||
hasKnownInvalidSecrets;