mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 09:08:38 +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 () => {
|
it("does not force-reset bootstrap when password is unavailable", async () => {
|
||||||
matrixJsClient.getCrypto = vi.fn(() => ({ on: vi.fn() }));
|
matrixJsClient.getCrypto = vi.fn(() => ({ on: vi.fn() }));
|
||||||
const client = new MatrixClient("https://matrix.example.org", "token", undefined, undefined, {
|
const client = new MatrixClient("https://matrix.example.org", "token", undefined, undefined, {
|
||||||
|
|||||||
@@ -309,9 +309,17 @@ export class MatrixClient {
|
|||||||
if (!crypto) {
|
if (!crypto) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const initial = await this.cryptoBootstrapper.bootstrap(crypto);
|
const initial = await this.cryptoBootstrapper.bootstrap(crypto, {
|
||||||
|
allowAutomaticCrossSigningReset: false,
|
||||||
|
});
|
||||||
if (!initial.crossSigningPublished || initial.ownDeviceVerified === 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 {
|
try {
|
||||||
const repaired = await this.cryptoBootstrapper.bootstrap(crypto, {
|
const repaired = await this.cryptoBootstrapper.bootstrap(crypto, {
|
||||||
forceResetCrossSigning: true,
|
forceResetCrossSigning: true,
|
||||||
@@ -757,7 +765,9 @@ export class MatrixClient {
|
|||||||
return await fail(err instanceof Error ? err.message : String(err));
|
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();
|
const status = await this.getOwnDeviceVerificationStatus();
|
||||||
if (!status.verified) {
|
if (!status.verified) {
|
||||||
return {
|
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 () => {
|
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({
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export type MatrixCryptoBootstrapperDeps<TRawEvent extends MatrixRawEvent> = {
|
|||||||
|
|
||||||
export type MatrixCryptoBootstrapOptions = {
|
export type MatrixCryptoBootstrapOptions = {
|
||||||
forceResetCrossSigning?: boolean;
|
forceResetCrossSigning?: boolean;
|
||||||
|
allowAutomaticCrossSigningReset?: boolean;
|
||||||
strict?: boolean;
|
strict?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,6 +52,7 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
|
|||||||
await this.bootstrapSecretStorage(crypto, strict);
|
await this.bootstrapSecretStorage(crypto, strict);
|
||||||
const crossSigning = await this.bootstrapCrossSigning(crypto, {
|
const crossSigning = await this.bootstrapCrossSigning(crypto, {
|
||||||
forceResetCrossSigning: options.forceResetCrossSigning === true,
|
forceResetCrossSigning: options.forceResetCrossSigning === true,
|
||||||
|
allowAutomaticCrossSigningReset: options.allowAutomaticCrossSigningReset !== false,
|
||||||
strict,
|
strict,
|
||||||
});
|
});
|
||||||
await this.bootstrapSecretStorage(crypto, strict);
|
await this.bootstrapSecretStorage(crypto, strict);
|
||||||
@@ -91,7 +93,11 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
|
|||||||
|
|
||||||
private async bootstrapCrossSigning(
|
private async bootstrapCrossSigning(
|
||||||
crypto: MatrixCryptoBootstrapApi,
|
crypto: MatrixCryptoBootstrapApi,
|
||||||
options: { forceResetCrossSigning: boolean; strict: boolean },
|
options: {
|
||||||
|
forceResetCrossSigning: boolean;
|
||||||
|
allowAutomaticCrossSigningReset: boolean;
|
||||||
|
strict: boolean;
|
||||||
|
},
|
||||||
): Promise<{ ready: boolean; published: boolean }> {
|
): Promise<{ ready: boolean; published: boolean }> {
|
||||||
const userId = await this.deps.getUserId();
|
const userId = await this.deps.getUserId();
|
||||||
const authUploadDeviceSigningKeys = this.createSigningKeysUiAuthCallback({
|
const authUploadDeviceSigningKeys = this.createSigningKeysUiAuthCallback({
|
||||||
@@ -156,6 +162,14 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
|
|||||||
authUploadDeviceSigningKeys,
|
authUploadDeviceSigningKeys,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} 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(
|
LogService.warn(
|
||||||
"MatrixClientLite",
|
"MatrixClientLite",
|
||||||
"Initial cross-signing bootstrap failed, trying reset:",
|
"Initial cross-signing bootstrap failed, trying reset:",
|
||||||
@@ -182,6 +196,10 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
|
|||||||
return { ready: true, published: true };
|
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.
|
// Fallback: recover from broken local/server state by creating a fresh identity.
|
||||||
try {
|
try {
|
||||||
await crypto.bootstrapCrossSigning({
|
await crypto.bootstrapCrossSigning({
|
||||||
|
|||||||
Reference in New Issue
Block a user