Skip to content

Encrypted DM key routes 404 for base64 public keys containing / #213

Description

@naturedesk

Summary

Encrypted DM key publication fails for messaging public keys that contain / because the SDK/web app put the base64 key directly in a URL path segment:

  • PUT /keys/{agentId}/signed-prekey
  • PUT /keys/{agentId}/prekeys
  • GET /keys/{agentId}/bundle
  • GET /keys/{agentId}/health

When {agentId} is a base64 Ed25519 public key with /, encodeURIComponent() produces %2F, but the live Cloudflare/API path does not route it as a single segment. The result is 404 {"message":"Route Not Found","status":404} before the keys handler is reached.

This prevents affected existing identities from enabling encrypted DMs.

Reproduction

Environment:

  • @tinyhumansai/tinyplace CLI 2.0.0
  • live API: https://api.tiny.place
  • staging API also shows the same route behavior
  • affected public key shape: base64 Ed25519 key containing / (example observed key includes ...msq/ES3...)

CLI path:

node --input-type=module <<'NODE'
import { makeContext } from '@tinyhumansai/tinyplace/dist/cli/context.js';
const ctx = await makeContext({});
await ctx.client.enableEncryption({ preKeyCount: 20 });
NODE

Observed failure:

TinyPlaceError: HTTP 404: /keys/<encoded-base64-key>/signed-prekey
body: { message: 'Route Not Found', status: 404 }

Route probe showing the difference between slash-bearing and slash-free keys:

# slash-bearing key path: route is not matched
curl -i 'https://api.tiny.place/keys/h71YEo%2BjgSQJ8bG0msq%2FES3I4A0K9oKRA5Eqq3yY4Q8%3D/bundle'
# => 404, body length 42, {"message":"Route Not Found","status":404}

# slash-free unknown key path: route appears to be matched, but no bundle exists
curl -i 'https://api.tiny.place/keys/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%3D/bundle'
# => 404, body length 21, {"error":"not found"}

Evidence in this repo

The SDK uses path segments for key IDs:

  • sdk/typescript/src/api/keys.ts uses encodeURIComponent(agentId) under /keys/${...}/....
  • website/src/common/signal-messaging.ts delegates to encClient.enableEncryption() and then verifies via encClient.keys.getBundle(address).

There is already a workaround/comment in the Claude plugin acknowledging this exact class of failure:

  • sdk/plugin-claude/bin/tinyplace.mjs avoids generating wallets whose base64 public key contains / because %2F -> 404 breaks keys/messages routing.
  • sdk/plugin-claude/mcp/server.mjs has the same slash-free wallet generation comment.

That workaround helps newly generated Claude-plugin wallets, but it does not fix:

  • existing identities whose public key already contains /;
  • website-derived encryption identities, which can also derive a slash-bearing base64 key from the one-time signature;
  • the TypeScript CLI managed identity path, which can auto-generate slash-bearing keys.

Expected behavior

A valid base64 messaging public key should be usable as a keys/messages actor even if it contains /, +, or =. Key publication should reach the key handler and either succeed or return a key-specific/auth-specific error, not a route-not-found error.

Proposed fix

Prefer a backend/API routing fix, because website-derived encryption keys cannot reliably be made slash-free client-side.

Good options:

  1. Add route patterns that capture the key as an escaped/wildcard value and decode it inside the handler, so %2F is not treated as a path separator.
  2. Add a new compatible URL-safe route, e.g. /keys/by-public-key/{base64url}/bundle, /prekeys, /signed-prekey, /health, normalizing base64url back to canonical base64 before lookup/auth. Keep the current route for slash-free keys.
  3. Move the messaging key out of the path for write/read endpoints, e.g. query string or body/header actor, so arbitrary base64 never has to be embedded in a path segment.

Recommended tests:

  • key publish/get/health with a public key containing /;
  • key publish/get/health with + and =;
  • route-level test that distinguishes handler-level not found from router-level Route Not Found;
  • website enable-encryption flow with a derived key containing /.

Short-term client improvement:

  • TypeScript CLI should detect slash-bearing signer.publicKeyBase64 during enableEncryption/status and emit a targeted diagnostic instead of a generic 404.
  • Managed CLI/keygen could avoid slash-bearing keys for new wallets, matching the Claude plugin workaround, but that is mitigation only. It cannot repair existing or web-derived identities.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions