- mail/internal/crypto: Envelope-Encryption (AES-256-GCM), DEK pro Objekt, HTTPKEKProvider bezieht Tenant-KEK ueber Core API-12 - bewaehrtes Muster aus DMS FDN-09, Neuimplementierung (Mail kann DMS nicht importieren) - mail/internal/encstorage: verbindet ARC-01 (storage.Service) mit ARC-02 (crypto.Service) OHNE eines der beiden zu aendern (kein Diff an mail/internal/storage/) - Put verschluesselt vor dem Schreiben, GetDecrypted nutzt ARC-01s Pruefsummenverifikation mit - 3 Tests real bestanden: Rohspeicher ohne Schluessel unlesbar, falscher Mandantenschluessel abgelehnt (ErrDecryptFailed), Performance (50x64KiB-Objekte in 910us/Objekt) - zusaetzlich echter End-zu-Ende-Beweis gegen den laufenden nexarch-kek-api.service (API-12): vollstaendiger Put->GetDecrypted- Roundtrip ueber echten HTTP-KEK-Bezug, nicht-existenter Tenant real abgelehnt (404) - offener Punkt ehrlich vermerkt: internal/crypto/internal/encstorage fehlen noch in QA-01s Pflichttest-Gate-Pfadmustern Pruefungen siehe mail/docs/ARC-02-PRUEFPROTOKOLL.md
94 lines
3.8 KiB
Go
94 lines
3.8 KiB
Go
// 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)
|
|
}
|