package services import ( "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "encoding/json" "fmt" "os" "strconv" "strings" "github.com/Fimeg/RedFlag/aggregator-server/internal/database/queries" "github.com/google/uuid" ) type SecuritySettingsService struct { settingsQueries *queries.SecuritySettingsQueries signingService *SigningService encryptionKey []byte } // NewSecuritySettingsService creates a new security settings service func NewSecuritySettingsService(settingsQueries *queries.SecuritySettingsQueries, signingService *SigningService) (*SecuritySettingsService, error) { // Get encryption key from environment or generate one keyStr := os.Getenv("REDFLAG_SETTINGS_ENCRYPTION_KEY") var key []byte var err error if keyStr != "" { key, err = base64.StdEncoding.DecodeString(keyStr) if err != nil { return nil, fmt.Errorf("invalid encryption key format: %w", err) } } else { // Generate a new key (in production, this should be persisted) key = make([]byte, 32) // AES-256 if _, err := rand.Read(key); err != nil { return nil, fmt.Errorf("failed to generate encryption key: %w", err) } } return &SecuritySettingsService{ settingsQueries: settingsQueries, signingService: signingService, encryptionKey: key, }, nil } // GetSetting retrieves a security setting with proper priority resolution func (s *SecuritySettingsService) GetSetting(category, key string) (interface{}, error) { // Priority 1: Environment variables if envValue := s.getEnvironmentValue(category, key); envValue != nil { return envValue, nil } // Priority 2: Config file values (this would be implemented based on your config structure) if configValue := s.getConfigValue(category, key); configValue != nil { return configValue, nil } // Priority 3: Database settings if dbSetting, err := s.settingsQueries.GetSetting(category, key); err == nil && dbSetting != nil { var value interface{} if dbSetting.IsEncrypted { decrypted, err := s.decrypt(dbSetting.Value) if err != nil { return nil, fmt.Errorf("failed to decrypt setting: %w", err) } if err := json.Unmarshal([]byte(decrypted), &value); err != nil { return nil, fmt.Errorf("failed to unmarshal decrypted setting: %w", err) } } else { if err := json.Unmarshal([]byte(dbSetting.Value), &value); err != nil { return nil, fmt.Errorf("failed to unmarshal setting: %w", err) } } return value, nil } // Priority 4: Hardcoded defaults if defaultValue := s.getDefaultValue(category, key); defaultValue != nil { return defaultValue, nil } return nil, fmt.Errorf("setting not found: %s.%s", category, key) } // SetSetting updates a security setting with validation and audit logging func (s *SecuritySettingsService) SetSetting(category, key string, value interface{}, userID uuid.UUID, reason string) error { // Validate the setting if err := s.ValidateSetting(category, key, value); err != nil { return fmt.Errorf("validation failed: %w", err) } // Check if setting is sensitive and should be encrypted isEncrypted := s.isSensitiveSetting(category, key) // Check if setting exists existing, err := s.settingsQueries.GetSetting(category, key) if err != nil { return fmt.Errorf("failed to check existing setting: %w", err) } var oldValue *string var settingID uuid.UUID if existing != nil { // Update existing setting updated, oldVal, err := s.settingsQueries.UpdateSetting(category, key, value, &userID) if err != nil { return fmt.Errorf("failed to update setting: %w", err) } oldValue = oldVal settingID = updated.ID } else { // Create new setting created, err := s.settingsQueries.CreateSetting(category, key, value, isEncrypted, &userID) if err != nil { return fmt.Errorf("failed to create setting: %w", err) } settingID = created.ID } // Create audit log valueJSON, _ := json.Marshal(value) if err := s.settingsQueries.CreateAuditLog( settingID, userID, "update", stringOrNil(oldValue), string(valueJSON), reason, ); err != nil { // Log error but don't fail the operation fmt.Printf("Warning: failed to create audit log: %v\n", err) } return nil } // GetAllSettings retrieves all security settings organized by category func (s *SecuritySettingsService) GetAllSettings() (map[string]map[string]interface{}, error) { // Get all default values first result := s.getDefaultSettings() // Override with database settings dbSettings, err := s.settingsQueries.GetAllSettings() if err != nil { return nil, fmt.Errorf("failed to get database settings: %w", err) } for _, setting := range dbSettings { var value interface{} if setting.IsEncrypted { decrypted, err := s.decrypt(setting.Value) if err != nil { return nil, fmt.Errorf("failed to decrypt setting %s.%s: %w", setting.Category, setting.Key, err) } if err := json.Unmarshal([]byte(decrypted), &value); err != nil { return nil, fmt.Errorf("failed to unmarshal decrypted setting %s.%s: %w", setting.Category, setting.Key, err) } } else { if err := json.Unmarshal([]byte(setting.Value), &value); err != nil { return nil, fmt.Errorf("failed to unmarshal setting %s.%s: %w", setting.Category, setting.Key, err) } } if result[setting.Category] == nil { result[setting.Category] = make(map[string]interface{}) } result[setting.Category][setting.Key] = value } // Override with config file settings for category, settings := range result { for key := range settings { if configValue := s.getConfigValue(category, key); configValue != nil { result[category][key] = configValue } } } // Override with environment variables for category, settings := range result { for key := range settings { if envValue := s.getEnvironmentValue(category, key); envValue != nil { result[category][key] = envValue } } } return result, nil } // GetSettingsByCategory retrieves all settings for a specific category func (s *SecuritySettingsService) GetSettingsByCategory(category string) (map[string]interface{}, error) { allSettings, err := s.GetAllSettings() if err != nil { return nil, err } if categorySettings, exists := allSettings[category]; exists { return categorySettings, nil } return nil, fmt.Errorf("category not found: %s", category) } // ValidateSetting validates a security setting value func (s *SecuritySettingsService) ValidateSetting(category, key string, value interface{}) error { switch fmt.Sprintf("%s.%s", category, key) { case "nonce_validation.timeout_seconds": if timeout, ok := value.(float64); ok { if timeout < 60 || timeout > 3600 { return fmt.Errorf("nonce timeout must be between 60 and 3600 seconds") } } else { return fmt.Errorf("nonce timeout must be a number") } case "command_signing.enforcement_mode", "update_signing.enforcement_mode", "machine_binding.enforcement_mode": if mode, ok := value.(string); ok { validModes := []string{"strict", "warning", "disabled"} valid := false for _, m := range validModes { if mode == m { valid = true break } } if !valid { return fmt.Errorf("enforcement mode must be one of: strict, warning, disabled") } } else { return fmt.Errorf("enforcement mode must be a string") } case "signature_verification.log_retention_days": if days, ok := value.(float64); ok { if days < 1 || days > 365 { return fmt.Errorf("log retention must be between 1 and 365 days") } } else { return fmt.Errorf("log retention must be a number") } case "command_signing.algorithm", "update_signing.algorithm": if algo, ok := value.(string); ok { if algo != "ed25519" { return fmt.Errorf("only ed25519 algorithm is currently supported") } } else { return fmt.Errorf("algorithm must be a string") } } return nil } // InitializeDefaultSettings creates default settings in the database if they don't exist func (s *SecuritySettingsService) InitializeDefaultSettings() error { defaults := s.getDefaultSettings() for category, settings := range defaults { for key, value := range settings { existing, err := s.settingsQueries.GetSetting(category, key) if err != nil { return fmt.Errorf("failed to check existing setting %s.%s: %w", category, key, err) } if existing == nil { isEncrypted := s.isSensitiveSetting(category, key) _, err := s.settingsQueries.CreateSetting(category, key, value, isEncrypted, nil) if err != nil { return fmt.Errorf("failed to create default setting %s.%s: %w", category, key, err) } } } } return nil } // Helper methods func (s *SecuritySettingsService) getDefaultSettings() map[string]map[string]interface{} { return map[string]map[string]interface{}{ "command_signing": { "enabled": true, "enforcement_mode": "strict", "algorithm": "ed25519", }, "update_signing": { "enabled": true, "enforcement_mode": "strict", "allow_unsigned": false, }, "nonce_validation": { "timeout_seconds": 600, "reject_expired": true, "log_expired_attempts": true, }, "machine_binding": { "enabled": true, "enforcement_mode": "strict", "strict_action": "reject", }, "signature_verification": { "log_level": "warn", "log_retention_days": 30, "log_failures": true, "alert_on_failure": true, }, } } func (s *SecuritySettingsService) getDefaultValue(category, key string) interface{} { defaults := s.getDefaultSettings() if cat, exists := defaults[category]; exists { if value, exists := cat[key]; exists { return value } } return nil } func (s *SecuritySettingsService) getEnvironmentValue(category, key string) interface{} { envKey := fmt.Sprintf("REDFLAG_%s_%s", strings.ToUpper(category), strings.ToUpper(key)) envValue := os.Getenv(envKey) if envValue == "" { return nil } // Try to parse as boolean if strings.ToLower(envValue) == "true" { return true } if strings.ToLower(envValue) == "false" { return false } // Try to parse as number if num, err := strconv.ParseFloat(envValue, 64); err == nil { return num } // Return as string return envValue } func (s *SecuritySettingsService) getConfigValue(category, key string) interface{} { // This would be implemented based on your config structure // For now, returning nil to prioritize env vars and database return nil } func (s *SecuritySettingsService) isSensitiveSetting(category, key string) bool { // Define which settings are sensitive and should be encrypted sensitive := map[string]bool{ "command_signing.private_key": true, "update_signing.private_key": true, "machine_binding.server_key": true, "encryption.master_key": true, } settingKey := fmt.Sprintf("%s.%s", category, key) return sensitive[settingKey] } func (s *SecuritySettingsService) encrypt(value string) (string, error) { block, err := aes.NewCipher(s.encryptionKey) if err != nil { return "", err } gcm, err := cipher.NewGCM(block) if err != nil { return "", err } nonce := make([]byte, gcm.NonceSize()) if _, err := rand.Read(nonce); err != nil { return "", err } ciphertext := gcm.Seal(nonce, nonce, []byte(value), nil) return base64.StdEncoding.EncodeToString(ciphertext), nil } func (s *SecuritySettingsService) decrypt(encryptedValue string) (string, error) { data, err := base64.StdEncoding.DecodeString(encryptedValue) if err != nil { return "", err } block, err := aes.NewCipher(s.encryptionKey) if err != nil { return "", err } gcm, err := cipher.NewGCM(block) if err != nil { return "", err } nonceSize := gcm.NonceSize() if len(data) < nonceSize { return "", fmt.Errorf("ciphertext too short") } nonce, ciphertext := data[:nonceSize], data[nonceSize:] plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) if err != nil { return "", err } return string(plaintext), nil } func stringOrNil(s *string) string { if s == nil { return "" } return *s } // GetNonceTimeout returns the current nonce validation timeout in seconds func (s *SecuritySettingsService) GetNonceTimeout() (int, error) { value, err := s.GetSetting("nonce_validation", "timeout_seconds") if err != nil { return 600, err // Return default on error } if timeout, ok := value.(float64); ok { return int(timeout), nil } return 600, nil // Return default if type is wrong } // GetEnforcementMode returns the enforcement mode for a given category func (s *SecuritySettingsService) GetEnforcementMode(category string) (string, error) { value, err := s.GetSetting(category, "enforcement_mode") if err != nil { return "strict", err // Return default on error } if mode, ok := value.(string); ok { return mode, nil } return "strict", nil // Return default if type is wrong } // IsSignatureVerificationEnabled returns whether signature verification is enabled for a category func (s *SecuritySettingsService) IsSignatureVerificationEnabled(category string) (bool, error) { value, err := s.GetSetting(category, "enabled") if err != nil { return true, err // Return default on error } if enabled, ok := value.(bool); ok { return enabled, nil } return true, nil // Return default if type is wrong }