From f6b8e7e5a21540738709571a5ca38bcf2ce9e84b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Tr=C3=A1vn=C3=ADk?= Date: Thu, 4 Dec 2025 00:04:26 +0100 Subject: [PATCH] feat: implement encryption for sensitive fields in volume backends --- .../modules/backends/smb/smb-backend.ts | 5 +++- .../modules/backends/webdav/webdav-backend.ts | 4 ++- app/server/modules/volumes/volume.service.ts | 25 +++++++++++++++++-- app/server/utils/crypto.ts | 11 +++++++- 4 files changed, 40 insertions(+), 5 deletions(-) diff --git a/app/server/modules/backends/smb/smb-backend.ts b/app/server/modules/backends/smb/smb-backend.ts index cdc112a..774e10f 100644 --- a/app/server/modules/backends/smb/smb-backend.ts +++ b/app/server/modules/backends/smb/smb-backend.ts @@ -1,6 +1,7 @@ import * as fs from "node:fs/promises"; import * as os from "node:os"; import { OPERATION_TIMEOUT } from "../../../core/constants"; +import { cryptoUtils } from "../../../utils/crypto"; import { toMessage } from "../../../utils/errors"; import { logger } from "../../../utils/logger"; import { getMountForPath } from "../../../utils/mountinfo"; @@ -33,10 +34,12 @@ const mount = async (config: BackendConfig, path: string) => { const run = async () => { await fs.mkdir(path, { recursive: true }); + const password = await cryptoUtils.decrypt(config.password); + const source = `//${config.server}/${config.share}`; const options = [ `user=${config.username}`, - `pass=${config.password}`, + `pass=${password}`, `vers=${config.vers}`, `port=${config.port}`, "uid=1000", diff --git a/app/server/modules/backends/webdav/webdav-backend.ts b/app/server/modules/backends/webdav/webdav-backend.ts index 1e9b72f..2467736 100644 --- a/app/server/modules/backends/webdav/webdav-backend.ts +++ b/app/server/modules/backends/webdav/webdav-backend.ts @@ -3,6 +3,7 @@ import * as fs from "node:fs/promises"; import * as os from "node:os"; import { promisify } from "node:util"; import { OPERATION_TIMEOUT } from "../../../core/constants"; +import { cryptoUtils } from "../../../utils/crypto"; import { toMessage } from "../../../utils/errors"; import { logger } from "../../../utils/logger"; import { getMountForPath } from "../../../utils/mountinfo"; @@ -49,8 +50,9 @@ const mount = async (config: BackendConfig, path: string) => { : ["uid=1000", "gid=1000", "file_mode=0664", "dir_mode=0775"]; if (config.username && config.password) { + const password = await cryptoUtils.decrypt(config.password); const secretsFile = "/etc/davfs2/secrets"; - const secretsContent = `${source} ${config.username} ${config.password}\n`; + const secretsContent = `${source} ${config.username} ${password}\n`; await fs.appendFile(secretsFile, secretsContent, { mode: 0o600 }); } diff --git a/app/server/modules/volumes/volume.service.ts b/app/server/modules/volumes/volume.service.ts index 7169d6f..b01cefa 100644 --- a/app/server/modules/volumes/volume.service.ts +++ b/app/server/modules/volumes/volume.service.ts @@ -8,6 +8,7 @@ import slugify from "slugify"; import { getCapabilities } from "../../core/capabilities"; import { db } from "../../db/db"; import { volumesTable } from "../../db/schema"; +import { cryptoUtils } from "../../utils/crypto"; import { toMessage } from "../../utils/errors"; import { generateShortId } from "../../utils/id"; import { getStatFs, type StatFs } from "../../utils/mountinfo"; @@ -19,6 +20,23 @@ import { logger } from "../../utils/logger"; import { serverEvents } from "../../core/events"; import type { BackendConfig } from "~/schemas/volumes"; +async function encryptSensitiveFields(config: BackendConfig): Promise { + switch (config.backend) { + case "smb": + return { + ...config, + password: await cryptoUtils.encrypt(config.password), + }; + case "webdav": + return { + ...config, + password: config.password ? await cryptoUtils.encrypt(config.password) : undefined, + }; + default: + return config; + } +} + const listVolumes = async () => { const volumes = await db.query.volumesTable.findMany({}); @@ -37,13 +55,14 @@ const createVolume = async (name: string, backendConfig: BackendConfig) => { } const shortId = generateShortId(); + const encryptedConfig = await encryptSensitiveFields(backendConfig); const [created] = await db .insert(volumesTable) .values({ shortId, name: slug, - config: backendConfig, + config: encryptedConfig, type: backendConfig.backend, }) .returning(); @@ -175,11 +194,13 @@ const updateVolume = async (name: string, volumeData: UpdateVolumeBody) => { await backend.unmount(); } + const encryptedConfig = volumeData.config ? await encryptSensitiveFields(volumeData.config) : undefined; + const [updated] = await db .update(volumesTable) .set({ name: newName, - config: volumeData.config, + config: encryptedConfig, type: volumeData.config?.backend, autoRemount: volumeData.autoRemount, updatedAt: Date.now(), diff --git a/app/server/utils/crypto.ts b/app/server/utils/crypto.ts index 651bebe..5394cef 100644 --- a/app/server/utils/crypto.ts +++ b/app/server/utils/crypto.ts @@ -5,6 +5,10 @@ const algorithm = "aes-256-gcm" as const; const keyLength = 32; const encryptionPrefix = "encv1"; +const isEncrypted = (val?: string): boolean => { + return typeof val === "string" && val.startsWith(encryptionPrefix); +}; + /** * Given a string, encrypts it using a randomly generated salt */ @@ -13,7 +17,7 @@ const encrypt = async (data: string) => { return data; } - if (data.startsWith(encryptionPrefix)) { + if (isEncrypted(data)) { return data; } @@ -34,6 +38,10 @@ const encrypt = async (data: string) => { * Given an encrypted string, decrypts it using the salt stored in the string */ const decrypt = async (encryptedData: string) => { + if (!isEncrypted(encryptedData)) { + return encryptedData; + } + const secret = await Bun.file(RESTIC_PASS_FILE).text(); const parts = encryptedData.split(":").slice(1); // Remove prefix @@ -58,4 +66,5 @@ const decrypt = async (encryptedData: string) => { export const cryptoUtils = { encrypt, decrypt, + isEncrypted, };