73 lines
2.1 KiB
TypeScript
73 lines
2.1 KiB
TypeScript
import crypto from "node:crypto";
|
|
|
|
const encryptedSecretPrefix = "ttenc:v1:";
|
|
|
|
let warnedAboutFallbackKey = false;
|
|
let cachedKey: Buffer | null = null;
|
|
|
|
function getEncryptionMaterial() {
|
|
const configuredKey = process.env.SECRET_ENCRYPTION_KEY ?? process.env.TICKETTRACKER_SECRET_KEY;
|
|
|
|
if (configuredKey) {
|
|
return configuredKey;
|
|
}
|
|
|
|
if (!process.env.DATABASE_URL) {
|
|
throw new Error("SECRET_ENCRYPTION_KEY is required to encrypt database secrets");
|
|
}
|
|
|
|
if (!warnedAboutFallbackKey) {
|
|
console.warn("SECRET_ENCRYPTION_KEY is not set; deriving database secret encryption from DATABASE_URL.");
|
|
warnedAboutFallbackKey = true;
|
|
}
|
|
|
|
return process.env.DATABASE_URL;
|
|
}
|
|
|
|
function getEncryptionKey() {
|
|
if (!cachedKey) {
|
|
cachedKey = crypto.createHash("sha256").update(getEncryptionMaterial()).digest();
|
|
}
|
|
|
|
return cachedKey;
|
|
}
|
|
|
|
export function isEncryptedSecret(value: string | null | undefined) {
|
|
return typeof value === "string" && value.startsWith(encryptedSecretPrefix);
|
|
}
|
|
|
|
export function encryptSecret(value: string) {
|
|
if (isEncryptedSecret(value)) {
|
|
return value;
|
|
}
|
|
|
|
const iv = crypto.randomBytes(12);
|
|
const cipher = crypto.createCipheriv("aes-256-gcm", getEncryptionKey(), iv);
|
|
const encrypted = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
|
|
const tag = cipher.getAuthTag();
|
|
|
|
return `${encryptedSecretPrefix}${iv.toString("base64url")}.${tag.toString("base64url")}.${encrypted.toString("base64url")}`;
|
|
}
|
|
|
|
export function decryptSecret(value: string | null | undefined) {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
|
|
if (!isEncryptedSecret(value)) {
|
|
return value;
|
|
}
|
|
|
|
const parts = value.slice(encryptedSecretPrefix.length).split(".");
|
|
|
|
if (parts.length !== 3) {
|
|
throw new Error("Encrypted database secret has an invalid format");
|
|
}
|
|
|
|
const [ivText, tagText, encryptedText] = parts;
|
|
const decipher = crypto.createDecipheriv("aes-256-gcm", getEncryptionKey(), Buffer.from(ivText, "base64url"));
|
|
decipher.setAuthTag(Buffer.from(tagText, "base64url"));
|
|
|
|
return Buffer.concat([decipher.update(Buffer.from(encryptedText, "base64url")), decipher.final()]).toString("utf8");
|
|
}
|