Files
openrouter-watcher/prompt/prompt-security-services.md
Luca Sacchi Ricciardi 2fdd9d16fd feat(security): T12 implement AES-256 encryption service
- Add EncryptionService with AES-256-GCM via cryptography.fernet
- Implement PBKDF2HMAC key derivation with SHA256 (100k iterations)
- Deterministic salt derived from master_key for consistency
- Methods: encrypt(), decrypt() with proper error handling
- 12 comprehensive tests with 100% coverage
- Handle InvalidToken, TypeError edge cases
2026-04-07 12:03:45 +02:00

14 KiB

Prompt: Security Services Implementation (T12-T16)

🎯 OBIETTIVO

Implementare la fase Security Services del progetto OpenRouter API Key Monitor seguendo rigorosamente TDD (Test-Driven Development).

Task da completare: T12, T13, T14, T15, T16


📋 CONTESTO

  • Repository: /home/google/Sources/LucaSacchiNet/openrouter-watcher
  • Specifiche: /home/google/Sources/LucaSacchiNet/openrouter-watcher/export/architecture.md (sezione 6)
  • Kanban: /home/google/Sources/LucaSacchiNet/openrouter-watcher/export/kanban.md
  • Stato Attuale: Database & Models completati (T01-T11), 132 test passanti
  • Progresso: 15% (11/74 task)

🔐 SPECIFICHE SICUREZZA (Da architecture.md)

Algoritmi di Sicurezza

Dato Algoritmo Implementazione
API Keys AES-256-GCM cryptography.fernet with custom key
Passwords bcrypt passlib.hash.bcrypt (12 rounds)
API Tokens SHA-256 Only hash stored, never plaintext
JWT HS256 python-jose with 256-bit secret

🔧 TASK DETTAGLIATI

T12: Implementare EncryptionService (AES-256-GCM)

Requisiti:

  • Creare src/openrouter_monitor/services/encryption.py
  • Implementare classe EncryptionService
  • Usare cryptography.fernet per AES-256-GCM
  • Key derivation con PBKDF2HMAC (SHA256, 100000 iterations)
  • Metodi: encrypt(plaintext: str) -> str, decrypt(ciphertext: str) -> str
  • Gestire eccezioni con messaggi chiari

Implementazione Riferimento:

from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import base64
import os

class EncryptionService:
    def __init__(self, master_key: str):
        self._fernet = self._derive_key(master_key)
    
    def _derive_key(self, master_key: str) -> Fernet:
        kdf = PBKDF2HMAC(
            algorithm=hashes.SHA256(),
            length=32,
            salt=os.urandom(16),  # ATTENZIONE: salt deve essere fisso per decrittazione!
            iterations=100000,
        )
        key = base64.urlsafe_b64encode(kdf.derive(master_key.encode()))
        return Fernet(key)

⚠️ NOTA CRITICA: Il salt deve essere fisso (derivato da master_key) oppure salvato insieme al ciphertext, altrimenti la decrittazione fallisce. Usa approccio: salt + ciphertext oppure deriva salt deterministico da master_key.

Test richiesti:

  • Test inizializzazione con master key valida
  • Test encrypt/decrypt roundtrip
  • Test ciphertext diverso da plaintext
  • Test decrittazione fallisce con chiave sbagliata
  • Test gestione eccezioni (InvalidToken)

T13: Implementare Password Hashing (bcrypt)

Requisiti:

  • Creare src/openrouter_monitor/services/password.py
  • Usare passlib.context.CryptContext con bcrypt
  • 12 rounds (default sicuro)
  • Funzioni: hash_password(password: str) -> str, verify_password(plain: str, hashed: str) -> bool
  • Validazione password: min 12 chars, uppercase, lowercase, digit, special char

Implementazione Riferimento:

from passlib.context import CryptContext

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def hash_password(password: str) -> str:
    return pwd_context.hash(password)

def verify_password(plain_password: str, hashed_password: str) -> bool:
    return pwd_context.verify(plain_password, hashed_password)

Test richiesti:

  • Test hash_password genera hash diverso ogni volta
  • Test verify_password ritorna True con password corretta
  • Test verify_password ritorna False con password sbagliata
  • Test validazione password strength
  • Test hash è sempre valido per bcrypt

T14: Implementare JWT Utilities

Requisiti:

  • Creare src/openrouter_monitor/services/jwt.py
  • Usare python-jose con algoritmo HS256
  • Funzioni:
    • create_access_token(data: dict, expires_delta: timedelta | None = None) -> str
    • decode_access_token(token: str) -> dict
    • verify_token(token: str) -> TokenData
  • JWT payload: sub (user_id), exp (expiration), iat (issued at)
  • Gestire eccezioni: JWTError, ExpiredSignatureError
  • Leggere SECRET_KEY da config

Implementazione Riferimento:

