// Package encstorage implementiert ARC-02s Anbindung an ARC-01: jedes // archivierte Objekt wird VOR dem Schreiben über // mail/internal/storage.Service verschlüsselt (Akzeptanzkriterium 1). // Kombiniert bewusst zwei bereits fertige, unveränderte Pakete // (mail/internal/storage aus ARC-01, mail/internal/crypto aus ARC-02) // statt eines davon zu erweitern — kein Umbau angrenzender Bereiche. // // Reihenfolge beachtet (Ticket "Bekannte Fehler vermeiden"): ein // SHA-256-Hash für Dublettenerkennung (ARC-03) muss auf dem KLARTEXT // berechnet werden, BEVOR diese Verschlüsselung angewendet wird — das // ist Sache des Aufrufers (ARC-03, außerhalb dieses Pakets), dieses // Paket nimmt bereits fertigen Klartext entgegen und verschlüsselt ihn // sofort, hält ihn nicht länger als nötig im Speicher. package encstorage import ( "bytes" "context" "fmt" "io" "gitea.perlbach24.de/scripte/nexarch/mail/internal/crypto" "gitea.perlbach24.de/scripte/nexarch/mail/internal/storage" ) // wrappedDEKKey ist der Sidecar-Objektschlüssel für den mit dem // Tenant-KEK verpackten DEK — liegt im selben Storage-Backend wie der // Chiffretext, analog zum Prüfsummen-Sidecar aus ARC-01. func wrappedDEKKey(key string) string { return key + ".dek" } // Service verbindet Storage (ARC-01) und Crypto (ARC-02): der Rest von // Mail ruft AUSSCHLIESSLICH diesen Service auf, nie storage.Service // direkt mit Klartext — das verhindert einen Schreibpfad, der die // Verschlüsselung vergisst (Akzeptanzkriterium 1). type Service struct { storage *storage.Service crypto *crypto.Service } func NewService(storageSvc *storage.Service, cryptoSvc *crypto.Service) *Service { return &Service{storage: storageSvc, crypto: cryptoSvc} } // Put verschlüsselt plaintext (Akzeptanzkriterium 1/2: neuer DEK je // Objekt, mit dem per API-12 bezogenen Tenant-KEK verpackt) und legt // Chiffretext + verpackten DEK über storage.Service ab (Prüfsumme, // Nutzungsmeldung — ARC-01 unverändert wiederverwendet). func (s *Service) Put(ctx context.Context, tenantSlug, key string, plaintext io.Reader, contentType string) error { env, err := s.crypto.Seal(ctx, tenantSlug, plaintext) if err != nil { return fmt.Errorf("encstorage: verschlüsseln: %w", err) } ciphertext, err := io.ReadAll(env.Ciphertext) if err != nil { return fmt.Errorf("encstorage: chiffretext lesen: %w", err) } if _, err := s.storage.Put(ctx, key, bytes.NewReader(ciphertext), int64(len(ciphertext)), contentType); err != nil { return fmt.Errorf("encstorage: chiffretext speichern: %w", err) } if _, err := s.storage.Put(ctx, wrappedDEKKey(key), bytes.NewReader(env.WrappedDEK), int64(len(env.WrappedDEK)), "application/octet-stream"); err != nil { return fmt.Errorf("encstorage: verpackten dek speichern: %w", err) } return nil } // GetDecrypted liest Chiffretext (über storage.GetVerified — ARC-01s // Prüfsummenverifikation wird dabei mitgenutzt) und verpackten DEK, // entpackt den DEK mit dem AKTUELLEN Tenant-KEK und entschlüsselt den // Inhalt (Akzeptanzkriterium 3: nur mit gültigem, mandantenbezogenem // Schlüssel möglich). func (s *Service) GetDecrypted(ctx context.Context, tenantSlug, key string) ([]byte, error) { ciphertext, err := s.storage.GetVerified(ctx, key) if err != nil { return nil, fmt.Errorf("encstorage: chiffretext lesen: %w", err) } dekReader, err := s.storage.Get(ctx, wrappedDEKKey(key)) if err != nil { return nil, fmt.Errorf("encstorage: verpackten dek lesen: %w", err) } defer func() { _ = dekReader.Close() }() wrappedDEK, err := io.ReadAll(dekReader) if err != nil { return nil, fmt.Errorf("encstorage: verpackten dek lesen: %w", err) } plaintextReader, err := s.crypto.Open(ctx, tenantSlug, wrappedDEK, bytes.NewReader(ciphertext)) if err != nil { return nil, err } return io.ReadAll(plaintextReader) }