// Package kek implementiert Core API-10: die zweistufige Schluesselhierarchie // fuer Envelope-Encryption (Master-KEK -> Tenant-KEK), die DMS (FDN-09) und // Mail (ARC-02) fuer ihre pro-Objekt-DEKs verwenden. Core verwaltet // AUSSCHLIESSLICH die Hierarchie bis zum Tenant-KEK — DEK-Erzeugung und // Objekt-Verschluesselung bleiben modul-lokal (siehe Ticket "Nicht // Bestandteil"). // // Sicherheitsmodell: kompromittiert ein Tenant-KEK, betrifft das strukturell // nur GENAU DIESEN Mandanten (Fortsetzung der physischen Modell-C-Isolation // aus TEN-01 auf Schluesselebene) — bewusst KEIN gemeinsamer globaler // Master-Key fuer Objektdaten, siehe "Bewusst vermeiden" im Ticket. package kek import ( "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "errors" "fmt" "io" "os" ) // MasterKeySize ist die geforderte Laenge fuer AES-256-GCM. const MasterKeySize = 32 var ( ErrMasterKeyNotSet = errors.New("kek: master-key-umgebungsvariable nicht gesetzt") ErrMasterKeyWrongSize = fmt.Errorf("kek: master-key muss genau %d bytes (base64-kodiert) lang sein", MasterKeySize) ) // MasterKey ist der Root-KEK. Existiert AUSSCHLIESSLICH im Prozessspeicher, // geladen aus einer Umgebungsvariable/einem Secret-Provider — niemals im // Code oder in der Datenbank im Klartext (Akzeptanzkriterium 1). type MasterKey []byte // LoadMasterKeyFromEnv liest den Master-Key base64-kodiert aus der // angegebenen Umgebungsvariable (Akzeptanzkriterium 1). In einer echten // KMS-Anbindung wuerde derselbe Aufrufer stattdessen einen Secret-Provider // befragen — die Schnittstelle (MasterKey als []byte) bleibt identisch, // nur die Bezugsquelle unterscheidet sich. func LoadMasterKeyFromEnv(envVar string) (MasterKey, error) { raw := os.Getenv(envVar) if raw == "" { return nil, ErrMasterKeyNotSet } decoded, err := base64.StdEncoding.DecodeString(raw) if err != nil { return nil, fmt.Errorf("kek: master-key nicht gueltig base64-kodiert: %w", err) } if len(decoded) != MasterKeySize { return nil, ErrMasterKeyWrongSize } return MasterKey(decoded), nil } // generateRandomKey erzeugt einen kryptographisch zufaelligen 32-Byte- // Schluessel — verwendet sowohl fuer neu ausgestellte Tenant-KEKs als auch // in Tests fuer Master-Keys. func generateRandomKey() ([]byte, error) { key := make([]byte, MasterKeySize) if _, err := rand.Read(key); err != nil { return nil, fmt.Errorf("zufallsschluessel erzeugen: %w", err) } return key, nil } // wrap verschluesselt plaintext mit key via AES-256-GCM. Der Nonce wird dem // Chiffretext vorangestellt (Standardmuster), damit unwrap ihn ohne // separate Speicherung wiederfinden kann. func wrap(key, plaintext []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { return nil, fmt.Errorf("aes-cipher erstellen: %w", err) } gcm, err := cipher.NewGCM(block) if err != nil { return nil, fmt.Errorf("gcm erstellen: %w", err) } nonce := make([]byte, gcm.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return nil, fmt.Errorf("nonce erzeugen: %w", err) } return gcm.Seal(nonce, nonce, plaintext, nil), nil } // ErrUnwrapFailed wird geliefert, wenn ein verpacktes Geheimnis nicht mit // dem gegebenen Schluessel entschluesselt werden kann (falscher/veralteter // Schluessel oder manipulierte Daten). var ErrUnwrapFailed = errors.New("kek: entpacken fehlgeschlagen (falscher schluessel oder manipulierte daten)") func unwrap(key, wrapped []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { return nil, fmt.Errorf("aes-cipher erstellen: %w", err) } gcm, err := cipher.NewGCM(block) if err != nil { return nil, fmt.Errorf("gcm erstellen: %w", err) } if len(wrapped) < gcm.NonceSize() { return nil, ErrUnwrapFailed } nonce, ciphertext := wrapped[:gcm.NonceSize()], wrapped[gcm.NonceSize():] plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) if err != nil { return nil, ErrUnwrapFailed } return plaintext, nil }