# Vault (/docs/api/gateway/vault)

Zero-knowledge credentials vault: encrypted sync, agent broker, approvals and device keys.

codai Vault keeps your credentials encrypted on your own devices: clients seal every item (XChaCha20-Poly1305, keys wrapped with the X-Wing post-quantum hybrid) before upload, so `/v1/vault/*` stores and syncs ciphertext only. Agents never receive secret values. They see a catalogue of handles (`{{vault:<slug>}}`), ask for a policy decision (tiers `t0`-`t4` or `deny`), wait for your approval when the tier requires it, and call brokered HTTPS endpoints whose responses are redacted.

The SDKs expose the whole surface as `codai.vault` (TypeScript and Python), with device keys under `codai.vault.devices`.

## Device keys and step-up [#device-keys-and-step-up]

An approval proves who decided. Besides the v1 signature of your vault key, a decision can carry a v2 **proof** from a hardware-backed **device key** (Android Keystore on phone and watch, Windows Hello on desktop) or from a **passkey**. Device keys are public keys only; the private key never leaves the secure hardware of the device.

### Enrolment [#enrolment]

1. `POST /v1/vault/devices/challenge` returns a single-use `nonce` (32 random bytes, base64url) valid for 5 minutes.
2. The device creates its key and both your vault key and the device key sign the enrolment message:
   `codai-vault/device-enroll/v1|<device_id>|<kind>|<spki_sha256>|<nonce>`, where `spki_sha256` is base64url(SHA-256(SubjectPublicKeyInfo DER)).
3. `POST /v1/vault/devices` sends `device_id` (`android-…`, `wear-…` or `desktop-…`), `kind` (`android_keystore` or `windows_hello`), `name`, `surface` (`phone`, `wear` or `desktop`), `public_key`, `nonce`, `vault_signature`, `device_signature` and, when available, an `attestation`. The answer is `201 { device }`.

Re-enrolling the same `device_id` replaces its key. `GET /v1/vault/devices` lists your keys (revoked ones included), `PATCH /v1/vault/devices/{id}` renames one and `DELETE /v1/vault/devices/{id}` revokes it at once: its next proof answers 403 `device_revoked`.

All binary values are base64url without padding. Android signs with ECDSA P-256 SHA-256 (DER); Windows Hello signs with RSASSA-PKCS1-v1_5 SHA-256.

### Attestation kinds [#attestation-kinds]

| `attestation` | Meaning |
| --- | --- |
| `android-strongbox` | Google key attestation verified, key held in a StrongBox secure element. |
| `android-tee` | Google key attestation verified, key held in the TEE. |
| `tpm` | A TPM attestation of the Windows Hello key verified against the Microsoft TPM roots. |
| `hello-unattested` | A Windows Hello key without a TPM attestation. |

Android keys must send `attestation: { android_chain: [...] }` (leaf first); the attestation challenge is the nonce bytes and the attested key must equal `public_key`. Windows Hello keys may send `attestation: { tpm: { attestation, cert_chain } }`; without it the key is `hello-unattested`, with an invalid one the enrolment fails. Errors: 400 `invalid_nonce`, `possession_failed`, `attestation_required`, `attestation_invalid`; 403 `invalid_signature`; 409 `device_id_taken`; 503 `revocation_unavailable` (Google's revocation list could not be checked, retry later).

### Approval messages [#approval-messages]

- v1 (vault key): `codai-vault/approve/v1|<approval_id>|<action_hash>|<decision>|<scope>`, sent as `signature`.
- v2 (device key or passkey): `codai-vault/approve/v2|<approval_id>|<action_hash>|<decision>|<scope>|<device_id>`, sent as `proof: { type: 'device', device_id, signature }`.
- v3 (device key or passkey + number matching): `codai-vault/approve/v3|<approval_id>|<action_hash>|<decision>|<scope>|<device_id>|<preview_digest>|<match_number>`, sent with `match_number` next to `proof`. `preview_digest` is base64url(SHA-256) of the canonical text of `request.preview` (`codai-vault/preview/v1`, then `title`, `agent`, `mode`, `host`, `action`, `purpose`, `task` as `key=value` lines, with `\`, newline and CR escaped), so the signature covers exactly what the approver saw.
- passkey: the WebAuthn challenge is base64url(SHA-256(the v2 message with device id `passkey`)), sent as `proof: { type: 'passkey', response }`.

Send at most one of `signature` and `proof`. `step_up_acr` is still accepted but ignored: a client-claimed step-up proves nothing.

### Rules by tier [#rules-by-tier]

- **Deny** needs no proof (a notification Deny button is enough).
- **T3 approve** accepts a valid v1 signature, device proof or passkey proof.
- **T4 approve** requires a device or passkey proof; a v1 signature answers 403 `step_up_required`. Every attestation kind above qualifies today.
- **Wear is T3-only**: a watch key cannot be attested as biometric-bound, so a proof from a `wear` device on a T4 approval answers 403 `wear_t3_only`.
- **Number matching**: every T3/T4 request gets a 2-digit `match_number`, returned only to the agent (`/vault/use`, `/vault/http`, `/vault/resolve`). The approver sees `request.preview` and three `request.match_choices`, never the number. A T4 approve on such a request needs `match_number` with a v3 proof: none answers 403 `match_required`; a wrong number denies the request at once and answers 403 `match_mismatch`. T3 approvals and denies still accept v1/v2.

Other proof errors (403): `invalid_signature`, `unknown_device`, `device_revoked`, `passkey_invalid`.

<OpenAPIPage document="./openapi/en/gateway.yaml" operations={[{path: '/v1/vault/devices/challenge',method: 'post'},{path: '/v1/vault/devices',method: 'post'},{path: '/v1/vault/devices',method: 'get'},{path: '/v1/vault/devices/{id}',method: 'patch'},{path: '/v1/vault/devices/{id}',method: 'delete'},{path: '/v1/vault/approvals/{id}/decide',method: 'post'}]} />
