187 lines
5 KiB
Go
187 lines
5 KiB
Go
// 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
|
|
}
|