Skip to content

Commit d4bb103

Browse files
committed
security: HMAC-SHA256 pepper for API keys, sha256 for memcached keys; ci: mypy_path for cli
1 parent d9e5559 commit d4bb103

4 files changed

Lines changed: 18 additions & 5 deletions

File tree

backend/src/infrastructure/auth/session/backends/memcached.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def _encode_key(self, key: str) -> bytes:
6363
The encoded key as bytes
6464
"""
6565
if len(key) > 240:
66-
key_hash = hashlib.md5(key.encode()).hexdigest()
66+
key_hash = hashlib.sha256(key.encode()).hexdigest()[:32]
6767
key = f"{key[:200]}:{key_hash}"
6868
return key.encode("utf-8")
6969

backend/src/modules/api_keys/service.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
"""API key management service for developer-facing products."""
22

33
import hashlib
4+
import hmac
45
import secrets
56
from datetime import UTC, datetime, timedelta
67
from typing import Any
78

89
from fastcrud.types import GetMultiResponseDict
910
from sqlalchemy.ext.asyncio import AsyncSession
1011

12+
from ...infrastructure.config.settings import get_settings
1113
from ...infrastructure.logging import get_logger
1214
from ..common.exceptions import PermissionDeniedError, ResourceNotFoundError
1315
from .crud import crud_api_keys, crud_key_permissions, crud_key_usage
@@ -31,6 +33,7 @@
3133
)
3234

3335
logger = get_logger()
36+
settings = get_settings()
3437

3538

3639
class 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,

backend/tests/unit/infrastructure/auth/session/backends/test_memcached.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def memcached_storage(mock_memcached):
4141
def encode_key(key):
4242
"""Helper function to encode a key the same way the storage class does."""
4343
if len(key) > 240:
44-
key_hash = hashlib.md5(key.encode()).hexdigest()
44+
key_hash = hashlib.sha256(key.encode()).hexdigest()[:32]
4545
key = f"{key[:200]}:{key_hash}"
4646
return key.encode("utf-8")
4747

cli/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ known-first-party = ["cli"]
4444

4545
[tool.mypy]
4646
python_version = "3.11"
47+
mypy_path = "src"
4748
warn_return_any = true
4849
warn_unused_configs = true
4950
warn_unused_ignores = true

0 commit comments

Comments
 (0)