mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
Matrix: preserve owner-signed verification state
This commit is contained in:
@@ -842,6 +842,53 @@ describe("MatrixClient crypto bootstrapping", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not force-reset bootstrap when the device is already signed by its owner", async () => {
|
||||
matrixJsClient.getCrypto = vi.fn(() => ({ on: vi.fn() }));
|
||||
const client = new MatrixClient("https://matrix.example.org", "token", undefined, undefined, {
|
||||
encryption: true,
|
||||
password: "secret-password",
|
||||
});
|
||||
const bootstrapSpy = vi.fn().mockResolvedValue({
|
||||
crossSigningReady: false,
|
||||
crossSigningPublished: false,
|
||||
ownDeviceVerified: true,
|
||||
});
|
||||
(
|
||||
client as unknown as {
|
||||
cryptoBootstrapper: { bootstrap: typeof bootstrapSpy };
|
||||
}
|
||||
).cryptoBootstrapper.bootstrap = bootstrapSpy;
|
||||
vi.spyOn(client, "getOwnDeviceVerificationStatus").mockResolvedValue({
|
||||
encryptionEnabled: true,
|
||||
userId: "@bot:example.org",
|
||||
deviceId: "DEVICE123",
|
||||
verified: true,
|
||||
localVerified: true,
|
||||
crossSigningVerified: false,
|
||||
signedByOwner: true,
|
||||
recoveryKeyStored: false,
|
||||
recoveryKeyCreatedAt: null,
|
||||
recoveryKeyId: null,
|
||||
backupVersion: null,
|
||||
backup: {
|
||||
serverVersion: null,
|
||||
activeVersion: null,
|
||||
trusted: null,
|
||||
matchesDecryptionKey: null,
|
||||
decryptionKeyCached: false,
|
||||
keyLoadAttempted: false,
|
||||
keyLoadError: null,
|
||||
},
|
||||
});
|
||||
|
||||
await client.start();
|
||||
|
||||
expect(bootstrapSpy).toHaveBeenCalledTimes(1);
|
||||
expect(bootstrapSpy.mock.calls[0]?.[1]).toEqual({
|
||||
allowAutomaticCrossSigningReset: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not force-reset bootstrap when password is unavailable", async () => {
|
||||
matrixJsClient.getCrypto = vi.fn(() => ({ on: vi.fn() }));
|
||||
const client = new MatrixClient("https://matrix.example.org", "token", undefined, undefined, {
|
||||
|
||||
@@ -309,9 +309,17 @@ export class MatrixClient {
|
||||
if (!crypto) {
|
||||
return;
|
||||
}
|
||||
const initial = await this.cryptoBootstrapper.bootstrap(crypto);
|
||||
const initial = await this.cryptoBootstrapper.bootstrap(crypto, {
|
||||
allowAutomaticCrossSigningReset: false,
|
||||
});
|
||||
if (!initial.crossSigningPublished || initial.ownDeviceVerified === false) {
|
||||
if (this.password?.trim()) {
|
||||
const status = await this.getOwnDeviceVerificationStatus();
|
||||
if (status.signedByOwner) {
|
||||
LogService.warn(
|
||||
"MatrixClientLite",
|
||||
"Cross-signing/bootstrap is incomplete for an already owner-signed device; skipping automatic reset and preserving the current identity. Restore the recovery key or run an explicit verification bootstrap if repair is needed.",
|
||||
);
|
||||
} else if (this.password?.trim()) {
|
||||
try {
|
||||
const repaired = await this.cryptoBootstrapper.bootstrap(crypto, {
|
||||
forceResetCrossSigning: true,
|
||||
@@ -757,7 +765,9 @@ export class MatrixClient {
|
||||
return await fail(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
|
||||
await this.cryptoBootstrapper.bootstrap(crypto);
|
||||
await this.cryptoBootstrapper.bootstrap(crypto, {
|
||||
allowAutomaticCrossSigningReset: false,
|
||||
});
|
||||
const status = await this.getOwnDeviceVerificationStatus();
|
||||
if (!status.verified) {
|
||||
return {
|
||||
|
||||
@@ -99,6 +99,36 @@ describe("MatrixCryptoBootstrapper", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("does not auto-reset cross-signing when automatic reset is disabled", async () => {
|
||||
const deps = createBootstrapperDeps();
|
||||
const bootstrapCrossSigning = vi.fn(async () => {});
|
||||
const crypto = createCryptoApi({
|
||||
bootstrapCrossSigning,
|
||||
isCrossSigningReady: vi.fn(async () => false),
|
||||
userHasCrossSigningKeys: vi.fn(async () => false),
|
||||
getDeviceVerificationStatus: vi.fn(async () => ({
|
||||
isVerified: () => true,
|
||||
localVerified: true,
|
||||
crossSigningVerified: false,
|
||||
signedByOwner: true,
|
||||
})),
|
||||
});
|
||||
const bootstrapper = new MatrixCryptoBootstrapper(
|
||||
deps as unknown as MatrixCryptoBootstrapperDeps<MatrixRawEvent>,
|
||||
);
|
||||
|
||||
await bootstrapper.bootstrap(crypto, {
|
||||
allowAutomaticCrossSigningReset: false,
|
||||
});
|
||||
|
||||
expect(bootstrapCrossSigning).toHaveBeenCalledTimes(1);
|
||||
expect(bootstrapCrossSigning).toHaveBeenCalledWith(
|
||||
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({
|
||||
|
||||
@@ -26,6 +26,7 @@ export type MatrixCryptoBootstrapperDeps<TRawEvent extends MatrixRawEvent> = {
|
||||
|
||||
export type MatrixCryptoBootstrapOptions = {
|
||||
forceResetCrossSigning?: boolean;
|
||||
allowAutomaticCrossSigningReset?: boolean;
|
||||
strict?: boolean;
|
||||
};
|
||||
|
||||
@@ -51,6 +52,7 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
|
||||
await this.bootstrapSecretStorage(crypto, strict);
|
||||
const crossSigning = await this.bootstrapCrossSigning(crypto, {
|
||||
forceResetCrossSigning: options.forceResetCrossSigning === true,
|
||||
allowAutomaticCrossSigningReset: options.allowAutomaticCrossSigningReset !== false,
|
||||
strict,
|
||||
});
|
||||
await this.bootstrapSecretStorage(crypto, strict);
|
||||
@@ -91,7 +93,11 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
|
||||
|
||||
private async bootstrapCrossSigning(
|
||||
crypto: MatrixCryptoBootstrapApi,
|
||||
options: { forceResetCrossSigning: boolean; strict: boolean },
|
||||
options: {
|
||||
forceResetCrossSigning: boolean;
|
||||
allowAutomaticCrossSigningReset: boolean;
|
||||
strict: boolean;
|
||||
},
|
||||
): Promise<{ ready: boolean; published: boolean }> {
|
||||
const userId = await this.deps.getUserId();
|
||||
const authUploadDeviceSigningKeys = this.createSigningKeysUiAuthCallback({
|
||||
@@ -156,6 +162,14 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
|
||||
authUploadDeviceSigningKeys,
|
||||
});
|
||||
} catch (err) {
|
||||
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:",
|
||||
@@ -182,6 +196,10 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
|
||||
return { ready: true, published: true };
|
||||
}
|
||||
|
||||
if (!options.allowAutomaticCrossSigningReset) {
|
||||
return { ready: firstPassReady, published: firstPassPublished };
|
||||
}
|
||||
|
||||
// Fallback: recover from broken local/server state by creating a fresh identity.
|
||||
try {
|
||||
await crypto.bootstrapCrossSigning({
|
||||
|
||||
Reference in New Issue
Block a user