"""API token generation and verification service. This module provides secure API token generation using cryptographically secure random generation and SHA-256 hashing. Only the hash is stored. """ import hashlib import secrets TOKEN_PREFIX = "or_api_" TOKEN_ENTROPY_BYTES = 48 # Results in ~64 URL-safe base64 chars def generate_api_token() -> tuple[str, str]: """Generate a new API token. Generates a cryptographically secure random token with format: 'or_api_' + 48 bytes of URL-safe base64 (~64 chars) Returns: Tuple of (plaintext_token, token_hash) where: - plaintext_token: The full token to show once to the user - token_hash: SHA-256 hash to store in database Example: >>> plaintext, hash = generate_api_token() >>> print(plaintext) 'or_api_x9QzGv2K...' >>> # Store hash in DB, show plaintext to user once """ # Generate cryptographically secure random token random_part = secrets.token_urlsafe(TOKEN_ENTROPY_BYTES) plaintext = f"{TOKEN_PREFIX}{random_part}" # Hash the entire token token_hash = hash_token(plaintext) return plaintext, token_hash def hash_token(plaintext: str) -> str: """Hash a token using SHA-256. Args: plaintext: The plaintext token to hash. Returns: Hexadecimal string of the SHA-256 hash. Raises: TypeError: If plaintext is not a string. """ if not isinstance(plaintext, str): raise TypeError("plaintext must be a string") return hashlib.sha256(plaintext.encode("utf-8")).hexdigest() def verify_api_token(plaintext: str, token_hash: str) -> bool: """Verify an API token against its stored hash. Uses timing-safe comparison to prevent timing attacks. Args: plaintext: The plaintext token provided by the user. token_hash: The SHA-256 hash stored in the database. Returns: True if the token matches the hash, False otherwise. Raises: TypeError: If either argument is not a string. """ if not isinstance(plaintext, str): raise TypeError("plaintext must be a string") if not isinstance(token_hash, str): raise TypeError("token_hash must be a string") # Compute hash of provided plaintext computed_hash = hash_token(plaintext) # Use timing-safe comparison to prevent timing attacks return secrets.compare_digest(computed_hash, token_hash)