feat: add web AES-256-GCM secret crypto and LYRA_SECRET_KEY plumbing
This commit is contained in:
@@ -2,3 +2,6 @@ POSTGRES_USER=lyra
|
|||||||
POSTGRES_PASSWORD=lyra
|
POSTGRES_PASSWORD=lyra
|
||||||
POSTGRES_DB=lyra
|
POSTGRES_DB=lyra
|
||||||
DATABASE_URL=postgresql://lyra:lyra@db:5432/lyra
|
DATABASE_URL=postgresql://lyra:lyra@db:5432/lyra
|
||||||
|
|
||||||
|
# 32 random bytes, base64. Generate with: openssl rand -base64 32
|
||||||
|
LYRA_SECRET_KEY=MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: ${DATABASE_URL}
|
DATABASE_URL: ${DATABASE_URL}
|
||||||
|
LYRA_SECRET_KEY: ${LYRA_SECRET_KEY}
|
||||||
ports:
|
ports:
|
||||||
# host:container — host 8770 avoids the Quill app on 3000 (and Lidarr on 8686)
|
# host:container — host 8770 avoids the Quill app on 3000 (and Lidarr on 8686)
|
||||||
- "8770:3000"
|
- "8770:3000"
|
||||||
@@ -33,6 +34,7 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: ${DATABASE_URL}
|
DATABASE_URL: ${DATABASE_URL}
|
||||||
|
LYRA_SECRET_KEY: ${LYRA_SECRET_KEY}
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user