8cd5392ec3
.env.example shipped a valid, PUBLIC base64 key — if a user didn't replace it, every credential got encrypted with a publicly-known key and everything still "worked," so the footgun was silent. - .env.example now ships an obvious placeholder (CHANGE_ME_generate_with_...) with a "you MUST replace this" note and the openssl hint. - web + worker validate LYRA_SECRET_KEY at startup: missing / placeholder / wrong-length ⇒ loud FATAL banner and refuse to start (web via the Next.js instrumentation hook, worker via _require_secret_key before touching the DB). The old public example key ⇒ a loud WARNING but not fatal, since an existing install may have encrypted its creds under it (rotating needs re-entry). - Validation lives in an edge-safe web/src/lib/secret-key.ts (no node:crypto) so the instrumentation hook bundles for both runtimes; crypto.ts re-exports. - README emphasizes generating a fresh key + keeping it stable. Verified live: worker exits 1 on placeholder/unset before any DB call; web instrumentation throws and refuses to serve on the placeholder. web 138 tests, worker 204/7-skip, tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
34 lines
1.4 KiB
TypeScript
34 lines
1.4 KiB
TypeScript
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
|
|
// Re-exported for existing callers; the checks live in an edge-safe module (secret-key.ts)
|
|
// so the instrumentation hook can import them without pulling in node:crypto.
|
|
export { secretKeyProblem, PLACEHOLDER_SECRET_KEY } from "./secret-key";
|
|
|
|
function key(): Buffer {
|
|
const b64 = process.env.LYRA_SECRET_KEY;
|
|
if (!b64) throw new Error("LYRA_SECRET_KEY is not set");
|
|
const k = Buffer.from(b64, "base64");
|
|
if (k.length !== 32) throw new Error("LYRA_SECRET_KEY must decode to 32 bytes");
|
|
return k;
|
|
}
|
|
|
|
export function encryptSecret(plaintext: string): string {
|
|
const iv = randomBytes(12);
|
|
const cipher = createCipheriv("aes-256-gcm", key(), iv);
|
|
const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
const tag = cipher.getAuthTag();
|
|
return JSON.stringify({
|
|
iv: iv.toString("base64"),
|
|
ct: ct.toString("base64"),
|
|
tag: tag.toString("base64"),
|
|
});
|
|
}
|
|
|
|
export function decryptSecret(envelope: string): string {
|
|
const { iv, ct, tag } = JSON.parse(envelope) as { iv: string; ct: string; tag: string };
|
|
const decipher = createDecipheriv("aes-256-gcm", key(), Buffer.from(iv, "base64"));
|
|
decipher.setAuthTag(Buffer.from(tag, "base64"));
|
|
const pt = Buffer.concat([decipher.update(Buffer.from(ct, "base64")), decipher.final()]);
|
|
return pt.toString("utf8");
|
|
}
|