Confidentiality

Your data is unreadable to anyone but you

Encryption is not a checkbox. It is a chain of cryptographic primitives — at rest, in transit, in proxy headers, in stored credentials, in error messages — and the chain is only as strong as its weakest link. We hold every link to a published, peer-reviewed standard.

Executive summary

High-risk fields use AES-256-GCM at rest. Transit uses TLS 1.3 with HSTS. Stored credentials, OAuth tokens, signing certificates, and webhook secrets are individually encrypted at the field level — a database dump alone does not yield usable secrets.

We also harden the layers people forget: client IPs are extracted from the rightmost public hop (never the spoofable leftmost), Unicode inputs are normalized against homoglyph attacks, and file paths are containment-checked.

Our commitments

Five rules across the cryptographic stack

01

No proprietary crypto, ever

Every primitive is a published, peer-reviewed standard with a NIST or IETF reference. We do not invent ciphers, hash modes, or key derivation functions.

02

Field-level encryption for high-risk values

Signing keys, OAuth tokens, webhook secrets, SAML private material — each carries field-level AES-256-GCM encryption and redact: true on the schema. They never reach logs, telemetry, or Inspect output.

03

The transport edge is the security boundary

TLS 1.3 with HSTS preload, certificate pinning on mobile, and trusted-proxy IP extraction. The rightmost public IP is the real client; the leftmost is whatever the attacker wrote in a header.

04

Inputs are sanitized at every layer

Unicode homoglyph normalization, atom-table protection on JSON deserialization, path traversal containment, and open-redirect URL validation. Each is its own audited module.

05

Post-quantum primitives are compiled in, with production wiring planned

CRYSTALS-Kyber and CRYSTALS-Dilithium primitives are present in our Rust NIF. Production callers are on the roadmap; no seals or credentials are signed with PQC today.

Implementation — at rest

Encryption at rest

Symmetric cipher AES-256-GCM NIST SP 800-38D
Authenticated encryption AEAD with 128-bit tag Tamper detection built in; no separate MAC needed
IV / nonce Random per record Generated with a CSPRNG; never reused
Implementation Erlang :crypto + Rust NIF aes-gcm, sha2 (Rust); :crypto (Erlang OTP) for field-level paths
Database storage Encrypted before INSERT No plaintext for high-risk fields in WAL, replication, or backups
Field-level encryption Per-record CredentialEncryption + CertificateEncryption with redact: true
Object storage Server-side AES-256 Provider-managed at the storage layer (S3 / Azure / etc.)

Implementation — in transit

Encryption in transit

Protocol TLS 1.3 RFC 8446 — 0-RTT disabled
Cipher suites AES-256-GCM, ChaCha20-Poly1305 Forward-secret only; no static RSA
HSTS max-age 63072000; includeSubDomains Strict-Transport-Security enforced at the edge
Certificate Let's Encrypt (auto-renew) ACME via Cloudflare; rotation before expiry, no manual touch
Edge Cloudflare proxy + Fly origin Origin only accepts traffic from Cloudflare ranges
HTTP/2 + HTTP/3 Both supported QUIC for mobile-network resilience
Mobile pinning Certificate pin Native iOS/macOS/Android clients pin our cert chain
Webhook outbound HTTPS only HTTP destinations rejected at endpoint creation

Implementation — input hardening

Where we don't trust user input

Encryption protects what we store. Input hardening protects what we accept.

Client IP extraction Rightmost public hop CF-Connecting-IP > Fly-Client-IP > X-Forwarded-For (rightmost public)
Atom safety String.to_existing_atom only Untrusted input never creates atoms; protects against atom-table exhaustion
Unicode normalization NFC + homoglyph filter Mixed-script names with confusable characters are flagged on input
Path containment Realpath check Every file operation verified inside the expected base directory
Redirect validation Allow-list per origin No open redirects; relative paths only for in-app redirects
URL validation Scheme + host check javascript:, data:, file:, and unknown schemes rejected
File magic bytes Verify on upload A "PDF" with the wrong magic bytes is rejected before storage

The full picture

What is built, what is being built, and what we chose not to build

Live today

Field-level AES-256-GCM at rest

Live

Credentials, MFA secrets, signing-key material, and webhook secrets are encrypted before INSERT.

TLS 1.3 with HSTS

Live

0-RTT disabled. Forward-secret cipher suites only. Origin restricted to Cloudflare proxy ranges.

Trusted-proxy IP extraction

Live

Rightmost public hop in X-Forwarded-For; CF-Connecting-IP and Fly-Client-IP take precedence. Centralized in IpExtraction module — never parsed manually elsewhere.

Atom-table protection on every JSON boundary

Live

String.to_existing_atom on all untrusted input. Atom-exhaustion DoS is impossible by construction.

Unicode homoglyph normalization on user-visible strings

Live

Mixed-script display names are flagged before they reach the directory or sealed documents.

File magic-byte verification on upload

Live

Mislabeled file uploads are rejected at the boundary. A renamed .exe cannot pose as a PDF.

Building now

Certificate Transparency monitoring

Building now

Automated alerts on any certificate issued for our domains by any CA. Detects mis-issuance within hours.

Mobile certificate pinning

Building now

