97 lines
3.0 KiB
TypeScript
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");
|
|
}
|