from jose import JWTError, jwt
from datetime import datetime, timedelta

SECRET_KEY = settings.secret_key
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_HOURS = 24

def create_access_token(data: dict, expires_delta: timedelta | None = None):
    to_encode = data.copy()
    expire = datetime.utcnow() + (expires_delta or timedelta(hours=ACCESS_TOKEN_EXPIRE_HOURS))
    to_encode.update({"exp": expire})
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

def decode_access_token(token: str) -> dict:
    return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])

Test richiesti:

  • Test create_access_token genera token valido
  • Test decode_access_token estrae payload corretto
  • Test token scaduto ritorna errore
  • Test token con firma invalida ritorna errore
  • Test token con algoritmo sbagliato ritorna errore
  • Test payload contiene exp, sub, iat

T15: Implementare API Token Generation

Requisiti:

  • Creare src/openrouter_monitor/services/token.py
  • Implementare generate_api_token() -> tuple[str, str]
  • Token format: or_api_ + 48 chars random (url-safe base64)
  • Hash: SHA-256 dell'intero token
  • Solo l'hash viene salvato nel DB (api_tokens.token_hash)
  • Il plaintext viene mostrato una sola volta al momento della creazione
  • Funzione verify_api_token(plaintext: str, token_hash: str) -> bool

Implementazione Riferimento:

import secrets
import hashlib

def generate_api_token() -> tuple[str, str]:
    token = "or_api_" + secrets.token_urlsafe(48)  # ~64 chars total
    token_hash = hashlib.sha256(token.encode()).hexdigest()
    return token, token_hash

def verify_api_token(plaintext: str, token_hash: str) -> bool:
    computed_hash = hashlib.sha256(plaintext.encode()).hexdigest()
    return secrets.compare_digest(computed_hash, token_hash)

Test richiesti:

  • Test generate_api_token ritorna (plaintext, hash)
  • Test token inizia con "or_api_"
  • Test hash è SHA-256 valido (64 hex chars)
  • Test verify_api_token True con token valido
  • Test verify_api_token False con token invalido
  • Test timing attack resistance (compare_digest)

T16: Scrivere Test per Servizi di Sicurezza

Requisiti:

  • Creare test completi per tutti i servizi:
    • tests/unit/services/test_encryption.py
    • tests/unit/services/test_password.py
    • tests/unit/services/test_jwt.py
    • tests/unit/services/test_token.py
  • Coverage >= 90% per ogni servizio
  • Test casi limite e errori
  • Test integrazione tra servizi (es. encrypt + save + decrypt)

Test richiesti per ogni servizio:

  • Unit test per ogni funzione pubblica
  • Test casi successo
  • Test casi errore (eccezioni)
  • Test edge cases (stringhe vuote, caratteri speciali, unicode)

🔄 WORKFLOW TDD OBBLIGATORIO

Per OGNI task (T12-T16):

┌─────────────────────────────────────────┐
│  1. RED - Scrivi il test che fallisce   │
│     • Test prima del codice             │
│     • Pattern AAA (Arrange-Act-Assert)  │
│     • Nomi descrittivi                  │
└─────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────┐
│  2. GREEN - Implementa codice minimo    │
│     • Solo codice necessario per test   │
│     • Nessun refactoring ancora         │
└─────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────┐
│  3. REFACTOR - Migliora il codice       │
│     • Pulisci duplicazioni              │
│     • Migliora nomi variabili           │
│     • Test rimangono verdi              │
└─────────────────────────────────────────┘

📁 STRUTTURA FILE DA CREARE

src/openrouter_monitor/
└── services/
    ├── __init__.py               # Esporta tutti i servizi
    ├── encryption.py             # T12 - AES-256-GCM
    ├── password.py               # T13 - bcrypt
    ├── jwt.py                    # T14 - JWT utilities
    └── token.py                  # T15 - API token generation

tests/unit/services/
├── __init__.py
├── test_encryption.py            # T12 + T16
├── test_password.py              # T13 + T16
├── test_jwt.py                   # T14 + T16
└── test_token.py                 # T15 + T16

🧪 REQUISITI TEST

Pattern AAA (Arrange-Act-Assert)

@pytest.mark.unit
def test_encrypt_decrypt_roundtrip_returns_original():
    # Arrange
    service = EncryptionService("test-key-32-chars-long!!")
    plaintext = "sensitive-api-key-12345"
    
    # Act
    encrypted = service.encrypt(plaintext)
    decrypted = service.decrypt(encrypted)
    
    # Assert
    assert decrypted == plaintext
    assert encrypted != plaintext

Marker Pytest

@pytest.mark.unit              # Logica pura
@pytest.mark.security          # Test sicurezza
@pytest.mark.slow              # Test lenti (bcrypt)

Fixtures Condivise (in conftest.py)

