Security model

Cryptography, keys, transport, server, audit, disclosure.

Written for the reader who wants to audit claims, not marketing copy. If you spot something wrong, we’d rather you tell us than publish it; see the disclosure program below.

1. Cryptographic stack

Primitives are standards-track and widely reviewed. No home-grown constructs.

FunctionPrimitiveNotes
Message body AES-256-GCM 128-bit authentication tag. Fresh 32-byte CEK per message, fresh 12-byte nonce.
Key agreement X25519 ECDH Ephemeral sender keypair per recipient device per message. No key reuse.
Key derivation HKDF-SHA256 info = "disapp/wrap/v1"; salt binds ephPub || recipientPub.
Envelope signature ECDSA P-256 + SHA-256 Non-extractable CryptoKey on web; StrongBox / TEE-backed on Android.
Password wrap PBKDF2-HMAC-SHA256 600 000 iterations, 16-byte random salt, wraps the X25519 scalar and refresh token in IndexedDB.
Transport TLS 1.3 / 1.2 (AEAD ECDHE only)Mozilla intermediate profile. OCSP stapling on. HSTS preload.
Post-quantum MLS-v20 hybrid wrap (scheduled) Wire format will gain an alternate wrap; current field remains valid.

2. Per-message envelope (wire format)

Each sent message produces three opaque blobs on the wire:

ciphertext   = bodyNonce(12)  || AES-GCM(CEK, bodyNonce, plaintext)   // ct+tag
wrapped_cek  = 0x01 | count(u16 BE) | entry[count]
  entry      = device_id(16, UUID big-endian)
             | ephPub(32)
             | wrapNonce(12)
             | AES-GCM(wrapKey, wrapNonce, CEK)           // 32 + 16 = 48 bytes
  wrapKey    = HKDF-SHA256(
                 ikm   = X25519(ephPriv, recipientPub),
                 salt  = ephPub || recipientPub,
                 info  = "disapp/wrap/v1",
                 len   = 32)
sender_sig   = ECDSA-P-256-SHA-256(device_key, ciphertext || wrapped_cek)
    

One entry per recipient device, plus a separate entry for the optional escrow pseudo-device (00000000-0000-0000-0000-000000000000) when escrow is enabled. The server sees the envelope and the signature; it has no key with which to decrypt. The Android and web clients share this format byte-for-byte; cross-client decryption is verified by unit tests in both codebases.

3. Key management

Device signing keys (ECDSA P-256)

Generated on first sign-in, never leave the device. On Android, bound to StrongBox where available, TEE otherwise, with setUserAuthenticationRequired gating use behind the device lockscreen. On the web, generated with extractable=false and stored as the CryptoKey handle in IndexedDB — the browser enforces non-extractability at the API boundary.

X25519 private scalar

Raw 32 bytes, because SubtleCrypto doesn’t expose X25519 natively on every browser yet. On the web, wrapped with an AES-GCM key derived from the user’s password via PBKDF2 (600 000 iterations, 16-byte salt). On Android, sealed by the StrongBox-backed master key. In neither case is the scalar persisted in plaintext.

Refresh token

Rotated on every refresh, wrapped with the same password-derived key as the X25519 scalar, stored in IndexedDB. A stolen browser profile without the password yields a refresh token that can’t be unwrapped.

Access token

In memory only; never written to storage, never inside localStorage. Short-lived (minutes) and signed by the backend; the WebSocket handshake accepts it via a single-use query parameter since browsers can’t set headers on WS upgrades.

4. Account & session security

  • Passwords: Argon2id at registration, with parameters tuned for the minimum enrollment latency the backend can guarantee under load. No password-strength meter; a floor of 12 characters is enforced.
  • Second factor: TOTP (RFC 6238, SHA-1, 6 digits, 30s step) with 10 one-time backup codes issued at enrollment. Codes are never re-shown. Disabling TOTP requires a valid current code.
  • Duress PIN: A second PIN set at registration. Entering it unlocks a decoy view and silently marks the session as dur in the access-token claim, propagating the fact into the audit log without alerting a shoulder-surfer.
  • Session revocation: Revoking a device invalidates its refresh token immediately. Live WebSocket sessions are closed server-side; any in-flight requests fail 401 at the gateway.
  • Rate limiting: Token-bucket limiters on /v1/auth/register, /login, /refresh, and /users/lookup, enforced both in-process and at the nginx edge.

