- 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
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.fernetper 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.CryptContextcon 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-josecon algoritmo HS256 - Funzioni:
create_access_token(data: dict, expires_delta: timedelta | None = None) -> strdecode_access_token(token: str) -> dictverify_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.pytests/unit/services/test_password.pytests/unit/services/test_jwt.pytests/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
secretsmodule per token random - Usare
secrets.compare_digestper 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 (
randommodule) - 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