# #012 Schlüssel erzeugen, importieren, exportieren **Status:** erledigt **Meilenstein:** v0.2.0 **Labels:** backend, ui **Geschätzt:** 4 h **Tatsächlich:** 0 h 25 min ## Ziel Schlüsselliste und -formular: erzeugen (ed25519, rsa-4096), importieren (Public oder Paar), exportieren; Kategorien, Tags, Ablaufdatum. ## Aufgaben - [x] Liste mit Filter - [x] Erzeugen-Dialog - [x] Import mit Prüfung - [x] Export Public Key - [x] Schlüsseldetail-Gerüst mit Verwendungen ## Akzeptanzkriterien - [x] Private Keys erscheinen nie in Listen-Antworten ## Abhängigkeiten #006 ## Notizen / Entscheidungen - **Migration `20261012100000_schluessel_vorgaenge`:** Vorgangsarten `SCHLUESSEL_ERZEUGEN`, `SCHLUESSEL_IMPORTIEREN`, `PRIVATE_KEY_LOESCHEN` (für #014 gleich mit), CHECK `schluessel_slug_gueltig` (`^[a-z0-9-]{1,64}$`, der Slug landet in Dateinamen) und `schluessel_geloescht_ohne_private_key`. - **Erzeugen und Paar-Import laufen im Worker** (`src/worker/ablaeufe/schluessel.ts`). Das Web prüft nur Metadaten (Slug frei, Person/Vorlage vorhanden, Ablauf nicht in der Vergangenheit) und reiht den Vorgang ein. Passphrase und importierter Private Key reisen wie das Bootstrap-Passwort mit `SSHKV_APP_SECRET` verschlüsselt im Job (`geheim`), nie in `Vorgang.eingabe`; der Worker löscht den Job danach. Der Worker prüft die Metadaten erneut. - **Kategorie:** Erzeugen und Paar-Import legen immer `DIENST` an. Personen-Keys speichert sshkv nie mit Private Key (DB-CHECK), Deployment-Keys verwaltet der Worker selbst. Der Public-Key-Import kennt `PERSON` und `DIENST`. - **Passphrase:** beim Erzeugen optional; gespeichert wird dann der passphrasengeschützte OpenSSH-Key (`passphraseGeschuetzt = true`). Beim Import dient sie nur zum Lesen von Public Key und Fingerprint und wird verworfen; gespeichert wird der Key so, wie er kam. Hinweis in der Oberfläche: „Für automatische Anmeldungen (Host-zu-Host) ungeeignet.“ - **Typ:** `RSA4096` nur bei genau 4096 Bit, sonst `ANDERER` (z. B. RSA-3072, ecdsa). Die RSA- Mindestlänge 2048 gilt beim Import. - **Slug:** aus dem Zweck abgeleitet (Umlaute gefaltet), bei Kollision `-2`, `-3` …; ein ausdrücklich gewünschter Kurzname muss frei sein. Slug, Typ und Kommentar sind danach nicht mehr änderbar – sie stehen schon in Dateinamen und authorized_keys-Zeilen. - **Status von Hand** nur `AKTIV` ↔ `ABGELAUFEN`; `ROTIERT`/`WIDERRUFEN` setzen #023/#024, Deployment-Keys sind ausgenommen (sonst erzeugte der Worker beim Start einen neuen). - **Ablaufdatum** ohne Angabe aus `Vorlage.standardLaufzeitTage`. - **„Private Key gespeichert?“** liest das Web aus `masterKeyVersion` (der CHECK `schluessel_umschlag_vollstaendig` koppelt sie an das Chiffrat) – das Chiffrat wird im Web nie selektiert. - **Grenze abgesichert:** `tests/schluessel-grenze.test.ts` verlangt für jeden zeilenliefernden `prisma.schluessel.*`-Aufruf außerhalb von `src/worker` ein `select` und verbietet `schluessel: true` als Relation; `tests/schluessel.test.ts` prüft, dass weder Vorgang, Protokoll noch pg-boss-Job nach Erzeugen/Import den Key oder die Passphrase enthalten. - **Platzhalter** für #016/#017: `src/app/(app)/schluessel/[id]/PublicAusrollenAktion.tsx` und `PrivateAusrollenAktion.tsx`, Props `{ schluesselId: string; darf: boolean }`, rendern `null`. - Personen-Keys zeigen im Detail nur die Karte „Public Key ausgerollt auf“ (eine Private-Karte wäre immer leer). - Prototyp `/schluessel/beispiel` samt `src/beispiel/schluessel.ts` entfernt; die Übersicht (noch Prototyp, #026) verlinkt auf `/schluessel`.