Encryption
This page answers: what encryption protects my data at each step of a BKey integration, and what key lives where? BKey’s guarantee is that sensitive material — approval tokens, vault secrets, payment payloads — is either protected end-to-end to the user’s phone, or signed by a key that never leaves the phone’s secure enclave. The backend is designed to be a courier, not a custodian.One-slide summary
| What | Algorithm | Where the key lives |
|---|---|---|
| Transport (every HTTP request) | TLS 1.2+ with modern cipher suites | Terminating LB / TLS-offload at the edge |
| Approval tokens (CIBA JWTs) | EdDSA (Ed25519) | Private key at BKey issuer; public key served via JWKS |
| Vault at rest — plaintext | Never stored | Only in your CLI process memory after decryption |
| Vault storage (CLI → phone) | X25519 ECDH → AES-256-GCM, v2 envelope | Vault private key in iOS Secure Enclave / Android Keystore (biometric-gated) |
| Vault access (phone → CLI) | X25519 ECDH → AES-256-GCM, ephemeral-ephemeral | CLI generates ephemeral keypair per request |
| Challenge binding (anti-MITM) | SHA-256 + secp256k1 signature | Device signing key (biometric-gated on-device) |
| x402 payment payloads | EIP-3009 USDC authorization, secp256k1 signature | On-device wallet key; server encrypts at-rest with AES-256-GCM before DB write |
| Ciphertext at rest in DB | AES-256-GCM with server DEK (where applicable) | Server-held DEK, rotated out-of-band |
- Approvals are signed, not encrypted. The signature is the proof.
- Vault fields are end-to-end encrypted between your CLI and the user’s phone. The backend sees only ciphertext.
- Every on-device key is biometric-gated — facial authentication is required to use it.
The developer integration flow — where encryption kicks in
Say you’re building an agent that reads a vault secret and uses it to deploy to production. Here’s every boundary where encryption matters:Step-by-step
1–3. Approval request (CIBA):bkey.approve() (TypeScript) or client.approve() (Python) starts here. Traffic to BKey is TLS 1.2+. BKey pushes a prompt to the phone; the phone signs the consent with the device’s Ed25519 key (gated on facial biometrics) and returns the signature through the same channel. BKey wraps it in a JWT signed by the issuer’s Ed25519 key.
4. Token verification:
Never trust the JWT on your side until you call verifyToken() from @bkey/node (or equivalent). That library fetches BKey’s public keys from /oauth/jwks — cached 5 minutes, refetched on kid miss — and verifies the EdDSA signature, iss, exp, aud, and required scopes. No HS256 confusion, no alg: none. See the verifyToken reference.
5. Vault access request:
Your SDK generates an ephemeral X25519 key pair (one per request). It sends the public key in the access request. The request is bound to challenge || itemName || purpose || ephemeralPubKey so a compromised backend can’t swap the key mid-flight.
6. Phone re-encrypts to your ephemeral key:
After facial biometrics unlock the vault key, the phone runs X25519 ECDH with your ephemeral public key, derives aesKey = SHA-256(sharedSecret), and encrypts the plaintext with AES-256-GCM. The ciphertext format is:
bkey vault access / bkey wrap decrypt in-process. The plaintext is never written to disk. With bkey wrap, it’s passed to the child process as an env var and discarded when the process exits.
8. Use the secret.
Vault storage — the other direction
Storing a secret goes through the same envelope in reverse:- The phone registers a long-lived X25519 public key with the backend (RFC 7748 §6.1; all-zeros rejected).
- The CLI generates an ephemeral X25519 keypair per store operation.
- CLI does ECDH with the phone’s public key, derives AES-256-GCM key via SHA-256, encrypts the fields as JSON.
- The
version(0x02)byte lets future envelopes coexist with old ones. - The backend only sees ciphertext; the phone does the matching decrypt.
Challenge binding — why we can’t be MITM’d
For vault access specifically, the signed consent is over:x402 payment payloads
Agent payments use a different path — the value at stake is an on-chain USDC transfer, not a secret.- Your agent calls
bkey.authorizeX402Payment({ amountCents, recipientAddress, chainId }). - If within the per-agent spending limit → auto-approved, BKey returns an EIP-3009
TransferWithAuthorizationpayload signed with the user’s secp256k1 wallet key. - If above the limit → CIBA biometric prompt. After the user approves, same signed payload, same format.
- Signed payloads are stored at rest encrypted with AES-256-GCM using a server-held DEK. This is defence-in-depth — the payload is already short-lived and publicly verifiable on-chain once submitted — but it means a DB leak doesn’t leak spend authorizations.
- Validity window: < 5 minutes. Front-run resistance comes from EIP-3009’s
ReceiveWithAuthorizationvariant where supported.
What the backend doesn’t have
A quick checklist for security reviewers:- ❌ No plaintext vault values at rest or in flight
- ❌ No cleartext CIBA signing key (device keys stay on device)
- ❌ No iCloud/Google Drive sync of on-device keys
- ❌ No bearer tokens with long-lived power — CIBA tokens are single-action, scoped, and JTI-trackable for replay protection
- ✅ Ciphertext, timestamps, DIDs, scopes, audit metadata
Transport details
- TLS 1.2+ on every endpoint, with modern cipher suite selection terminated at the edge.
- HSTS enforced on
api.bkey.id. - No plaintext fallback — HTTP connections are redirected to HTTPS.
Token format reference
CIBA and OAuth tokens from BKey are JWTs with:@bkey/node’s verifyToken().
Further reading
- CIBA flow — the approval protocol in detail
- MCP integration — why server-side
verifyToken()is the load-bearing check - x402 payments — agent payments end-to-end
- CLI vault envelope decrypt:
typescript/packages/cli/src/commands/wrap.ts - SDK access-request flow:
typescript/packages/sdk/src/poll.ts - Token verification:
@bkey/nodeon npm