mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-21 13:55:33 +02:00
feat: encrypt environment variables at rest with AES-256-GCM
This commit is contained in:
15
packages/server/src/lib/encryption-secret.ts
Normal file
15
packages/server/src/lib/encryption-secret.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { readSecret } from "../db/constants";
|
||||
|
||||
const { ENCRYPTION_KEY, ENCRYPTION_KEY_FILE } = process.env;
|
||||
|
||||
function resolveEncryptionSecret(): string | undefined {
|
||||
if (ENCRYPTION_KEY) {
|
||||
return ENCRYPTION_KEY;
|
||||
}
|
||||
if (ENCRYPTION_KEY_FILE) {
|
||||
return readSecret(ENCRYPTION_KEY_FILE);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const encryptionSecret = resolveEncryptionSecret();
|
||||
65
packages/server/src/lib/encryption.ts
Normal file
65
packages/server/src/lib/encryption.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
createCipheriv,
|
||||
createDecipheriv,
|
||||
createHmac,
|
||||
randomBytes,
|
||||
} from "node:crypto";
|
||||
import { betterAuthSecret } from "./auth-secret";
|
||||
import { encryptionSecret } from "./encryption-secret";
|
||||
|
||||
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];
|
||||
|
||||
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);
|
||||
for (const key of decryptionKeys) {
|
||||
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.",
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user