Skip to content

Key prefix has no uniqueness guarantee — prefix collision silently targets the wrong key #34

@matthias-hausberger

Description

@matthias-hausberger

Summary

The short prefix used to identify keys in CLI/API operations (kfl_user_<3 hex chars> / kfl_sys_<4 hex chars>) is generated without any collision check and has no UNIQUE constraint in the database. A duplicate prefix causes kfl keys put, kfl keys revoke, and any other prefix-based operation to silently act on whichever matching row the DB returns first — the wrong key gets updated or revoked.

Details

Prefix keyspace is small:

  • kfl_user_ + 3 hex chars = 4,096 possible values
  • kfl_sys_ + 4 hex chars = 65,536 possible values

No collision guard at generation time (packages/server/src/routes/keys.ts:33–37, packages/server/src/routes/bootstrap.ts:57–59): the prefix is derived by slicing the first 12 chars of prefix + generateRandomHex(32). There is no retry loop or uniqueness probe before insertion.

No UNIQUE constraint on key_prefix (packages/server/src/db/schema.ts:8): only key_hash (the full key) is unique. The prefix column is NOT NULL only.

All prefix-based lookups use .limit(1): PUT /keys/:prefix, DELETE /keys/:prefix, and getKeyByPrefix all query by key_prefix and take the first result — no collision detection.

Note: The full key value is collision-safe (32 random hex chars, hashed with UNIQUE constraint). Only the short prefix used by the CLI/API is affected.

Steps to Reproduce

Collision probability grows with key count (birthday problem). With ~64 user keys the chance of at least one kfl_user_ prefix collision exceeds 50%. At that point:

  1. Two keys share the same prefix, e.g. kfl_user_a1b
  2. kfl keys put kfl_user_a1b --scope ... silently updates the wrong key
  3. kfl keys revoke kfl_user_a1b silently revokes the wrong key

No error is returned.

Workaround

None at the CLI level. A careful operator can inspect raw DB rows to detect duplicates, but there is no CLI or API surface to do so.

Affected Commands

Exactly two CLI commands look up a key by prefix:

CLI command Server route DB query
kfl keys revoke <prefix> (keys.ts:99–102) DELETE /keys/:prefixhandleRevokeKey (keys.ts:132–148) revokeKeyByPrefix (queries.ts:47–56)
kfl keys put <prefix> (keys.ts:104–131) PUT /keys/:prefixhandleUpdateKey (keys.ts:150–211) getKeyByPrefix + updateKeyScopes (queries.ts:58–81)

kfl keys list and kfl keys create do not take a prefix as input — they are not affected.

Potential Fix

Two complementary changes:

  1. Add a UNIQUE constraint on key_prefix in the D1 schema (new migration). This turns a silent collision into a hard DB error.
  2. Add a retry loop at prefix generation time: generate → check for existing prefix in DB → retry on collision (bounded, e.g. max 5 attempts). This prevents the error from ever reaching the user.

Do nots: For best UX, do NOT increase prefix entropy (e.g. 6 hex chars for user keys) to make collisions negligible in practice.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions