""" Security utilities for passwordless authentication JWT signing, nonce generation, challenge/response """ import os import secrets import hashlib import hmac import base64 from datetime import datetime, timedelta from typing import Dict, Optional, Tuple import jwt from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding, rsa from cryptography.exceptions import InvalidSignature class SecurityManager: """ Security manager for passwordless authentication Features: - JWT token creation/verification - Cryptographic nonce generation - Challenge/response for device signing - Token expiration management """ def __init__(self): self.secret_key = os.getenv("JWT_SECRET", "quixzoom_jwt_secret_change_in_production") self.algorithm = os.getenv("JWT_ALGORITHM", "HS256") self.access_token_expire = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "60")) self.refresh_token_expire = int(os.getenv("REFRESH_TOKEN_EXPIRE_DAYS", "7")) self.auth_request_expire = int(os.getenv("AUTH_REQUEST_EXPIRE_MINUTES", "5")) def generate_nonce(self, length: int = 32) -> str: """Generate cryptographically secure nonce""" return secrets.token_urlsafe(length) def generate_challenge(self, request_token: str, nonce: str) -> str: """ Generate a challenge for device signing Combines request token and nonce with timestamp """ timestamp = datetime.utcnow().isoformat() challenge_data = f"{request_token}:{nonce}:{timestamp}" # Create HMAC of challenge data challenge = hmac.new( self.secret_key.encode(), challenge_data.encode(), hashlib.sha256 ).hexdigest() return challenge def verify_challenge_response( self, challenge: str, response: str, public_key_pem: str ) -> bool: """ Verify device challenge response Args: challenge: Original challenge response: Device's signed response (base64 encoded signature) public_key_pem: Device's public key in PEM format Returns: bool: True if signature is valid """ try: # Load public key public_key = serialization.load_pem_public_key( public_key_pem.encode() ) # Decode signature signature = base64.b64decode(response) # Verify signature public_key.verify( signature, challenge.encode(), padding.PKCS1v15(), hashes.SHA256() ) return True except InvalidSignature: return False except Exception: return False def create_access_token( self, user_id: str, device_id: str, session_id: str, additional_claims: Optional[Dict] = None ) -> str: """Create JWT access token""" now = datetime.utcnow() expires = now + timedelta(minutes=self.access_token_expire) payload = { "sub": user_id, "device_id": device_id, "session_id": session_id, "type": "access", "iat": now, "exp": expires, "jti": self.generate_nonce(16) # Unique token ID } if additional_claims: payload.update(additional_claims) return jwt.encode(payload, self.secret_key, algorithm=self.algorithm) def create_refresh_token( self, user_id: str, session_id: str ) -> str: """Create JWT refresh token""" now = datetime.utcnow() expires = now + timedelta(days=self.refresh_token_expire) payload = { "sub": user_id, "session_id": session_id, "type": "refresh", "iat": now, "exp": expires, "jti": self.generate_nonce(16) } return jwt.encode(payload, self.secret_key, algorithm=self.algorithm) def verify_token(self, token: str, expected_type: str = "access") -> Dict: """ Verify JWT token Args: token: JWT token string expected_type: Expected token type ("access" or "refresh") Returns: Dict: Token payload Raises: jwt.ExpiredSignatureError: Token expired jwt.InvalidTokenError: Invalid token ValueError: Wrong token type """ payload = jwt.decode(token, self.secret_key, algorithms=[self.algorithm]) # Check token type token_type = payload.get("type") if token_type != expected_type: raise ValueError(f"Expected {expected_type} token, got {token_type}") return payload def create_request_token(self) -> str: """Create unique request token for auth flow""" return f"req_{secrets.token_urlsafe(24)}" def create_qr_code_data( self, request_token: str, nonce: str, base_url: str = "quixzoom://auth" ) -> str: """ Create QR code data string Format: quixzoom://auth?token=&nonce= """ return f"{base_url}?token={request_token}&nonce={nonce}" def hash_fingerprint(self, fingerprint: str) -> str: """Hash device fingerprint for storage""" return hashlib.sha256(fingerprint.encode()).hexdigest() def generate_key_pair(self) -> Tuple[str, str]: """ Generate RSA key pair for device Returns: Tuple of (private_key_pem, public_key_pem) """ private_key = rsa.generate_private_key( public_exponent=65537, key_size=2048 ) private_pem = private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption() ).decode() public_pem = private_key.public_key().public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo ).decode() return private_pem, public_pem def is_token_expired(self, token: str) -> bool: """Check if token is expired without raising exception""" try: jwt.decode(token, self.secret_key, algorithms=[self.algorithm]) return False except jwt.ExpiredSignatureError: return True except jwt.InvalidTokenError: return True # Global security manager instance security_manager = SecurityManager()