mirror of https://github.com/openclaw/openclaw.git
153 lines
4.6 KiB
TypeScript
153 lines
4.6 KiB
TypeScript
import crypto from "node:crypto";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { runExec } from "../process/exec.js";
|
|
import { ensureDirForFile, normalizePositiveInt } from "./shared.js";
|
|
|
|
export const DEFAULT_SOPS_TIMEOUT_MS = 5_000;
|
|
const MAX_SOPS_OUTPUT_BYTES = 10 * 1024 * 1024;
|
|
|
|
function toSopsPath(value: string): string {
|
|
return value.replaceAll(path.sep, "/");
|
|
}
|
|
|
|
function resolveFilenameOverride(params: { targetPath: string }): string {
|
|
return toSopsPath(path.resolve(params.targetPath));
|
|
}
|
|
|
|
function resolveSopsCwd(params: { targetPath: string; configPath?: string }): string {
|
|
if (typeof params.configPath === "string" && params.configPath.trim().length > 0) {
|
|
return path.dirname(params.configPath);
|
|
}
|
|
return path.dirname(params.targetPath);
|
|
}
|
|
|
|
function normalizeTimeoutMs(value: number | undefined): number {
|
|
return normalizePositiveInt(value, DEFAULT_SOPS_TIMEOUT_MS);
|
|
}
|
|
|
|
function isTimeoutError(message: string | undefined): boolean {
|
|
return typeof message === "string" && message.toLowerCase().includes("timed out");
|
|
}
|
|
|
|
type SopsErrorContext = {
|
|
missingBinaryMessage: string;
|
|
operationLabel: string;
|
|
};
|
|
|
|
function toSopsError(err: unknown, params: SopsErrorContext): Error {
|
|
const error = err as NodeJS.ErrnoException & { message?: string };
|
|
if (error.code === "ENOENT") {
|
|
return new Error(params.missingBinaryMessage, { cause: err });
|
|
}
|
|
return new Error(`${params.operationLabel}: ${String(error.message ?? err)}`, {
|
|
cause: err,
|
|
});
|
|
}
|
|
|
|
export async function decryptSopsJsonFile(params: {
|
|
path: string;
|
|
timeoutMs?: number;
|
|
missingBinaryMessage: string;
|
|
configPath?: string;
|
|
}): Promise<unknown> {
|
|
const timeoutMs = normalizeTimeoutMs(params.timeoutMs);
|
|
const cwd = resolveSopsCwd({
|
|
targetPath: params.path,
|
|
configPath: params.configPath,
|
|
});
|
|
try {
|
|
const args: string[] = [];
|
|
if (typeof params.configPath === "string" && params.configPath.trim().length > 0) {
|
|
args.push("--config", params.configPath);
|
|
}
|
|
args.push("--decrypt", "--output-type", "json", params.path);
|
|
const { stdout } = await runExec("sops", args, {
|
|
timeoutMs,
|
|
maxBuffer: MAX_SOPS_OUTPUT_BYTES,
|
|
cwd,
|
|
});
|
|
return JSON.parse(stdout) as unknown;
|
|
} catch (err) {
|
|
const error = err as NodeJS.ErrnoException & { message?: string };
|
|
if (isTimeoutError(error.message)) {
|
|
throw new Error(`sops decrypt timed out after ${timeoutMs}ms for ${params.path}.`, {
|
|
cause: err,
|
|
});
|
|
}
|
|
throw toSopsError(err, {
|
|
missingBinaryMessage: params.missingBinaryMessage,
|
|
operationLabel: `sops decrypt failed for ${params.path}`,
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function encryptSopsJsonFile(params: {
|
|
path: string;
|
|
payload: Record<string, unknown>;
|
|
timeoutMs?: number;
|
|
missingBinaryMessage: string;
|
|
configPath?: string;
|
|
}): Promise<void> {
|
|
ensureDirForFile(params.path);
|
|
const timeoutMs = normalizeTimeoutMs(params.timeoutMs);
|
|
const tmpPlain = path.join(
|
|
path.dirname(params.path),
|
|
`${path.basename(params.path)}.${process.pid}.${crypto.randomUUID()}.plain.tmp`,
|
|
);
|
|
const tmpEncrypted = path.join(
|
|
path.dirname(params.path),
|
|
`${path.basename(params.path)}.${process.pid}.${crypto.randomUUID()}.enc.tmp`,
|
|
);
|
|
|
|
fs.writeFileSync(tmpPlain, `${JSON.stringify(params.payload, null, 2)}\n`, "utf8");
|
|
fs.chmodSync(tmpPlain, 0o600);
|
|
|
|
try {
|
|
const filenameOverride = resolveFilenameOverride({
|
|
targetPath: params.path,
|
|
});
|
|
const cwd = resolveSopsCwd({
|
|
targetPath: params.path,
|
|
configPath: params.configPath,
|
|
});
|
|
const args: string[] = [];
|
|
if (typeof params.configPath === "string" && params.configPath.trim().length > 0) {
|
|
args.push("--config", params.configPath);
|
|
}
|
|
args.push(
|
|
"--encrypt",
|
|
"--filename-override",
|
|
filenameOverride,
|
|
"--input-type",
|
|
"json",
|
|
"--output-type",
|
|
"json",
|
|
"--output",
|
|
tmpEncrypted,
|
|
tmpPlain,
|
|
);
|
|
await runExec("sops", args, {
|
|
timeoutMs,
|
|
maxBuffer: MAX_SOPS_OUTPUT_BYTES,
|
|
cwd,
|
|
});
|
|
fs.renameSync(tmpEncrypted, params.path);
|
|
fs.chmodSync(params.path, 0o600);
|
|
} catch (err) {
|
|
const error = err as NodeJS.ErrnoException & { message?: string };
|
|
if (isTimeoutError(error.message)) {
|
|
throw new Error(`sops encrypt timed out after ${timeoutMs}ms for ${params.path}.`, {
|
|
cause: err,
|
|
});
|
|
}
|
|
throw toSopsError(err, {
|
|
missingBinaryMessage: params.missingBinaryMessage,
|
|
operationLabel: `sops encrypt failed for ${params.path}`,
|
|
});
|
|
} finally {
|
|
fs.rmSync(tmpPlain, { force: true });
|
|
fs.rmSync(tmpEncrypted, { force: true });
|
|
}
|
|
}
|