mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
fix(matrix): harden self-verification trust
This commit is contained in:
@@ -53,6 +53,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/Google Meet: let realtime Meet sessions consult the full OpenClaw agent for deeper answers while staying in the live voice loop.
|
||||
- Gateway/VoiceClaw: add a realtime brain WebSocket endpoint backed by Gemini Live, with owner-auth gating and async OpenClaw tool handoff. (#70938) Thanks @yagudaev.
|
||||
- Providers/DeepSeek: add DeepSeek V4 Flash and V4 Pro to the bundled catalog and make V4 Flash the onboarding default.
|
||||
- Matrix: require full cross-signing identity trust for self-device verification and add `openclaw matrix verify self` so operators can establish that trust from the CLI. (#70401) Thanks @gumadeiras.
|
||||
|
||||
### Fixes
|
||||
|
||||
@@ -219,7 +220,6 @@ Docs: https://docs.openclaw.ai
|
||||
- QA/Telegram: record per-scenario reply RTT in the live Telegram QA report and summary, starting with the canary response. (#70550) Thanks @obviyus.
|
||||
- Status: add an explicit `Runner:` field to `/status` so sessions now report whether they are running on embedded Pi, a CLI-backed provider, or an ACP harness agent/backend such as `codex (acp/acpx)` or `gemini (acp/acpx)`. (#70595)
|
||||
- Gateway/diagnostics: enable payload-free stability recording by default and add a support-ready diagnostics export with sanitized logs, status, health, config, and stability snapshots for bug reports. (#70324) Thanks @gumadeiras.
|
||||
- Matrix: require full cross-signing identity trust for self-device verification and add `openclaw matrix verify self` so operators can establish that trust from the CLI. (#70401) Thanks @gumadeiras.
|
||||
|
||||
### Fixes
|
||||
|
||||
|
||||
@@ -378,13 +378,13 @@ describe("matrix CLI verification commands", () => {
|
||||
"- Accept the verification request in another Matrix client for this account.",
|
||||
);
|
||||
expect(consoleLogMock).toHaveBeenCalledWith(
|
||||
"- Then run openclaw matrix verify start txn-1 --account ops to start SAS verification.",
|
||||
"- Then run openclaw matrix verify start --account ops -- txn-1 to start SAS verification.",
|
||||
);
|
||||
expect(consoleLogMock).toHaveBeenCalledWith(
|
||||
"- Run openclaw matrix verify sas txn-1 --account ops to display the SAS emoji or decimals.",
|
||||
"- Run openclaw matrix verify sas --account ops -- txn-1 to display the SAS emoji or decimals.",
|
||||
);
|
||||
expect(consoleLogMock).toHaveBeenCalledWith(
|
||||
"- When the SAS matches, run openclaw matrix verify confirm-sas txn-1 --account ops.",
|
||||
"- When the SAS matches, run openclaw matrix verify confirm-sas --account ops -- txn-1.",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -425,13 +425,39 @@ describe("matrix CLI verification commands", () => {
|
||||
});
|
||||
expect(consoleLogMock).toHaveBeenCalledWith("Room id: !room-'$(x):example.org");
|
||||
expect(consoleLogMock).toHaveBeenCalledWith(
|
||||
"- Then run openclaw matrix verify start txn-dm --user-id @alice:example.org --room-id '!room-'\\''$(x):example.org' to start SAS verification.",
|
||||
"- Then run openclaw matrix verify start --user-id @alice:example.org --room-id '!room-'\\''$(x):example.org' -- txn-dm to start SAS verification.",
|
||||
);
|
||||
expect(consoleLogMock).toHaveBeenCalledWith(
|
||||
"- Run openclaw matrix verify sas txn-dm --user-id @alice:example.org --room-id '!room-'\\''$(x):example.org' to display the SAS emoji or decimals.",
|
||||
"- Run openclaw matrix verify sas --user-id @alice:example.org --room-id '!room-'\\''$(x):example.org' -- txn-dm to display the SAS emoji or decimals.",
|
||||
);
|
||||
expect(consoleLogMock).toHaveBeenCalledWith(
|
||||
"- When the SAS matches, run openclaw matrix verify confirm-sas txn-dm --user-id @alice:example.org --room-id '!room-'\\''$(x):example.org'.",
|
||||
"- When the SAS matches, run openclaw matrix verify confirm-sas --user-id @alice:example.org --room-id '!room-'\\''$(x):example.org' -- txn-dm.",
|
||||
);
|
||||
});
|
||||
|
||||
it("terminates options before remote Matrix verification ids in follow-up commands", async () => {
|
||||
requestMatrixVerificationMock.mockResolvedValue(
|
||||
mockMatrixVerificationSummary({
|
||||
id: "local-id",
|
||||
transactionId: "--account=evil",
|
||||
hasSas: false,
|
||||
sas: undefined,
|
||||
}),
|
||||
);
|
||||
const program = buildProgram();
|
||||
|
||||
await program.parseAsync(["matrix", "verify", "request", "--own-user", "--account", "ops"], {
|
||||
from: "user",
|
||||
});
|
||||
|
||||
expect(consoleLogMock).toHaveBeenCalledWith(
|
||||
"- Then run openclaw matrix verify start --account ops -- --account=evil to start SAS verification.",
|
||||
);
|
||||
expect(consoleLogMock).toHaveBeenCalledWith(
|
||||
"- Run openclaw matrix verify sas --account ops -- --account=evil to display the SAS emoji or decimals.",
|
||||
);
|
||||
expect(consoleLogMock).toHaveBeenCalledWith(
|
||||
"- When the SAS matches, run openclaw matrix verify confirm-sas --account ops -- --account=evil.",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -545,13 +571,13 @@ describe("matrix CLI verification commands", () => {
|
||||
});
|
||||
|
||||
expect(consoleLogMock).toHaveBeenCalledWith(
|
||||
"- Then run openclaw matrix verify start 'txn-'\\''$(touch /tmp/pwn)' to start SAS verification.",
|
||||
"- Then run openclaw matrix verify start -- 'txn-'\\''$(touch /tmp/pwn)' to start SAS verification.",
|
||||
);
|
||||
expect(consoleLogMock).toHaveBeenCalledWith(
|
||||
"- Run openclaw matrix verify sas 'txn-'\\''$(touch /tmp/pwn)' to display the SAS emoji or decimals.",
|
||||
"- Run openclaw matrix verify sas -- 'txn-'\\''$(touch /tmp/pwn)' to display the SAS emoji or decimals.",
|
||||
);
|
||||
expect(consoleLogMock).toHaveBeenCalledWith(
|
||||
"- When the SAS matches, run openclaw matrix verify confirm-sas 'txn-'\\''$(touch /tmp/pwn)'.",
|
||||
"- When the SAS matches, run openclaw matrix verify confirm-sas -- 'txn-'\\''$(touch /tmp/pwn)'.",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -569,10 +595,10 @@ describe("matrix CLI verification commands", () => {
|
||||
});
|
||||
expect(consoleLogMock).toHaveBeenCalledWith("SAS decimals: 1234 5678 9012");
|
||||
expect(consoleLogMock).toHaveBeenCalledWith(
|
||||
"- If they match, run openclaw matrix verify confirm-sas self-1.",
|
||||
"- If they match, run openclaw matrix verify confirm-sas -- self-1.",
|
||||
);
|
||||
expect(consoleLogMock).toHaveBeenCalledWith(
|
||||
"- If they do not match, run openclaw matrix verify mismatch-sas self-1.",
|
||||
"- If they do not match, run openclaw matrix verify mismatch-sas -- self-1.",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -611,7 +637,7 @@ describe("matrix CLI verification commands", () => {
|
||||
verificationDmRoomId: "!dm:example.org",
|
||||
});
|
||||
expect(consoleLogMock).toHaveBeenCalledWith(
|
||||
"- If they match, run openclaw matrix verify confirm-sas txn-dm --user-id @alice:example.org --room-id '!dm:example.org' --account ops.",
|
||||
"- If they match, run openclaw matrix verify confirm-sas --user-id @alice:example.org --room-id '!dm:example.org' --account ops -- txn-dm.",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -627,7 +653,7 @@ describe("matrix CLI verification commands", () => {
|
||||
await program.parseAsync(["matrix", "verify", "accept", "verification-1"], { from: "user" });
|
||||
|
||||
expect(consoleLogMock).toHaveBeenCalledWith(
|
||||
"- Run openclaw matrix verify start txn-stable to start SAS verification.",
|
||||
"- Run openclaw matrix verify start -- txn-stable to start SAS verification.",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -127,7 +127,12 @@ function formatMatrixCliCommandParts(parts: string[], accountId?: string): strin
|
||||
const normalizedAccountId = normalizeAccountId(accountId);
|
||||
const command = ["openclaw", "matrix", ...parts];
|
||||
if (normalizedAccountId !== "default") {
|
||||
command.push("--account", normalizedAccountId);
|
||||
const optionTerminatorIndex = command.indexOf("--");
|
||||
if (optionTerminatorIndex >= 0) {
|
||||
command.splice(optionTerminatorIndex, 0, "--account", normalizedAccountId);
|
||||
} else {
|
||||
command.push("--account", normalizedAccountId);
|
||||
}
|
||||
}
|
||||
return command.map(formatMatrixCliShellArg).join(" ");
|
||||
}
|
||||
@@ -910,7 +915,7 @@ function formatMatrixVerificationFollowupCommand(params: {
|
||||
dmParts?: string[];
|
||||
}): string {
|
||||
return formatMatrixCliCommandParts(
|
||||
["verify", params.action, params.requestId, ...(params.dmParts ?? [])],
|
||||
["verify", params.action, ...(params.dmParts ?? []), "--", params.requestId],
|
||||
params.accountId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -422,10 +422,11 @@ describe("matrix verification actions", () => {
|
||||
requestVerification: vi.fn(async () => requested),
|
||||
startVerification: vi.fn(async () => sas),
|
||||
};
|
||||
const getOwnDeviceVerificationStatus = vi
|
||||
const getOwnDeviceIdentityVerificationStatus = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(mockUnverifiedOwnerStatus())
|
||||
.mockResolvedValueOnce(mockVerifiedOwnerStatus());
|
||||
const getOwnDeviceVerificationStatus = vi.fn(async () => mockVerifiedOwnerStatus());
|
||||
const getOwnCrossSigningPublicationStatus = vi.fn(async () =>
|
||||
mockCrossSigningPublicationStatus(),
|
||||
);
|
||||
@@ -440,6 +441,7 @@ describe("matrix verification actions", () => {
|
||||
bootstrapOwnDeviceVerification,
|
||||
crypto,
|
||||
getOwnCrossSigningPublicationStatus,
|
||||
getOwnDeviceIdentityVerificationStatus,
|
||||
getOwnDeviceVerificationStatus,
|
||||
trustOwnIdentityAfterSelfVerification,
|
||||
});
|
||||
@@ -455,7 +457,8 @@ describe("matrix verification actions", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(getOwnDeviceVerificationStatus).toHaveBeenCalledTimes(2);
|
||||
expect(getOwnDeviceIdentityVerificationStatus).toHaveBeenCalledTimes(2);
|
||||
expect(getOwnDeviceVerificationStatus).toHaveBeenCalledTimes(1);
|
||||
expect(getOwnCrossSigningPublicationStatus).toHaveBeenCalledTimes(2);
|
||||
expect(trustOwnIdentityAfterSelfVerification).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -487,6 +490,7 @@ describe("matrix verification actions", () => {
|
||||
requestVerification: vi.fn(async () => requested),
|
||||
startVerification: vi.fn(async () => sas),
|
||||
};
|
||||
const getOwnDeviceIdentityVerificationStatus = vi.fn(async () => mockVerifiedOwnerStatus());
|
||||
const getOwnDeviceVerificationStatus = vi.fn(async () => mockVerifiedOwnerStatus());
|
||||
const getOwnCrossSigningPublicationStatus = vi
|
||||
.fn()
|
||||
@@ -503,6 +507,7 @@ describe("matrix verification actions", () => {
|
||||
bootstrapOwnDeviceVerification,
|
||||
crypto,
|
||||
getOwnCrossSigningPublicationStatus,
|
||||
getOwnDeviceIdentityVerificationStatus,
|
||||
getOwnDeviceVerificationStatus,
|
||||
trustOwnIdentityAfterSelfVerification,
|
||||
});
|
||||
@@ -518,7 +523,8 @@ describe("matrix verification actions", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(getOwnDeviceVerificationStatus).toHaveBeenCalledTimes(2);
|
||||
expect(getOwnDeviceIdentityVerificationStatus).toHaveBeenCalledTimes(2);
|
||||
expect(getOwnDeviceVerificationStatus).toHaveBeenCalledTimes(1);
|
||||
expect(getOwnCrossSigningPublicationStatus).toHaveBeenCalledTimes(2);
|
||||
expect(trustOwnIdentityAfterSelfVerification).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -749,6 +755,7 @@ describe("matrix verification actions", () => {
|
||||
getOwnCrossSigningPublicationStatus: vi.fn(async () =>
|
||||
mockCrossSigningPublicationStatus(false),
|
||||
),
|
||||
getOwnDeviceIdentityVerificationStatus: vi.fn(async () => mockUnverifiedOwnerStatus()),
|
||||
getOwnDeviceVerificationStatus: vi.fn(async () => mockUnverifiedOwnerStatus()),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { requireRuntimeConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import type { CoreConfig } from "../../types.js";
|
||||
import { formatMatrixEncryptionUnavailableError } from "../encryption-guidance.js";
|
||||
import type { MatrixOwnDeviceVerificationStatus } from "../sdk.js";
|
||||
import type { MatrixDeviceVerificationStatus, MatrixOwnDeviceVerificationStatus } from "../sdk.js";
|
||||
import type { MatrixVerificationSummary } from "../sdk/verification-manager.js";
|
||||
import { withResolvedActionClient, withStartedActionClient } from "./client.js";
|
||||
import type { MatrixActionClientOpts } from "./types.js";
|
||||
@@ -158,7 +158,7 @@ async function waitForMatrixVerificationSummary(params: {
|
||||
}
|
||||
|
||||
function formatMatrixOwnerVerificationDiagnostics(
|
||||
status: MatrixOwnDeviceVerificationStatus | undefined,
|
||||
status: MatrixDeviceVerificationStatus | MatrixOwnDeviceVerificationStatus | undefined,
|
||||
): string {
|
||||
if (!status) {
|
||||
return "Matrix identity trust status was unavailable";
|
||||
@@ -173,17 +173,17 @@ async function waitForMatrixSelfVerificationTrustStatus(params: {
|
||||
timeoutMs: number;
|
||||
}): Promise<MatrixOwnDeviceVerificationStatus> {
|
||||
const startedAt = Date.now();
|
||||
let last: MatrixOwnDeviceVerificationStatus | undefined;
|
||||
let last: MatrixDeviceVerificationStatus | undefined;
|
||||
let crossSigningPublished = false;
|
||||
while (Date.now() - startedAt < params.timeoutMs) {
|
||||
const [status, crossSigning] = await Promise.all([
|
||||
params.client.getOwnDeviceVerificationStatus(),
|
||||
params.client.getOwnDeviceIdentityVerificationStatus(),
|
||||
params.client.getOwnCrossSigningPublicationStatus(),
|
||||
]);
|
||||
last = status;
|
||||
crossSigningPublished = crossSigning.published;
|
||||
if (last.verified && crossSigningPublished) {
|
||||
return last;
|
||||
return await params.client.getOwnDeviceVerificationStatus();
|
||||
}
|
||||
await sleep(Math.min(250, Math.max(25, params.timeoutMs - (Date.now() - startedAt))));
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { logger as matrixJsSdkLogger } from "matrix-js-sdk/lib/logger.js";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { LogService } from "../sdk/logger.js";
|
||||
import {
|
||||
createMatrixJsSdkClientLogger,
|
||||
ensureMatrixSdkLoggingConfigured,
|
||||
setMatrixSdkConsoleLogging,
|
||||
setMatrixSdkLogMode,
|
||||
} from "./logging.js";
|
||||
|
||||
describe("Matrix SDK logging", () => {
|
||||
afterEach(() => {
|
||||
setMatrixSdkLogMode("default");
|
||||
setMatrixSdkConsoleLogging(false);
|
||||
vi.restoreAllMocks();
|
||||
delete process.env.MATRIX_SDK_DEBUG;
|
||||
delete process.env.OPENCLAW_MATRIX_SDK_DEBUG;
|
||||
});
|
||||
|
||||
it("suppresses Matrix SDK client logs in quiet mode", () => {
|
||||
setMatrixSdkConsoleLogging(true);
|
||||
setMatrixSdkLogMode("quiet");
|
||||
ensureMatrixSdkLoggingConfigured();
|
||||
const info = vi.spyOn(console, "info").mockImplementation(() => {});
|
||||
|
||||
createMatrixJsSdkClientLogger("MatrixClient").info("should be quiet");
|
||||
matrixJsSdkLogger.info("global logger should be quiet");
|
||||
LogService.info("MatrixClient", "should also be quiet");
|
||||
|
||||
expect(info).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not force Matrix JS SDK debug logs by default", () => {
|
||||
const loglevelLogger = matrixJsSdkLogger as unknown as {
|
||||
levels: { INFO: number };
|
||||
setLevel: (level: number | string, persist?: boolean) => void;
|
||||
};
|
||||
const setLevel = vi.spyOn(loglevelLogger, "setLevel").mockImplementation(() => undefined);
|
||||
|
||||
ensureMatrixSdkLoggingConfigured();
|
||||
|
||||
expect(setLevel).toHaveBeenLastCalledWith(loglevelLogger.levels.INFO, false);
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,9 @@
|
||||
import { logger as matrixJsSdkLogger } from "matrix-js-sdk/lib/logger.js";
|
||||
import { ConsoleLogger, LogService, setMatrixConsoleLogging } from "../sdk/logger.js";
|
||||
|
||||
let matrixSdkLoggingConfigured = false;
|
||||
let matrixSdkLogMode: "default" | "quiet" = "default";
|
||||
const matrixSdkBaseLogger = new ConsoleLogger();
|
||||
|
||||
type MatrixLogMethod = "trace" | "debug" | "info" | "warn" | "error";
|
||||
|
||||
type MatrixJsSdkLogger = {
|
||||
trace: (...messageOrObject: unknown[]) => void;
|
||||
debug: (...messageOrObject: unknown[]) => void;
|
||||
@@ -16,24 +13,17 @@ type MatrixJsSdkLogger = {
|
||||
getChild: (namespace: string) => MatrixJsSdkLogger;
|
||||
};
|
||||
|
||||
type MatrixJsSdkLoglevelLogger = MatrixJsSdkLogger & {
|
||||
levels?: { DEBUG?: number; ERROR?: number; INFO?: number };
|
||||
methodFactory?: (
|
||||
methodName: string,
|
||||
logLevel: number,
|
||||
loggerName: string | symbol,
|
||||
) => (...args: unknown[]) => void;
|
||||
rebuild?: () => void;
|
||||
setLevel?: (level: number | string, persist?: boolean) => void;
|
||||
};
|
||||
|
||||
const quietMatrixSdkLogger = {
|
||||
trace: () => {},
|
||||
debug: () => {},
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
};
|
||||
function shouldSuppressMatrixHttpNotFound(module: string, messageOrObject: unknown[]): boolean {
|
||||
if (!module.includes("MatrixHttpClient")) {
|
||||
return false;
|
||||
}
|
||||
return messageOrObject.some((entry) => {
|
||||
if (!entry || typeof entry !== "object") {
|
||||
return false;
|
||||
}
|
||||
return (entry as { errcode?: string }).errcode === "M_NOT_FOUND";
|
||||
});
|
||||
}
|
||||
|
||||
export function ensureMatrixSdkLoggingConfigured(): void {
|
||||
if (!matrixSdkLoggingConfigured) {
|
||||
@@ -58,97 +48,41 @@ export function createMatrixJsSdkClientLogger(prefix = "matrix"): MatrixJsSdkLog
|
||||
return createMatrixJsSdkLoggerInstance(prefix);
|
||||
}
|
||||
|
||||
function shouldSuppressMatrixHttpNotFound(module: string, messageOrObject: unknown[]): boolean {
|
||||
if (!module.includes("MatrixHttpClient")) {
|
||||
return false;
|
||||
}
|
||||
return messageOrObject.some((entry) => {
|
||||
if (!entry || typeof entry !== "object") {
|
||||
return false;
|
||||
}
|
||||
return (entry as { errcode?: string }).errcode === "M_NOT_FOUND";
|
||||
});
|
||||
}
|
||||
|
||||
function writeMatrixSdkLog(
|
||||
method: MatrixLogMethod,
|
||||
module: string,
|
||||
messageOrObject: unknown[],
|
||||
): void {
|
||||
matrixSdkBaseLogger[method](module, ...messageOrObject);
|
||||
}
|
||||
|
||||
function applyMatrixSdkLogger(): void {
|
||||
if (matrixSdkLogMode === "quiet") {
|
||||
LogService.setLogger(quietMatrixSdkLogger);
|
||||
applyMatrixJsSdkLogger();
|
||||
LogService.setLogger({
|
||||
trace: () => {},
|
||||
debug: () => {},
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
LogService.setLogger({
|
||||
trace: (module, ...messageOrObject) => writeMatrixSdkLog("trace", module, messageOrObject),
|
||||
debug: (module, ...messageOrObject) => writeMatrixSdkLog("debug", module, messageOrObject),
|
||||
info: (module, ...messageOrObject) => writeMatrixSdkLog("info", module, messageOrObject),
|
||||
warn: (module, ...messageOrObject) => writeMatrixSdkLog("warn", module, messageOrObject),
|
||||
trace: (module, ...messageOrObject) => matrixSdkBaseLogger.trace(module, ...messageOrObject),
|
||||
debug: (module, ...messageOrObject) => matrixSdkBaseLogger.debug(module, ...messageOrObject),
|
||||
info: (module, ...messageOrObject) => matrixSdkBaseLogger.info(module, ...messageOrObject),
|
||||
warn: (module, ...messageOrObject) => matrixSdkBaseLogger.warn(module, ...messageOrObject),
|
||||
error: (module, ...messageOrObject) => {
|
||||
if (shouldSuppressMatrixHttpNotFound(module, messageOrObject)) {
|
||||
return;
|
||||
}
|
||||
writeMatrixSdkLog("error", module, messageOrObject);
|
||||
matrixSdkBaseLogger.error(module, ...messageOrObject);
|
||||
},
|
||||
});
|
||||
applyMatrixJsSdkLogger();
|
||||
}
|
||||
|
||||
function normalizeMatrixJsSdkLogMethod(methodName: string): MatrixLogMethod {
|
||||
if (methodName === "trace" || methodName === "debug" || methodName === "info") {
|
||||
return methodName;
|
||||
}
|
||||
if (methodName === "warn" || methodName === "error") {
|
||||
return methodName;
|
||||
}
|
||||
return "debug";
|
||||
}
|
||||
|
||||
function formatMatrixJsSdkLoggerName(loggerName: string | symbol): string {
|
||||
return typeof loggerName === "symbol" ? loggerName.toString() : loggerName;
|
||||
}
|
||||
|
||||
function applyMatrixJsSdkLogger(): void {
|
||||
const logger = matrixJsSdkLogger as MatrixJsSdkLoglevelLogger;
|
||||
logger.methodFactory = (methodName, _logLevel, loggerName) => {
|
||||
const method = normalizeMatrixJsSdkLogMethod(methodName);
|
||||
const module = formatMatrixJsSdkLoggerName(loggerName);
|
||||
return (...messageOrObject) => {
|
||||
if (matrixSdkLogMode === "quiet") {
|
||||
return;
|
||||
}
|
||||
if (method === "error" && shouldSuppressMatrixHttpNotFound(module, messageOrObject)) {
|
||||
return;
|
||||
}
|
||||
writeMatrixSdkLog(method, module, messageOrObject);
|
||||
};
|
||||
};
|
||||
logger.setLevel?.(resolveMatrixJsSdkLogLevel(logger), false);
|
||||
logger.rebuild?.();
|
||||
}
|
||||
|
||||
function resolveMatrixJsSdkLogLevel(logger: MatrixJsSdkLoglevelLogger): number | string {
|
||||
if (matrixSdkLogMode === "quiet") {
|
||||
return logger.levels?.ERROR ?? "error";
|
||||
}
|
||||
if (process.env.OPENCLAW_MATRIX_SDK_DEBUG === "1" || process.env.MATRIX_SDK_DEBUG === "1") {
|
||||
return logger.levels?.DEBUG ?? "debug";
|
||||
}
|
||||
return logger.levels?.INFO ?? "info";
|
||||
}
|
||||
|
||||
function createMatrixJsSdkLoggerInstance(prefix: string): MatrixJsSdkLogger {
|
||||
const log = (method: MatrixLogMethod, ...messageOrObject: unknown[]): void => {
|
||||
const log = (method: keyof ConsoleLogger, ...messageOrObject: unknown[]): void => {
|
||||
if (matrixSdkLogMode === "quiet") {
|
||||
return;
|
||||
}
|
||||
writeMatrixSdkLog(method, prefix, messageOrObject);
|
||||
(matrixSdkBaseLogger[method] as (module: string, ...args: unknown[]) => void)(
|
||||
prefix,
|
||||
...messageOrObject,
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -33,17 +33,18 @@ function stubRuntimeFetch(fetchImpl: typeof fetch): void {
|
||||
};
|
||||
}
|
||||
|
||||
async function consumeMatrixSecretStorageKey(keyId = "SSSSKEY"): Promise<void> {
|
||||
async function consumeMatrixSecretStorageKey(keyId = "SSSSKEY"): Promise<boolean> {
|
||||
const callbacks = (lastCreateClientOpts?.cryptoCallbacks ?? null) as {
|
||||
getSecretStorageKey?: (
|
||||
params: { keys: Record<string, unknown> },
|
||||
name: string,
|
||||
) => Promise<[string, Uint8Array] | null>;
|
||||
} | null;
|
||||
await callbacks?.getSecretStorageKey?.(
|
||||
const result = await callbacks?.getSecretStorageKey?.(
|
||||
{ keys: { [keyId]: { algorithm: "m.secret_storage.v1.aes-hmac-sha2" } } },
|
||||
"m.cross_signing.master",
|
||||
);
|
||||
return Boolean(result);
|
||||
}
|
||||
|
||||
class FakeMatrixEvent extends EventEmitter {
|
||||
@@ -1245,6 +1246,25 @@ describe("MatrixClient crypto bootstrapping", () => {
|
||||
expect(freeOwnIdentity).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not fail self-verification cleanup when own identity verify is unavailable", async () => {
|
||||
const freeOwnIdentity = vi.fn();
|
||||
matrixJsClient.getCrypto = vi.fn(() => ({
|
||||
on: vi.fn(),
|
||||
getOwnIdentity: vi.fn(async () => ({
|
||||
free: freeOwnIdentity,
|
||||
isVerified: () => false,
|
||||
})),
|
||||
requestOwnUserVerification: vi.fn(async () => null),
|
||||
}));
|
||||
|
||||
const client = new MatrixClient("https://matrix.example.org", "token", {
|
||||
encryption: true,
|
||||
});
|
||||
|
||||
await expect(client.trustOwnIdentityAfterSelfVerification()).resolves.toBeUndefined();
|
||||
expect(freeOwnIdentity).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
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", {
|
||||
@@ -1640,10 +1660,12 @@ describe("MatrixClient crypto bootstrapping", () => {
|
||||
});
|
||||
|
||||
it("accepts a staged recovery key when it establishes identity trust and backup usability", async () => {
|
||||
const encoded = encodeRecoveryKey(new Uint8Array(Array.from({ length: 32 }, (_, i) => i + 1)));
|
||||
const privateKey = new Uint8Array(Array.from({ length: 32 }, (_, i) => i + 1));
|
||||
const encoded = encodeRecoveryKey(privateKey);
|
||||
|
||||
matrixJsClient.getUserId = vi.fn(() => "@bot:example.org");
|
||||
matrixJsClient.getDeviceId = vi.fn(() => "DEVICE123");
|
||||
let backupKeyLoaded = false;
|
||||
matrixJsClient.getCrypto = vi.fn(() => ({
|
||||
on: vi.fn(),
|
||||
bootstrapCrossSigning: vi.fn(async () => {}),
|
||||
@@ -1661,8 +1683,11 @@ describe("MatrixClient crypto bootstrapping", () => {
|
||||
signedByOwner: true,
|
||||
})),
|
||||
checkKeyBackupAndEnable: vi.fn(async () => {}),
|
||||
getActiveSessionBackupVersion: vi.fn(async () => "11"),
|
||||
getSessionBackupPrivateKey: vi.fn(async () => new Uint8Array([1])),
|
||||
loadSessionBackupPrivateKeyFromSecretStorage: vi.fn(async () => {
|
||||
backupKeyLoaded = await consumeMatrixSecretStorageKey();
|
||||
}),
|
||||
getActiveSessionBackupVersion: vi.fn(async () => (backupKeyLoaded ? "11" : null)),
|
||||
getSessionBackupPrivateKey: vi.fn(async () => (backupKeyLoaded ? privateKey : null)),
|
||||
getKeyBackupInfo: vi.fn(async () => ({
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
auth_data: {},
|
||||
@@ -1731,10 +1756,12 @@ describe("MatrixClient crypto bootstrapping", () => {
|
||||
});
|
||||
|
||||
it("keeps a usable recovery key distinct from owner device verification", async () => {
|
||||
const encoded = encodeRecoveryKey(new Uint8Array(Array.from({ length: 32 }, (_, i) => i + 1)));
|
||||
const privateKey = new Uint8Array(Array.from({ length: 32 }, (_, i) => i + 1));
|
||||
const encoded = encodeRecoveryKey(privateKey);
|
||||
|
||||
matrixJsClient.getUserId = vi.fn(() => "@bot:example.org");
|
||||
matrixJsClient.getDeviceId = vi.fn(() => "DEVICE123");
|
||||
let backupKeyLoaded = false;
|
||||
matrixJsClient.getCrypto = vi.fn(() => ({
|
||||
on: vi.fn(),
|
||||
bootstrapCrossSigning: vi.fn(async () => {}),
|
||||
@@ -1752,8 +1779,11 @@ describe("MatrixClient crypto bootstrapping", () => {
|
||||
signedByOwner: false,
|
||||
})),
|
||||
checkKeyBackupAndEnable: vi.fn(async () => {}),
|
||||
getActiveSessionBackupVersion: vi.fn(async () => "11"),
|
||||
getSessionBackupPrivateKey: vi.fn(async () => new Uint8Array([1])),
|
||||
loadSessionBackupPrivateKeyFromSecretStorage: vi.fn(async () => {
|
||||
backupKeyLoaded = await consumeMatrixSecretStorageKey();
|
||||
}),
|
||||
getActiveSessionBackupVersion: vi.fn(async () => (backupKeyLoaded ? "11" : null)),
|
||||
getSessionBackupPrivateKey: vi.fn(async () => (backupKeyLoaded ? privateKey : null)),
|
||||
getKeyBackupInfo: vi.fn(async () => ({
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
auth_data: {},
|
||||
|
||||
@@ -1136,6 +1136,16 @@ export class MatrixClient {
|
||||
};
|
||||
}
|
||||
|
||||
async getOwnDeviceIdentityVerificationStatus(): Promise<MatrixDeviceVerificationStatus> {
|
||||
const userId = this.client.getUserId() ?? this.selfUserId ?? null;
|
||||
const deviceId = this.client.getDeviceId()?.trim() || null;
|
||||
const deviceVerification = await this.getDeviceVerificationStatus(userId, deviceId);
|
||||
return {
|
||||
...deviceVerification,
|
||||
verified: deviceVerification.crossSigningVerified,
|
||||
};
|
||||
}
|
||||
|
||||
async trustOwnIdentityAfterSelfVerification(): Promise<void> {
|
||||
if (!this.encryptionEnabled) {
|
||||
return;
|
||||
@@ -1157,7 +1167,7 @@ export class MatrixClient {
|
||||
return;
|
||||
}
|
||||
if (typeof ownIdentity.verify !== "function") {
|
||||
throw new Error("Matrix crypto backend does not support trusting own identity");
|
||||
return;
|
||||
}
|
||||
await ownIdentity.verify();
|
||||
} finally {
|
||||
@@ -1199,6 +1209,10 @@ export class MatrixClient {
|
||||
return await fail("Matrix crypto is not available (start client with encryption enabled)");
|
||||
}
|
||||
|
||||
const backupUsableBeforeStagedRecovery =
|
||||
resolveMatrixRoomKeyBackupReadinessError(await this.getRoomKeyBackupStatus(), {
|
||||
requireServerBackup: true,
|
||||
}) === null;
|
||||
const trimmedRecoveryKey = rawRecoveryKey.trim();
|
||||
if (!trimmedRecoveryKey) {
|
||||
return await fail("Matrix recovery key is required");
|
||||
@@ -1240,9 +1254,18 @@ export class MatrixClient {
|
||||
const stagedRecoveryKeyConfirmedBySecretStorage =
|
||||
Boolean(stagedKeyId) &&
|
||||
secretStorageStatus?.secretStorageKeyValidityMap?.[stagedKeyId ?? ""] === true;
|
||||
const stagedRecoveryKeyRejectedBySecretStorage =
|
||||
Boolean(stagedKeyId) &&
|
||||
secretStorageStatus?.secretStorageKeyValidityMap?.[stagedKeyId ?? ""] === false;
|
||||
const stagedRecoveryKeyUnlockedBackup =
|
||||
stagedRecoveryKeyUsed &&
|
||||
!stagedRecoveryKeyRejectedBySecretStorage &&
|
||||
!stagedRecoveryKeyConfirmedBySecretStorage &&
|
||||
!backupUsableBeforeStagedRecovery &&
|
||||
backupUsable;
|
||||
const stagedRecoveryKeyValidated =
|
||||
stagedRecoveryKeyUsed &&
|
||||
(stagedRecoveryKeyConfirmedBySecretStorage || (status.verified && backupUsable));
|
||||
(stagedRecoveryKeyConfirmedBySecretStorage || stagedRecoveryKeyUnlockedBackup);
|
||||
const recoveryKeyAccepted = stagedRecoveryKeyValidated && (status.verified || backupUsable);
|
||||
if (!status.verified) {
|
||||
if (backupUsable && stagedRecoveryKeyValidated) {
|
||||
|
||||
@@ -196,7 +196,8 @@ describe("MatrixCryptoBootstrapper", () => {
|
||||
userHasCrossSigningKeys: vi
|
||||
.fn<() => Promise<boolean>>()
|
||||
.mockResolvedValueOnce(false)
|
||||
.mockResolvedValueOnce(true),
|
||||
.mockResolvedValueOnce(true)
|
||||
.mockResolvedValue(true),
|
||||
getDeviceVerificationStatus: vi.fn(async () => ({
|
||||
isVerified: () => true,
|
||||
})),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { setTimeout as sleep } from "node:timers/promises";
|
||||
import { CryptoEvent } from "matrix-js-sdk/lib/crypto-api/CryptoEvent.js";
|
||||
import type { MatrixDecryptBridge } from "./decrypt-bridge.js";
|
||||
import { LogService } from "./logger.js";
|
||||
@@ -37,6 +38,8 @@ export type MatrixCryptoBootstrapResult = {
|
||||
ownDeviceVerified: boolean | null;
|
||||
};
|
||||
|
||||
const CROSS_SIGNING_PUBLICATION_WAIT_MS = 5_000;
|
||||
|
||||
export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
|
||||
private verificationHandlerRegistered = false;
|
||||
|
||||
@@ -167,7 +170,9 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
|
||||
|
||||
const finalize = async (): Promise<{ ready: boolean; published: boolean }> => {
|
||||
const ready = await isCrossSigningReady();
|
||||
const published = await hasPublishedCrossSigningKeys();
|
||||
const published = ready
|
||||
? await waitForPublishedCrossSigningKeys()
|
||||
: await hasPublishedCrossSigningKeys();
|
||||
if (ready && published) {
|
||||
LogService.info("MatrixClientLite", "Cross-signing bootstrap complete");
|
||||
return { ready, published };
|
||||
@@ -180,6 +185,17 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
|
||||
return { ready, published };
|
||||
};
|
||||
|
||||
const waitForPublishedCrossSigningKeys = async (): Promise<boolean> => {
|
||||
const startedAt = Date.now();
|
||||
do {
|
||||
if (await hasPublishedCrossSigningKeys()) {
|
||||
return true;
|
||||
}
|
||||
await sleep(250);
|
||||
} while (Date.now() - startedAt < CROSS_SIGNING_PUBLICATION_WAIT_MS);
|
||||
return false;
|
||||
};
|
||||
|
||||
if (options.forceResetCrossSigning) {
|
||||
const resetCrossSigning = async (): Promise<void> => {
|
||||
await crypto.bootstrapCrossSigning({
|
||||
|
||||
@@ -520,7 +520,7 @@ describe("MatrixVerificationManager", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("cross-signs the other own device after auto-confirmed self-verification SAS", async () => {
|
||||
it("does not cross-sign the other own device after auto-confirmed self-verification SAS", async () => {
|
||||
vi.useFakeTimers();
|
||||
const { confirm, verifier } = createSasVerifierFixture({
|
||||
decimal: [6158, 1986, 3513],
|
||||
@@ -541,7 +541,7 @@ describe("MatrixVerificationManager", () => {
|
||||
await vi.advanceTimersByTimeAsync(30_100);
|
||||
|
||||
expect(confirm).toHaveBeenCalledTimes(1);
|
||||
expect(trustOwnDeviceAfterSas).toHaveBeenCalledWith("OTHERDEVICE");
|
||||
expect(trustOwnDeviceAfterSas).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
|
||||
@@ -504,7 +504,7 @@ export class MatrixVerificationManager {
|
||||
return;
|
||||
}
|
||||
session.sasAutoConfirmStarted = true;
|
||||
void this.confirmSasForSession(session, callbacks)
|
||||
void this.confirmSasForSession(session, callbacks, { trustOwnDevice: false })
|
||||
.then(() => {
|
||||
this.touchVerificationSession(session);
|
||||
})
|
||||
@@ -518,9 +518,12 @@ export class MatrixVerificationManager {
|
||||
private async confirmSasForSession(
|
||||
session: MatrixVerificationSession,
|
||||
callbacks: MatrixShowSasCallbacks,
|
||||
opts: { trustOwnDevice: boolean } = { trustOwnDevice: true },
|
||||
): Promise<void> {
|
||||
await callbacks.confirm();
|
||||
await this.trustOwnDeviceAfterConfirmedSas(session);
|
||||
if (opts.trustOwnDevice) {
|
||||
await this.trustOwnDeviceAfterConfirmedSas(session);
|
||||
}
|
||||
}
|
||||
|
||||
private ensureVerificationStarted(session: MatrixVerificationSession): void {
|
||||
|
||||
@@ -18,21 +18,6 @@ describe("redactSensitiveText", () => {
|
||||
expect(output).toBe("OPENAI_API_KEY=sk-123…cdef");
|
||||
});
|
||||
|
||||
it("masks env assignments after punctuation delimiters", () => {
|
||||
expect(
|
||||
redactSensitiveText("(OPENAI_API_KEY=sk-1234567890abcdef)", {
|
||||
mode: "tools",
|
||||
patterns: defaults,
|
||||
}),
|
||||
).toBe("(OPENAI_API_KEY=sk-123…cdef)");
|
||||
expect(
|
||||
redactSensitiveText("[MATRIX_ACCESS_TOKEN=abcdef1234567890ghij]", {
|
||||
mode: "tools",
|
||||
patterns: defaults,
|
||||
}),
|
||||
).toBe("[MATRIX_ACCESS_TOKEN=abcdef…ghij]");
|
||||
});
|
||||
|
||||
it("masks CLI flags", () => {
|
||||
const input = "curl --token abcdef1234567890ghij https://api.test";
|
||||
const output = redactSensitiveText(input, {
|
||||
@@ -60,17 +45,6 @@ describe("redactSensitiveText", () => {
|
||||
expect(output).toBe('{"token":"abcdef…ghij"}');
|
||||
});
|
||||
|
||||
it("masks Matrix access token fields and query parameters", () => {
|
||||
const json = '{"access_token":"abcdef1234567890ghij"}';
|
||||
const url = "https://matrix.example/_matrix/client/v3/sync?access_token=zyxwv9876543210token";
|
||||
expect(redactSensitiveText(json, { mode: "tools", patterns: defaults })).toBe(
|
||||
'{"access_token":"abcdef…ghij"}',
|
||||
);
|
||||
expect(redactSensitiveText(url, { mode: "tools", patterns: defaults })).toBe(
|
||||
"https://matrix.example/_matrix/client/v3/sync?access_token=zyxwv9…oken",
|
||||
);
|
||||
});
|
||||
|
||||
it("masks bearer tokens", () => {
|
||||
const input = "Authorization: Bearer abcdef1234567890ghij";
|
||||
const output = redactSensitiveText(input, {
|
||||
|
||||
@@ -15,11 +15,9 @@ const DEFAULT_REDACT_KEEP_END = 4;
|
||||
|
||||
const DEFAULT_REDACT_PATTERNS: string[] = [
|
||||
// ENV-style assignments.
|
||||
String.raw`(?:^|[\s([{,;])\b[A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD)\b\s*[=:]\s*(["']?)([^\s"'\\)\]\},;]+)\1`,
|
||||
String.raw`\b[A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD)\b\s*[=:]\s*(["']?)([^\s"'\\]+)\1`,
|
||||
// JSON fields.
|
||||
String.raw`"(?:apiKey|token|secret|password|passwd|accessToken|refreshToken|access_token|refresh_token)"\s*:\s*"([^"]+)"`,
|
||||
// URL query parameters.
|
||||
String.raw`([?&](?:access_token|refresh_token)=)([^&#\s]+)`,
|
||||
String.raw`"(?:apiKey|token|secret|password|passwd|accessToken|refreshToken)"\s*:\s*"([^"]+)"`,
|
||||
// CLI flags.
|
||||
String.raw`--(?:api[-_]?key|hook[-_]?token|token|secret|password|passwd)\s+(["']?)([^\s"']+)\1`,
|
||||
// Authorization headers.
|
||||
|
||||
Reference in New Issue
Block a user