Files
mev-beta/internal/secure/config_manager.go
2025-09-14 06:21:10 -05:00

292 lines
7.7 KiB
Go

package secure
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"io"
"os"
"strings"
"github.com/fraktal/mev-beta/internal/logger"
)
// ConfigManager handles secure configuration management
type ConfigManager struct {
logger *logger.Logger
aesGCM cipher.AEAD
key []byte
}
// NewConfigManager creates a new secure configuration manager
func NewConfigManager(logger *logger.Logger) (*ConfigManager, error) {
// Get encryption key from environment or generate one
keyStr := os.Getenv("MEV_BOT_CONFIG_KEY")
if keyStr == "" {
return nil, errors.New("MEV_BOT_CONFIG_KEY environment variable not set")
}
// Create SHA-256 hash of the key for AES-256
key := sha256.Sum256([]byte(keyStr))
// Create AES cipher
block, err := aes.NewCipher(key[:])
if err != nil {
return nil, fmt.Errorf("failed to create AES cipher: %w", err)
}
// Create GCM mode
aesGCM, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("failed to create GCM mode: %w", err)
}
return &ConfigManager{
logger: logger,
aesGCM: aesGCM,
key: key[:],
}, nil
}
// EncryptValue encrypts a configuration value
func (cm *ConfigManager) EncryptValue(plaintext string) (string, error) {
// Create a random nonce
nonce := make([]byte, cm.aesGCM.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", fmt.Errorf("failed to generate nonce: %w", err)
}
// Encrypt the plaintext
ciphertext := cm.aesGCM.Seal(nonce, nonce, []byte(plaintext), nil)
// Encode to base64 for storage
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// DecryptValue decrypts a configuration value
func (cm *ConfigManager) DecryptValue(ciphertext string) (string, error) {
// Decode from base64
data, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return "", fmt.Errorf("failed to decode base64: %w", err)
}
// Check minimum length (nonce size)
nonceSize := cm.aesGCM.NonceSize()
if len(data) < nonceSize {
return "", errors.New("ciphertext too short")
}
// Extract nonce and ciphertext
nonce, ciphertext_bytes := data[:nonceSize], data[nonceSize:]
// Decrypt
plaintext, err := cm.aesGCM.Open(nil, nonce, ciphertext_bytes, nil)
if err != nil {
return "", fmt.Errorf("failed to decrypt: %w", err)
}
return string(plaintext), nil
}
// GetSecureValue gets a secure value from environment with fallback to encrypted storage
func (cm *ConfigManager) GetSecureValue(key string) (string, error) {
// First try environment variable
if value := os.Getenv(key); value != "" {
return value, nil
}
// Try encrypted environment variable
encryptedKey := key + "_ENCRYPTED"
if encryptedValue := os.Getenv(encryptedKey); encryptedValue != "" {
return cm.DecryptValue(encryptedValue)
}
return "", fmt.Errorf("secure value not found for key: %s", key)
}
// SecureConfig holds encrypted configuration values
type SecureConfig struct {
manager *ConfigManager
values map[string]string
}
// NewSecureConfig creates a new secure configuration
func NewSecureConfig(manager *ConfigManager) *SecureConfig {
return &SecureConfig{
manager: manager,
values: make(map[string]string),
}
}
// Set stores a value securely
func (sc *SecureConfig) Set(key, value string) error {
encrypted, err := sc.manager.EncryptValue(value)
if err != nil {
return fmt.Errorf("failed to encrypt value for key %s: %w", key, err)
}
sc.values[key] = encrypted
return nil
}
// Get retrieves a value securely
func (sc *SecureConfig) Get(key string) (string, error) {
// Check local encrypted storage first
if encrypted, exists := sc.values[key]; exists {
return sc.manager.DecryptValue(encrypted)
}
// Fallback to secure environment lookup
return sc.manager.GetSecureValue(key)
}
// GetRequired retrieves a required value, returning error if not found
func (sc *SecureConfig) GetRequired(key string) (string, error) {
value, err := sc.Get(key)
if err != nil {
return "", fmt.Errorf("required configuration value missing: %s", key)
}
if strings.TrimSpace(value) == "" {
return "", fmt.Errorf("required configuration value empty: %s", key)
}
return value, nil
}
// GetWithDefault retrieves a value with a default fallback
func (sc *SecureConfig) GetWithDefault(key, defaultValue string) string {
value, err := sc.Get(key)
if err != nil {
return defaultValue
}
return value
}
// LoadFromEnvironment loads configuration from environment variables
func (sc *SecureConfig) LoadFromEnvironment(keys []string) error {
for _, key := range keys {
value, err := sc.manager.GetSecureValue(key)
if err != nil {
sc.manager.logger.Warn(fmt.Sprintf("Could not load secure config for %s: %v", key, err))
continue
}
// Store encrypted in memory
if err := sc.Set(key, value); err != nil {
return fmt.Errorf("failed to store secure config for %s: %w", key, err)
}
}
return nil
}
// Clear removes all stored values from memory
func (sc *SecureConfig) Clear() {
// Zero out the map entries before clearing
for key := range sc.values {
// Overwrite with zeros
sc.values[key] = strings.Repeat("0", len(sc.values[key]))
delete(sc.values, key)
}
}
// Validate checks that all required configuration is present
func (sc *SecureConfig) Validate(requiredKeys []string) error {
var missingKeys []string
for _, key := range requiredKeys {
if _, err := sc.GetRequired(key); err != nil {
missingKeys = append(missingKeys, key)
}
}
if len(missingKeys) > 0 {
return fmt.Errorf("missing required configuration keys: %s", strings.Join(missingKeys, ", "))
}
return nil
}
// GenerateConfigKey generates a new encryption key for configuration
func GenerateConfigKey() (string, error) {
key := make([]byte, 32) // 256-bit key
if _, err := rand.Read(key); err != nil {
return "", fmt.Errorf("failed to generate random key: %w", err)
}
return base64.StdEncoding.EncodeToString(key), nil
}
// ConfigValidator provides validation utilities
type ConfigValidator struct {
logger *logger.Logger
}
// NewConfigValidator creates a new configuration validator
func NewConfigValidator(logger *logger.Logger) *ConfigValidator {
return &ConfigValidator{
logger: logger,
}
}
// ValidateURL validates that a URL is properly formatted and uses HTTPS
func (cv *ConfigValidator) ValidateURL(url string) error {
if url == "" {
return errors.New("URL cannot be empty")
}
if !strings.HasPrefix(url, "https://") && !strings.HasPrefix(url, "wss://") {
return errors.New("URL must use HTTPS or WSS protocol")
}
// Additional validation could go here (DNS lookup, connection test, etc.)
return nil
}
// ValidateAPIKey validates that an API key meets minimum security requirements
func (cv *ConfigValidator) ValidateAPIKey(key string) error {
if key == "" {
return errors.New("API key cannot be empty")
}
if len(key) < 32 {
return errors.New("API key must be at least 32 characters")
}
// Check for basic entropy (not all same character, contains mixed case, etc.)
if strings.Count(key, string(key[0])) == len(key) {
return errors.New("API key lacks sufficient entropy")
}
return nil
}
// ValidateAddress validates an Ethereum address
func (cv *ConfigValidator) ValidateAddress(address string) error {
if address == "" {
return errors.New("address cannot be empty")
}
if !strings.HasPrefix(address, "0x") {
return errors.New("address must start with 0x")
}
if len(address) != 42 { // 0x + 40 hex chars
return errors.New("address must be 42 characters long")
}
// Validate hex format
for i, char := range address[2:] {
if !((char >= '0' && char <= '9') || (char >= 'a' && char <= 'f') || (char >= 'A' && char <= 'F')) {
return fmt.Errorf("invalid hex character at position %d: %c", i+2, char)
}
}
return nil
}