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

1"""Security utilities for API key management and hashing.""" 

2 

3import hashlib 

4import secrets 

5from datetime import datetime, timezone 

6 

7 

8def generate_api_key(prefix: str = "idx_live") -> tuple[str, str, str]: 

9 """ 

10 Generate a new API key with secure random token. 

11 

12 Args: 

13 prefix: Key prefix (idx_live or idx_test) 

14 

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) 

23 

24 # Construct full key: prefix_randompart 

25 full_key = f"{prefix}_{random_part}" 

26 

27 # Key prefix for display (first 12 characters) 

28 key_prefix = full_key[:12] 

29 

30 # Hash for storage (SHA-256) 

31 key_hash = hash_api_key(full_key) 

32 

33 return full_key, key_prefix, key_hash 

34 

35 

36def hash_api_key(api_key: str) -> str: 

37 """ 

38 Hash an API key using SHA-256. 

39 

40 Args: 

41 api_key: The plaintext API key 

42 

43 Returns: 

44 Hexadecimal SHA-256 hash (64 characters) 

45 """ 

46 return hashlib.sha256(api_key.encode()).hexdigest() 

47 

48 

49def verify_api_key(plaintext_key: str, stored_hash: str) -> bool: 

50 """ 

51 Verify an API key against its stored hash. 

52 

53 Args: 

54 plaintext_key: The API key provided by the user 

55 stored_hash: The stored hash from the database 

56 

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) 

62 

63 

64def is_key_expired(expires_at: datetime | None) -> bool: 

65 """ 

66 Check if an API key has expired. 

67 

68 Args: 

69 expires_at: Expiration datetime (None = no expiration) 

70 

71 Returns: 

72 True if expired, False otherwise 

73 """ 

74 if expires_at is None: 

75 return False 

76 

77 return datetime.now(timezone.utc) > expires_at 

78 

79 

80def generate_test_api_key() -> tuple[str, str, str]: 

81 """ 

82 Generate a test API key (for development/testing). 

83 

84 Returns: 

85 Same as generate_api_key() but with 'idx_test' prefix 

86 """ 

87 return generate_api_key(prefix="idx_test")