Coverage for src / idx_api / security.py: 55%
20 statements
« prev ^ index » next coverage.py v7.13.1, created at 2025-12-28 11:09 -0700
« prev ^ index » next coverage.py v7.13.1, created at 2025-12-28 11:09 -0700
1"""Security utilities for API key management and hashing."""
3import hashlib
4import secrets
5from datetime import datetime, timezone
8def generate_api_key(prefix: str = "idx_live") -> tuple[str, str, str]:
9 """
10 Generate a new API key with secure random token.
12 Args:
13 prefix: Key prefix (idx_live or idx_test)
15 Returns:
16 tuple: (full_key, key_prefix, key_hash)
17 - full_key: Complete key to return to user (ONLY SHOWN ONCE)
18 - key_prefix: First 12 chars for display (e.g., "idx_live_abc")
19 - key_hash: SHA-256 hash for database storage
20 """
21 # Generate 32 random bytes = 43 base64 characters (urlsafe)
22 random_part = secrets.token_urlsafe(32)
24 # Construct full key: prefix_randompart
25 full_key = f"{prefix}_{random_part}"
27 # Key prefix for display (first 12 characters)
28 key_prefix = full_key[:12]
30 # Hash for storage (SHA-256)
31 key_hash = hash_api_key(full_key)
33 return full_key, key_prefix, key_hash
36def hash_api_key(api_key: str) -> str:
37 """
38 Hash an API key using SHA-256.
40 Args:
41 api_key: The plaintext API key
43 Returns:
44 Hexadecimal SHA-256 hash (64 characters)
45 """
46 return hashlib.sha256(api_key.encode()).hexdigest()
49def verify_api_key(plaintext_key: str, stored_hash: str) -> bool:
50 """
51 Verify an API key against its stored hash.
53 Args:
54 plaintext_key: The API key provided by the user
55 stored_hash: The stored hash from the database
57 Returns:
58 True if the key matches, False otherwise
59 """
60 computed_hash = hash_api_key(plaintext_key)
61 return secrets.compare_digest(computed_hash, stored_hash)
64def is_key_expired(expires_at: datetime | None) -> bool:
65 """
66 Check if an API key has expired.
68 Args:
69 expires_at: Expiration datetime (None = no expiration)
71 Returns:
72 True if expired, False otherwise
73 """
74 if expires_at is None:
75 return False
77 return datetime.now(timezone.utc) > expires_at
80def generate_test_api_key() -> tuple[str, str, str]:
81 """
82 Generate a test API key (for development/testing).
84 Returns:
85 Same as generate_api_key() but with 'idx_test' prefix
86 """
87 return generate_api_key(prefix="idx_test")