Security and threat model
What we trust, and what we cannot. A plain-English read of the OrangeRails security model — what's in our threat model, what's out, and what assumptions a customer has to accept to use the service.
The one-sentence summary
OrangeRails holds ciphertext only. We cannot read your financial data, by the architecture, not by promise. A breach of our database leaks opaque bytes, not bank logins or transactions.
What we protect against
| Threat | How we handle it |
|---|---|
| Server-side breach (read). An attacker copies our database. | All credentials, transaction memos, counterparty handles, and (on Stealth Sync sources) addresses + amounts are AES-256-GCM encrypted in the user's browser before they reach us. Server-side decryption is impossible without the user's vault password. |
| Operator insider abuse. An OrangeRails employee tries to peek at user data. | Same answer. We can't decrypt either. Operator access to the production database doesn't yield plaintext. |
| Network MITM. An attacker on the path between user and server. | All traffic is HTTPS with TLS 1.2+ over HTTP/2 or HTTP/3, certs via Let's Encrypt. The Connect widget verifies the parent window's origin before accepting an INIT message. |
| Compromised upstream source. Coinbase, Blink, etc., gets breached. | The credential we hold for that source is encrypted at rest, so the attacker can't pivot from our DB to other accounts. But we can't undo the upstream breach itself. |
| Replay attacks. An old credential blob is replayed against our edge function. | Credentials are bound to a single user + subaccount via RLS. Replay against a different subaccount fails. |
| Plaid-style data monetization. Any business model that requires us to read your data. | Structurally impossible. No analytics on transaction contents. No "anonymized" dataset to sell. The only thing we know about a user is: when they synced, which provider, how many transactions (count only). |
What we don't protect against
| Threat | Why it's out of scope |
|---|---|
| Compromised user device. Malware on your machine reads the vault password. | If you lose your machine, you lose. We can't help with endpoint security. |
| Compromised vault password. Phished, brute-forced, or shoulder-surfed. | Same. Recovery via Shamir 2-of-3 (Phase 1.5a) limits blast radius but doesn't prevent it. |
| Upstream sources collecting their own data. Plaid, Coinbase, Strike see what their own APIs return. | Not our system; we never see what they see, but we also can't stop them from seeing it. |
| Statistical analysis of timing or metadata. "User synced 3 times today" leaks behavioral signal. | We log minimum metadata for billing + ops. Documented in the privacy policy. |
| Quantum attacks on stored ciphertext. Long-term threat. | PR #20 shipped hybrid X25519 + ML-KEM-768 + ML-DSA-65 for the keys that matter. Per-record AES-256-GCM is still classical; post-quantum work continues in Phase 4. |
Cryptographic primitives
| Layer | Primitive |
|---|---|
| Vault password derivation | Argon2id (parameters: 64 MiB memory, 3 iterations, 4 parallelism — tuned to ~1s on a 2026 laptop) |
| Master encryption key wrap | Hybrid X25519 + ML-KEM-768 |
| Per-record encryption | AES-256-GCM with a fresh 12-byte IV per record |
| Co-admin signatures | ML-DSA-65 |
| Transport | TLS 1.2+ via Cloudflare Pages |
Key files: src/lib/vault.ts (KDF + AES wrappers), src/lib/crypto-fields.ts (per-table helpers).
What's in the database, by table
| Table | Plaintext columns | Ciphertext columns |
|---|---|---|
users |
id, email (auth.users) | — |
subaccounts |
id, platform_id, external_user_id, created_at | — |
connections |
id, subaccount_id, provider_type, created_at | encrypted_credentials, encrypted_label |
transactions |
id, connection_id, external_id, timestamp, direction, type, status | encrypted_payload, encrypted_counterparty, encrypted_description |
source_wallets |
id, connection_id | encrypted_label |
customer_vault_meta |
customer_id, salt, KDF params, KEM/sig public keys | vault_verifier_ciphertext, enc_mek_ciphertext, kem_secret_wrapped, sig_secret_wrapped |
customer_recovery_shares |
customer_id, share_index, threshold metadata | share_ciphertext |
Operators can see provider types, counts, and timestamps. They cannot see amounts, counterparties, descriptions, or credentials.
Stealth Sync (on-chain Bitcoin) specifics
Stealth Sync adds an additional layer for xpub-watch sources:
- The xpub itself never leaves the browser. It's sealed before storage.
- Address derivation happens in WASM inside the browser.
- Block filter matching (BIP 158) also happens in the browser; filter files come from a public CDN with no auth and no logs.
- Block bodies come from our own Bitcoin Core node, not a third party.
- Each transaction OR persists is a sealed envelope; the server cannot tell which addresses appeared in which transactions.
See Stealth Sync — Architecture for the full deep dive.
Bug bounty + disclosure
- Security email: [email protected]
- PGP key: published at https://orangerails.com/.well-known/security.txt (Phase 1)
- Coordinated disclosure: 90 days. We acknowledge within 48 hours, ship a fix or mitigation within 30 days for high-severity, 90 days otherwise.
- Bug bounty program: planned for Phase 2 (β). HackerOne or Bugcrowd, $5,000/quarter initial budget.
Don't open public GitHub issues for security vulnerabilities. Email first.
Independent reviews
- Cryptographic implementation review — planned for Phase 2 (β). Target firms: Trail of Bits, NCC Group, Cure53. Budget $30k–$80k.
- SOC 2 Type 1 — planned for Phase 3 (1.0).
- SOC 2 Type 2 — planned post-1.0.
The point of these reviews is that you don't have to trust our blog post — you get a third-party attestation. Reports will be published with findings + remediations.
What customers should do
- Pick a strong vault password. It's the root of the trust hierarchy. We cannot reset it.
- Save the Shamir recovery share (PDF/QR) we generate at vault setup. If you lose your password and don't have the recovery share, your encrypted data is gone.
- Keep your upstream credentials scoped read-only. Strike, Coinbase, etc. let you generate API keys with read-only scopes. Use them. OR doesn't need write access to anything.
- Self-host if you can't trust the operator. Apache 2.0, run it yourself, eliminate even the residual ops trust.