# 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:** ```python 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:** ```python 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:** ```python 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:** ```python 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) ```python @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 ```python @pytest.mark.unit # Logica pura @pytest.mark.security # Test sicurezza @pytest.mark.slow # Test lenti (bcrypt) ``` ### Fixtures Condivise (in conftest.py) ```python @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 ```python 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 ```python 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 ```python 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 ```python 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: ```markdown ### ๐Ÿ” 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: ```bash 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