feat: add web AES-256-GCM secret crypto and LYRA_SECRET_KEY plumbing

This commit is contained in:
Jonathan
2026-07-10 19:50:16 +02:00
parent 45cf9e8527
commit 91ea32c4b8
4 changed files with 62 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
import { describe, it, expect, beforeAll } from "vitest";
import { encryptSecret, decryptSecret } from "./crypto";
beforeAll(() => {
// base64 of the 32-byte ASCII string "0123456789abcdef0123456789abcdef"
process.env.LYRA_SECRET_KEY = "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=";
});
describe("crypto", () => {
it("round-trips a secret", () => {
const env = encryptSecret("hunter2");
expect(env).not.toContain("hunter2");
expect(decryptSecret(env)).toBe("hunter2");
});
it("produces a well-formed envelope", () => {
const env = JSON.parse(encryptSecret("x"));
expect(typeof env.iv).toBe("string");
expect(typeof env.ct).toBe("string");
expect(typeof env.tag).toBe("string");
});
it("rejects a tampered envelope", () => {
const env = JSON.parse(encryptSecret("secret"));
env.ct = Buffer.from("garbage").toString("base64");
expect(() => decryptSecret(JSON.stringify(env))).toThrow();
});
});
+29
View File
@@ -0,0 +1,29 @@
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
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");
}