mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-24 02:24:27 +00:00
Matrix-js: auto-repair cross-signing bootstrap
This commit is contained in:
@@ -2,6 +2,7 @@ import { EventEmitter } from "node:events";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { encodeRecoveryKey } from "matrix-js-sdk/lib/crypto-api/recovery-key.js";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
class FakeMatrixEvent extends EventEmitter {
|
||||
@@ -691,6 +692,60 @@ describe("MatrixClient crypto bootstrapping", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("retries bootstrap with forced reset when initial publish/verification is incomplete", 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()
|
||||
.mockResolvedValueOnce({
|
||||
crossSigningReady: false,
|
||||
crossSigningPublished: false,
|
||||
ownDeviceVerified: false,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
crossSigningReady: true,
|
||||
crossSigningPublished: true,
|
||||
ownDeviceVerified: true,
|
||||
});
|
||||
(
|
||||
client as unknown as {
|
||||
cryptoBootstrapper: { bootstrap: typeof bootstrapSpy };
|
||||
}
|
||||
).cryptoBootstrapper.bootstrap = bootstrapSpy;
|
||||
|
||||
await client.start();
|
||||
|
||||
expect(bootstrapSpy).toHaveBeenCalledTimes(2);
|
||||
expect(bootstrapSpy.mock.calls[1]?.[1]).toEqual({
|
||||
forceResetCrossSigning: true,
|
||||
strict: true,
|
||||
});
|
||||
});
|
||||
|
||||
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, {
|
||||
encryption: true,
|
||||
});
|
||||
const bootstrapSpy = vi.fn().mockResolvedValue({
|
||||
crossSigningReady: false,
|
||||
crossSigningPublished: false,
|
||||
ownDeviceVerified: false,
|
||||
});
|
||||
(
|
||||
client as unknown as {
|
||||
cryptoBootstrapper: { bootstrap: typeof bootstrapSpy };
|
||||
}
|
||||
).cryptoBootstrapper.bootstrap = bootstrapSpy;
|
||||
|
||||
await client.start();
|
||||
|
||||
expect(bootstrapSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("provides secret storage callbacks and resolves stored recovery key", async () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "matrix-sdk-test-"));
|
||||
const recoveryKeyPath = path.join(tmpDir, "recovery-key.json");
|
||||
@@ -748,4 +803,148 @@ describe("MatrixClient crypto bootstrapping", () => {
|
||||
await vi.advanceTimersByTimeAsync(120_000);
|
||||
expect(databasesSpy.mock.calls.length).toBe(callsAfterStop);
|
||||
});
|
||||
|
||||
it("reports own verification status when crypto marks device as verified", async () => {
|
||||
matrixJsClient.getUserId = vi.fn(() => "@bot:example.org");
|
||||
matrixJsClient.getDeviceId = vi.fn(() => "DEVICE123");
|
||||
matrixJsClient.getCrypto = vi.fn(() => ({
|
||||
on: vi.fn(),
|
||||
bootstrapCrossSigning: vi.fn(async () => {}),
|
||||
bootstrapSecretStorage: vi.fn(async () => {}),
|
||||
requestOwnUserVerification: vi.fn(async () => null),
|
||||
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,
|
||||
});
|
||||
await client.start();
|
||||
|
||||
const status = await client.getOwnDeviceVerificationStatus();
|
||||
expect(status.encryptionEnabled).toBe(true);
|
||||
expect(status.verified).toBe(true);
|
||||
expect(status.userId).toBe("@bot:example.org");
|
||||
expect(status.deviceId).toBe("DEVICE123");
|
||||
});
|
||||
|
||||
it("verifies with a provided recovery key and reports success", async () => {
|
||||
const encoded = encodeRecoveryKey(new Uint8Array(Array.from({ length: 32 }, (_, i) => i + 1)));
|
||||
expect(encoded).toBeTypeOf("string");
|
||||
|
||||
matrixJsClient.getUserId = vi.fn(() => "@bot:example.org");
|
||||
matrixJsClient.getDeviceId = vi.fn(() => "DEVICE123");
|
||||
const bootstrapSecretStorage = vi.fn(async () => {});
|
||||
const bootstrapCrossSigning = vi.fn(async () => {});
|
||||
const getSecretStorageStatus = vi.fn(async () => ({
|
||||
ready: true,
|
||||
defaultKeyId: "SSSSKEY",
|
||||
secretStorageKeyValidityMap: { SSSSKEY: true },
|
||||
}));
|
||||
const getDeviceVerificationStatus = vi.fn(async () => ({
|
||||
isVerified: () => true,
|
||||
localVerified: true,
|
||||
crossSigningVerified: true,
|
||||
signedByOwner: true,
|
||||
}));
|
||||
matrixJsClient.getCrypto = vi.fn(() => ({
|
||||
on: vi.fn(),
|
||||
bootstrapCrossSigning,
|
||||
bootstrapSecretStorage,
|
||||
requestOwnUserVerification: vi.fn(async () => null),
|
||||
getSecretStorageStatus,
|
||||
getDeviceVerificationStatus,
|
||||
}));
|
||||
|
||||
const recoveryDir = fs.mkdtempSync(path.join(os.tmpdir(), "matrix-sdk-verify-key-"));
|
||||
const client = new MatrixClient("https://matrix.example.org", "token", undefined, undefined, {
|
||||
encryption: true,
|
||||
recoveryKeyPath: path.join(recoveryDir, "recovery-key.json"),
|
||||
});
|
||||
await client.start();
|
||||
|
||||
const result = await client.verifyWithRecoveryKey(encoded as string);
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.verified).toBe(true);
|
||||
expect(result.recoveryKeyStored).toBe(true);
|
||||
expect(result.deviceId).toBe("DEVICE123");
|
||||
expect(bootstrapSecretStorage).toHaveBeenCalled();
|
||||
expect(bootstrapCrossSigning).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reports bootstrap failure when cross-signing keys are not published", async () => {
|
||||
matrixJsClient.getUserId = vi.fn(() => "@bot:example.org");
|
||||
matrixJsClient.getDeviceId = vi.fn(() => "DEVICE123");
|
||||
matrixJsClient.getCrypto = vi.fn(() => ({
|
||||
on: vi.fn(),
|
||||
bootstrapCrossSigning: vi.fn(async () => {}),
|
||||
bootstrapSecretStorage: vi.fn(async () => {}),
|
||||
requestOwnUserVerification: vi.fn(async () => null),
|
||||
isCrossSigningReady: vi.fn(async () => false),
|
||||
userHasCrossSigningKeys: vi.fn(async () => false),
|
||||
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: false,
|
||||
selfSigningKeyPublished: false,
|
||||
userSigningKeyPublished: false,
|
||||
published: false,
|
||||
});
|
||||
|
||||
const result = await client.bootstrapOwnDeviceVerification();
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain(
|
||||
"Cross-signing bootstrap finished but server keys are still not published",
|
||||
);
|
||||
});
|
||||
|
||||
it("reports bootstrap success when own device is verified and keys are published", async () => {
|
||||
matrixJsClient.getUserId = vi.fn(() => "@bot:example.org");
|
||||
matrixJsClient.getDeviceId = vi.fn(() => "DEVICE123");
|
||||
matrixJsClient.getCrypto = vi.fn(() => ({
|
||||
on: vi.fn(),
|
||||
bootstrapCrossSigning: vi.fn(async () => {}),
|
||||
bootstrapSecretStorage: vi.fn(async () => {}),
|
||||
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,
|
||||
});
|
||||
|
||||
const result = await client.bootstrapOwnDeviceVerification();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.verification.verified).toBe(true);
|
||||
expect(result.crossSigning.published).toBe(true);
|
||||
expect(result.cryptoBootstrap).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "matrix-js-sdk";
|
||||
import { VerificationMethod } from "matrix-js-sdk/lib/types.js";
|
||||
import { MatrixCryptoBootstrapper } from "./sdk/crypto-bootstrap.js";
|
||||
import type { MatrixCryptoBootstrapResult } from "./sdk/crypto-bootstrap.js";
|
||||
import { createMatrixCryptoFacade, type MatrixCryptoFacade } from "./sdk/crypto-facade.js";
|
||||
import { MatrixDecryptBridge } from "./sdk/decrypt-bridge.js";
|
||||
import { matrixEventToRaw, parseMxc } from "./sdk/event-helpers.js";
|
||||
@@ -20,6 +21,7 @@ import { type HttpMethod, type QueryParams } from "./sdk/transport.js";
|
||||
import type {
|
||||
MatrixClientEventMap,
|
||||
MatrixCryptoBootstrapApi,
|
||||
MatrixDeviceVerificationStatusLike,
|
||||
MatrixRawEvent,
|
||||
MessageEventContent,
|
||||
} from "./sdk/types.js";
|
||||
@@ -39,6 +41,54 @@ export type {
|
||||
TextualMessageEventContent,
|
||||
} from "./sdk/types.js";
|
||||
|
||||
export type MatrixOwnDeviceVerificationStatus = {
|
||||
encryptionEnabled: boolean;
|
||||
userId: string | null;
|
||||
deviceId: string | null;
|
||||
verified: boolean;
|
||||
localVerified: boolean;
|
||||
crossSigningVerified: boolean;
|
||||
signedByOwner: boolean;
|
||||
recoveryKeyStored: boolean;
|
||||
recoveryKeyCreatedAt: string | null;
|
||||
recoveryKeyId: string | null;
|
||||
backupVersion: string | null;
|
||||
};
|
||||
|
||||
export type MatrixRecoveryKeyVerificationResult = MatrixOwnDeviceVerificationStatus & {
|
||||
success: boolean;
|
||||
verifiedAt?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export type MatrixOwnCrossSigningPublicationStatus = {
|
||||
userId: string | null;
|
||||
masterKeyPublished: boolean;
|
||||
selfSigningKeyPublished: boolean;
|
||||
userSigningKeyPublished: boolean;
|
||||
published: boolean;
|
||||
};
|
||||
|
||||
export type MatrixVerificationBootstrapResult = {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
verification: MatrixOwnDeviceVerificationStatus;
|
||||
crossSigning: MatrixOwnCrossSigningPublicationStatus;
|
||||
pendingVerifications: number;
|
||||
cryptoBootstrap: MatrixCryptoBootstrapResult | null;
|
||||
};
|
||||
|
||||
function isMatrixDeviceVerified(
|
||||
status: MatrixDeviceVerificationStatusLike | null | undefined,
|
||||
): boolean {
|
||||
return (
|
||||
status?.isVerified?.() === true ||
|
||||
status?.localVerified === true ||
|
||||
status?.crossSigningVerified === true ||
|
||||
status?.signedByOwner === true
|
||||
);
|
||||
}
|
||||
|
||||
export class MatrixClient {
|
||||
private readonly client: MatrixJsClient;
|
||||
private readonly emitter = new EventEmitter();
|
||||
@@ -46,10 +96,12 @@ export class MatrixClient {
|
||||
private readonly localTimeoutMs: number;
|
||||
private readonly initialSyncLimit?: number;
|
||||
private readonly encryptionEnabled: boolean;
|
||||
private readonly password?: string;
|
||||
private readonly idbSnapshotPath?: string;
|
||||
private readonly cryptoDatabasePrefix?: string;
|
||||
private bridgeRegistered = false;
|
||||
private started = false;
|
||||
private cryptoBootstrapped = false;
|
||||
private selfUserId: string | null;
|
||||
private readonly dmRoomIds = new Set<string>();
|
||||
private cryptoInitialized = false;
|
||||
@@ -88,6 +140,7 @@ export class MatrixClient {
|
||||
this.localTimeoutMs = Math.max(1, opts.localTimeoutMs ?? 60_000);
|
||||
this.initialSyncLimit = opts.initialSyncLimit;
|
||||
this.encryptionEnabled = opts.encryption === true;
|
||||
this.password = opts.password;
|
||||
this.idbSnapshotPath = opts.idbSnapshotPath;
|
||||
this.cryptoDatabasePrefix = opts.cryptoDatabasePrefix;
|
||||
this.selfUserId = opts.userId?.trim() || null;
|
||||
@@ -176,6 +229,7 @@ export class MatrixClient {
|
||||
await this.client.startClient({
|
||||
initialSyncLimit: this.initialSyncLimit,
|
||||
});
|
||||
await this.bootstrapCryptoIfNeeded();
|
||||
this.started = true;
|
||||
this.emitOutstandingInviteEvents();
|
||||
await this.refreshDmCache().catch(noop);
|
||||
@@ -196,6 +250,45 @@ export class MatrixClient {
|
||||
this.started = false;
|
||||
}
|
||||
|
||||
private async bootstrapCryptoIfNeeded(): Promise<void> {
|
||||
if (!this.encryptionEnabled || !this.cryptoInitialized || this.cryptoBootstrapped) {
|
||||
return;
|
||||
}
|
||||
const crypto = this.client.getCrypto() as MatrixCryptoBootstrapApi | undefined;
|
||||
if (!crypto) {
|
||||
return;
|
||||
}
|
||||
const initial = await this.cryptoBootstrapper.bootstrap(crypto);
|
||||
if (!initial.crossSigningPublished || initial.ownDeviceVerified === false) {
|
||||
if (this.password?.trim()) {
|
||||
try {
|
||||
const repaired = await this.cryptoBootstrapper.bootstrap(crypto, {
|
||||
forceResetCrossSigning: true,
|
||||
strict: true,
|
||||
});
|
||||
if (repaired.crossSigningPublished && repaired.ownDeviceVerified !== false) {
|
||||
LogService.info(
|
||||
"MatrixClientLite",
|
||||
"Cross-signing/bootstrap recovered after forced reset",
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
LogService.warn(
|
||||
"MatrixClientLite",
|
||||
"Failed to recover cross-signing/bootstrap with forced reset:",
|
||||
err,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
LogService.warn(
|
||||
"MatrixClientLite",
|
||||
"Cross-signing/bootstrap incomplete and no password is configured for UIA fallback",
|
||||
);
|
||||
}
|
||||
}
|
||||
this.cryptoBootstrapped = true;
|
||||
}
|
||||
|
||||
private async initializeCryptoIfNeeded(): Promise<void> {
|
||||
if (!this.encryptionEnabled || this.cryptoInitialized) {
|
||||
return;
|
||||
@@ -210,11 +303,6 @@ export class MatrixClient {
|
||||
});
|
||||
this.cryptoInitialized = true;
|
||||
|
||||
const crypto = this.client.getCrypto() as MatrixCryptoBootstrapApi | undefined;
|
||||
if (crypto) {
|
||||
await this.cryptoBootstrapper.bootstrap(crypto);
|
||||
}
|
||||
|
||||
// Persist the crypto store after successful init (captures fresh keys on first run).
|
||||
await persistIdbToDisk({
|
||||
snapshotPath: this.idbSnapshotPath,
|
||||
@@ -412,6 +500,241 @@ export class MatrixClient {
|
||||
});
|
||||
}
|
||||
|
||||
async getOwnDeviceVerificationStatus(): Promise<MatrixOwnDeviceVerificationStatus> {
|
||||
const recoveryKey = this.recoveryKeyStore.getRecoveryKeySummary();
|
||||
const userId = this.client.getUserId() ?? this.selfUserId ?? null;
|
||||
const deviceId = this.client.getDeviceId()?.trim() || null;
|
||||
|
||||
if (!this.encryptionEnabled) {
|
||||
return {
|
||||
encryptionEnabled: false,
|
||||
userId,
|
||||
deviceId,
|
||||
verified: false,
|
||||
localVerified: false,
|
||||
crossSigningVerified: false,
|
||||
signedByOwner: false,
|
||||
recoveryKeyStored: Boolean(recoveryKey),
|
||||
recoveryKeyCreatedAt: recoveryKey?.createdAt ?? null,
|
||||
recoveryKeyId: recoveryKey?.keyId ?? null,
|
||||
backupVersion: null,
|
||||
};
|
||||
}
|
||||
|
||||
const crypto = this.client.getCrypto() as MatrixCryptoBootstrapApi | undefined;
|
||||
let deviceStatus: MatrixDeviceVerificationStatusLike | null = null;
|
||||
if (crypto && userId && deviceId && typeof crypto.getDeviceVerificationStatus === "function") {
|
||||
deviceStatus = await crypto.getDeviceVerificationStatus(userId, deviceId).catch(() => null);
|
||||
}
|
||||
|
||||
return {
|
||||
encryptionEnabled: true,
|
||||
userId,
|
||||
deviceId,
|
||||
verified: isMatrixDeviceVerified(deviceStatus),
|
||||
localVerified: deviceStatus?.localVerified === true,
|
||||
crossSigningVerified: deviceStatus?.crossSigningVerified === true,
|
||||
signedByOwner: deviceStatus?.signedByOwner === true,
|
||||
recoveryKeyStored: Boolean(recoveryKey),
|
||||
recoveryKeyCreatedAt: recoveryKey?.createdAt ?? null,
|
||||
recoveryKeyId: recoveryKey?.keyId ?? null,
|
||||
backupVersion: await this.resolveRoomKeyBackupVersion(),
|
||||
};
|
||||
}
|
||||
|
||||
async verifyWithRecoveryKey(
|
||||
rawRecoveryKey: string,
|
||||
): Promise<MatrixRecoveryKeyVerificationResult> {
|
||||
if (!this.encryptionEnabled) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Matrix encryption is disabled for this client",
|
||||
encryptionEnabled: false,
|
||||
userId: this.client.getUserId() ?? this.selfUserId ?? null,
|
||||
deviceId: this.client.getDeviceId()?.trim() || null,
|
||||
verified: false,
|
||||
localVerified: false,
|
||||
crossSigningVerified: false,
|
||||
signedByOwner: false,
|
||||
recoveryKeyStored: false,
|
||||
recoveryKeyCreatedAt: null,
|
||||
recoveryKeyId: null,
|
||||
backupVersion: null,
|
||||
};
|
||||
}
|
||||
|
||||
const crypto = this.client.getCrypto() as MatrixCryptoBootstrapApi | undefined;
|
||||
if (!crypto) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Matrix crypto is not available (start client with encryption enabled)",
|
||||
...(await this.getOwnDeviceVerificationStatus()),
|
||||
};
|
||||
}
|
||||
|
||||
const trimmedRecoveryKey = rawRecoveryKey.trim();
|
||||
if (!trimmedRecoveryKey) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Matrix recovery key is required",
|
||||
...(await this.getOwnDeviceVerificationStatus()),
|
||||
};
|
||||
}
|
||||
|
||||
let defaultKeyId: string | null | undefined = undefined;
|
||||
if (typeof crypto.getSecretStorageStatus === "function") {
|
||||
const status = await crypto.getSecretStorageStatus().catch(() => null);
|
||||
defaultKeyId = status?.defaultKeyId;
|
||||
}
|
||||
|
||||
try {
|
||||
this.recoveryKeyStore.storeEncodedRecoveryKey({
|
||||
encodedPrivateKey: trimmedRecoveryKey,
|
||||
keyId: defaultKeyId,
|
||||
});
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
...(await this.getOwnDeviceVerificationStatus()),
|
||||
};
|
||||
}
|
||||
|
||||
await this.cryptoBootstrapper.bootstrap(crypto);
|
||||
const status = await this.getOwnDeviceVerificationStatus();
|
||||
if (!status.verified) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
"Matrix device is still unverified after applying recovery key. Verify your recovery key and ensure cross-signing is available.",
|
||||
...status,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
verifiedAt: new Date().toISOString(),
|
||||
...status,
|
||||
};
|
||||
}
|
||||
|
||||
async getOwnCrossSigningPublicationStatus(): Promise<MatrixOwnCrossSigningPublicationStatus> {
|
||||
const userId = this.client.getUserId() ?? this.selfUserId ?? null;
|
||||
if (!userId) {
|
||||
return {
|
||||
userId: null,
|
||||
masterKeyPublished: false,
|
||||
selfSigningKeyPublished: false,
|
||||
userSigningKeyPublished: false,
|
||||
published: false,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const response = (await this.doRequest("POST", "/_matrix/client/v3/keys/query", undefined, {
|
||||
device_keys: { [userId]: [] as string[] },
|
||||
})) as {
|
||||
master_keys?: Record<string, unknown>;
|
||||
self_signing_keys?: Record<string, unknown>;
|
||||
user_signing_keys?: Record<string, unknown>;
|
||||
};
|
||||
const masterKeyPublished = Boolean(response.master_keys?.[userId]);
|
||||
const selfSigningKeyPublished = Boolean(response.self_signing_keys?.[userId]);
|
||||
const userSigningKeyPublished = Boolean(response.user_signing_keys?.[userId]);
|
||||
return {
|
||||
userId,
|
||||
masterKeyPublished,
|
||||
selfSigningKeyPublished,
|
||||
userSigningKeyPublished,
|
||||
published: masterKeyPublished && selfSigningKeyPublished && userSigningKeyPublished,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
userId,
|
||||
masterKeyPublished: false,
|
||||
selfSigningKeyPublished: false,
|
||||
userSigningKeyPublished: false,
|
||||
published: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async bootstrapOwnDeviceVerification(params?: {
|
||||
recoveryKey?: string;
|
||||
forceResetCrossSigning?: boolean;
|
||||
}): Promise<MatrixVerificationBootstrapResult> {
|
||||
const pendingVerifications = async (): Promise<number> =>
|
||||
this.crypto ? (await this.crypto.listVerifications()).length : 0;
|
||||
if (!this.encryptionEnabled) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Matrix encryption is disabled for this client",
|
||||
verification: await this.getOwnDeviceVerificationStatus(),
|
||||
crossSigning: await this.getOwnCrossSigningPublicationStatus(),
|
||||
pendingVerifications: await pendingVerifications(),
|
||||
cryptoBootstrap: null,
|
||||
};
|
||||
}
|
||||
|
||||
let bootstrapError: string | undefined;
|
||||
let bootstrapSummary: MatrixCryptoBootstrapResult | null = null;
|
||||
try {
|
||||
await this.initializeCryptoIfNeeded();
|
||||
const crypto = this.client.getCrypto() as MatrixCryptoBootstrapApi | undefined;
|
||||
if (!crypto) {
|
||||
throw new Error("Matrix crypto is not available (start client with encryption enabled)");
|
||||
}
|
||||
|
||||
const rawRecoveryKey = params?.recoveryKey?.trim();
|
||||
if (rawRecoveryKey) {
|
||||
let defaultKeyId: string | null | undefined = undefined;
|
||||
if (typeof crypto.getSecretStorageStatus === "function") {
|
||||
const status = await crypto.getSecretStorageStatus().catch(() => null);
|
||||
defaultKeyId = status?.defaultKeyId;
|
||||
}
|
||||
this.recoveryKeyStore.storeEncodedRecoveryKey({
|
||||
encodedPrivateKey: rawRecoveryKey,
|
||||
keyId: defaultKeyId,
|
||||
});
|
||||
}
|
||||
|
||||
bootstrapSummary = await this.cryptoBootstrapper.bootstrap(crypto, {
|
||||
forceResetCrossSigning: params?.forceResetCrossSigning === true,
|
||||
strict: true,
|
||||
});
|
||||
} catch (err) {
|
||||
bootstrapError = err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
|
||||
const verification = await this.getOwnDeviceVerificationStatus();
|
||||
const crossSigning = await this.getOwnCrossSigningPublicationStatus();
|
||||
const success = verification.verified && crossSigning.published;
|
||||
const error =
|
||||
bootstrapError ??
|
||||
(success
|
||||
? undefined
|
||||
: "Matrix verification bootstrap did not produce a verified device with published cross-signing keys");
|
||||
return {
|
||||
success,
|
||||
error,
|
||||
verification,
|
||||
crossSigning,
|
||||
pendingVerifications: await pendingVerifications(),
|
||||
cryptoBootstrap: bootstrapSummary,
|
||||
};
|
||||
}
|
||||
|
||||
private async resolveRoomKeyBackupVersion(): Promise<string | null> {
|
||||
try {
|
||||
const response = (await this.doRequest("GET", "/_matrix/client/v3/room_keys/version")) as {
|
||||
version?: string;
|
||||
};
|
||||
return response.version?.trim() || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private registerBridge(): void {
|
||||
if (this.bridgeRegistered) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user