feat(worker): Private Keys im Worker versiegeln, Deployment-Key (#006)

Der zweite Teil von #006 bindet den Kern an Datenbank und Worker. Nur
src/worker/geheimnisse.ts liest Chiffrat und Data-Key; ein struktureller
Test haelt fest, dass der Webprozess nichts von dort importiert und so
nie an den Master-Key kommt.

Die Zusatzdaten binden jedes Chiffrat an ID und Fingerprint seines
Schluessels: kopiert man es in eine andere Zeile, laesst es sich dort
nicht oeffnen. Der Deployment-Key entsteht beim ersten Start; ein Test
prueft, dass ein pg_dump keinen Klartext enthaelt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 09:33:18 +02:00
co-authored by Claude Opus 5.5
parent 855d77cf17
commit 9b807c13ae
5 changed files with 318 additions and 5 deletions
+41
View File
@@ -0,0 +1,41 @@
import { readFileSync, readdirSync, statSync } from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
/**
* Strukturell (#006): Nur der Worker liest Chiffrat und Data-Key, und der Webprozess
* importiert nichts aus src/worker – sonst laege der Master-Key-Zugriff im Web.
*/
function dateien(verzeichnis: string): string[] {
return readdirSync(verzeichnis).flatMap((name) => {
const pfad = path.join(verzeichnis, name);
if (statSync(pfad).isDirectory()) return name === "generated" ? [] : dateien(pfad);
return /\.(ts|tsx)$/.test(name) ? [pfad.split(path.sep).join("/")] : [];
});
}
const ALLE = dateien("src");
/** Dateien, die die Namen tragen, ohne die Datenbank zu lesen – mit Begruendung. */
const NAMEN_ERLAUBT: Record<string, string> = {
"src/worker/geheimnisse.ts": "die eine Stelle, die liest",
"src/lib/krypto/umschlag.ts": "Feldname des Umschlags, rein rechnerisch",
"src/lib/protokoll-schwaerzen.ts": "Feldname in der Schwaerz-Liste",
};
describe("Grenze um die Private Keys", () => {
it("nur src/worker/geheimnisse.ts liest privatVerschluesselt und dekVerschluesselt", () => {
const verstoesse = ALLE.filter(
(datei) =>
!NAMEN_ERLAUBT[datei] && /privatVerschluesselt|dekVerschluesselt/.test(readFileSync(datei, "utf8")),
);
expect(verstoesse).toEqual([]);
});
it("nichts ausserhalb von src/worker importiert aus src/worker", () => {
const verstoesse = ALLE.filter(
(datei) => !datei.startsWith("src/worker/") && /from ["']@\/worker\//.test(readFileSync(datei, "utf8")),
);
expect(verstoesse).toEqual([]);
});
});