What BKey’s vault is
A normal secrets manager holds your API keys in someone else’s database. The operator has read access in principle; a breach gives the attacker everything at once; a pushed config change can rotate who sees what without you noticing. BKey’s vault flips that. The server stores ciphertext and metadata only. The decryption key material lives in the Secure Enclave / Keystore on the user’s phone, and plaintext appears on the server’s wire only long enough to relay an envelope the server itself cannot open — one that was sealed to an ephemeral public key the caller generated for that single access. The mental model is less “secret manager” and more “the user’s phone is a hardware vault, and the cloud is a mailbox for sealed envelopes.”The two flows
Everything the vault does is a composition of two flows: store once, access per-use. Both require the user to confirm with facial biometrics on their phone.Store
Access
Who this is for
Anyone running code that needs a secret, where “the secret lives on a server we trust” is an answer you’re tired of giving:- CLIs and scripts —
curl,psql,kubectlwith a token pulled from the vault at invocation time instead of$HOME/.env. - AI agents — a long-running agent can ask for a secret when it needs one, with the user confirming each access on their phone. No “give the agent read access to all my keys forever.”
- CI and one-shot jobs — nightly backfill, manual migration, break-glass access. Each run gets a fresh release tied to an approver.
- Team shared credentials — the team owner’s phone holds the vault, and each team member’s agent client asks for per-use access. Rotation = revoke the agent client; there’s no cached plaintext to chase.
The reference example
A working CLI lives atexamples/typescript/vault-access that stores and retrieves single-scalar secrets using @bkey/sdk.
store, open the vault in the BKey mobile app once so the phone generates its X25519 keypair and publishes the public half.
Store and access:
key field interoperate with the main CLI’s placeholder syntax:
The minimal pattern
Store
Access
Design rules
Always set a meaningfulpurpose. The phone shows it to the user next to the item name at approval time. “Nightly backfill — cron job on api-01” lets the user decide safely. “CLI access” does not — an attacker could trigger that and it would look normal.
One access request = one release. The returned ID and its underlying jti are nonces. Don’t cache the sealed ciphertext and reuse it; make a fresh request every time. The phone treats replays as suspicious and the server will reject them.
New ephemeral keypair per access. The forward-secrecy guarantee depends on the private key living only for the lifetime of one request. Reusing it across accesses means a single past-key compromise opens every previously sealed envelope.
Tight expiresInSecs. 300 seconds is a reasonable default — long enough to pick up a phone, short enough that a queued request can’t wait hours for the device to be set down unattended. Don’t extend this unless you have a specific reason.
Don’t log the plaintext. The SDK’s pollAccessRequest returns an object with e2eeCiphertext; the plaintext only exists after your decrypt step. Keep it that way — write it into an env var, a file with tight perms, or a memory buffer, and don’t let it escape to your normal log pipeline.
The server-side boolean isn’t trust. status === 'approved' from the poll helper means the CIBA-style state machine finished cleanly; it doesn’t mean the ciphertext is authentic. The authentication comes from AES-GCM’s auth tag inside the envelope. If decipher.final() throws, don’t fall back — refuse.
What happens when things go wrong
Denial or timeout.pollAccessRequest / pollStoreRequest throw with messages like “Access request was denied by the user.” or “Access request timed out after 120s.” Don’t retry blindly — the user declined for a reason, and a tight retry loop looks like an attack to the phone.
“No vault encryption key found.” The owner hasn’t opened the vault in the BKey app yet. The phone generates the vault keypair lazily on first use and publishes the public key; until then, there’s no key to encrypt against.
Decryption failure after approval. If the auth tag fails, treat it as tampering. Don’t output a partial buffer; raise and halt.
Repeated pushes with no response. The user’s device may be offline. The expiresInSecs clock still runs. Plan your retries with back-off, not hammering.
Beyond the example
- Combine with per-action approval.
vault-accesstells you what secret to use; gating the action that uses it behindbkey.approve()tells you whether to use it for this specific invocation. For high-stakes flows, do both — fetch the secret, then approve its use — so even a compromised vault client can’t silently make requests on your behalf. - Multi-field items. The example uses a single
keyfield.createStoreRequestaccepts an arbitrary JSON object; use separate fields forclient_id,client_secret,webhook_url, etc., and request specificfieldPaths on access so the phone releases only what’s needed. - Auditing. Every access and store produces a server-side record (who asked, when, what item, what purpose, approved/denied). This is your audit trail — it’s the whole point of replacing “password in a .env file” with per-use release.
References
@bkey/sdkon npm — vault + approval client- Reference example source
@bkey/clivault command — the canonical envelope format- X25519 key exchange (RFC 7748)
- AES-GCM authenticated encryption (NIST SP 800-38D)
- MCP integration guide — combine vault release with per-tool approval