Skip to content

API Key Hashing

Context and Problem Statement

The proxy authenticates machine-to-machine clients via API keys. How should API keys be stored and compared? Storing plaintext keys in configuration or memory would leak secrets if the configuration is exposed (logs, env dumps, version control).

Decision Drivers

  • API keys are long-lived credentials — a leak has lasting impact
  • Configuration files may be committed to version control or visible in container env
  • Key comparison must be constant-time-safe against timing attacks (though SHA-256 digest comparison mitigates this naturally)
  • The hashing scheme must be simple enough for operators to generate hashes offline (e.g., echo -n "key" | sha256sum)

Considered Options

  • Store plaintext keys and compare directly
  • Store bcrypt/argon2 hashes (password-style)
  • Store SHA-256 hex digests

Decision Outcome

Chosen option: "Store SHA-256 hex digests", because it provides a strong one-way transform while remaining simple to generate and fast to verify.

How it works

  1. Configuration stores only the SHA-256 hex digest of each API key
  2. At validation time, the incoming key is hashed with SHA-256 and compared against stored digests
  3. Plaintext keys are never persisted — not in config, not in memory after hashing
# Generating a key hash for configuration:
#   echo -n "my-api-key" | sha256sum
#   → a1b2c3d4...

# InMemoryApiKeyValidator stores only hashes:
class InMemoryApiKeyValidator:
    def add_key(self, api_key: str, principal: Principal) -> None:
        key_hash = hashlib.sha256(api_key.encode()).hexdigest()
        self._keys[key_hash] = principal

    def validate(self, api_key: str) -> KeyValidationResult:
        key_hash = hashlib.sha256(api_key.encode()).hexdigest()
        principal = self._keys.get(key_hash)
        ...

Why not bcrypt/argon2?

  • API keys are high-entropy random strings (unlike passwords) — dictionary attacks are not a concern
  • bcrypt/argon2 are intentionally slow, adding latency to every authenticated request
  • SHA-256 is sufficient for high-entropy secrets and is available in Python's stdlib

Consequences

  • Good, because plaintext keys never appear in stored configuration
  • Good, because SHA-256 is fast — negligible overhead per request
  • Good, because operators can generate hashes with standard CLI tools (sha256sum, openssl dgst)
  • Neutral, because if the hash store is leaked, an attacker cannot reverse the keys (one-way)
  • Bad, because SHA-256 provides no brute-force protection for low-entropy keys — operators must generate strong keys

Confirmation

  • InMemoryApiKeyValidator stores only hashes; add_key() hashes before storage
  • ApiKeyEntryDto configuration model has a key_hash field (not key or api_key)
  • Test test_hashing_is_sha256 verifies the hash matches hashlib.sha256

More Information

  • Related: ADR-0007 In-Memory Adapters
  • Affected files: src/hdc_auth_proxy/adapters/memory/api_key_validator.py, src/hdc_auth_proxy/adapters/dto/api_key.py