mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-09-08 23:50:48 +02:00
481 lines
14 KiB
Go
481 lines
14 KiB
Go
package keyrotation
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"reflect"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/starfleetcptn/gomft/internal/encryption"
|
|
"github.com/starfleetcptn/gomft/internal/encryption/audit"
|
|
"github.com/starfleetcptn/gomft/internal/encryption/rotationmodel"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// For backward compatibility
|
|
type RotationOptions = rotationmodel.RotationOptions
|
|
|
|
// RotationUtility provides comprehensive capabilities for rotating encryption keys
|
|
// across multiple database models with detailed auditing and progress tracking
|
|
type RotationUtility struct {
|
|
db *gorm.DB
|
|
oldService *encryption.EncryptionService
|
|
newService *encryption.EncryptionService
|
|
auditor *audit.SecurityAuditor
|
|
testingHooks map[string]func(interface{}) error
|
|
mu sync.Mutex
|
|
options RotationOptions
|
|
}
|
|
|
|
// NewRotationUtility creates a new RotationUtility
|
|
func NewRotationUtility(
|
|
db *gorm.DB,
|
|
oldService, newService *encryption.EncryptionService,
|
|
auditor *audit.SecurityAuditor,
|
|
options RotationOptions,
|
|
) (*RotationUtility, error) {
|
|
if db == nil {
|
|
return nil, fmt.Errorf("database connection is required")
|
|
}
|
|
|
|
if oldService == nil {
|
|
return nil, fmt.Errorf("old encryption service is required")
|
|
}
|
|
|
|
if newService == nil {
|
|
return nil, fmt.Errorf("new encryption service is required")
|
|
}
|
|
|
|
if auditor == nil {
|
|
auditor = audit.GetGlobalAuditor()
|
|
}
|
|
|
|
// Set default options
|
|
if options.BatchSize <= 0 {
|
|
options.BatchSize = 100
|
|
}
|
|
|
|
if options.MaxErrors <= 0 {
|
|
options.MaxErrors = 50
|
|
}
|
|
|
|
if options.Parallelism <= 0 {
|
|
options.Parallelism = 1
|
|
}
|
|
|
|
if options.Timeout <= 0 {
|
|
options.Timeout = 24 * time.Hour // Default long timeout
|
|
}
|
|
|
|
if options.WorkerTimeout <= 0 {
|
|
options.WorkerTimeout = 30 * time.Minute
|
|
}
|
|
|
|
return &RotationUtility{
|
|
db: db,
|
|
oldService: oldService,
|
|
newService: newService,
|
|
auditor: auditor,
|
|
options: options,
|
|
testingHooks: make(map[string]func(interface{}) error),
|
|
}, nil
|
|
}
|
|
|
|
// RegisterTestingHook registers a hook for testing purposes
|
|
func (r *RotationUtility) RegisterTestingHook(name string, hook func(interface{}) error) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.testingHooks[name] = hook
|
|
}
|
|
|
|
// runHook runs a testing hook if it exists
|
|
func (r *RotationUtility) runHook(name string, data interface{}) error {
|
|
r.mu.Lock()
|
|
hook, exists := r.testingHooks[name]
|
|
r.mu.Unlock()
|
|
|
|
if exists && hook != nil {
|
|
return hook(data)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RotateKeysForModels performs key rotation for multiple model types with detailed monitoring
|
|
func (r *RotationUtility) RotateKeysForModels(ctx context.Context, models []interface{}) (*RotationStats, error) {
|
|
// Create master context with timeout
|
|
masterCtx, cancel := context.WithTimeout(ctx, r.options.Timeout)
|
|
defer cancel()
|
|
|
|
// Track overall stats
|
|
overallStats := &RotationStats{
|
|
StartTime: time.Now(),
|
|
Errors: make([]string, 0),
|
|
}
|
|
|
|
// Create key rotator
|
|
rotator, err := NewKeyRotator(r.db, r.oldService, r.newService, r.auditor)
|
|
if err != nil {
|
|
return overallStats, fmt.Errorf("failed to create key rotator: %w", err)
|
|
}
|
|
|
|
// Apply options
|
|
rotator.SetDryRun(r.options.DryRun)
|
|
rotator.SetBatchSize(r.options.BatchSize)
|
|
rotator.SetMaxErrors(r.options.MaxErrors)
|
|
|
|
// Log the start of rotation
|
|
r.auditor.LogKeyRotationEventWithDescription(
|
|
"starting",
|
|
"pending",
|
|
true,
|
|
fmt.Sprintf("Starting key rotation for %d model types (dry run: %v)", len(models), r.options.DryRun),
|
|
0,
|
|
)
|
|
|
|
// Process all models (sequentially)
|
|
for _, model := range models {
|
|
// Check if context is canceled
|
|
select {
|
|
case <-masterCtx.Done():
|
|
overallStats.Errors = append(overallStats.Errors, fmt.Sprintf("key rotation aborted: %v", masterCtx.Err()))
|
|
return overallStats, masterCtx.Err()
|
|
default:
|
|
// Continue processing
|
|
}
|
|
|
|
// Get model type info
|
|
modelType := reflect.TypeOf(model)
|
|
if modelType.Kind() == reflect.Ptr {
|
|
modelType = modelType.Elem()
|
|
}
|
|
modelName := modelType.Name()
|
|
|
|
// Run pre-rotation hook if any
|
|
if err := r.runHook("pre_rotation_"+modelName, model); err != nil {
|
|
overallStats.Errors = append(overallStats.Errors, fmt.Sprintf("pre-rotation hook failed for %s: %v", modelName, err))
|
|
continue
|
|
}
|
|
|
|
// Log model rotation start
|
|
r.auditor.LogKeyRotationEventWithDescription(
|
|
"starting",
|
|
"pending",
|
|
true,
|
|
fmt.Sprintf("Starting key rotation for model: %s", modelName),
|
|
0,
|
|
)
|
|
|
|
// Create a worker context with timeout
|
|
workerCtx, workerCancel := context.WithTimeout(masterCtx, r.options.WorkerTimeout)
|
|
|
|
// Create a goroutine to handle timeouts
|
|
rotationDone := make(chan struct{})
|
|
var modelStats *RotationStats
|
|
var rotationErr error
|
|
|
|
go func() {
|
|
// Perform the actual rotation
|
|
modelStats, rotationErr = rotator.RotateKeys(model, "")
|
|
close(rotationDone)
|
|
}()
|
|
|
|
// Wait for rotation to complete or timeout
|
|
select {
|
|
case <-workerCtx.Done():
|
|
if workerCtx.Err() == context.DeadlineExceeded {
|
|
errorMsg := fmt.Sprintf("key rotation for model %s timed out after %v", modelName, r.options.WorkerTimeout)
|
|
overallStats.Errors = append(overallStats.Errors, errorMsg)
|
|
|
|
// Log timeout error
|
|
r.auditor.LogKeyRotationEventWithDescription(
|
|
"old",
|
|
"new",
|
|
false,
|
|
errorMsg,
|
|
0,
|
|
)
|
|
}
|
|
case <-rotationDone:
|
|
// Rotation completed
|
|
}
|
|
|
|
// Clean up the worker context
|
|
workerCancel()
|
|
|
|
// Check for rotation errors
|
|
if rotationErr != nil {
|
|
overallStats.Errors = append(overallStats.Errors, fmt.Sprintf("failed to rotate keys for %s: %v", modelName, rotationErr))
|
|
|
|
// Log rotation error
|
|
r.auditor.LogKeyRotationEventWithDescription(
|
|
"old",
|
|
"new",
|
|
false,
|
|
fmt.Sprintf("Key rotation failed for model %s: %v", modelName, rotationErr),
|
|
0,
|
|
)
|
|
|
|
continue
|
|
}
|
|
|
|
// Update overall stats
|
|
if modelStats != nil {
|
|
overallStats.TotalRecords += modelStats.TotalRecords
|
|
overallStats.ProcessedRecords += modelStats.ProcessedRecords
|
|
overallStats.SkippedRecords += modelStats.SkippedRecords
|
|
overallStats.FailedRecords += modelStats.FailedRecords
|
|
overallStats.Errors = append(overallStats.Errors, modelStats.Errors...)
|
|
|
|
// Call progress callback if set
|
|
if r.options.ProgressCallback != nil {
|
|
r.options.ProgressCallback(modelName, modelStats.ProcessedRecords, modelStats.TotalRecords)
|
|
}
|
|
|
|
// Log progress
|
|
successRate := 0.0
|
|
if modelStats.TotalRecords > 0 {
|
|
successRate = float64(modelStats.ProcessedRecords) / float64(modelStats.TotalRecords) * 100
|
|
}
|
|
|
|
r.auditor.LogKeyRotationEventWithDescription(
|
|
"old",
|
|
"new",
|
|
true,
|
|
fmt.Sprintf("Completed key rotation for model %s: %d/%d records (%.1f%%) processed, %d skipped, %d failed",
|
|
modelName, modelStats.ProcessedRecords, modelStats.TotalRecords, successRate,
|
|
modelStats.SkippedRecords, modelStats.FailedRecords),
|
|
0,
|
|
)
|
|
}
|
|
|
|
// Run post-rotation hook if any
|
|
if err := r.runHook("post_rotation_"+modelName, model); err != nil {
|
|
overallStats.Errors = append(overallStats.Errors, fmt.Sprintf("post-rotation hook failed for %s: %v", modelName, err))
|
|
}
|
|
}
|
|
|
|
// Complete overall stats
|
|
overallStats.EndTime = time.Now()
|
|
overallStats.ElapsedTime = overallStats.EndTime.Sub(overallStats.StartTime)
|
|
|
|
// Calculate overall success rate
|
|
successRate := 0.0
|
|
if overallStats.TotalRecords > 0 {
|
|
successRate = float64(overallStats.ProcessedRecords) / float64(overallStats.TotalRecords) * 100
|
|
}
|
|
|
|
// Log completion
|
|
r.auditor.LogKeyRotationEventWithDescription(
|
|
"old",
|
|
"new",
|
|
len(overallStats.Errors) == 0,
|
|
fmt.Sprintf("Completed key rotation for all models: %d/%d records (%.1f%%) processed, %d skipped, %d failed, %d errors in %s",
|
|
overallStats.ProcessedRecords, overallStats.TotalRecords, successRate,
|
|
overallStats.SkippedRecords, overallStats.FailedRecords, len(overallStats.Errors),
|
|
overallStats.ElapsedTime),
|
|
0,
|
|
)
|
|
|
|
return overallStats, nil
|
|
}
|
|
|
|
// FindModelsWithEncryptedFields automatically finds all database models with encrypted fields
|
|
func (r *RotationUtility) FindModelsWithEncryptedFields() ([]interface{}, error) {
|
|
// This is a placeholder - in a real implementation, we would scan the codebase
|
|
// or database schema to automatically detect models with encrypted fields
|
|
// Since that requires knowledge of the codebase structure, this would be
|
|
// customized for the specific application
|
|
|
|
return []interface{}{}, fmt.Errorf("automatic model detection not implemented, provide models explicitly")
|
|
}
|
|
|
|
// ValidateRotation tests the key rotation on sample records without saving changes
|
|
func (r *RotationUtility) ValidateRotation(models []interface{}) (map[string]bool, error) {
|
|
results := make(map[string]bool)
|
|
|
|
// Save current options to restore later
|
|
originalDryRun := r.options.DryRun
|
|
originalBatchSize := r.options.BatchSize
|
|
|
|
// Set temporary options for validation
|
|
r.options.DryRun = true
|
|
r.options.BatchSize = 10 // Test with small batch
|
|
|
|
// Create a context with short timeout
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
|
defer cancel()
|
|
|
|
// Run rotation with dry run mode
|
|
stats, err := r.RotateKeysForModels(ctx, models)
|
|
|
|
// Restore original options
|
|
r.options.DryRun = originalDryRun
|
|
r.options.BatchSize = originalBatchSize
|
|
|
|
if err != nil {
|
|
return results, fmt.Errorf("validation failed: %w", err)
|
|
}
|
|
|
|
// Process results for each model
|
|
for _, model := range models {
|
|
modelType := reflect.TypeOf(model)
|
|
if modelType.Kind() == reflect.Ptr {
|
|
modelType = modelType.Elem()
|
|
}
|
|
modelName := modelType.Name()
|
|
|
|
// Check if there were errors for this model
|
|
hasModelErrors := false
|
|
for _, errMsg := range stats.Errors {
|
|
if strings.Contains(errMsg, modelName) {
|
|
hasModelErrors = true
|
|
break
|
|
}
|
|
}
|
|
|
|
results[modelName] = !hasModelErrors
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
// CreateEncryptionMigrationPlan creates a detailed plan for migrating data to a new encryption key
|
|
func (r *RotationUtility) CreateEncryptionMigrationPlan(models []interface{}) (*EncryptionMigrationPlan, error) {
|
|
plan := &EncryptionMigrationPlan{
|
|
ModelPlans: make(map[string]*ModelMigrationPlan),
|
|
EstimatedDuration: 0,
|
|
EstimatedRecords: 0,
|
|
RecommendedOptions: r.options, // Start with current options
|
|
}
|
|
|
|
// Calculate record counts for each model
|
|
totalRecords := 0
|
|
for _, model := range models {
|
|
modelType := reflect.TypeOf(model)
|
|
if modelType.Kind() == reflect.Ptr {
|
|
modelType = modelType.Elem()
|
|
}
|
|
modelName := modelType.Name()
|
|
|
|
// Get record count
|
|
var count int64
|
|
if err := r.db.Model(model).Count(&count).Error; err != nil {
|
|
return nil, fmt.Errorf("failed to count records for %s: %w", modelName, err)
|
|
}
|
|
|
|
encryptedFields := r.identifyEncryptedFields(model)
|
|
|
|
// Create model plan
|
|
modelPlan := &ModelMigrationPlan{
|
|
ModelName: modelName,
|
|
RecordCount: int(count),
|
|
EstimatedTime: r.estimateMigrationTime(int(count), len(encryptedFields)),
|
|
EncryptedFields: encryptedFields,
|
|
BatchSizeRec: r.calculateOptimalBatchSize(int(count)),
|
|
}
|
|
|
|
plan.ModelPlans[modelName] = modelPlan
|
|
totalRecords += int(count)
|
|
plan.EstimatedDuration += modelPlan.EstimatedTime
|
|
}
|
|
|
|
plan.EstimatedRecords = totalRecords
|
|
|
|
// Calculate optimal batch size and parallelism based on total record count
|
|
plan.RecommendedOptions.BatchSize = r.calculateOptimalBatchSize(totalRecords)
|
|
plan.RecommendedOptions.Parallelism = r.calculateOptimalParallelism(totalRecords)
|
|
|
|
return plan, nil
|
|
}
|
|
|
|
// identifyEncryptedFields finds all encrypted fields in a model
|
|
func (r *RotationUtility) identifyEncryptedFields(model interface{}) []string {
|
|
fields := []string{}
|
|
|
|
// Get model value and type
|
|
modelType := reflect.TypeOf(model)
|
|
if modelType.Kind() == reflect.Ptr {
|
|
modelType = modelType.Elem()
|
|
}
|
|
|
|
// Skip if not a struct
|
|
if modelType.Kind() != reflect.Struct {
|
|
return fields
|
|
}
|
|
|
|
// Scan all fields for encrypted ones
|
|
for i := 0; i < modelType.NumField(); i++ {
|
|
field := modelType.Field(i)
|
|
|
|
// Look for fields starting with "Encrypted"
|
|
if strings.HasPrefix(field.Name, "Encrypted") && field.Type.Kind() == reflect.String {
|
|
fields = append(fields, field.Name)
|
|
}
|
|
}
|
|
|
|
return fields
|
|
}
|
|
|
|
// calculateOptimalBatchSize determines the optimal batch size based on record count
|
|
func (r *RotationUtility) calculateOptimalBatchSize(recordCount int) int {
|
|
// This is a simplistic approach - in a real system, this would be based on
|
|
// benchmarking and system characteristics
|
|
if recordCount < 1000 {
|
|
return 100
|
|
} else if recordCount < 10000 {
|
|
return 250
|
|
} else if recordCount < 100000 {
|
|
return 500
|
|
} else {
|
|
return 1000
|
|
}
|
|
}
|
|
|
|
// calculateOptimalParallelism determines the optimal parallelism level
|
|
func (r *RotationUtility) calculateOptimalParallelism(recordCount int) int {
|
|
// Simple heuristic - adjust based on actual system performance
|
|
cpuCount := runtime.NumCPU()
|
|
|
|
if recordCount < 10000 {
|
|
return 1
|
|
} else if recordCount < 100000 {
|
|
return min(2, cpuCount)
|
|
} else {
|
|
return min(4, cpuCount)
|
|
}
|
|
}
|
|
|
|
// min returns the minimum of two integers
|
|
func min(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
// estimateMigrationTime provides a rough estimate of time needed for migration
|
|
func (r *RotationUtility) estimateMigrationTime(recordCount, fieldCount int) time.Duration {
|
|
// This is a very rough estimate - in a real system, this would be based on
|
|
// benchmarking results and system characteristics
|
|
|
|
// Assume roughly 10ms per record per field
|
|
msPerRecordField := 10
|
|
|
|
// Calculate total time in milliseconds
|
|
totalTimeMs := recordCount * fieldCount * msPerRecordField
|
|
|
|
// Add overhead
|
|
totalTimeMs = int(float64(totalTimeMs) * 1.2) // 20% overhead
|
|
|
|
return time.Duration(totalTimeMs) * time.Millisecond
|
|
}
|
|
|
|
// For backward compatibility
|
|
type EncryptionMigrationPlan = rotationmodel.EncryptionMigrationPlan
|
|
|
|
// For backward compatibility
|
|
type ModelMigrationPlan = rotationmodel.ModelMigrationPlan
|