diff --git a/app/api/api_tokens.py b/app/api/api_tokens.py index eb48bb12..e460c1d5 100644 --- a/app/api/api_tokens.py +++ b/app/api/api_tokens.py @@ -37,6 +37,10 @@ DbSession = Annotated[Session, Depends(get_db)] TOKEN_PREFIX = "de_" #: Number of random bytes for the token body (32 → 43 URL-safe chars). TOKEN_BYTES = 32 +#: PBKDF2 iteration count for hashing API tokens. +TOKEN_HASH_ITERATIONS = 100_000 +#: PBKDF2 salt for API token hashing (not secret, but fixed for determinism). +TOKEN_HASH_SALT = b"api-token-v1" # --------------------------------------------------------------------------- @@ -70,7 +74,7 @@ def generate_api_token() -> str: def hash_token(token: str) -> str: - """Return the SHA-256 hex digest of *token*. + """Return a PBKDF2-HMAC-SHA256 hex digest of *token*. Args: token: The plaintext API token. @@ -78,7 +82,13 @@ def hash_token(token: str) -> str: Returns: 64-character lowercase hex string. """ - return hashlib.sha256(token.encode("utf-8")).hexdigest() + dk = hashlib.pbkdf2_hmac( + "sha256", + token.encode("utf-8"), + TOKEN_HASH_SALT, + TOKEN_HASH_ITERATIONS, + ) + return dk.hex() # --------------------------------------------------------------------------- diff --git a/tests/test_api_tokens.py b/tests/test_api_tokens.py index 2f92e6c8..03445677 100644 --- a/tests/test_api_tokens.py +++ b/tests/test_api_tokens.py @@ -426,11 +426,14 @@ class TestTokenUtils: assert hash_token(token) == hash_token(token) @pytest.mark.unit - def test_hash_token_is_sha256(self): - """Token hash should be a SHA-256 hex digest.""" + def test_hash_token_output_properties(self): + """Token hash should be a 64-character lowercase hex digest.""" from app.api.api_tokens import hash_token token = "de_test_token_value" - expected = hashlib.sha256(token.encode()).hexdigest() - assert hash_token(token) == expected - assert len(hash_token(token)) == 64 + h = hash_token(token) + assert isinstance(h, str) + assert len(h) == 64 + # All characters should be valid lowercase hex digits. + int(h, 16) + assert h == h.lower()