// Package crypto implementiert ARC-02: Envelope-Encryption für Objekte // at rest. Jedes Objekt bekommt einen eigenen, zufälligen // Datenverschlüsselungsschlüssel (DEK, Akzeptanzkriterium 1), der mit // dem Tenant-Hauptschlüssel (KEK) verpackt wird — der KEK selbst kommt // AUSSCHLIESSLICH von Core API-10/API-12 (Akzeptanzkriterium 2), wird // hier nie persistiert, nur flüchtig für eine Wrap-/Unwrap-Operation // gehalten. Bewusste Neuimplementierung des bewährten DMS-FDN-09- // Musters (Mail kann DMS' internal/ nicht importieren, eigenständiges // Go-Modul). package crypto import ( "bytes" "crypto/aes" "crypto/cipher" "crypto/rand" "errors" "fmt" "io" ) const ( DEKSize = 32 KEKSize = 32 ) // ErrDecryptFailed wird geliefert, wenn ein Chiffretext nicht // entschlüsselt werden kann — falscher Schlüssel ODER manipulierte // Daten (Pflichtprüfung 2: GCM-Auth-Tag erkennt Manipulation // zuverlässig, AEAD unterscheidet die beiden Ursachen bewusst nicht). var ErrDecryptFailed = errors.New("crypto: entschlüsselung fehlgeschlagen (falscher schlüssel oder manipulierte daten)") 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 } 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 } func UnwrapDEK(kek, wrappedDEK []byte) ([]byte, error) { return open(kek, wrappedDEK) } // EncryptStream verschlüsselt den gesamten Inhalt von r mit dek // (AES-256-GCM). Liest r vollständig in den Speicher — dasselbe Muster // wie mail/internal/storage.S3Driver.Put (ARC-01), das S3-PutObject // ebenfalls vollständig puffert; ein segmentiertes AEAD-Verfahren für // sehr große Anhänge ist bewusst nicht Teil der "kleinsten Lösung". 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: verschlüsseln: %w", err) } return bytes.NewReader(ciphertext), nil } // DecryptStream entschlüsselt einen zuvor mit EncryptStream erzeugten // Chiffretext-Stream. 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 }