- 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
68 lines
2.1 KiB
Go
68 lines
2.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).
|
|
type Envelope struct {
|
|
Ciphertext io.Reader
|
|
WrappedDEK []byte
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
func NewService(kek KEKProvider) *Service {
|
|
return &Service{kek: kek}
|
|
}
|
|
|
|
// 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).
|
|
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
|
|
}
|
|
return &Envelope{Ciphertext: ciphertext, WrappedDEK: wrappedDEK}, 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.
|
|
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)
|
|
}
|