matrix-js: enable key backup creation in verify bootstrap

This commit is contained in:
Gustavo Madeira Santana
2026-02-25 15:34:41 -05:00
parent 950fd1913f
commit 5598535aa9
4 changed files with 122 additions and 8 deletions

View File

@@ -54,15 +54,13 @@ function printVerificationStatus(status: {
console.log("Verified: yes");
console.log(`User: ${status.userId ?? "unknown"}`);
console.log(`Device: ${status.deviceId ?? "unknown"}`);
if (status.backupVersion) {
console.log(`Backup version: ${status.backupVersion}`);
}
} else {
console.log("Verified: no");
console.log(`User: ${status.userId ?? "unknown"}`);
console.log(`Device: ${status.deviceId ?? "unknown"}`);
console.log("Run 'openclaw matrix-js verify device <key>' to verify this device.");
}
console.log(`Backup version: ${status.backupVersion ?? "none"}`);
console.log(`Recovery key stored: ${status.recoveryKeyStored ? "yes" : "no"}`);
printTimestamp("Recovery key created at", status.recoveryKeyCreatedAt);
console.log(`Pending verifications: ${status.pendingVerifications}`);
@@ -143,6 +141,7 @@ export function registerMatrixJsCli(params: { program: Command }): void {
console.log(
`Cross-signing published: ${result.crossSigning.published ? "yes" : "no"} (master=${result.crossSigning.masterKeyPublished ? "yes" : "no"}, self=${result.crossSigning.selfSigningKeyPublished ? "yes" : "no"}, user=${result.crossSigning.userSigningKeyPublished ? "yes" : "no"})`,
);
console.log(`Backup version: ${result.verification.backupVersion ?? "none"}`);
printTimestamp("Recovery key created at", result.verification.recoveryKeyCreatedAt);
console.log(`Pending verifications: ${result.pendingVerifications}`);
if (!result.success) {
@@ -179,9 +178,7 @@ export function registerMatrixJsCli(params: { program: Command }): void {
console.log("Device verification completed successfully.");
console.log(`User: ${result.userId ?? "unknown"}`);
console.log(`Device: ${result.deviceId ?? "unknown"}`);
if (result.backupVersion) {
console.log(`Backup version: ${result.backupVersion}`);
}
console.log(`Backup version: ${result.backupVersion ?? "none"}`);
printTimestamp("Recovery key created at", result.recoveryKeyCreatedAt);
printTimestamp("Verified at", result.verifiedAt);
} else {

View File

@@ -948,6 +948,100 @@ describe("MatrixClient crypto bootstrapping", () => {
expect(result.cryptoBootstrap).not.toBeNull();
});
it("creates a key backup during bootstrap when none exists on the server", async () => {
matrixJsClient.getUserId = vi.fn(() => "@bot:example.org");
matrixJsClient.getDeviceId = vi.fn(() => "DEVICE123");
const bootstrapSecretStorage = vi.fn(async () => {});
matrixJsClient.getCrypto = vi.fn(() => ({
on: vi.fn(),
bootstrapCrossSigning: vi.fn(async () => {}),
bootstrapSecretStorage,
requestOwnUserVerification: vi.fn(async () => null),
isCrossSigningReady: vi.fn(async () => true),
userHasCrossSigningKeys: vi.fn(async () => true),
getDeviceVerificationStatus: vi.fn(async () => ({
isVerified: () => true,
localVerified: true,
crossSigningVerified: true,
signedByOwner: true,
})),
}));
const client = new MatrixClient("https://matrix.example.org", "token", undefined, undefined, {
encryption: true,
});
vi.spyOn(client, "getOwnCrossSigningPublicationStatus").mockResolvedValue({
userId: "@bot:example.org",
masterKeyPublished: true,
selfSigningKeyPublished: true,
userSigningKeyPublished: true,
published: true,
});
let backupChecks = 0;
vi.spyOn(client, "doRequest").mockImplementation(async (_method, endpoint) => {
if (String(endpoint).includes("/room_keys/version")) {
backupChecks += 1;
return backupChecks >= 2 ? { version: "7" } : {};
}
return {};
});
const result = await client.bootstrapOwnDeviceVerification();
expect(result.success).toBe(true);
expect(result.verification.backupVersion).toBe("7");
expect(bootstrapSecretStorage).toHaveBeenCalledWith(
expect.objectContaining({ setupNewKeyBackup: true }),
);
});
it("does not recreate key backup during bootstrap when one already exists", async () => {
matrixJsClient.getUserId = vi.fn(() => "@bot:example.org");
matrixJsClient.getDeviceId = vi.fn(() => "DEVICE123");
const bootstrapSecretStorage = vi.fn(async () => {});
matrixJsClient.getCrypto = vi.fn(() => ({
on: vi.fn(),
bootstrapCrossSigning: vi.fn(async () => {}),
bootstrapSecretStorage,
requestOwnUserVerification: vi.fn(async () => null),
isCrossSigningReady: vi.fn(async () => true),
userHasCrossSigningKeys: vi.fn(async () => true),
getDeviceVerificationStatus: vi.fn(async () => ({
isVerified: () => true,
localVerified: true,
crossSigningVerified: true,
signedByOwner: true,
})),
}));
const client = new MatrixClient("https://matrix.example.org", "token", undefined, undefined, {
encryption: true,
});
vi.spyOn(client, "getOwnCrossSigningPublicationStatus").mockResolvedValue({
userId: "@bot:example.org",
masterKeyPublished: true,
selfSigningKeyPublished: true,
userSigningKeyPublished: true,
published: true,
});
vi.spyOn(client, "doRequest").mockImplementation(async (_method, endpoint) => {
if (String(endpoint).includes("/room_keys/version")) {
return { version: "9" };
}
return {};
});
const result = await client.bootstrapOwnDeviceVerification();
expect(result.success).toBe(true);
expect(result.verification.backupVersion).toBe("9");
expect(
bootstrapSecretStorage.mock.calls.some(([opts]) =>
Boolean((opts as { setupNewKeyBackup?: boolean } | undefined)?.setupNewKeyBackup),
),
).toBe(false);
});
it("does not report bootstrap errors when final verification state is healthy", async () => {
matrixJsClient.getUserId = vi.fn(() => "@bot:example.org");
matrixJsClient.getDeviceId = vi.fn(() => "DEVICE123");

View File

@@ -723,6 +723,7 @@ export class MatrixClient {
forceResetCrossSigning: params?.forceResetCrossSigning === true,
strict: true,
});
await this.ensureRoomKeyBackupEnabled(crypto);
} catch (err) {
bootstrapError = err instanceof Error ? err.message : String(err);
}
@@ -755,6 +756,25 @@ export class MatrixClient {
}
}
private async ensureRoomKeyBackupEnabled(crypto: MatrixCryptoBootstrapApi): Promise<void> {
const existingVersion = await this.resolveRoomKeyBackupVersion();
if (existingVersion) {
return;
}
LogService.info(
"MatrixClientLite",
"No room key backup version found on server, creating one via secret storage bootstrap",
);
await this.recoveryKeyStore.bootstrapSecretStorageWithRecoveryKey(crypto, {
setupNewKeyBackup: true,
});
const createdVersion = await this.resolveRoomKeyBackupVersion();
if (!createdVersion) {
throw new Error("Matrix room key backup is still missing after bootstrap");
}
LogService.info("MatrixClientLite", `Room key backup enabled (version ${createdVersion})`);
}
private registerBridge(): void {
if (this.bridgeRegistered) {
return;

View File

@@ -126,7 +126,10 @@ export class MatrixRecoveryKeyStore {
return this.getRecoveryKeySummary() ?? {};
}
async bootstrapSecretStorageWithRecoveryKey(crypto: MatrixCryptoBootstrapApi): Promise<void> {
async bootstrapSecretStorageWithRecoveryKey(
crypto: MatrixCryptoBootstrapApi,
options: { setupNewKeyBackup?: boolean } = {},
): Promise<void> {
let status: MatrixSecretStorageStatus | null = null;
if (typeof crypto.getSecretStorageStatus === "function") {
try {
@@ -193,7 +196,7 @@ export class MatrixRecoveryKeyStore {
setupNewSecretStorage?: boolean;
setupNewKeyBackup?: boolean;
} = {
setupNewKeyBackup: false,
setupNewKeyBackup: options.setupNewKeyBackup === true,
};
if (shouldRecreateSecretStorage) {