Public Key Pinning on the iOS/macOS/Android native clients to prevent CA-substitution attacks on hostile networks.

Roadmap

Hybrid post-quantum TLS

Roadmap

X25519MLKEM768 (TLS 1.3 hybrid key exchange) once Cloudflare and Fly support is generally available.

Production wiring for post-quantum signatures

Roadmap

CRYSTALS-Kyber and CRYSTALS-Dilithium primitives are present in the Rust NIF. Schema columns and signing callers are not yet wired.

Encrypted client-side telemetry

Roadmap

If we ever ship product analytics, the payload is encrypted client-side and we never see PII or document content.

Per-organization encryption boundary

Roadmap

Org-scoped data encryption keys so a compromise in one tenant cannot decrypt another tenant. Already true for documents; expanding to all org-scoped data.

Considered & rejected

DANE (DNS-based authentication of named entities)

Considered & rejected

DANE is the right idea, but DNSSEC adoption is too low and operationally fragile to build security on.

Why we rejected it: DANE depends on DNSSEC, which depends on every resolver in the chain implementing it correctly. In practice, that's still a small minority. We use Certificate Transparency monitoring instead — it gets us the same detection of CA mis-issuance with broader real-world coverage.

Customer-supplied symmetric ciphers

Considered & rejected

If the customer asks for a non-standard cipher, the right answer is "AES-256-GCM," not "yes, let me roll that for you."

Why we rejected it: every "we accept your custom crypto" is a way to ship a worse default. The customers who actually need a different cipher (zero, in our pipeline) can wrap our envelope themselves before upload. We do not multiply the implementation surface to accommodate hypothetical asks.

TLS 1.2 fallback

Considered & rejected

TLS 1.3 is universally supported by clients we care about. Fallback paths are attack surfaces.

Why we rejected it: TLS 1.2 has known weaknesses (CBC padding oracles, downgrade attacks) and unbounded cipher-suite negotiation. Modern browsers, mobile OSes, and language stdlibs all speak TLS 1.3. Forcing 1.3 means saying no to a tiny long-tail of clients to keep the security floor high for everyone else.

Self-signed certificates anywhere in the production path

Considered & rejected

A self-signed cert is a "trust me" — exactly the property our Public Key Infrastructure exists to remove.

Why we rejected it: even for "internal" mTLS we use a customer-rooted CA chain, not self-signed. The whole point of PKI is that trust is delegated to a verifiable third party. Self-signed shortcuts are how internal-tools secrets escape into production.

Hashing user passwords with SHA-256

Considered & rejected

A fast hash on a low-entropy input is exactly the wrong tool. We use Bcrypt.

Why we rejected it: SHA-256 is fast — that's the feature for content hashing and the bug for password storage. Bcrypt has a tunable work factor specifically designed to slow down brute-force attempts. Different problem, different tool.

Compliance mappings

Controls this surface satisfies

SOC 2 CC6.7

Data Transmission and Disposal

TLS 1.3 in transit; AES-256-GCM at rest; cryptographic erasure on delete

ISO 27001 A.10.1.1

Cryptographic controls

NIST/IETF-standardized primitives; key sizes documented

ISO 27001 A.13.1.1

Network controls

HSTS preload; trusted-proxy IP extraction; HTTPS-only webhooks

HIPAA §164.312(e)(1)

Transmission Security

TLS 1.3 with forward-secret cipher suites only

HIPAA §164.312(a)(2)(iv)

Encryption and Decryption

AES-256-GCM at rest with field-level keys for high-risk data

GDPR Art. 32

Security of processing

State-of-the-art cryptography; pseudonymization where appropriate

For compliance teams

Questions you don't need to call to ask

Where are the encryption keys stored?
Field-level AES-256-GCM uses module-scoped keys held outside the database in the host secret store (Fly secrets in production). A database read alone does not yield usable secrets.
Is post-quantum cryptography in use today, or is this marketing?
Not in use today. CRYSTALS-Kyber and CRYSTALS-Dilithium primitives are compiled into our Rust NIF and unit-tested in isolation. Production callers (seal signing, credential manifests) are on the roadmap. We will update this page when PQC is wired into a signing path.
What happens cryptographically when an organization is deleted?
Field-level secrets associated with the organization are deleted as part of account teardown. Sealed documents are retained per the soft-delete policy described in our Privacy Policy.
How do you protect against key compromise?
Field-level encryption for high-risk secrets means a database read alone is not enough; the attacker also needs the encryption key. Keys live outside the database in the host secret store. Rotation is supported and audit-logged.
Why do you extract the rightmost public IP from X-Forwarded-For instead of the first?
The leftmost IP is whatever the client wrote — it can be spoofed trivially. The rightmost public IP is the last hop the proxy chain actually saw. Using the leftmost for rate limiting or audit means an attacker controls their own log entry. We centralize this in one IP extraction module and forbid anyone else from parsing X-Forwarded-For.
Can we audit the encryption implementation?
Yes. The Rust NIF source (native/crypto/) is in the repository and uses the rsa, aes-gcm, sha2, and pqcrypto-* crates — audited, open-source primitives. We do not roll our own block-cipher modes or KDFs. Customer security teams can review under NDA.

Encryption that holds up to your auditor and your future

Every primitive on this page is a published standard. Every claim is verifiable in source.