How your secrets stay secret
Your passphrase never leaves
You type it client-side. ACP never receives it over the wire. Not on sign-up, not ever.
What we can and cannot see
Even someone with full database access cannot read your server secrets.
What actually lands on disk
The real shape of a stored row and a signed bridge request. No readable secrets, anywhere.
organization_credentials ├─ org_id "org_3kf9..." ← plain text ├─ argon2_salt "9a1c4f…e02b" (16 B) ← public, per-org ├─ wrapped_dek "AY8kQ2…Lf9w==" ← passphrase-locked, unreadable ├─ bridge_secret "AbZ3rT…0pQ==" ← AES-256-GCM(vault key) ├─ bot_token "Ac91mN…7xV==" ← AES-256-GCM(vault key) ├─ signing_privkey "Ad77kP…2sR==" ← AES-256-GCM(vault key) └─ cipher_version 1 ← plain text
Pull the database and you get this. Every secret column is AES-256-GCM scrambled text.
POST /acp-bridge/players HTTP/1.1 Authorization: Bearer acp_q_b6f1… ← rotatable, useless alone X-ACP-Timestamp: 1717372800 ← ±30 s window X-ACP-Nonce: 7f3a9c12-… ← 5-min memory, single-use X-ACP-Signature: ed25519:9c4f…a17e ← signs ts+nonce+method+path+sha256(body) # Replay the same bytes 31 s later → 401 stale_timestamp # Replay within window, reused nonce → 401 nonce_seen # Change one body byte → 401 bad_signature
Stealing the bearer token alone gets an attacker a 401 on every replay variation.
Leaked tokens cannot be replayed.
acp-query requests are signed per-request with an ed25519 key locked under your vault key. You paste the matching public key into your server's config. ACP never holds it unlocked.
The signing input includes a timestamp (±30s window) and a one-time nonce kept for 5 minutes. Even if a token is intercepted, replaying it without a fresh signature is rejected.
Threat model
An honest account of what this scheme protects and what it does not.
Technical specifications
Primitives and parameters, no hand-waving.