- 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
81 lines
2.6 KiB
Go
81 lines
2.6 KiB
Go
package crypto
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
)
|
|
|
|
// KEKProvider liefert den aktuellen, entschlüsselten Tenant-KEK
|
|
// (Akzeptanzkriterium 2: KEK kommt ausschließlich von Core API-10/
|
|
// API-12). Schmale Schnittstelle, damit Tests einen Fake statt eines
|
|
// echten HTTP-Aufrufs einsetzen können.
|
|
type KEKProvider interface {
|
|
TenantKEK(ctx context.Context, tenantSlug string) ([]byte, error)
|
|
}
|
|
|
|
// tenantKEKResponse entspricht Core internal/kek.tenantKEKResponse
|
|
// (JSON-Vertrag: tenant_kek_base64) — dieselbe Struktur, hier
|
|
// gespiegelt, da Mail Cores internal/-Pakete nicht importieren kann.
|
|
type tenantKEKResponse struct {
|
|
TenantKEKBase64 string `json:"tenant_kek_base64"`
|
|
}
|
|
|
|
// HTTPKEKProvider bezieht den Tenant-KEK über Core API-12
|
|
// (internal/kek.Handler.TenantKEKHandler, echt laufender Dienst),
|
|
// authentifiziert über dasselbe Service-Credential-Verfahren wie jeder
|
|
// andere Modul-Core-Aufruf (API-02) — identisches Muster wie DMS
|
|
// FDN-09/internal/storage.HTTPUsageReporter aus ARC-01.
|
|
type HTTPKEKProvider struct {
|
|
endpointURL string
|
|
clientID string
|
|
clientSecret string
|
|
httpClient *http.Client
|
|
}
|
|
|
|
func NewHTTPKEKProvider(endpointURL, clientID, clientSecret string, httpClient *http.Client) *HTTPKEKProvider {
|
|
if httpClient == nil {
|
|
httpClient = http.DefaultClient
|
|
}
|
|
return &HTTPKEKProvider{endpointURL: endpointURL, clientID: clientID, clientSecret: clientSecret, httpClient: httpClient}
|
|
}
|
|
|
|
func (p *HTTPKEKProvider) TenantKEK(ctx context.Context, tenantSlug string) ([]byte, error) {
|
|
u, err := url.Parse(p.endpointURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("crypto: kek-endpunkt-url ungültig: %w", err)
|
|
}
|
|
q := u.Query()
|
|
q.Set("tenant", tenantSlug)
|
|
u.RawQuery = q.Encode()
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("crypto: kek-anfrage aufbauen: %w", err)
|
|
}
|
|
req.Header.Set("X-Nexarch-Client-Id", p.clientID)
|
|
req.Header.Set("X-Nexarch-Client-Secret", p.clientSecret)
|
|
|
|
resp, err := p.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("crypto: kek-anfrage senden: %w", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("crypto: kek-bezug von core abgelehnt: status %d", resp.StatusCode)
|
|
}
|
|
|
|
var body tenantKEKResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
return nil, fmt.Errorf("crypto: kek-antwort dekodieren: %w", err)
|
|
}
|
|
kek, err := base64.StdEncoding.DecodeString(body.TenantKEKBase64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("crypto: kek base64-dekodieren: %w", err)
|
|
}
|
|
return kek, nil
|
|
}
|