Current Situation
API keys (particularly POLYGON_API_KEY) are currently stored in plain text in the SQLite database:
CREATE TABLE settings (
name TEXT PRIMARY KEY,
value TEXT, -- ⚠️ Plain text storage
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
Risk: Anyone with file system access to the .db file can read API keys directly using any SQLite browser or command-line tool.
Impact
- Confidentiality: API keys are exposed to anyone with database file access
- Attack Surface: Backups, file sharing, or compromised systems expose credentials
- Compliance: Violates security best practices for credential management
Proposed Solution: AES Encryption
Implement AES-256-GCM encryption for sensitive settings values before storing in the database.
Implementation Approach
-
Encryption Layer (internal/security/encryption.go)
- AES-256-GCM encryption/decryption functions
- Key derivation from master password using Argon2id
- Salt storage in database for key derivation
-
Master Key Management (Phase 1)
- Store encrypted master key in
~/.wheeler/master.key with restricted permissions (600)
- User sets password on first run
- Key derived using Argon2id (memory-hard, resistant to GPU attacks)
-
Master Key Management (Phase 2 - Future Enhancement)
- Optional system keyring integration (Linux: Secret Service API, macOS: Keychain, Windows: Credential Manager)
- Environment variable fallback for CI/CD and Docker deployments
-
Database Updates
- Add
encrypted boolean column to settings table
- Migrate existing plain text values with warning
- Transparent encryption/decryption in
SettingService
Benefits
- ✅ Maintains current UI/UX (Settings page works unchanged)
- ✅ Protects against casual database inspection
- ✅ Backward compatible (migration path for existing databases)
- ✅ Good balance between security and usability
Implementation Sketch
// internal/security/encryption.go
package security
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"io"
"golang.org/x/crypto/argon2"
)
type Encryptor struct {
key []byte
}
func NewEncryptor(password, salt []byte) (*Encryptor, error) {
// Argon2id parameters (OWASP recommended)
key := argon2.IDKey(password, salt, 3, 64*1024, 4, 32)
return &Encryptor{key: key}, nil
}
func (e *Encryptor) Encrypt(plaintext string) (string, error) {
block, _ := aes.NewCipher(e.key)
gcm, _ := cipher.NewGCM(block)
nonce := make([]byte, gcm.NonceSize())
io.ReadFull(rand.Reader, nonce)
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
func (e *Encryptor) Decrypt(ciphertext string) (string, error) {
data, _ := base64.StdEncoding.DecodeString(ciphertext)
block, _ := aes.NewCipher(e.key)
gcm, _ := cipher.NewGCM(block)
nonceSize := gcm.NonceSize()
nonce, encrypted := data[:nonceSize], data[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, encrypted, nil)
return string(plaintext), err
}
// Update SettingService.GetValue()
func (s *SettingService) GetValue(name string) string {
setting, err := s.GetByName(name)
if err != nil || setting.Value == nil {
return ""
}
// Decrypt if encrypted
if setting.Encrypted {
decrypted, err := s.encryptor.Decrypt(*setting.Value)
if err != nil {
log.Printf("Failed to decrypt setting %s: %v", name, err)
return ""
}
return decrypted
}
return *setting.Value
}
Migration Plan
- Add
encrypted BOOLEAN DEFAULT 0 column to settings table
- On first run with new version, prompt user to set master password
- Re-encrypt existing sensitive settings (API keys)
- Display migration status in Settings UI
Alternatives Considered
- Environment Variables Only: Simple but poor UX for non-technical users
- System Keyring: Platform-specific, complex integration
- No Encryption: Current state (unacceptable for production)
References
Related Files
internal/models/setting.go - Setting service (needs encryption integration)
internal/database/schema.sql - Database schema (needs encrypted column)
internal/web/settings_handlers.go - Settings UI handlers
Current Situation
API keys (particularly
POLYGON_API_KEY) are currently stored in plain text in the SQLite database:Risk: Anyone with file system access to the
.dbfile can read API keys directly using any SQLite browser or command-line tool.Impact
Proposed Solution: AES Encryption
Implement AES-256-GCM encryption for sensitive settings values before storing in the database.
Implementation Approach
Encryption Layer (
internal/security/encryption.go)Master Key Management (Phase 1)
~/.wheeler/master.keywith restricted permissions (600)Master Key Management (Phase 2 - Future Enhancement)
Database Updates
encryptedboolean column tosettingstableSettingServiceBenefits
Implementation Sketch
Migration Plan
encrypted BOOLEAN DEFAULT 0column tosettingstableAlternatives Considered
References
Related Files
internal/models/setting.go- Setting service (needs encryption integration)internal/database/schema.sql- Database schema (needsencryptedcolumn)internal/web/settings_handlers.go- Settings UI handlers