mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-09 05:25:18 +00:00
feat: export full keyring in backup encryption key file
This commit is contained in:
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
decryptValue,
|
decryptValue,
|
||||||
encryptValue,
|
encryptValue,
|
||||||
exportEncryptionKey,
|
exportEncryptionKeys,
|
||||||
isEncrypted,
|
isEncrypted,
|
||||||
} from "@dokploy/server/lib/encryption";
|
} from "@dokploy/server/lib/encryption";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
@@ -44,8 +44,8 @@ describe("encryptValue / decryptValue", () => {
|
|||||||
expect(() => decryptValue(tampered)).toThrow(/BETTER_AUTH_SECRET/);
|
expect(() => decryptValue(tampered)).toThrow(/BETTER_AUTH_SECRET/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("exports the primary key as 32-byte hex for backups", () => {
|
it("exports the derived keys as 32-byte hex lines for backups", () => {
|
||||||
expect(exportEncryptionKey()).toMatch(/^[0-9a-f]{64}$/);
|
expect(exportEncryptionKeys()).toMatch(/^[0-9a-f]{64}(\n[0-9a-f]{64})*$/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -28,31 +28,41 @@ const decryptionKeys = encryptionSecret
|
|||||||
? [primaryKey, deriveKey(betterAuthSecret)]
|
? [primaryKey, deriveKey(betterAuthSecret)]
|
||||||
: [primaryKey];
|
: [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
|
// 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.
|
// decryption fallback so restored values keep working on the new server.
|
||||||
const loadRestoredKey = (): Buffer | undefined => {
|
const loadRestoredKeys = (): Buffer[] => {
|
||||||
if (restoredKey) {
|
if (restoredKeys?.length) {
|
||||||
return restoredKey;
|
return restoredKeys;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const { BASE_PATH } = paths();
|
const { BASE_PATH } = paths();
|
||||||
const hex = readFileSync(
|
const keys = readFileSync(
|
||||||
join(BASE_PATH, ENCRYPTION_KEY_BACKUP_FILE),
|
join(BASE_PATH, ENCRYPTION_KEY_BACKUP_FILE),
|
||||||
"utf8",
|
"utf8",
|
||||||
).trim();
|
)
|
||||||
const key = Buffer.from(hex, "hex");
|
.split("\n")
|
||||||
if (key.length === 32 && !key.equals(primaryKey)) {
|
.map((line) => line.trim())
|
||||||
restoredKey = key;
|
.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 {
|
} catch {
|
||||||
// No restored key file present.
|
// No restored key file present.
|
||||||
}
|
}
|
||||||
return restoredKey;
|
return restoredKeys ?? [];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isEncrypted = (value: string) =>
|
export const isEncrypted = (value: string) =>
|
||||||
@@ -79,8 +89,7 @@ export const decryptValue = (value: string): string => {
|
|||||||
const iv = payload.subarray(0, IV_LENGTH);
|
const iv = payload.subarray(0, IV_LENGTH);
|
||||||
const authTag = payload.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH);
|
const authTag = payload.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH);
|
||||||
const encrypted = payload.subarray(IV_LENGTH + AUTH_TAG_LENGTH);
|
const encrypted = payload.subarray(IV_LENGTH + AUTH_TAG_LENGTH);
|
||||||
const restored = loadRestoredKey();
|
const keys = [...decryptionKeys, ...loadRestoredKeys()];
|
||||||
const keys = restored ? [...decryptionKeys, restored] : decryptionKeys;
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
try {
|
try {
|
||||||
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
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 { IS_CLOUD, paths } from "@dokploy/server/constants";
|
||||||
import {
|
import {
|
||||||
ENCRYPTION_KEY_BACKUP_FILE,
|
ENCRYPTION_KEY_BACKUP_FILE,
|
||||||
exportEncryptionKey,
|
exportEncryptionKeys,
|
||||||
} from "@dokploy/server/lib/encryption";
|
} from "@dokploy/server/lib/encryption";
|
||||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||||
import {
|
import {
|
||||||
@@ -92,7 +92,7 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
|
|||||||
// the encryption keyring picks it up as a decryption fallback.
|
// the encryption keyring picks it up as a decryption fallback.
|
||||||
await writeFile(
|
await writeFile(
|
||||||
join(tempDir, "filesystem", ENCRYPTION_KEY_BACKUP_FILE),
|
join(tempDir, "filesystem", ENCRYPTION_KEY_BACKUP_FILE),
|
||||||
exportEncryptionKey(),
|
exportEncryptionKeys(),
|
||||||
{ mode: 0o600 },
|
{ mode: 0o600 },
|
||||||
);
|
);
|
||||||
writeStream.write("Included encryption key in backup\n");
|
writeStream.write("Included encryption key in backup\n");
|
||||||
|
|||||||
Reference in New Issue
Block a user