Gateway API Key: Plaintext Storage & Non-Constant-Time Comparison¶
Evidence Level: F (directly proven by source code)
Analysis Baseline:4f843556
TL;DR¶
Gateway API Key verification tries plaintext comparison first, falling back to SHA256 hash. Both paths use Python's == instead of constant-time comparison. SHA256 has no salt. Comments label this security regression as "progress."
1. Verification Path: Plaintext First¶
backend/app/api/gateway.py:45-48:
# First try plaintext (new behavior)
result = await db.execute(
select(Agent).where(
Agent.api_key_hash == api_key, # ← plaintext direct comparison
Verification tries plaintext matching first, falling back to hash only on failure. The comment labels plaintext as "new behavior"—meaning the plan is to eventually delete the hash fallback and switch entirely to plaintext storage.
2. Non-Constant-Time Comparison¶
The gateway uses Python's == operator to compare API keys:
Python's == for string comparison returns early at the first mismatching character, making the comparison time proportional to the length of the matching prefix. An attacker can use timing attacks to gradually guess the key prefix.
Contrast: WhatsApp and Slack webhook verification use hmac.compare_digest—constant-time comparison.
3. Unsalted Hash¶
SHA256 with no salt. If the migration to plaintext storage is completed, a database breach means total compromise. Even if hashing is retained, the lack of salt means identical keys produce identical hashes, enabling batch comparison by attackers.
4. Comment Wording¶
The comments label "plaintext storage" as "new behavior" and "progress," and "hash storage" as "legacy." This suggests the project's security direction is from hash to plaintext—the opposite of industry best practice.
5. Relationship to AI Coding¶
- Comments reflect prompt intent: The developer (or AI) was asked to "add plaintext comparison as new behavior"—the AI faithfully executed it and added comments marking the state
- No security awareness: The AI won't proactively say "storing API keys in plaintext is a security risk and should not be labeled as progress"—that requires security judgment
- Non-constant-time comparison:
==is the most natural way to compare strings in Python—the AI won't proactively choosehmac.compare_digestunless the prompt explicitly requires it - WhatsApp/Slack's
hmac.compare_digestis the result of a different developer/task—the same security property is implemented inconsistently across modules
6. Related Findings¶
- 010 · Gateway API Key: Old and New Logic Coexist: Creation and verification logic is inconsistent (migration half-product)