Files
songs2yt/lib/crypto/secrets.ts
T
2026-07-17 02:23:59 +02:00

56 lines
1.7 KiB
TypeScript

import { createCipheriv, createDecipheriv, createHash, randomBytes } from "crypto";
const ENC_PREFIX = "enc:v1:";
function getEncryptionKey() {
const secret = process.env.TOKEN_ENCRYPTION_KEY || process.env.NEXTAUTH_SECRET;
if (!secret) {
throw new Error("TOKEN_ENCRYPTION_KEY or NEXTAUTH_SECRET must be set to encrypt secrets");
}
return createHash("sha256").update(secret).digest();
}
/** Encrypt a secret for DB storage (AES-256-GCM). Idempotent if already encrypted. */
export function encryptSecret(plaintext: string): string {
if (!plaintext) return plaintext;
if (plaintext.startsWith(ENC_PREFIX)) return plaintext;
const iv = randomBytes(12);
const cipher = createCipheriv("aes-256-gcm", getEncryptionKey(), iv);
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
const tag = cipher.getAuthTag();
return [
ENC_PREFIX,
iv.toString("base64url"),
".",
tag.toString("base64url"),
".",
encrypted.toString("base64url"),
].join("");
}
/** Decrypt a stored secret. Legacy plaintext values are returned as-is. */
export function decryptSecret(value: string): string {
if (!value) return value;
if (!value.startsWith(ENC_PREFIX)) return value;
const payload = value.slice(ENC_PREFIX.length);
const [ivB64, tagB64, dataB64] = payload.split(".");
if (!ivB64 || !tagB64 || !dataB64) {
throw new Error("Invalid encrypted secret format");
}
const decipher = createDecipheriv(
"aes-256-gcm",
getEncryptionKey(),
Buffer.from(ivB64, "base64url"),
);
decipher.setAuthTag(Buffer.from(tagB64, "base64url"));
const decrypted = Buffer.concat([
decipher.update(Buffer.from(dataB64, "base64url")),
decipher.final(),
]);
return decrypted.toString("utf8");
}