Skip to content

Security Vulnerability: Plain Text API Key Storage #54

@phbrgnomo

Description

@phbrgnomo

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

  1. 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
  2. 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)
  3. 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
  4. 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

  1. Add encrypted BOOLEAN DEFAULT 0 column to settings table
  2. On first run with new version, prompt user to set master password
  3. Re-encrypt existing sensitive settings (API keys)
  4. Display migration status in Settings UI

Alternatives Considered

  1. Environment Variables Only: Simple but poor UX for non-technical users
  2. System Keyring: Platform-specific, complex integration
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions