Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/dokploy/__test__/env/encryption.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
decryptValue,
encryptValue,
exportEncryptionKey,
exportEncryptionKeys,
isEncrypted,
} from "@dokploy/server/lib/encryption";
import { afterEach, describe, expect, it, vi } from "vitest";
Expand Down Expand Up @@ -44,8 +44,8 @@ describe("encryptValue / decryptValue", () => {
expect(() => decryptValue(tampered)).toThrow(/BETTER_AUTH_SECRET/);
});

it("exports the primary key as 32-byte hex for backups", () => {
expect(exportEncryptionKey()).toMatch(/^[0-9a-f]{64}$/);
it("exports the derived keys as 32-byte hex lines for backups", () => {
expect(exportEncryptionKeys()).toMatch(/^[0-9a-f]{64}(\n[0-9a-f]{64})*$/);
});
});

Expand Down
37 changes: 23 additions & 14 deletions packages/server/src/lib/encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,31 +28,41 @@ const decryptionKeys = encryptionSecret
? [primaryKey, deriveKey(betterAuthSecret)]
: [primaryKey];

export const exportEncryptionKey = () => primaryKey.toString("hex");
// 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 restoredKey: Buffer | undefined;
let restoredKeys: Buffer[] | undefined;

// A backup created with "include encryption key" places the original
// server's key at BASE_PATH when restored; use it as a last-resort
// 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 loadRestoredKey = (): Buffer | undefined => {
if (restoredKey) {
return restoredKey;
const loadRestoredKeys = (): Buffer[] => {
if (restoredKeys?.length) {
return restoredKeys;
}
try {
const { BASE_PATH } = paths();
const hex = readFileSync(
const keys = readFileSync(
join(BASE_PATH, ENCRYPTION_KEY_BACKUP_FILE),
"utf8",
).trim();
const key = Buffer.from(hex, "hex");
if (key.length === 32 && !key.equals(primaryKey)) {
restoredKey = key;
)
.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 restoredKey;
return restoredKeys ?? [];
};

export const isEncrypted = (value: string) =>
Expand All @@ -79,8 +89,7 @@ export const decryptValue = (value: string): string => {
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 restored = loadRestoredKey();
const keys = restored ? [...decryptionKeys, restored] : decryptionKeys;
const keys = [...decryptionKeys, ...loadRestoredKeys()];
for (const key of keys) {
try {
const decipher = createDecipheriv("aes-256-gcm", key, iv);
Expand Down
4 changes: 2 additions & 2 deletions packages/server/src/utils/backups/web-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { join } from "node:path";
import { IS_CLOUD, paths } from "@dokploy/server/constants";
import {
ENCRYPTION_KEY_BACKUP_FILE,
exportEncryptionKey,
exportEncryptionKeys,
} from "@dokploy/server/lib/encryption";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import {
Expand Down Expand Up @@ -92,7 +92,7 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
// the encryption keyring picks it up as a decryption fallback.
await writeFile(
join(tempDir, "filesystem", ENCRYPTION_KEY_BACKUP_FILE),
exportEncryptionKey(),
exportEncryptionKeys(),
{ mode: 0o600 },
);
writeStream.write("Included encryption key in backup\n");
Expand Down
Loading