145 lines
4.8 KiB
Go
145 lines
4.8 KiB
Go
package kek
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
var ErrNoTenantKEK = errors.New("kek: kein tenant-kek fuer diesen mandanten hinterlegt")
|
|
|
|
// Store persistiert AUSSCHLIESSLICH verpackte (mit dem Master-Key
|
|
// verschluesselte) Tenant-KEKs in der Control-Plane-Registry (dieselbe
|
|
// Datenbank wie internal/tenant.Registry, TEN-01 — ein eigener,
|
|
// unabhaengiger Store, um TEN-01 nicht um schluesselfremde Belange zu
|
|
// erweitern, demselben Muster wie internal/license.Store).
|
|
type Store struct {
|
|
pool *pgxpool.Pool
|
|
}
|
|
|
|
func NewStore(pool *pgxpool.Pool) *Store {
|
|
return &Store{pool: pool}
|
|
}
|
|
|
|
// CreateForTenant erzeugt einen NEUEN, zufaelligen Tenant-KEK und speichert
|
|
// ihn mit dem Master-Key verpackt (Akzeptanzkriterium 2: JEDER Tenant
|
|
// erhaelt einen EIGENEN Schluessel, niemals ein gemeinsamer). Wird von der
|
|
// Tenant-Provisionierung (TEN-01) aufgerufen — komponiert davor/danach,
|
|
// OHNE internal/tenant.Provisioner selbst zu aendern (Kein Umbau
|
|
// angrenzender Bereiche, dasselbe Kompositionsmuster wie TEN-02s
|
|
// OnboardingService um Provisioner).
|
|
func (s *Store) CreateForTenant(ctx context.Context, tenantID string, masterKey MasterKey) ([]byte, error) {
|
|
plainKEK, err := generateRandomKey()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
wrapped, err := wrap(masterKey, plainKEK)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("tenant-kek verpacken: %w", err)
|
|
}
|
|
|
|
if _, err := s.pool.Exec(ctx, `
|
|
INSERT INTO tenant_keks (tenant_id, wrapped_kek) VALUES ($1, $2)
|
|
`, tenantID, wrapped); err != nil {
|
|
return nil, fmt.Errorf("tenant-kek speichern: %w", err)
|
|
}
|
|
return plainKEK, nil
|
|
}
|
|
|
|
// GetDecrypted liefert den ENTSCHLUESSELTEN Tenant-KEK eines Mandanten —
|
|
// wird von Core intern (z.B. fuer den HTTP-Handler in handler.go) sowie in
|
|
// Tests verwendet.
|
|
func (s *Store) GetDecrypted(ctx context.Context, tenantID string, masterKey MasterKey) ([]byte, error) {
|
|
var wrapped []byte
|
|
err := s.pool.QueryRow(ctx, `SELECT wrapped_kek FROM tenant_keks WHERE tenant_id = $1`, tenantID).Scan(&wrapped)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, ErrNoTenantKEK
|
|
}
|
|
return nil, fmt.Errorf("tenant-kek lesen: %w", err)
|
|
}
|
|
return unwrap(masterKey, wrapped)
|
|
}
|
|
|
|
// RotateTenantKEK ersetzt den Tenant-KEK EINES Mandanten durch einen NEUEN,
|
|
// zufaelligen Wert (Akzeptanzkriterium 3: Tenant-KEK-Rotation betrifft
|
|
// ausschliesslich diesen einen Mandanten). Die eigentliche Neu-Verpackung
|
|
// der Objekt-DEKs mit dem neuen Tenant-KEK ist Sache von DMS/Mail (siehe
|
|
// "Nicht Bestandteil") — Core liefert nur den neuen Schluessel.
|
|
func (s *Store) RotateTenantKEK(ctx context.Context, tenantID string, masterKey MasterKey) ([]byte, error) {
|
|
newPlainKEK, err := generateRandomKey()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
wrapped, err := wrap(masterKey, newPlainKEK)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("neuen tenant-kek verpacken: %w", err)
|
|
}
|
|
|
|
tag, err := s.pool.Exec(ctx, `
|
|
UPDATE tenant_keks SET wrapped_kek = $2, rotated_at = now() WHERE tenant_id = $1
|
|
`, tenantID, wrapped)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("tenant-kek rotieren: %w", err)
|
|
}
|
|
if tag.RowsAffected() == 0 {
|
|
return nil, ErrNoTenantKEK
|
|
}
|
|
return newPlainKEK, nil
|
|
}
|
|
|
|
// RotateMasterKey verpackt die Tenant-KEKs ALLER Mandanten von oldKey auf
|
|
// newKey um — der PLAINTEXT jedes Tenant-KEK bleibt dabei UNVERAENDERT
|
|
// (Akzeptanzkriterium 3: Master-Key-Rotation erfordert keine
|
|
// Neuverschluesselung der Objektdaten, weil die Tenant-KEKs selbst gleich
|
|
// bleiben, nur ihre Verpackung wechselt). Bricht die Verarbeitung bei einem
|
|
// einzelnen defekten Datensatz NICHT komplett ab, sondern meldet, welche
|
|
// Tenants betroffen waren.
|
|
func (s *Store) RotateMasterKey(ctx context.Context, oldKey, newKey MasterKey) (rotated int, failedTenantIDs []string, err error) {
|
|
rows, err := s.pool.Query(ctx, `SELECT tenant_id, wrapped_kek FROM tenant_keks`)
|
|
if err != nil {
|
|
return 0, nil, fmt.Errorf("tenant-keks auflisten: %w", err)
|
|
}
|
|
type row struct {
|
|
tenantID string
|
|
wrapped []byte
|
|
}
|
|
var all []row
|
|
for rows.Next() {
|
|
var r row
|
|
if err := rows.Scan(&r.tenantID, &r.wrapped); err != nil {
|
|
rows.Close()
|
|
return 0, nil, fmt.Errorf("tenant-kek-zeile lesen: %w", err)
|
|
}
|
|
all = append(all, r)
|
|
}
|
|
rows.Close()
|
|
if err := rows.Err(); err != nil {
|
|
return 0, nil, err
|
|
}
|
|
|
|
for _, r := range all {
|
|
plainKEK, err := unwrap(oldKey, r.wrapped)
|
|
if err != nil {
|
|
failedTenantIDs = append(failedTenantIDs, r.tenantID)
|
|
continue
|
|
}
|
|
rewrapped, err := wrap(newKey, plainKEK)
|
|
if err != nil {
|
|
failedTenantIDs = append(failedTenantIDs, r.tenantID)
|
|
continue
|
|
}
|
|
if _, err := s.pool.Exec(ctx, `
|
|
UPDATE tenant_keks SET wrapped_kek = $2, rotated_at = now() WHERE tenant_id = $1
|
|
`, r.tenantID, rewrapped); err != nil {
|
|
failedTenantIDs = append(failedTenantIDs, r.tenantID)
|
|
continue
|
|
}
|
|
rotated++
|
|
}
|
|
return rotated, failedTenantIDs, nil
|
|
}
|