@pytest.fixture
def encryption_service():
    return EncryptionService("test-encryption-key-32bytes")

@pytest.fixture
def sample_password():
    return "SecurePass123!@#"

@pytest.fixture
def jwt_secret():
    return "jwt-secret-key-32-chars-long!!"

🛡️ VINCOLI TECNICI

EncryptionService Requirements

class EncryptionService:
    """AES-256-GCM encryption for sensitive data (API keys)."""
    
    def __init__(self, master_key: str):
        """Initialize with master key (min 32 chars recommended)."""
        
    def encrypt(self, plaintext: str) -> str:
        """Encrypt plaintext, return base64-encoded ciphertext."""
        
    def decrypt(self, ciphertext: str) -> str:
        """Decrypt ciphertext, return plaintext."""
        
    def _derive_key(self, master_key: str) -> Fernet:
        """Derive Fernet key from master key."""

Password Service Requirements

from passlib.context import CryptContext

pwd_context = CryptContext(
    schemes=["bcrypt"],
    deprecated="auto",
    bcrypt__rounds=12  # Esplicito per chiarezza
)

def hash_password(password: str) -> str:
    """Hash password with bcrypt (12 rounds)."""
    
def verify_password(plain_password: str, hashed_password: str) -> bool:
    """Verify password against hash."""
    
def validate_password_strength(password: str) -> bool:
    """Validate password complexity. Min 12 chars, upper, lower, digit, special."""

JWT Service Requirements

from jose import jwt, JWTError
from datetime import datetime, timedelta

def create_access_token(
    data: dict,
    expires_delta: timedelta | None = None
) -> str:
    """Create JWT access token."""
    
def decode_access_token(token: str) -> dict:
    """Decode and validate JWT token."""
    
def verify_token(token: str) -> TokenData:
    """Verify token and return TokenData."""

API Token Service Requirements

import secrets
import hashlib

def generate_api_token() -> tuple[str, str]:
    """Generate API token. Returns (plaintext, hash)."""
    
def verify_api_token(plaintext: str, token_hash: str) -> bool:
    """Verify API token against hash (timing-safe)."""
    
def hash_token(plaintext: str) -> str:
    """Hash token with SHA-256."""

📊 AGGIORNAMENTO PROGRESS

Dopo ogni task completato, aggiorna: /home/google/Sources/LucaSacchiNet/openrouter-watcher/export/progress.md

Esempio:

### 🔐 Security Services (T12-T16)

- [x] T12: EncryptionService (AES-256) - Completato [timestamp]
- [x] T13: Password Hashing (bcrypt) - Completato [timestamp]
- [ ] T14: JWT Utilities - In progress
- [ ] T15: API Token Generation
- [ ] T16: Security Tests

**Progresso sezione:** 40% (2/5 task)
**Progresso totale:** 18% (13/74 task)

CRITERI DI ACCETTAZIONE

  • T12: EncryptionService funzionante con AES-256-GCM
  • T13: Password hashing con bcrypt (12 rounds) + validation
  • T14: JWT utilities con create/decode/verify
  • T15: API token generation con SHA-256 hash
  • T16: Test completi per tutti i servizi (coverage >= 90%)
  • Tutti i test passano (pytest tests/unit/services/)
  • Nessuna password/token in plaintext nei log
  • 5 commit atomici (uno per task)
  • progress.md aggiornato con tutti i task completati

🚀 COMANDO DI VERIFICA

Al termine, esegui:

cd /home/google/Sources/LucaSacchiNet/openrouter-watcher
pytest tests/unit/services/ -v --cov=src/openrouter_monitor/services

# Verifica coverage >= 90%
pytest tests/unit/services/ --cov-report=term-missing

🔒 CONSIDERAZIONI SICUREZZA

Do's

  • Usare secrets module per token random
  • Usare secrets.compare_digest per confronti timing-safe
  • Usare bcrypt con 12+ rounds
  • Validare sempre input prima di processare
  • Gestire eccezioni senza leakare informazioni sensibili
  • Loggare operazioni di sicurezza (non dati sensibili)

Don'ts

  • MAI loggare password o token in plaintext
  • MAI usare RNG non crittografico (random module)
  • MAI hardcodare chiavi segrete
  • MAI ignorare eccezioni di decrittazione
  • MAI confrontare hash con == (usa compare_digest)

📝 NOTE

  • Usa SEMPRE path assoluti: /home/google/Sources/LucaSacchiNet/openrouter-watcher/
  • Segui le convenzioni in .opencode/agents/tdd-developer.md
  • Task devono essere verificabili in < 2 ore ciascuno
  • Documenta bug complessi in /docs/bug_ledger.md
  • Usa conventional commits: feat(security): T12 implement AES-256 encryption service

AGENTE: @tdd-developer
INIZIA CON: T12 - EncryptionService