feat(schluessel): erzeugen, importieren, exportieren (#012)

Schluessel werden jetzt verwaltet statt nur im Prototyp gezeigt: Liste
mit Filter, Anlegen (erzeugen oder importieren), Detail nach dem
Prototyp aus #002, Bearbeiten der Metadaten und Download <slug>.pub.

Warum so:
- Erzeugen und Import eines Paars laufen als Vorgang im Worker, weil
  nur er den Master-Key hat. Passphrase und importierter Private Key
  reisen wie das Bootstrap-Passwort mit SSHKV_APP_SECRET verschluesselt
  im Job, nie in Vorgang.eingabe; der Worker loescht den Job danach.
- Ein reiner Public Key ist kein Geheimnis und wird direkt im Web
  angelegt, mit verstaendlicher Meldung bei einem Duplikat.
- Das Web liest nie das Chiffrat: ob ein Private Key gespeichert ist,
  verraet masterKeyVersion (per CHECK an das Chiffrat gekoppelt). Ein
  struktureller Test verlangt fuer jede zeilenliefernde Abfrage auf
  Schluessel ausserhalb des Workers ein festes select.
- Migration: Vorgangsarten fuer Erzeugen, Import und (fuer #014) das
  Loeschen des Private Keys; CHECKs fuer den Slug (landet in
  Dateinamen) und "geloescht nur ohne Chiffrat".
- Platzhalter PublicAusrollenAktion/PrivateAusrollenAktion fuer #016/#017.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 10:29:52 +02:00
co-authored by Claude Opus 5.5
parent f0833aa1db
commit 263143134f
33 changed files with 3322 additions and 287 deletions
+34 -2
View File
@@ -126,13 +126,16 @@ export type NeuerSchluessel = {
/**
* Erzeugt ein Schluesselpaar und versiegelt den Private Key sofort. Die ID wird hier
* vergeben, weil sie in die Zusatzdaten des Umschlags eingeht.
* vergeben, weil sie in die Zusatzdaten des Umschlags eingeht. Mit `passphrase` liegt im
* Umschlag der bereits passphrasengeschuetzte OpenSSH-Key; die Passphrase selbst wird nie
* gespeichert (Freigabe Frage 6, #012).
*/
export async function schluesselpaarVersiegeltErzeugen(
typ: ErzeugbarerSchluesseltyp,
kommentar: string,
passphrase?: string,
): Promise<NeuerSchluessel> {
const paar = await schluesselpaarErzeugen(typ, { kommentar });
const paar = await schluesselpaarErzeugen(typ, { kommentar, ...(passphrase ? { passphrase } : {}) });
const id = randomUUID();
return {
id,
@@ -142,6 +145,35 @@ export async function schluesselpaarVersiegeltErzeugen(
};
}
/**
* Versiegelt einen importierten Private Key (#012) so, wie er kam – ein
* passphrasengeschuetzter Key bleibt es. Die ID wird hier vergeben (Zusatzdaten).
*/
export async function importiertenKeyVersiegeln(
fingerprint: string,
privateKeyOpenssh: string,
): Promise<{ readonly id: string; readonly versiegelt: VersiegelterKey }> {
const id = randomUUID();
return { id, versiegelt: await privateKeyVersiegeln(id, fingerprint, privateKeyOpenssh) };
}
/**
* Verwirft den gespeicherten Private Key endgueltig (#014): Chiffrat, Data-Key und Version
* auf null, Zeitpunkt festhalten. Liefert false, wenn keiner (mehr) gespeichert war.
*/
export async function privateKeyVerwerfen(schluesselId: string): Promise<boolean> {
const ergebnis = await prisma.schluessel.updateMany({
where: { id: schluesselId, privatVerschluesselt: { not: null } },
data: {
privatVerschluesselt: null,
dekVerschluesselt: null,
masterKeyVersion: null,
privatGeloeschtAm: new Date(),
},
});
return ergebnis.count === 1;
}
/**
* Stellt beim Start des Workers sicher, dass es einen Deployment-Key der Instanz gibt
* (#006). Gibt die ID zurueck. Gleichzeitige Starts scheitern am partiellen Index