























11// Matrix tests cover crypto bootstrap plugin behavior.
22import { beforeEach, describe, expect, it, vi, type Mock } from "vitest";
33import { MatrixCryptoBootstrapper, type MatrixCryptoBootstrapperDeps } from "./crypto-bootstrap.js";
4+import type { MatrixRecoveryKeyStore } from "./recovery-key-store.js";
45import type { MatrixCryptoBootstrapApi, MatrixRawEvent } from "./types.js";
5667type BootstrapCrossSigningMock = Mock<MatrixCryptoBootstrapApi["bootstrapCrossSigning"]>;
@@ -39,12 +40,15 @@ function createBootstrapperDeps() {
3940return {
4041getUserId: vi.fn(async () => "@bot:example.org"),
4142getPassword: vi.fn<() => string | undefined>(() => "super-secret-password"),
43+canUnlockSecretStorage: vi.fn(async () => true),
4244getDeviceId: vi.fn(() => "DEVICE123"),
4345verificationManager: {
4446trackVerificationRequest: vi.fn(),
4547},
4648recoveryKeyStore: {
47-bootstrapSecretStorageWithRecoveryKey: vi.fn(async () => {}),
49+bootstrapSecretStorageWithRecoveryKey: vi.fn<
50+MatrixRecoveryKeyStore["bootstrapSecretStorageWithRecoveryKey"]
51+>(async () => {}),
4852},
4953decryptBridge: {
5054bindCryptoRetrySignals: vi.fn(),
@@ -132,17 +136,6 @@ function createForcedResetHarness(bootstrapCrossSigning: BootstrapCrossSigningMo
132136});
133137}
134138135-function expectForcedResetCrossSigningCalls(
136-bootstrapCrossSigning: BootstrapCrossSigningMock,
137-params: { setupNewCall: number; totalCalls: number },
138-) {
139-expect(bootstrapCrossSigning).toHaveBeenCalledTimes(params.totalCalls);
140-expectBootstrapCrossSigningCall(bootstrapCrossSigning, params.setupNewCall, {
141-setupNewCrossSigning: true,
142-});
143-expectBootstrapCrossSigningCall(bootstrapCrossSigning, params.totalCalls);
144-}
145-146139async function bootstrapWithVerificationRequestListener(overrides?: {
147140deps?: Partial<ReturnType<typeof createBootstrapperDeps>>;
148141crypto?: Partial<MatrixCryptoBootstrapApi>;
@@ -406,32 +399,72 @@ describe("MatrixCryptoBootstrapper", () => {
406399);
407400408401expect(deps.recoveryKeyStore.bootstrapSecretStorageWithRecoveryKey).not.toHaveBeenCalled();
402+expect(bootstrapCrossSigning).toHaveBeenCalledTimes(1);
409403});
410404411-it("recreates secret storage and retries a forced reset when stale server SSSS blocks it", async () => {
405+it("rejects forced reset before mutation when the active recovery key is unavailable", async () => {
406+const deps = createBootstrapperDeps();
407+deps.canUnlockSecretStorage = vi.fn(async () => false);
408+const bootstrapCrossSigning = vi.fn(async () => {});
409+const crypto = createCryptoApi({ bootstrapCrossSigning });
410+const bootstrapper = new MatrixCryptoBootstrapper(
411+deps as unknown as MatrixCryptoBootstrapperDeps<MatrixRawEvent>,
412+);
413+414+await expect(
415+bootstrapper.bootstrap(crypto, {
416+strict: true,
417+forceResetCrossSigning: true,
418+allowSecretStorageRecreateWithoutRecoveryKey: true,
419+}),
420+).rejects.toThrow(
421+"Forced cross-signing reset requires the active Matrix recovery key; supply it before retrying",
422+);
423+424+expect(deps.recoveryKeyStore.bootstrapSecretStorageWithRecoveryKey).not.toHaveBeenCalled();
425+expect(bootstrapCrossSigning).not.toHaveBeenCalled();
426+});
427+428+it("fails closed without recreating SSSS when forced reset cannot unlock it", async () => {
412429const bootstrapCrossSigning = vi
413430.fn<() => Promise<void>>()
414-.mockRejectedValueOnce(new Error("getSecretStorageKey callback returned falsey"))
415-.mockResolvedValueOnce(undefined);
431+.mockRejectedValueOnce(new Error("getSecretStorageKey callback returned falsey"));
416432const { deps, crypto, bootstrapper } = createForcedResetHarness(bootstrapCrossSigning);
417433418-await bootstrapper.bootstrap(crypto, {
419-strict: true,
434+await expect(
435+bootstrapper.bootstrap(crypto, {
436+strict: true,
437+forceResetCrossSigning: true,
438+allowSecretStorageRecreateWithoutRecoveryKey: true,
439+}),
440+).rejects.toThrow(
441+"Forced cross-signing reset cannot access secret storage; restore the Matrix recovery key before retrying",
442+);
443+444+expect(deps.recoveryKeyStore.bootstrapSecretStorageWithRecoveryKey).not.toHaveBeenCalled();
445+expect(bootstrapCrossSigning).toHaveBeenCalledTimes(1);
446+expectBootstrapCrossSigningCall(bootstrapCrossSigning, 1, { setupNewCrossSigning: true });
447+});
448+449+it("does not repair SSSS after a non-strict forced reset failure", async () => {
450+const bootstrapCrossSigning = vi.fn(async () => {
451+throw new Error("getSecretStorageKey callback returned falsey");
452+});
453+const { deps, crypto, bootstrapper } = createForcedResetHarness(bootstrapCrossSigning);
454+455+const result = await bootstrapper.bootstrap(crypto, {
456+strict: false,
420457forceResetCrossSigning: true,
421458allowSecretStorageRecreateWithoutRecoveryKey: true,
422459});
423460424-expect(deps.recoveryKeyStore.bootstrapSecretStorageWithRecoveryKey).toHaveBeenCalledWith(
425-crypto,
426-{
427-allowSecretStorageRecreateWithoutRecoveryKey: true,
428-forceNewSecretStorage: true,
429-},
430-);
431-expectForcedResetCrossSigningCalls(bootstrapCrossSigning, {
432-setupNewCall: 2,
433-totalCalls: 3,
461+expect(result).toEqual({
462+crossSigningReady: false,
463+crossSigningPublished: false,
464+ownDeviceVerified: null,
434465});
466+expect(deps.recoveryKeyStore.bootstrapSecretStorageWithRecoveryKey).not.toHaveBeenCalled();
467+expect(bootstrapCrossSigning).toHaveBeenCalledOnce();
435468});
436469437470it("re-exports cross-signing keys after forced reset creates secret storage", async () => {
@@ -444,16 +477,16 @@ describe("MatrixCryptoBootstrapper", () => {
444477allowSecretStorageRecreateWithoutRecoveryKey: true,
445478});
446479480+expect(deps.recoveryKeyStore.bootstrapSecretStorageWithRecoveryKey).toHaveBeenCalledOnce();
447481expect(deps.recoveryKeyStore.bootstrapSecretStorageWithRecoveryKey).toHaveBeenCalledWith(
448482crypto,
449-{
483+expect.objectContaining({
450484allowSecretStorageRecreateWithoutRecoveryKey: true,
451-},
485+}),
452486);
453-expectForcedResetCrossSigningCalls(bootstrapCrossSigning, {
454-setupNewCall: 1,
455-totalCalls: 2,
456-});
487+// No redundant second bootstrapCrossSigning call — no double reset (gh-78396).
488+expect(bootstrapCrossSigning).toHaveBeenCalledTimes(1);
489+expectBootstrapCrossSigningCall(bootstrapCrossSigning, 1, { setupNewCrossSigning: true });
457490});
458491459492it("trusts the fresh own identity after a forced cross-signing reset", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。