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 46cd37bc0d
commit a3573ac71f
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 () => { it("fails in strict mode when cross-signing keys are still unpublished", async () => {
const deps = createBootstrapperDeps(); const deps = createBootstrapperDeps();
const crypto = createCryptoApi({ const crypto = createCryptoApi({

View File

@@ -58,6 +58,8 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
const crossSigning = await this.bootstrapCrossSigning(crypto, { const crossSigning = await this.bootstrapCrossSigning(crypto, {
forceResetCrossSigning: options.forceResetCrossSigning === true, forceResetCrossSigning: options.forceResetCrossSigning === true,
allowAutomaticCrossSigningReset: options.allowAutomaticCrossSigningReset !== false, allowAutomaticCrossSigningReset: options.allowAutomaticCrossSigningReset !== false,
allowSecretStorageRecreateWithoutRecoveryKey:
options.allowSecretStorageRecreateWithoutRecoveryKey === true,
strict, strict,
}); });
await this.bootstrapSecretStorage(crypto, { await this.bootstrapSecretStorage(crypto, {
@@ -105,6 +107,7 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
options: { options: {
forceResetCrossSigning: boolean; forceResetCrossSigning: boolean;
allowAutomaticCrossSigningReset: boolean; allowAutomaticCrossSigningReset: boolean;
allowSecretStorageRecreateWithoutRecoveryKey: boolean;
strict: boolean; strict: boolean;
}, },
): Promise<{ ready: boolean; published: boolean }> { ): Promise<{ ready: boolean; published: boolean }> {
@@ -171,30 +174,47 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
authUploadDeviceSigningKeys, authUploadDeviceSigningKeys,
}); });
} catch (err) { } 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( LogService.warn(
"MatrixClientLite", "MatrixClientLite",
"Initial cross-signing bootstrap failed and automatic reset is disabled:", "Initial cross-signing bootstrap failed and automatic reset is disabled:",
err, err,
); );
return { ready: false, published: false }; return { ready: false, published: false };
} } else {
LogService.warn( LogService.warn(
"MatrixClientLite", "MatrixClientLite",
"Initial cross-signing bootstrap failed, trying reset:", "Initial cross-signing bootstrap failed, trying reset:",
err, err,
); );
try { try {
await crypto.bootstrapCrossSigning({ await crypto.bootstrapCrossSigning({
setupNewCrossSigning: true, setupNewCrossSigning: true,
authUploadDeviceSigningKeys, authUploadDeviceSigningKeys,
}); });
} catch (resetErr) { } catch (resetErr) {
LogService.warn("MatrixClientLite", "Failed to bootstrap cross-signing:", resetErr); LogService.warn("MatrixClientLite", "Failed to bootstrap cross-signing:", resetErr);
if (options.strict) { if (options.strict) {
throw resetErr instanceof Error ? resetErr : new Error(String(resetErr)); 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: { options: {
setupNewKeyBackup?: boolean; setupNewKeyBackup?: boolean;
allowSecretStorageRecreateWithoutRecoveryKey?: boolean; allowSecretStorageRecreateWithoutRecoveryKey?: boolean;
forceNewSecretStorage?: boolean;
} = {}, } = {},
): Promise<void> { ): Promise<void> {
let status: MatrixSecretStorageStatus | null = null; let status: MatrixSecretStorageStatus | null = null;
@@ -185,6 +186,7 @@ export class MatrixRecoveryKeyStore {
}; };
const shouldRecreateSecretStorage = const shouldRecreateSecretStorage =
options.forceNewSecretStorage === true ||
!hasDefaultSecretStorageKey || !hasDefaultSecretStorageKey ||
(!recoveryKey && status?.ready === false) || (!recoveryKey && status?.ready === false) ||
hasKnownInvalidSecrets; hasKnownInvalidSecrets;