package encstorage import ( "bytes" "context" "errors" "os" "path/filepath" "strings" "testing" "time" "gitea.perlbach24.de/scripte/nexarch/mail/internal/crypto" "gitea.perlbach24.de/scripte/nexarch/mail/internal/storage" ) type fakeUsageReporter struct{} func (fakeUsageReporter) Report(_ context.Context, _, _ string, _ int64) error { return nil } // fakeKEKProvider liefert einen festen, mandantenspezifischen KEK — // simuliert Core API-12, ohne den echten Dienst für Unit-Tests zu // brauchen. "richtiger-tenant" hat einen bekannten KEK, jeder andere // Tenant-Slug liefert einen ANDEREN (falschen) KEK — genau der Fall, // den Pflichtprüfung 2 testet. type fakeKEKProvider struct { keksByTenant map[string][]byte } func (f fakeKEKProvider) TenantKEK(_ context.Context, tenantSlug string) ([]byte, error) { if kek, ok := f.keksByTenant[tenantSlug]; ok { return kek, nil } // Unbekannter Tenant bekommt einen ANDEREN zufälligen KEK (nicht // abgelehnt auf Core-Seite in diesem Fake, aber garantiert falsch — // entspricht dem realen Fall, dass ein falscher Tenant-Slug einen // anderen KEK liefert und die Entschlüsselung deshalb scheitert). return bytes.Repeat([]byte{0xFF}, crypto.KEKSize), nil } func newTestServiceWithDir(t *testing.T) (*Service, string) { t.Helper() dir := t.TempDir() storageSvc := storage.NewService(storage.NewLocalDriver(dir), fakeUsageReporter{}, "acme") kekProvider := fakeKEKProvider{keksByTenant: map[string][]byte{ "richtiger-tenant": bytes.Repeat([]byte{0x42}, crypto.KEKSize), }} cryptoSvc := crypto.NewService(kekProvider) return NewService(storageSvc, cryptoSvc), dir } // TestPut_RawStorageAccessWithoutKeyYieldsNoReadableContent ist die // geforderte Pflichtprüfung 1: Zugriff auf Rohspeicher ohne Schlüssel // liefert keine lesbaren Inhalte. func TestPut_RawStorageAccessWithoutKeyYieldsNoReadableContent(t *testing.T) { svc, dir := newTestServiceWithDir(t) ctx := context.Background() key := storage.ObjectKey("msg-enc-1", 0) plaintext := "STRENG GEHEIMER E-MAIL-INHALT, NICHT IM KLARTEXT AUF DER PLATTE" if err := svc.Put(ctx, "richtiger-tenant", key, strings.NewReader(plaintext), "text/plain"); err != nil { t.Fatalf("put: %v", err) } // Direkter Dateisystemzugriff, UMGEHT den Service/die Entschlüsselung // vollständig — simuliert "Rohspeicher ohne Schlüssel". raw, err := os.ReadFile(filepath.Join(dir, filepath.FromSlash(key))) if err != nil { t.Fatalf("rohdatei lesen: %v", err) } if bytes.Contains(raw, []byte(plaintext)) { t.Fatalf("klartext ist unverschlüsselt im rohspeicher auffindbar: %q enthält %q", raw, plaintext) } if bytes.Contains(raw, []byte("STRENG GEHEIMER")) || bytes.Contains(raw, []byte("GEHEIM")) { t.Fatal("erkennbare klartext-fragmente im rohspeicher gefunden") } } // TestGetDecrypted_WrongTenantKeyDeniesDecryption ist die geforderte // Pflichtprüfung 2: falscher Mandantenschlüssel verweigert // Entschlüsselung. func TestGetDecrypted_WrongTenantKeyDeniesDecryption(t *testing.T) { svc, _ := newTestServiceWithDir(t) ctx := context.Background() key := storage.ObjectKey("msg-enc-2", 0) plaintext := "inhalt fuer richtiger-tenant" if err := svc.Put(ctx, "richtiger-tenant", key, strings.NewReader(plaintext), "text/plain"); err != nil { t.Fatalf("put: %v", err) } // Korrekter Tenant kann entschlüsseln. got, err := svc.GetDecrypted(ctx, "richtiger-tenant", key) if err != nil { t.Fatalf("getdecrypted (richtiger tenant): %v", err) } if string(got) != plaintext { t.Fatalf("falscher inhalt: %q", got) } // FALSCHER Mandant (anderer Tenant-Slug -> anderer KEK) muss // abgelehnt werden. _, err = svc.GetDecrypted(ctx, "falscher-tenant", key) if !errors.Is(err, crypto.ErrDecryptFailed) { t.Fatalf("erwartet ErrDecryptFailed bei falschem mandantenschlüssel, habe: %v", err) } } // TestPut_AcceptableEncryptionOverhead ist die geforderte // Pflichtprüfung 3: Performance-Test bestätigt akzeptablen Overhead // durch Verschlüsselung. func TestPut_AcceptableEncryptionOverhead(t *testing.T) { svc, _ := newTestServiceWithDir(t) ctx := context.Background() // ~64 KiB Nutzlast, realistische Anhanggröße. payload := strings.Repeat("x", 64*1024) const count = 50 start := time.Now() for i := 0; i < count; i++ { key := storage.ObjectKey("msg-perf", i) if err := svc.Put(ctx, "richtiger-tenant", key, strings.NewReader(payload), "application/octet-stream"); err != nil { t.Fatalf("put #%d: %v", i, err) } } elapsed := time.Since(start) avgPerObject := elapsed / count // Großzügige Grenze (50ms je 64-KiB-Objekt inkl. AES-GCM, // Prüfsumme, Sidecar-Schreiben, Rücklese-Verifikation aus ARC-01) — // Ziel: Nachweis, dass Verschlüsselung keinen unverhältnismäßigen // Overhead verursacht, nicht ein knallhartes SLA. if avgPerObject > 50*time.Millisecond { t.Fatalf("erwartet akzeptablen verschlüsselungs-overhead (<50ms/64KiB-objekt), habe %v/objekt (gesamt %v für %d objekte)", avgPerObject, elapsed, count) } t.Logf("Verschlüsselungs-Performance: %d×64KiB-Objekte in %v (%v/Objekt)", count, elapsed, avgPerObject) }