Files
boss/src/lib/boss-storage-secrets.ts
2026-03-29 16:05:25 +08:00

97 lines
3.0 KiB
TypeScript

import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
const STORAGE_SECRET_KEY_FILE = "storage-secret.key";
const STORAGE_SECRET_VERSION = "v1";
const STORAGE_SECRET_ALGORITHM = "aes-256-gcm";
function detectRuntimeRoot(startDir: string) {
let current = startDir;
while (true) {
if (
path.basename(current) === "boss" &&
path.basename(path.dirname(current)) === "code"
) {
return current;
}
const parent = path.dirname(current);
if (parent === current) {
return startDir;
}
current = parent;
}
}
function resolveRuntimeRoot() {
if (process.env.BOSS_RUNTIME_ROOT?.trim()) {
return path.resolve(process.env.BOSS_RUNTIME_ROOT);
}
if (process.env.BOSS_STATE_FILE?.trim()) {
return path.dirname(path.dirname(path.resolve(process.env.BOSS_STATE_FILE)));
}
return detectRuntimeRoot(process.cwd());
}
function resolveStorageSecretKeyPath() {
return path.join(resolveRuntimeRoot(), "data", STORAGE_SECRET_KEY_FILE);
}
function deriveKeyFromSource(source: string) {
return createHash("sha256").update(source.normalize("NFKC")).digest();
}
async function getStorageSecretKey() {
if (process.env.BOSS_STORAGE_SECRET_KEY?.trim()) {
return deriveKeyFromSource(process.env.BOSS_STORAGE_SECRET_KEY);
}
const keyPath = resolveStorageSecretKeyPath();
try {
const existing = await readFile(keyPath, "utf8");
return deriveKeyFromSource(existing.trim());
} catch {
const generated = randomBytes(32).toString("hex");
await mkdir(path.dirname(keyPath), { recursive: true });
await writeFile(keyPath, `${generated}\n`, { encoding: "utf8", mode: 0o600 });
return deriveKeyFromSource(generated);
}
}
export async function encryptStorageSecret(plainText: string) {
const normalized = plainText.trim();
if (!normalized) {
throw new Error("ALIYUN_OSS_SECRET_REQUIRED");
}
const key = await getStorageSecretKey();
const iv = randomBytes(12);
const cipher = createCipheriv(STORAGE_SECRET_ALGORITHM, key, iv);
const encrypted = Buffer.concat([cipher.update(normalized, "utf8"), cipher.final()]);
const tag = cipher.getAuthTag();
return [
STORAGE_SECRET_VERSION,
iv.toString("hex"),
tag.toString("hex"),
encrypted.toString("hex"),
].join(":");
}
export async function decryptStorageSecret(cipherText: string) {
const [version, ivHex, tagHex, payloadHex] = cipherText.split(":");
if (version !== STORAGE_SECRET_VERSION || !ivHex || !tagHex || !payloadHex) {
throw new Error("INVALID_STORAGE_SECRET_FORMAT");
}
const key = await getStorageSecretKey();
const decipher = createDecipheriv(
STORAGE_SECRET_ALGORITHM,
key,
Buffer.from(ivHex, "hex"),
);
decipher.setAuthTag(Buffer.from(tagHex, "hex"));
const decrypted = Buffer.concat([
decipher.update(Buffer.from(payloadHex, "hex")),
decipher.final(),
]);
return decrypted.toString("utf8");
}