Skip to content

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:

Agent.api_key_hash == api_key

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

hashlib.sha256(raw_key.encode()).hexdigest()

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

# First try plaintext (new behavior)
# ... fallback to hashed (legacy)

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

  1. 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
  2. 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
  3. Non-constant-time comparison: == is the most natural way to compare strings in Python—the AI won't proactively choose hmac.compare_digest unless the prompt explicitly requires it
  4. WhatsApp/Slack's hmac.compare_digest is the result of a different developer/task—the same security property is implemented inconsistently across modules