package crypto import ( "context" "fmt" "io" ) // Envelope ist das Ergebnis einer Seal-Operation: der Chiffretext- // Stream plus der mit dem Tenant-KEK verpackte DEK, der zusammen mit // dem Objekt persistiert werden muss (siehe mail/internal/encstorage). // KEKVersion identifiziert (ARC-08), MIT welcher Tenant-KEK-Version der // DEK verpackt wurde — 0, solange kein KEKVersionStore konfiguriert ist // (Rückwärtskompatibilität, siehe WithVersionStore). type Envelope struct { Ciphertext io.Reader WrappedDEK []byte KEKVersion int } // Service verbindet KEKProvider mit den Envelope-Operationen — Aufrufer // (mail/internal/encstorage) rufen ausschließlich Service auf, nie die // Einzelfunktionen aus envelope.go direkt. type Service struct { kek KEKProvider versions *KEKVersionStore } func NewService(kek KEKProvider) *Service { return &Service{kek: kek} } // WithVersionStore aktiviert die Tenant-KEK-Versionsverfolgung (ARC-08). // Ohne aufgerufenes WithVersionStore verhält sich Service exakt wie vor // ARC-08 (KEKVersion bleibt 0, OpenAtVersion fällt auf Open zurück) — // bestehende Aufrufer (z. B. encstorage) sind unverändert lauffähig. func (s *Service) WithVersionStore(store *KEKVersionStore) *Service { s.versions = store return s } // Seal erzeugt einen neuen DEK (Akzeptanzkriterium 1), verschlüsselt // plaintext damit und verpackt den DEK mit dem aktuellen Tenant-KEK // (Akzeptanzkriterium 2 — der KEK wird bei JEDEM Aufruf frisch von Core // bezogen, nie zwischengespeichert außer in der optionalen // KEK-Versionshistorie für spätere Altbestands-Lesezugriffe). func (s *Service) Seal(ctx context.Context, tenantSlug string, plaintext io.Reader) (*Envelope, error) { dek, err := GenerateDEK() if err != nil { return nil, err } ciphertext, err := EncryptStream(dek, plaintext) if err != nil { return nil, err } kek, err := s.kek.TenantKEK(ctx, tenantSlug) if err != nil { return nil, fmt.Errorf("crypto: tenant-kek beziehen: %w", err) } wrappedDEK, err := WrapDEK(kek, dek) if err != nil { return nil, err } var kekVersion int if s.versions != nil { kekVersion, err = s.versions.RecordIfNew(ctx, tenantSlug, kek) if err != nil { return nil, fmt.Errorf("crypto: kek-version erfassen: %w", err) } } return &Envelope{Ciphertext: ciphertext, WrappedDEK: wrappedDEK, KEKVersion: kekVersion}, nil } // Open entpackt den DEK mit dem AKTUELLEN Tenant-KEK (Akzeptanzkriterium // 3: nur mit gültigem, mandantenbezogenem Schlüssel möglich — ein // falscher Tenant-Slug liefert entweder einen falschen KEK von Core // [dann schlägt UnwrapDEK fehl] oder Core verweigert den Zugriff direkt) // und entschlüsselt ciphertext damit. Nach einer Tenant-KEK-Rotation bei // Core funktioniert Open nur noch für Objekte, die mit dem NEUEN KEK // versiegelt wurden — für Altbestand siehe OpenAtVersion. func (s *Service) Open(ctx context.Context, tenantSlug string, wrappedDEK []byte, ciphertext io.Reader) (io.Reader, error) { kek, err := s.kek.TenantKEK(ctx, tenantSlug) if err != nil { return nil, fmt.Errorf("crypto: tenant-kek beziehen: %w", err) } dek, err := UnwrapDEK(kek, wrappedDEK) if err != nil { return nil, err } return DecryptStream(dek, ciphertext) } // OpenAtVersion entpackt den DEK mit der beim Seal aufgezeichneten // historischen Tenant-KEK-Version statt mit dem aktuellen Core-KEK // (ARC-08 Akzeptanzkriterium 3: Altbestand bleibt nach einer // Hauptschlüssel-Rotation lesbar). Ist kekVersion 0 oder kein // KEKVersionStore konfiguriert, verhält es sich wie Open (Rückwärts- // kompatibilität für vor ARC-08 versiegelte Objekte). func (s *Service) OpenAtVersion(ctx context.Context, tenantSlug string, kekVersion int, wrappedDEK []byte, ciphertext io.Reader) (io.Reader, error) { if kekVersion == 0 || s.versions == nil { return s.Open(ctx, tenantSlug, wrappedDEK, ciphertext) } kek, err := s.versions.Get(ctx, tenantSlug, kekVersion) if err != nil { return nil, fmt.Errorf("crypto: historischen tenant-kek beziehen: %w", err) } dek, err := UnwrapDEK(kek, wrappedDEK) if err != nil { return nil, err } return DecryptStream(dek, ciphertext) }