11"""API key management service for developer-facing products."""
22
33import hashlib
4+ import hmac
45import secrets
56from datetime import UTC , datetime , timedelta
67from typing import Any
78
89from fastcrud .types import GetMultiResponseDict
910from sqlalchemy .ext .asyncio import AsyncSession
1011
12+ from ...infrastructure .config .settings import get_settings
1113from ...infrastructure .logging import get_logger
1214from ..common .exceptions import PermissionDeniedError , ResourceNotFoundError
1315from .crud import crud_api_keys , crud_key_permissions , crud_key_usage
3133)
3234
3335logger = get_logger ()
36+ settings = get_settings ()
3437
3538
3639class APIKeyService :
@@ -54,13 +57,22 @@ def _generate_api_key(self) -> tuple[str, str, str]:
5457 raw_key = secrets .token_urlsafe (self .key_length )
5558 prefix = raw_key [: self .key_prefix_length ]
5659 api_key = f"fai_{ prefix } _{ raw_key [self .key_prefix_length :]} "
57- key_hash = hashlib . sha256 (api_key . encode ()). hexdigest ( )
60+ key_hash = self . _hash_api_key (api_key )
5861
5962 return api_key , prefix , key_hash
6063
6164 def _hash_api_key (self , api_key : str ) -> str :
62- """Hash an API key for storage or comparison."""
63- return hashlib .sha256 (api_key .encode ()).hexdigest ()
65+ """Hash an API key for storage or comparison.
66+
67+ HMAC-SHA256 with SECRET_KEY as the pepper. Deterministic so DB lookup
68+ by `key_hash` stays O(1); the server-side secret means a stolen
69+ `key_hash` column alone cannot be used to forge or verify keys offline.
70+ """
71+ return hmac .new (
72+ settings .SECRET_KEY .encode ("utf-8" ),
73+ api_key .encode ("utf-8" ),
74+ hashlib .sha256 ,
75+ ).hexdigest ()
6476
6577 async def create_api_key (
6678 self ,
0 commit comments