// Package crypto implementiert FDN-09: Envelope-Encryption fuer Objekte // at rest. Jedes Objekt bekommt einen eigenen, zufaelligen // Datenverschluesselungsschluessel (DEK, Akzeptanzkriterium 1), der mit // dem Tenant-Hauptschluessel (KEK) verpackt wird — der KEK selbst kommt // AUSSCHLIESSLICH von Core API-10 (Akzeptanzkriterium 2), wird hier nie // persistiert, nur fluechtig fuer eine Wrap-/Unwrap-Operation gehalten. package crypto import ( "bytes" "crypto/aes" "crypto/cipher" "crypto/rand" "errors" "fmt" "io" ) // DEKSize/KEKSize sind die geforderten Laengen fuer AES-256-GCM. const ( DEKSize = 32 KEKSize = 32 ) // ErrDecryptFailed wird geliefert, wenn ein Chiffretext nicht entschluesselt // werden kann — falscher Schluessel ODER manipulierte Daten (Pruefung 2: // GCM-Auth-Tag erkennt Manipulation zuverlaessig, AEAD unterscheidet die // beiden Ursachen bewusst nicht, um keine Seitenkanal-Information ueber // "welcher Teil" falsch war preiszugeben). var ErrDecryptFailed = errors.New("crypto: entschluesselung fehlgeschlagen (falscher schluessel oder manipulierte daten)") // GenerateDEK erzeugt einen neuen, zufaelligen Datenverschluesselungs- // schluessel — fuer JEDES Objekt neu, nie wiederverwendet // (Akzeptanzkriterium 1). func GenerateDEK() ([]byte, error) { dek := make([]byte, DEKSize) if _, err := rand.Read(dek); err != nil { return nil, fmt.Errorf("crypto: dek erzeugen: %w", err) } return dek, nil } func seal(key, plaintext []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { return nil, fmt.Errorf("crypto: aes-cipher erstellen: %w", err) } gcm, err := cipher.NewGCM(block) if err != nil { return nil, fmt.Errorf("crypto: gcm erstellen: %w", err) } nonce := make([]byte, gcm.NonceSize()) if _, err := rand.Read(nonce); err != nil { return nil, fmt.Errorf("crypto: nonce erzeugen: %w", err) } return gcm.Seal(nonce, nonce, plaintext, nil), nil } func open(key, sealed []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { return nil, fmt.Errorf("crypto: aes-cipher erstellen: %w", err) } gcm, err := cipher.NewGCM(block) if err != nil { return nil, fmt.Errorf("crypto: gcm erstellen: %w", err) } if len(sealed) < gcm.NonceSize() { return nil, ErrDecryptFailed } nonce, ciphertext := sealed[:gcm.NonceSize()], sealed[gcm.NonceSize():] plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) if err != nil { return nil, ErrDecryptFailed } return plaintext, nil } // WrapDEK verpackt einen DEK mit dem Tenant-KEK (Envelope-Verfahren). func WrapDEK(kek, dek []byte) ([]byte, error) { wrapped, err := seal(kek, dek) if err != nil { return nil, fmt.Errorf("crypto: dek verpacken: %w", err) } return wrapped, nil } // UnwrapDEK entpackt einen zuvor mit WrapDEK verpackten DEK. func UnwrapDEK(kek, wrappedDEK []byte) ([]byte, error) { return open(kek, wrappedDEK) } // RewrapDEK verpackt einen bereits vorhandenen DEK-Wrapper von oldKEK auf // newKEK um, OHNE den DEK selbst oder den Objekt-Chiffretext zu veraendern // (Akzeptanzkriterium 3: KEK-Rotation ohne Neuverschluesselung aller // Objekte — nur der DEK-Wrapper wird neu verpackt). func RewrapDEK(oldKEK, newKEK, wrappedDEK []byte) ([]byte, error) { dek, err := UnwrapDEK(oldKEK, wrappedDEK) if err != nil { return nil, fmt.Errorf("crypto: dek mit altem kek entpacken: %w", err) } rewrapped, err := WrapDEK(newKEK, dek) if err != nil { return nil, fmt.Errorf("crypto: dek mit neuem kek verpacken: %w", err) } return rewrapped, nil } // EncryptStream verschluesselt den gesamten Inhalt von r mit dek // (AES-256-GCM) und liefert den Chiffretext als Stream. Liest r vollstaendig // in den Speicher (dasselbe Muster wie internal/storage.S3Driver.Put aus // FDN-03, das S3-PutObject ebenfalls vollstaendig puffert) — fuer sehr // grosse Dateien waere ein segmentiertes AEAD-Verfahren noetig, das ist // bewusst nicht Teil der "kleinsten Loesung" dieser Kachel. func EncryptStream(dek []byte, r io.Reader) (io.Reader, error) { plaintext, err := io.ReadAll(r) if err != nil { return nil, fmt.Errorf("crypto: klartext lesen: %w", err) } ciphertext, err := seal(dek, plaintext) if err != nil { return nil, fmt.Errorf("crypto: verschluesseln: %w", err) } return bytes.NewReader(ciphertext), nil } // DecryptStream entschluesselt einen zuvor mit EncryptStream erzeugten // Chiffretext-Stream. Liefert ErrDecryptFailed bei manipuliertem Chiffretext // (Pruefung 2, GCM-Auth-Tag). func DecryptStream(dek []byte, r io.Reader) (io.Reader, error) { ciphertext, err := io.ReadAll(r) if err != nil { return nil, fmt.Errorf("crypto: chiffretext lesen: %w", err) } plaintext, err := open(dek, ciphertext) if err != nil { return nil, err } return bytes.NewReader(plaintext), nil }