Tenant-KEK-Rotation ohne Neuverschlüsselung des Archivbestands (Envelope-Encryption bleibt aus ARC-02 unverändert, Objekt-DEKs werden nicht angefasst). Core (API-10, RotateTenantKEK) ersetzt den Tenant-KEK durch einen neuen Wert und hält keine Historie vor — TenantKEKHandler liefert immer nur den aktuellen Schlüssel. Damit Mail Altbestand nach einer Rotation weiterhin lesen kann, versioniert Mail selbst jeden bezogenen Tenant-KEK: - crypto/kekversions.go: KEKVersionStore, lokal verschlüsselt mit eigenem Wrap-Schlüssel (nur über Umgebungsvariable), erkennt Rotation automatisch (RecordIfNew), erlaubt gezieltes Sperren einer Version (Revoke). - crypto/service.go: Service.WithVersionStore (optional, Open bleibt für Rückwärtskompatibilität unverändert), Seal zeichnet die verwendete KEK-Version auf, neue Methode OpenAtVersion liest mit historischer statt aktueller Version. - encstorage.go: neuer .dek.version-Sidecar (gleiches Muster wie der bestehende .dek-Sidecar), GetDecrypted nutzt OpenAtVersion; fehlender Sidecar (Altobjekte vor ARC-08) fällt auf Version 0 zurück, identisches Verhalten wie vorher. Prüfungen (alle real durchgeführt, siehe mail/docs/ARC-08-PRUEFPROTOKOLL.md): 1. TestRotation_OldArchiveStaysReadableAfterMasterKeyRotation: Altbestand nach realer Rotation weiterhin lesbar über OpenAtVersion, naives Open mit dem neuen Schlüssel schlägt für das alte Objekt real fehl. 2. TestRotation_CompromisedOldKeyCanBeRevoked: gesperrte Version blockiert Lesezugriff real, andere Versionen bleiben unberührt. 3. Rotationsvorgang vollständig durchgespielt (siehe Prüfprotokoll). Kein Umbau: storage/dedup/indexworker/search unverändert, bestehende ARC-02-Tests (encstorage_test.go) unverändert weiterhin grün. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
115 lines
4.1 KiB
Go
115 lines
4.1 KiB
Go
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)
|
|
}
|