Schluesselbund: ein Hauptschluessel, mehrfach verpackt (#190)

SecretKeyStore.Wrap band den Hauptschluessel unter Windows per DPAPI ans
Benutzerkonto und gab ihn sonst ungewrappt zurueck - eine Zeile, die man beim
Lesen ueberspringt und die auf dem Zielsystem "im Klartext" bedeutet.

Dahinter steht jetzt ein SecretKeyRing: derselbe Hauptschluessel, mehrfach
verpackt, je Fach ein ISecretProvider (dpapi, passphrase, file). Fuer Aufrufer
aendert sich nichts - GetOrCreateKey gibt weiterhin 32 Byte zurueck. Genau dafuer
war die Naht da.

WARUM MEHRERE FAECHER UND NICHT EIN BESSERES
Ein Schluessel an der Maschine ueberlebt keinen Umzug; einer an einer Passphrase
verlangt bei jedem Start jemanden, der tippt - und Dauerbetrieb ohne Menschen ist
der Zweck (#214). Der Bund entscheidet sich nicht: Im Alltag oeffnet das
Maschinenfach ohne Rueckfrage, nach einem Umzug die Passphrase, und beide fuehren
zu DEMSELBEN Schluessel. Alle enc:v2-Werte bleiben lesbar.

Und deshalb darf die Schluesseldatei jetzt ueberhaupt in eine Sicherung: Sobald
ein Wiederherstellungsfach da ist, steht der Hauptschluessel in keiner Fassung im
Klartext darin.

Nicht zu verwechseln mit der Sicherung mit Passphrase: Die schluesselt jeden WERT
einzeln ins Archiv um, fuer den Transport einer Instanz. Das Wiederherstellungs-
fach schluesselt den HAUPTSCHLUESSEL um, damit eine bestehende Installation
weiterlaeuft - unbeaufsichtigt ueber CLAWD_SECRET_PASSPHRASE. Die Unterscheidung
steht in docs/Roadmap.md bei #190.

Beide Altformate werden uebernommen: 32 rohe Byte werden zum Fach file, ein
DPAPI-Block zum Fach dpapi. Derselbe Schluessel, nur anders verpackt.

Zwei Zusagen, mit Tests festgehalten:
- Geht KEIN Fach auf, wird abgebrochen statt ein neuer Schluessel erzeugt. Ein
  neuer Hauptschluessel machte jeden bestehenden Wert wortlos unlesbar.
- Ein Umzug legt den Schluessel NICHT im Klartext ab, nur damit der naechste Start
  bequemer wird.

Beim automatischen Weg wird nur hinzugefuegt, nie weggenommen; weggenommen wird
nur in SetRecoveryPassphrase, wo ein Mensch gerade getippt hat.

#190 bleibt offen: Unter Linux greift weiterhin das Fach file. Neu ist, dass es
beim Start als Warnung im Protokoll steht statt unsichtbar zu sein. Womit das
Maschinenfach dort gefuellt wird, ist Entscheidung #180 - diese Schnittstelle ist
die Stelle, an der die Antwort eingesetzt wird.

834 Tests gruen, 0 Warnungen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-10 15:34:16 +02:00
co-authored by Claude Opus 5
parent e3403087c2
commit 99f8a89fd0
7 changed files with 1148 additions and 60 deletions
+35
View File
@@ -373,6 +373,41 @@ Steht vor #190, weil die Antwort das Backend bestimmt, das dort gebaut wird.
### [SEC] #190 — L1: Der Hauptschlüssel liegt unter Linux im Klartext
> **Stand 2026-09-10: Die Naht ist gebaut, die Linux-Antwort steht noch aus.**
>
> `SecretKeyStore` hält nicht mehr einen verpackten Schlüssel, sondern einen
> **Schlüsselbund** (`SecretKeyRing`): denselben Hauptschlüssel, mehrfach verpackt.
> Jedes Fach gehört einem `ISecretProvider` — `dpapi` (Windows), `passphrase`
> (Wiederherstellung), `file` (nur Dateirechte, der Zustand *dieses* Befunds, jetzt
> beim Namen genannt).
>
> **Warum mehrere Fächer und nicht ein besseres.** Ein Schlüssel an der Maschine
> überlebt keinen Umzug; einer an einer Passphrase verlangt bei jedem Start jemanden,
> der tippt — und der Dauerbetrieb ohne Menschen ist der Zweck (#214). Der Bund
> entscheidet sich nicht: Im Alltag öffnet das Maschinenfach ohne Rückfrage, nach
> einem Umzug die Passphrase, und beide führen zu **demselben** Schlüssel. Alle
> `enc:v2`-Werte bleiben lesbar, nichts muss neu eingetragen werden.
>
> **Zwei Wege, die sich ähneln und nicht dasselbe sind** — das ist die Stelle, an der
> man beim Lesen falsch abbiegt:
>
> | | Sicherung mit Passphrase (`BackupService`) | Wiederherstellungsfach (`SecretKeyRing`) |
> |---|---|---|
> | Was umgeschlüsselt wird | jeder **Wert** einzeln, in das Archiv hinein | der **Hauptschlüssel**, in der Schlüsseldatei |
> | Wofür | eine Instanz auf eine andere Maschine bringen | eine bestehende Installation weiterbenutzen, wenn das Maschinenfach nicht mehr aufgeht |
> | Wann entschieden | beim Ziehen der Sicherung | einmal, vorher |
> | Ohne Menschen | nein, die Passphrase gehört zur Sicherung | ja, über `CLAWD_SECRET_PASSPHRASE` |
>
> **Was noch fehlt, ist genau #180:** Womit das Maschinenfach unter Linux gefüllt wird.
> Bis dahin greift dort `file` — dieselbe Schutzstufe wie vorher, aber sie steht jetzt
> als Warnung im Protokoll und in `SecretKeyStore.Problems`, statt eine stille Zeile
> `: key` zu sein. Der Befund bleibt deshalb offen.
>
> Zwei Zusagen sind mit Tests festgehalten: Geht **kein** Fach auf, wird abgebrochen
> statt ein neuer Schlüssel erzeugt (der machte jeden bestehenden Wert wortlos
> unlesbar). Und ein Umzug legt den Schlüssel **nicht** im Klartext ab, nur damit der
> nächste Start bequemer wird.
`SecretKeyStore.Wrap` bindet den Schlüssel unter Windows per DPAPI ans Benutzerkonto
und gibt ihn sonst ungewrappt zurück; der einzige Schutz ist dann `0600` aus
`AppPaths.RestrictToOwner`. Auf einer dauerhaft laufenden, netzgebundenen Maschine ist