mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-11 17:05:25 +02:00
feat: export full keyring in backup encryption key file
This commit is contained in:
6
apps/dokploy/__test__/env/encryption.test.ts
vendored
6
apps/dokploy/__test__/env/encryption.test.ts
vendored
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
decryptValue,
|
||||
encryptValue,
|
||||
exportEncryptionKey,
|
||||
exportEncryptionKeys,
|
||||
isEncrypted,
|
||||
} from "@dokploy/server/lib/encryption";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
@@ -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})*$/);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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) =>
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user