108 lines · 3.4 KB · TypeScript
RawDownload
import {	createCipheriv,	createDecipheriv,	createHmac,	randomBytes,} from "node:crypto";import { readFileSync } from "node:fs";import { join } from "node:path";import { paths } from "@dokploy/server/constants";import { betterAuthSecret } from "./auth-secret";import { encryptionSecret } from "./encryption-secret"; export const ENCRYPTION_KEY_BACKUP_FILE = "encryption.key"; const ENCRYPTION_PREFIX = "enc:v1:";const IV_LENGTH = 12;const AUTH_TAG_LENGTH = 16; const deriveKey = (secret: string) =>	createHmac("sha256", secret).update("dokploy:db-encryption:v1").digest(); const primaryKey = deriveKey(encryptionSecret ?? betterAuthSecret); // Installs that adopt a dedicated ENCRYPTION_KEY still hold values encrypted// with the auth-secret-derived key; keep it as a decrypt fallback so they// lazily re-encrypt on the next write instead of becoming unreadable.const decryptionKeys = encryptionSecret	? [primaryKey, deriveKey(betterAuthSecret)]	: [primaryKey]; // Derived keys only — never the raw secrets. A leaked key can decrypt// stored values, but the raw BETTER_AUTH_SECRET could also forge sessions.export const exportEncryptionKeys = () =>	decryptionKeys.map((key) => key.toString("hex")).join("\n"); let restoredKeys: Buffer[] | undefined; // A backup created with "include encryption key" places the original// server's keys at BASE_PATH when restored; use them as a last-resort// decryption fallback so restored values keep working on the new server.const loadRestoredKeys = (): Buffer[] => {	if (restoredKeys?.length) {		return restoredKeys;	}	try {		const { BASE_PATH } = paths();		const keys = readFileSync(			join(BASE_PATH, ENCRYPTION_KEY_BACKUP_FILE),			"utf8",		)			.split("\n")			.map((line) => line.trim())			.filter(Boolean)			.map((hex) => Buffer.from(hex, "hex"))			.filter(				(key) =>					key.length === 32 && !decryptionKeys.some((own) => own.equals(key)),			);		if (keys.length) {			restoredKeys = keys;		}	} catch {		// No restored key file present.	}	return restoredKeys ?? [];}; export const isEncrypted = (value: string) =>	value.startsWith(ENCRYPTION_PREFIX); export const encryptValue = (value: string): string => {	if (!value || isEncrypted(value)) {		return value;	}	const iv = randomBytes(IV_LENGTH);	const cipher = createCipheriv("aes-256-gcm", primaryKey, iv);	const encrypted = Buffer.concat([		cipher.update(value, "utf8"),		cipher.final(),	]);	return `${ENCRYPTION_PREFIX}${Buffer.concat([iv, cipher.getAuthTag(), encrypted]).toString("base64")}`;}; export const decryptValue = (value: string): string => {	if (!value || !isEncrypted(value)) {		return value;	}	const payload = Buffer.from(value.slice(ENCRYPTION_PREFIX.length), "base64");	const iv = payload.subarray(0, IV_LENGTH);	const authTag = payload.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH);	const encrypted = payload.subarray(IV_LENGTH + AUTH_TAG_LENGTH);	const keys = [...decryptionKeys, ...loadRestoredKeys()];	for (const key of keys) {		try {			const decipher = createDecipheriv("aes-256-gcm", key, iv);			decipher.setAuthTag(authTag);			return Buffer.concat([				decipher.update(encrypted),				decipher.final(),			]).toString("utf8");		} catch {			// GCM auth failed for this key; try the next one.		}	}	throw new Error(		"Failed to decrypt a stored secret. This usually means ENCRYPTION_KEY or BETTER_AUTH_SECRET changed after the value was encrypted.",	);};