diff --git a/lib/cli/src/crewai_cli/model_catalog.py b/lib/cli/src/crewai_cli/model_catalog.py index bb8dd0c99..b6ff1494d 100644 --- a/lib/cli/src/crewai_cli/model_catalog.py +++ b/lib/cli/src/crewai_cli/model_catalog.py @@ -25,6 +25,7 @@ from __future__ import annotations from collections.abc import Callable import contextlib import hashlib +import hmac import json import os from pathlib import Path @@ -626,7 +627,14 @@ def _cache_key(provider_key: str) -> str: api_key = _provider_api_key(provider_key) if not api_key: return f"{provider_key}#nokey" - digest = hashlib.sha256(api_key.encode("utf-8")).hexdigest()[:12] + # HMAC with the credential as the key (not as hash input). SHA-256 alone on + # API-key material trips CodeQL py/weak-sensitive-data-hashing; keyed HMAC is + # the right construction for a local cache partition id. + digest = hmac.new( + api_key.encode("utf-8"), + b"crewai.model_catalog.cache_v1", + hashlib.sha256, + ).hexdigest()[:12] return f"{provider_key}#{digest}" diff --git a/lib/cli/tests/test_model_catalog.py b/lib/cli/tests/test_model_catalog.py index e5c2043c7..a0d80a6a3 100644 --- a/lib/cli/tests/test_model_catalog.py +++ b/lib/cli/tests/test_model_catalog.py @@ -2,6 +2,8 @@ from __future__ import annotations +import hashlib +import hmac import json import time @@ -583,6 +585,14 @@ def test_cache_key_hashes_key_and_never_stores_it(monkeypatch): key = mc._cache_key("openai") assert key.startswith("openai#") and key != "openai#nokey" assert "sk-super-secret" not in key # only a digest, never the raw key + # Credential is the HMAC key (not SHA-256 hash input) so CodeQL + # py/weak-sensitive-data-hashing does not flag password-style hashing. + expected = hmac.new( + b"sk-super-secret", + b"crewai.model_catalog.cache_v1", + hashlib.sha256, + ).hexdigest()[:12] + assert key == f"openai#{expected}" def test_dynamic_cache_expires_after_catalog_ttl(monkeypatch):