Trust and data
Braket holds a studio's most sensitive assets: your Steam publisher key, your normal Web API key, SMTP credentials, and all of your tournament data. This chapter documents how those are protected: the post-quantum credential vault that wraps your secrets, the signed and verifiable data export that proves how your data is stored, the hard per-tenant isolation, and the audit log. It is accurate to src/platform/vault.ts, src/platform/export.ts, and the platform schema.
The credential vault
Your Steam keys never sit in the database as plaintext. They are sealed in a post-quantum credential vault using an envelope scheme that is quantum-resistant by hybrid construction. The algorithm identifier stored with every secret is:
AES256GCM+HPKE(X25519+ML-KEM-768)
The envelope scheme
Each secret is protected in two layers:
- Data layer. The secret is encrypted with AES-256-GCM under a fresh 256-bit data encryption key (DEK) generated per secret. The ciphertext is stored as
iv|ct|tag(hex). - Key-wrapping layer. That DEK is itself encrypted with AES-256-GCM under a key-encryption key (KEK) derived by HKDF-SHA256 over the concatenation of two shared secrets: an X25519 ECDH exchange and an ML-KEM-768 (FIPS 203) encapsulation, both to the platform vault keypairs. The wrapped DEK is stored as
ephPub|kemCt|iv|ct|tag(hex).
Because the KEK mixes a classical (X25519) and a post-quantum (ML-KEM-768) shared secret, an attacker must break both to recover a KEK. This is what defends against harvest-now-decrypt-later: even an adversary who records ciphertext today and gains a quantum computer later still faces the classical half, and vice versa.
The GCM additional authenticated data binds each secret to its tenant and kind (<tenantId>|<kind>|v1), so ciphertext cannot be transplanted between tenants or secret kinds.
Vault keys and the master key
The platform holds one active keypair per kind (vault_x25519, vault_mlkem768, and mldsa65_signing for signing). The public halves are stored in the clear; the private halves are stored AES-256-GCM encrypted under a master key. The master key is VAULT_MASTER_KEY (64 hex chars = 32 bytes), held in the environment and KMS-backed in production. In real (non-mock) mode the master key is required: the vault refuses to fall back to a session-secret-derived key. Only in mock/dev does it derive a deterministic key from the session secret so hermetic tests work.
What a secret's lifecycle looks like
- Store / rotate (
putSecret): only ciphertext ever touches the database. Re-storing a secret rotates it: a new DEK, a new wrapped DEK, and arotatedFrompointer to the previous row. - Use (
openSecret): the DEK is unwrapped and the secret decrypted transiently for a single Steam call, stampinglastUsedAt. Plaintext is never persisted. - Metadata (
listSecretMeta): returns ciphertext-only metadata (kind, algorithm, timestamps, a 48-char ciphertext prefix and wrapped-DEK prefix, whether rotated), never plaintext. This is what the Trust page and export show. - Delete (
shredTenantSecrets): removes a tenant's live secret ciphertext (crypto-shredding). Note that because the platform vault keypairs do not rotate today, backups taken before deletion remain decryptable until they age out of retention; true per-tenant destroy-on-delete wrapping is tracked as hardening.
The secret kinds stored are web_api_key, publisher_api_key, and smtp_pass.
The signed data export and the Trust page
Every arena can produce a complete, signed data export, the "show me exactly how my data is stored" feature exposed on the Trust page. The export format is braket-export.v1 and it contains everything the tenant owns:
- Tenant record: slug, name, Steam AppID, status, custom domain, base URL, theme, admin SteamIDs, created-at.
- All data tables:
users,tournaments,registrations,groups,matches,result_reports,trophy_grants,invites,placements. - Secrets as ciphertext metadata only: the
listSecretMetaoutput, which proves the export cannot leak your keys because it never contains plaintext. - The audit log.
- A storage description: how isolation, secrets-at-rest, and backups work, in plain language.
How the signature works
The whole payload is serialized as canonical JSON (RFC-8785-style: object keys sorted recursively, so any third party reproduces the exact signed bytes regardless of key order), hashed with SHA-256, and the hash is signed with the platform's ML-DSA-65 (FIPS 204) key. The bundle carries integrity.sha256, and signature with alg: "ML-DSA-65", the signer's publicKeyHex, and the signatureHex.
Verifying an export offline
Studios can verify an export offline:
npx tsx src/dev/verifyExport.ts <export.json>
Verification is strict about the signer key. It recomputes the canonical JSON hash and checks it against integrity.sha256, then verifies the ML-DSA-65 signature, but only against a platform public key you supply out of band (from the Trust page or a published constant). The key embedded in the bundle is never trusted on its own, because an attacker could sign a forged bundle with their own keypair and embed their own public key. If the signer key does not match the trusted platform key, verification fails with signer key does not match the trusted platform key; a modified payload fails with payload hash mismatch; an invalid signature fails with ML-DSA-65 signature invalid.
The same ML-DSA-65 signing key is also the foundation for signing other attestations (for example event-chain heads) as those features land.
Per-tenant data isolation
Isolation is physical, not just logical: one SQLite database file per tenant. The platform database is only a tenant registry; a tenant's actual data lives in its own file (<slug>.db), with no shared tables between tenants. The export's storage description states this directly: "One SQLite database per tenant (<slug>.db), no shared tables with other tenants." Combined with the vault's per-tenant AAD binding, there is no path by which one arena's request can read another arena's data or secrets.
Backups are continuous Litestream replication to S3 with 30-day retention. Deleting a tenant removes its live secret ciphertext immediately; backup copies age out within the retention window.
Audit logging
Sensitive actions are recorded in a per-tenant audit log (audit_log): the tenant id, the acting admin's SteamID64, the action name, and metadata. It captures things like secret changes (secret.set), API-key creation and revocation (api_key.created, api_key.revoked), and result overrides. The audit log is included in the signed data export, so the record of who did what is itself covered by the export's integrity signature.
Summary of the guarantees
| Concern | Mechanism |
|---|---|
| Steam keys at rest | AES-256-GCM ciphertext, DEK wrapped by hybrid X25519 + ML-KEM-768 to the vault keys |
| Harvest-now-decrypt-later | Post-quantum ML-KEM-768 in the hybrid KEK; breaking one primitive is not enough |
| Master key handling | VAULT_MASTER_KEY required in real mode, KMS-backed; private vault keys sealed under it |
| Export integrity | Canonical-JSON SHA-256 signed with ML-DSA-65; verify offline against a pinned platform key |
| Secret exposure in exports | Secrets appear only as ciphertext metadata, never plaintext |
| Tenant isolation | One SQLite database file per tenant; per-tenant AAD binding on secrets |
| Accountability | Per-tenant audit log, included in the signed export |