5. Server-side posture

  • Single Go binary (disappd) behind nginx 1.24+. No other languages, runtimes, or long-running daemons in the trust boundary.
  • TLS 1.3 + 1.2, ECDHE-only cipher suites, OCSP stapling, HSTS with preload; includeSubDomains; max-age=63072000.
  • Strict Content-Security-Policy: default-src 'self'; script-src 'self'; connect-src 'self' wss://disapp.io:8443; object-src 'none'; frame-ancestors 'none'. No third-party scripts. Inline styles allowed; inline scripts are not.
  • Server does not hold plaintext message bodies. It does not hold decryption keys. It does not hold the user’s password; only a PBKDF-derived verifier.
  • Audit log is append-only and hash-chained: each entry stores the SHA-256 of the previous entry, so any silent rewrite is detectable. Periodic root-hash snapshots are published for third-party verification.
  • Administrative commands (litigation hold, key rotation, etc.) emit audit entries with the acting admin’s UUID; no command bypasses this log.

6. Compliance escrow (optional)

When an organization opts in, every outgoing message gets an additional wrap entry keyed to an X25519 pseudo-device whose public key is the organization’s escrow key. Critically:

  • The escrow wrap is computed on the client. The server can verify the entry exists but cannot produce it.
  • The private escrow key lives outside the server — typically in an HSM held by the organization’s compliance officer.
  • Escrow is off by default and visible when on; a one-line indicator in the UI reflects the server’s /v1/crypto/escrow-pubkey response.
  • If you don’t want escrow, you don’t get escrow. There is no silent alternative.

7. Threat model

We design against:

  • A fully compromised server, including the operator. Plaintext doesn’t touch it. A malicious server can drop, delay, or reorder messages, but it cannot read them and it cannot forge signed envelopes.
  • A silently backdoored client, via the Android signing key or a compromised web build. Mitigated partially by hardware-bound device keys, by publishing build hashes, and by the hash-chained audit log. Fully mitigated only by reproducible builds — on the roadmap, not shipped.
  • Device seizure with password. Duress PIN mitigates coercion. StrongBox / TEE binding raises the bar on offline attacks against stolen databases.
  • Passive network observers. Transport is TLS 1.3 or 1.2 AEAD only; traffic analysis remains possible at the metadata layer (who talked to whom, when).
  • Active network attackers / malicious CAs. Certificates pinned by HSTS preload; CT logs monitored.

We do not currently design against:

  • Fine-grained traffic-analysis resistance at the metadata layer. Server sees who is online and who messages whom; a padded / cover-traffic mode is planned, not shipped.
  • Post-compromise security of individual messages. The current wire format is not forward-secret; MLS migration adds that.
  • Hardware-level attacks on the user’s device (cold-boot, side-channel, debug probes).

8. Disclosure program

We welcome reports and treat them professionally. Please read /.well-known/security.txt, then mail security@disapp.io.

  • Coordinated disclosure, 90-day standard timeline — adjustable by negotiation.
  • Public acknowledgement (with permission) in the release notes and on this page.
  • We do not threaten researchers with legal action for good-faith testing within the scope below. No CFAA cosplay.

In scope

  • The splash at disapp.io and the web SPA at disapp.io/web/.
  • The backend API at disapp.io:8443/v1/*.
  • The Android client’s cryptographic implementation.

Out of scope

  • Anything touching a third party we don’t run (we don’t have many; see Privacy).
  • Rate-limit stress tests against production. Please coordinate.
  • Social-engineering of our team members.
Last updated 2026-04-20 ← Back to home · security.txt