diff --git a/app/server/modules/volumes/volume-config-secrets.ts b/app/server/modules/volumes/volume-config-secrets.ts index 2f9bf1c7..1bda9efe 100644 --- a/app/server/modules/volumes/volume-config-secrets.ts +++ b/app/server/modules/volumes/volume-config-secrets.ts @@ -32,3 +32,7 @@ export const mapVolumeConfigSecrets = async ( export const encryptVolumeConfig = async (config: BackendConfig): Promise => { return await mapVolumeConfigSecrets(config, cryptoUtils.sealSecret); }; + +export const decryptVolumeConfig = async (config: BackendConfig): Promise => { + return await mapVolumeConfigSecrets(config, cryptoUtils.resolveSecret); +}; diff --git a/app/server/modules/volumes/volume.service.ts b/app/server/modules/volumes/volume.service.ts index 0c5f4ba3..074433e0 100644 --- a/app/server/modules/volumes/volume.service.ts +++ b/app/server/modules/volumes/volume.service.ts @@ -20,7 +20,7 @@ import { volumeConfigSchema, type BackendConfig } from "~/schemas/volumes"; import { getOrganizationId } from "~/server/core/request-context"; import { isNodeJSErrnoException } from "~/server/utils/fs"; import { asShortId, type ShortId } from "~/server/utils/branded"; -import { encryptVolumeConfig } from "./volume-config-secrets"; +import { decryptVolumeConfig, encryptVolumeConfig } from "./volume-config-secrets"; type EnsureHealthyVolumeResult = | { @@ -80,7 +80,7 @@ const createVolume = async (name: string, backendConfig: BackendConfig) => { throw new InternalServerError("Failed to create volume"); } - const backend = createVolumeBackend(created); + const backend = createVolumeBackend({ ...created, config: await decryptVolumeConfig(created.config) }); const { error, status } = await backend.mount(); await db @@ -114,7 +114,7 @@ const mountVolume = async (shortId: ShortId) => { throw new NotFoundError("Volume not found"); } - const backend = createVolumeBackend(volume); + const backend = createVolumeBackend({ ...volume, config: await decryptVolumeConfig(volume.config) }); await backend.unmount(); const { error, status } = await backend.mount(); @@ -219,7 +219,7 @@ const updateVolume = async (shortId: ShortId, volumeData: UpdateVolumeBody) => { } if (configChanged) { - const backend = createVolumeBackend(updated); + const backend = createVolumeBackend({ ...updated, config: await decryptVolumeConfig(updated.config) }); const { error, status } = await backend.mount(); await db .update(volumesTable) @@ -235,18 +235,16 @@ const updateVolume = async (shortId: ShortId, volumeData: UpdateVolumeBody) => { const testConnection = async (backendConfig: BackendConfig) => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "zerobyte-test-")); try { - const encryptedConfig = await encryptVolumeConfig(backendConfig); - const mockVolume = { id: 0, shortId: asShortId("test"), name: "test-connection", path: tempDir, - config: encryptedConfig, + config: backendConfig, createdAt: Date.now(), updatedAt: Date.now(), lastHealthCheck: Date.now(), - type: encryptedConfig.backend, + type: backendConfig.backend, status: "unmounted" as const, lastError: null, provisioningId: null, diff --git a/apps/agent/src/volume-host/backends/sftp.ts b/apps/agent/src/volume-host/backends/sftp.ts index 23ed84e5..021ea77f 100644 --- a/apps/agent/src/volume-host/backends/sftp.ts +++ b/apps/agent/src/volume-host/backends/sftp.ts @@ -1,4 +1,5 @@ import * as fs from "node:fs/promises"; +import { createHash } from "node:crypto"; import * as os from "node:os"; import * as path from "node:path"; import { spawn } from "node:child_process"; @@ -10,8 +11,9 @@ import { withTimeout } from "../timeout"; import type { BackendConfig, VolumeBackend } from "../types"; import { executeUnmount } from "./utils"; -const getPrivateKeyPath = (mountPath: string) => path.join(SSH_KEYS_DIR, `${path.basename(mountPath)}.key`); -const getKnownHostsPath = (mountPath: string) => path.join(SSH_KEYS_DIR, `${path.basename(mountPath)}.known_hosts`); +const getMountPathHash = (mountPath: string) => createHash("sha256").update(mountPath).digest("hex").slice(0, 16); +const getPrivateKeyPath = (mountPath: string) => path.join(SSH_KEYS_DIR, `${getMountPathHash(mountPath)}.key`); +const getKnownHostsPath = (mountPath: string) => path.join(SSH_KEYS_DIR, `${getMountPathHash(mountPath)}.known_hosts`); const runSshfs = async (args: string[], password?: string) => new Promise((resolve, reject) => { @@ -138,7 +140,6 @@ const mount = async (config: BackendConfig, mountPath: string) => { const args = [`${config.username}@${config.host}:${config.path || ""}`, mountPath, "-o", options.join(",")]; if (config.password) args.push("-o", "password_stdin"); - logger.info(`Executing sshfs: sshfs ${args.join(" ")}`); await runSshfs(args, config.password); diff --git a/apps/agent/src/volume-host/operations.ts b/apps/agent/src/volume-host/operations.ts index ae5feabb..ac8adaf2 100644 --- a/apps/agent/src/volume-host/operations.ts +++ b/apps/agent/src/volume-host/operations.ts @@ -16,18 +16,19 @@ export const listVolumeFiles = async ( ) => { const volumePath = getVolumePath(volume); const requestedPath = subPath ? path.join(volumePath, subPath) : volumePath; - const normalizedPath = path.normalize(requestedPath); - const relative = path.relative(volumePath, normalizedPath); - - if (relative.startsWith("..") || path.isAbsolute(relative)) { - throw new Error("Invalid path"); - } - const pageSize = Math.min(Math.max(limit, 1), MAX_PAGE_SIZE); const startOffset = Math.max(offset, 0); try { - const dirents = await fs.readdir(normalizedPath, { withFileTypes: true }); + const realVolumeRoot = await fs.realpath(volumePath); + const realRequestedPath = await fs.realpath(requestedPath); + const relative = path.relative(realVolumeRoot, realRequestedPath); + + if (relative.startsWith("..") || path.isAbsolute(relative)) { + throw new Error("Invalid path"); + } + + const dirents = await fs.readdir(realRequestedPath, { withFileTypes: true }); dirents.sort((a, b) => { const aIsDir = a.isDirectory(); @@ -45,11 +46,11 @@ export const listVolumeFiles = async ( const entries = ( await Promise.all( paginatedDirents.map(async (dirent) => { - const fullPath = path.join(normalizedPath, dirent.name); + const fullPath = path.join(realRequestedPath, dirent.name); try { const stats = await fs.stat(fullPath); - const relativePath = path.relative(volumePath, fullPath); + const relativePath = path.relative(realVolumeRoot, fullPath); return { name: dirent.name,