// Package secureconfig provides secure access to configuration secrets. // It wraps sensitive configuration values in memguard-protected buffers // to prevent secret leakage through memory dumps. package secureconfig import ( "sync" "github.com/awnumar/memguard" "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config" ) // SecureConfigProvider provides secure access to configuration secrets. // Secrets are stored in memguard LockedBuffers and wiped when no longer needed. type SecureConfigProvider struct { mu sync.RWMutex // Cached secure buffers - created on first access jwtSecret *memguard.LockedBuffer dbPassword *memguard.LockedBuffer cachePassword *memguard.LockedBuffer s3AccessKey *memguard.LockedBuffer s3SecretKey *memguard.LockedBuffer mailgunAPIKey *memguard.LockedBuffer // Original config for initial loading cfg *config.Config } // NewSecureConfigProvider creates a new secure config provider from the given config. // The original config secrets are copied to secure buffers and should be cleared // from the original config after this call. func NewSecureConfigProvider(cfg *config.Config) *SecureConfigProvider { provider := &SecureConfigProvider{ cfg: cfg, } // Pre-load secrets into secure buffers provider.loadSecrets() return provider } // loadSecrets copies secrets from config into memguard buffers. // SECURITY: Original config strings remain in memory but secure buffers provide // additional protection for long-lived secret access. func (p *SecureConfigProvider) loadSecrets() { p.mu.Lock() defer p.mu.Unlock() // JWT Secret if p.cfg.JWT.Secret != "" { p.jwtSecret = memguard.NewBufferFromBytes([]byte(p.cfg.JWT.Secret)) } // Database Password if p.cfg.Database.Password != "" { p.dbPassword = memguard.NewBufferFromBytes([]byte(p.cfg.Database.Password)) } // Cache Password if p.cfg.Cache.Password != "" { p.cachePassword = memguard.NewBufferFromBytes([]byte(p.cfg.Cache.Password)) } // S3 Access Key if p.cfg.S3.AccessKey != "" { p.s3AccessKey = memguard.NewBufferFromBytes([]byte(p.cfg.S3.AccessKey)) } // S3 Secret Key if p.cfg.S3.SecretKey != "" { p.s3SecretKey = memguard.NewBufferFromBytes([]byte(p.cfg.S3.SecretKey)) } // Mailgun API Key if p.cfg.Mailgun.APIKey != "" { p.mailgunAPIKey = memguard.NewBufferFromBytes([]byte(p.cfg.Mailgun.APIKey)) } } // JWTSecret returns the JWT secret as a secure byte slice. // The returned bytes should not be stored - use immediately and let GC collect. func (p *SecureConfigProvider) JWTSecret() []byte { p.mu.RLock() defer p.mu.RUnlock() if p.jwtSecret == nil || !p.jwtSecret.IsAlive() { return nil } return p.jwtSecret.Bytes() } // DatabasePassword returns the database password as a secure byte slice. func (p *SecureConfigProvider) DatabasePassword() []byte { p.mu.RLock() defer p.mu.RUnlock() if p.dbPassword == nil || !p.dbPassword.IsAlive() { return nil } return p.dbPassword.Bytes() } // CachePassword returns the cache password as a secure byte slice. func (p *SecureConfigProvider) CachePassword() []byte { p.mu.RLock() defer p.mu.RUnlock() if p.cachePassword == nil || !p.cachePassword.IsAlive() { return nil } return p.cachePassword.Bytes() } // S3AccessKey returns the S3 access key as a secure byte slice. func (p *SecureConfigProvider) S3AccessKey() []byte { p.mu.RLock() defer p.mu.RUnlock() if p.s3AccessKey == nil || !p.s3AccessKey.IsAlive() { return nil } return p.s3AccessKey.Bytes() } // S3SecretKey returns the S3 secret key as a secure byte slice. func (p *SecureConfigProvider) S3SecretKey() []byte { p.mu.RLock() defer p.mu.RUnlock() if p.s3SecretKey == nil || !p.s3SecretKey.IsAlive() { return nil } return p.s3SecretKey.Bytes() } // MailgunAPIKey returns the Mailgun API key as a secure byte slice. func (p *SecureConfigProvider) MailgunAPIKey() []byte { p.mu.RLock() defer p.mu.RUnlock() if p.mailgunAPIKey == nil || !p.mailgunAPIKey.IsAlive() { return nil } return p.mailgunAPIKey.Bytes() } // Destroy securely wipes all cached secrets from memory. // Should be called during application shutdown. func (p *SecureConfigProvider) Destroy() { p.mu.Lock() defer p.mu.Unlock() if p.jwtSecret != nil && p.jwtSecret.IsAlive() { p.jwtSecret.Destroy() } if p.dbPassword != nil && p.dbPassword.IsAlive() { p.dbPassword.Destroy() } if p.cachePassword != nil && p.cachePassword.IsAlive() { p.cachePassword.Destroy() } if p.s3AccessKey != nil && p.s3AccessKey.IsAlive() { p.s3AccessKey.Destroy() } if p.s3SecretKey != nil && p.s3SecretKey.IsAlive() { p.s3SecretKey.Destroy() } if p.mailgunAPIKey != nil && p.mailgunAPIKey.IsAlive() { p.mailgunAPIKey.Destroy() } p.jwtSecret = nil p.dbPassword = nil p.cachePassword = nil p.s3AccessKey = nil p.s3SecretKey = nil p.mailgunAPIKey = nil } // Config returns the underlying config for non-secret access. // Prefer using the specific secret accessor methods for sensitive data. func (p *SecureConfigProvider) Config() *config.Config { return p.cfg }