import 'server-only' import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises' import { dirname, join, resolve, sep } from 'node:path' import { env } from '@/lib/env' import { entschluesselnBinaer, verschluesselnBinaer } from '@/lib/speicher/umschlag' import { dateiRoutePfad, schluesselErfordern, zugriffTokenErzeugen, TOKEN_STANDARD_SEKUNDEN, } from '@/lib/speicher/schluessel' /** * Treiber "datei": Ablage in einem Verzeichnis auf dem Server. * * Standard in der Entwicklung und fuer kleine Installationen, bei denen * kein Objektspeicher gewuenscht ist - die Dateien liegen dann auf der * verschluesselten Platte des eigenen Servers und verlassen sie nie. * * Zusaetzlich wird jede Datei anwendungsseitig mit AES-256-GCM * verschluesselt. Grund ist derselbe wie bei der Feldverschluesselung: ein * kopiertes Backup oder ein versehentlich falsch gerechtetes Verzeichnis * gibt sonst Bescheide, Arztbriefe und Kontoauszuege im Klartext preis. * Der Schluessel liegt in der Umgebung, nicht neben den Daten. * * Der Umschlag selbst steht in src/lib/speicher/umschlag.ts - der * Objektspeicher benutzt denselben. */ function wurzel(): string { return resolve(process.cwd(), env.dateiSpeicherPfad) } /** * Baut den Dateipfad - und prueft danach noch einmal, dass er wirklich * unterhalb des Ablageverzeichnisses liegt. Der Schluessel ist zwar bereits * streng geprueft, aber diese zweite Sperre kostet nichts. * * Was sie NICHT abfaengt, obwohl hier lange das Gegenteil stand: Symlinks. * resolve() rechnet nur mit Zeichenketten und folgt keinem Link - dafuer * braeuchte es realpath(). Ein untergeschobener Symlink im Ablageverzeichnis * wuerde also nicht auffallen. * * Nachgeruestet wird das trotzdem nicht, und zwar aus einem Grund und nicht * aus Bequemlichkeit: Wer dort einen Symlink anlegen kann, hat bereits * Schreibrechte im Datenverzeichnis des Servers - dann ist der Umweg ueber * einen Link die Sorge, die am wenigsten draengt. Der Satz musste * verschwinden, weil er eine Sicherheit versprach, die es nie gab. */ function pfadFuer(schluessel: string): string { const geprueft = schluesselErfordern(schluessel) const basis = wurzel() const ziel = resolve(basis, ...geprueft.split('/')) if (ziel !== basis && !ziel.startsWith(basis + sep)) { throw new Error('Der Ablagepfad läge außerhalb des Dokumentenverzeichnisses.') } return ziel } export async function schreiben(schluessel: string, inhalt: Buffer): Promise { const pfad = pfadFuer(schluessel) await mkdir(dirname(pfad), { recursive: true }) await writeFile(pfad, verschluesselnBinaer(inhalt, schluessel), { mode: 0o600 }) } export async function lesen(schluessel: string): Promise { const roh = await readFile(pfadFuer(schluessel)) return entschluesselnBinaer(roh, schluessel) } export async function entfernen(schluessel: string): Promise { await rm(pfadFuer(schluessel), { force: true }) } export async function existiert(schluessel: string): Promise { try { const angaben = await stat(pfadFuer(schluessel)) return angaben.isFile() } catch { return false } } /** * Zeigt auf die eigene, ebenfalls signierte Route. Diese Route prueft * zusaetzlich die Berechtigung des angemeldeten Benutzers und schreibt * einen Audit-Eintrag - ein Link allein oeffnet also nichts. */ export async function signierteUrl( schluessel: string, sekunden: number = TOKEN_STANDARD_SEKUNDEN, ): Promise { return dateiRoutePfad(zugriffTokenErzeugen(schluessel, sekunden)) } // ----------------------------------------------------------------------------- // Bereitschaftspruefung // ----------------------------------------------------------------------------- // // Ohne Umschlag und ohne die strenge Schluesselpruefung: Der Probeschluessel // gehoert zu keiner betreuten Person und faellt durch das Muster. function probepfad(schluessel: string): string { const basis = wurzel() const ziel = resolve(basis, ...schluessel.split('/')) if (ziel !== basis && !ziel.startsWith(basis + sep)) { throw new Error('Der Ablagepfad läge außerhalb des Dokumentenverzeichnisses.') } return ziel } export async function rohSchreiben(schluessel: string, inhalt: Buffer): Promise { const pfad = probepfad(schluessel) await mkdir(dirname(pfad), { recursive: true }) await writeFile(pfad, inhalt, { mode: 0o600 }) } export async function rohLesen(schluessel: string): Promise { return readFile(probepfad(schluessel)) } export async function rohEntfernen(schluessel: string): Promise { await rm(probepfad(schluessel), { force: true }) } /** Nur fuer Wartungsskripte: das Ablageverzeichnis. */ export function ablageverzeichnis(): string { return wurzel() } /** Sorgt dafuer, dass das Verzeichnis existiert (Start, Tests, Skripte). */ export async function verzeichnisAnlegen(): Promise { await mkdir(join(wurzel(), 'betreute'), { recursive: true }) }