Security

Glassbreak holds nothing it could decrypt.

Even if our entire infrastructure were compromised, your secrets remain unreadable. The architecture below is how we keep that promise — and how you can verify it.

The zero-knowledge contract

Three guarantees, each backed by an implementation detail you can audit in the codebase.

  1. 1. Your passphrase never reaches our servers.

    In the browser, your passphrase is fed to scrypt (N=2^16, r=8, p=2) with a 16-byte random salt to derive a 256-bit AES key. That derived key unwraps your RSA + ML-KEM private keys locally. The passphrase is kept in JavaScript memory only, never serialised, and never sent over the wire — not at login, not at approval, not at decryption. Server-side password verification uses Argon2id (t=4, m=128 MB, p=2) with a salted hash. Two independent KDFs for two independent jobs.

  2. 2. Two secret modes, one storage layer.

    Glassbreak stores secrets in two distinct modes — personal and team — and the database enforces the boundary, not policy:

    • Personal (T=1, N=1)— a single "share" that is the AES key itself, wrapped to the owner's RSA + ML-KEM keys. Used for individual credentials a single user holds. No Shamir math is invoked; only the owner can decrypt, by virtue of holding their own private keys.
    • Team (T-of-N, T >= 2, N >= T) — the AES key is split with Shamir's Secret Sharing over GF(28). Recovery requires T independently approving members. Below the threshold the shares carry zero information about the key — a property of the polynomial, not a policy decision.

    A database CHECK constraint on the secrets table allows only those two combinations: (T=1, N=1) or (T>=2, N>=T). No in-between, no server-held extra share, no escrow, no per-secret service key. The team-secret quorum property follows from the storage shape, not from the application layer.

  3. 3. Approval is a relay, not a decryption.

    The approve endpoint accepts only {encryptedShareForRequester, shareIndex}. No passphrase, no plaintext share, no key. The approver decrypts their share locally with their own keys, re-encrypts it to the requester's public keys (RSA-OAEP + ML-KEM hybrid), and the server forwards the ciphertext verbatim. The supplied shareIndexis validated against the approver's actual secret_shares.share_index row, so a malicious approver cannot lie about which point on the polynomial they are providing. A unique constraint on (request_id, share_index) stops two approvers from submitting the same index. Migration 008 permanently removed the tables and code paths that had ever held server-decryptable material.

Cryptographic primitives

Every primitive listed here is traceable to a specific file in the repository. No marketing nouns, no "military-grade" — just the algorithms we actually call.

WhatHow we do it
Secret encryptionAES-256-GCM with a random 96-bit IV per encryption, via the browser WebCrypto SubtleCrypto API.
Key derivation (browser)scrypt (N=2^16, r=8, p=2), 16-byte random salt, 32-byte output. Memory-hard, ASIC-resistant. Used to wrap private keys locally.
Password hashing (server)Argon2id (t=4, m=131072, p=2), 16-byte salt, constant-time verify. A pre-computed dummy hash with a startup-random salt is used on unknown-email login paths to make response times indistinguishable.
Public-key encryption to a userHybrid: RSA-OAEP with an 8192-bit modulus and SHA-512, wrapped under ML-KEM-1024 (NIST PQC Level 5) via the Noble post-quantum library. A share survives a classical break of one half and a quantum break of the other.
Secret splittingShamir's Secret Sharing over GF(28), T-of-N with T>=2, N>=T. Personal-mode secrets (T=1, N=1) skip Shamir entirely — the lone "share" is the AES key itself, wrapped to the owner's public keys.
Authentication tokensJWT HS256, 15-minute access token TTL. Header carries kid for key rotation; iss and aud are pinned and validated on every request. Issuance is audit-logged with IP and user agent.
Refresh tokens48 random bytes (base64url), 30-day TTL, SHA-256 hashed at rest. Family-reuse detection: presenting an already- revoked refresh token revokes the entire family.
Cross-vertical sync authHMAC-SHA256. Canonical string binds method, path, sorted query, Unix timestamp (±5 min window), SHA-256 of body, and a 16-byte nonce. Replay cache enforces single-use within the window. No bypass on any production-like platform identifier.
CSRF protection (web)Double-submit token. 32-byte random gb_csrf cookie, rotated on login and refresh, echoed in the X-CSRF-Token header, compared constant-time. Origin/Referer pinned to an explicit allowlist (no wildcards).

Libraries we trust

We don't roll our own crypto. Every primitive above is implemented by a small set of vetted, narrow-scope libraries — chosen because they are auditable, pinned in our lockfile, and published by maintainers with a track record of disclosing and fixing issues openly.

LibraryWhereUsed for
@noble/hashesserver, web, mobileArgon2id (server-side password verification), scrypt (browser key derivation), SHA-256/SHA-512, HMAC, and randomBytes. Audit-friendly: zero-dependency, hand-written TypeScript, no native bindings.
@noble/ciphersmobileAES-256-GCM in environments where WebCrypto SubtleCrypto is unavailable or behaves inconsistently across React Native runtimes. Web uses crypto.subtle instead.
@noble/post-quantumserver, web, mobileML-KEM-1024 (Kyber, NIST FIPS 203) for the post-quantum half of every share-wrapping operation. Same maintainer family as @noble/hashes; pinned to the FIPS-203 spec, not an earlier draft.
joseserverJWT signing and verification (EdDSA / Ed25519, with a kid-based key map so we can rotate signing keys without re-deploying verifiers). Replaces a previous jsonwebtoken dependency.
@simplewebauthn/serverserver, webWebAuthn / FIDO2 registration and authentication ceremonies. Relying-party ID is derived from CORE_DOMAIN so a phishing origin cannot relay a registration; pinned in tests.
Node cryptoservertimingSafeEqual for constant-time hash comparison, HMAC-SHA256 for cross-vertical sync, and randomBytes for nonces and pepper material. The hardened, native OpenSSL-backed path.
WebCrypto SubtleCryptowebAES-256-GCM, RSA-OAEP encrypt / decrypt, key import and export. The browser's native crypto provider; nothing here touches a third-party JavaScript implementation when it can avoid it.
Wycheproof test vectorsCIGoogle's known-answer test vectors for HMAC-SHA256, Ed25519, and AES-GCM, vendored at a pinned commit and run on every build. A library change that breaks a vector fails CI before it ships.

Exact versions are pinned in each package-lock.json in the public repository. We do not auto-bump crypto dependencies — every change goes through a code review and a fresh Wycheproof run.

Post-quantum exposure

A sufficiently large quantum computer would break the classical public-key algorithms in widespread use today (RSA, Diffie-Hellman, ECDSA, EdDSA). It would not meaningfully weaken symmetric primitives like AES-256, HMAC, Argon2id, or scrypt. Our position is built around that distinction.

We treat "harvest now, decrypt later" as a real threat for anything an adversary could record off the wire today and retain. The table below is honest about what we protect against that threat and what we do not.

SurfacePQ-safe?Why
Secret content (the bytes themselves)YesEach share is wrapped under both RSA-OAEP-8192 and ML-KEM-1024. A quantum break of the RSA half still leaves Kyber intact, and Kyber is the standardised FIPS-203 KEM. The AES-256-GCM ciphertext underneath is itself PQ-safe by symmetric-strength margin.
Database at restYesCloud-provider AES-256 envelope encryption. Grover's algorithm reduces effective strength to ~128-bit, which is still well above today's computational feasibility ceiling.
Password hashesYesArgon2id is symmetric and memory-hard. Quantum search offers a quadratic speed-up at best, which a sensible Argon2 parameter set already accounts for.
Cross-vertical sync auth (HMAC-SHA256)YesSymmetric MAC; same reasoning as AES-256.
TLS in transitPartialToday's TLS handshakes use X25519 / P-256 ECDHE, which is not PQ-safe. An adversary recording our TLS sessions could decrypt them once a cryptanalytically relevant quantum computer exists. What they would learn: API metadata, short-lived JWT bodies, request shapes. What they would not: the E2E ciphertext inside, which is already wrapped under Kyber. We will adopt hybrid post-quantum TLS (X25519+Kyber768 / X-Wing) as soon as our edge providers ship it as a non-experimental default.
Session tokens (JWT signatures)PartialWe sign with EdDSA (Ed25519). A quantum break of Ed25519 would let an attacker forge access tokens. Mitigation: access tokens have a 15-minute TTL, so the forgery window is finite, and a forged token cannot decrypt anything because content keys are held client-side. Tracking the standards process for ML-DSA (Dilithium, FIPS 204) signing for the next rotation cycle.
Third-party signatures (Stripe webhooks, etc.)Vendor-boundWe verify webhook signatures with whichever algorithm the vendor uses (HMAC-SHA256 for Stripe, ECDSA for some others). Our PQ posture here moves when theirs does. None of these signatures protect content, only the integrity of provider-to-us notifications.

The single sentence: your content survives a quantum break; your session tokens have a finite harvest window. If you record a Glassbreak TLS session today and wait for a CRQC, what you decrypt is the envelope, not the message inside.

Where the keys live

Your browser holds your passphrase. The passphrase derives a key. The key unwraps your private keys. Your private keys unwrap your shares. Shamir recombines the shares into an AES key. The AES key decrypts the secret. Every arrow that crosses the network carries ciphertext only.

   BROWSER (you)                              SERVER (us)
   ─────────────                              ───────────
   passphrase                                 (never sees passphrase)
        │
        ▼ scrypt
   derived AES key
        │
        ▼ unwraps
   RSA + ML-KEM private keys ───┐
        │                       │
        ▼ encrypt new secret    │
   AES-256-GCM ciphertext       │
        │                       │
        ▼ Shamir split          │     ┌───────────────────┐
   shares[1..N]                 │     │ encrypted blob    │
        │                       │     │ encrypted shares  │  ─── store ──▶
        ▼ wrap each share       │     │   (per recipient) │
   RSA-OAEP + ML-KEM ────────── │ ──▶ │ audit metadata    │
                                │     └───────────────────┘
                                │
   ╔════════════════════════════════════════════════════════╗
   ║  APPROVAL FLOW — server-as-relay                       ║
   ╠════════════════════════════════════════════════════════╣
   ║                                                        ║
   ║  approver browser              server                  ║
   ║  ────────────────              ──────                  ║
   ║  unwrap own share                                      ║
   ║       │                                                ║
   ║       ▼ re-encrypt to                                  ║
   ║       requester's keys ────▶ store ciphertext verbatim ║
   ║                              validate shareIndex       ║
   ║                              against approver's row    ║
   ║                                                        ║
   ╚════════════════════════════════════════════════════════╝

   requester browser              server
   ─────────────────              ──────
   fetch shares       ◀────────── pure assembly:
   unwrap each                    encrypted blob
   Shamir combine                 + own share
   AES-GCM decrypt                + relayed shares
   plaintext secret               (no server decrypt)

What the server can and cannot see

Glassbreak protects secret material. Metadata is intentional — humans need to read audit logs and approval prompts. Here is the precise split, documented in ARCHITECTURE.md.

Plaintext to the server

  • Email address, display name
  • Secret name (in audit logs and notifications)
  • Stated reason on a decryption request
  • Team membership and role
  • Timestamps and IP for security-relevant actions
  • Encrypted ciphertext blobs (opaque)
  • Public keys (RSA SPKI, ML-KEM raw)

Never available to the server

  • Vault passphrase
  • Plaintext Shamir shares
  • Plaintext secret bytes
  • The AES-256 secret-encryption key
  • User RSA private key (kept encrypted at rest)
  • User ML-KEM private key (kept encrypted at rest)
  • Derived key from your scrypt KDF

A compromised database read would expose who is requesting what — but not the secret value itself. We chose readable audit trails over an encrypted-reason mode that would block compliance review; that tradeoff is documented and revisitable.

Defence in depth

The cryptographic core is the last line of defence, not the only one. Layers above it:

  • HttpOnly + Secure + SameSite=Strict cookies for the web access and refresh tokens. The refresh cookie is scoped to /api/auth/refresh only. The CSRF cookie is deliberately readable by JavaScript — that is the double-submit pattern, not a leak.
  • Strict Content Security Policy served from both the Caddy origin and the Fastly edge. object-src 'none', frame-ancestors 'none', base-uri 'self', form-action 'self', upgrade-insecure-requests. HSTS preload at max-age=63072000 (two years).
  • Per-IP and per-user rate limits backed by the same Postgres database that holds the rest of application state — counters live in the rate_limit_counters table per vertical. The in-memory backend hard-fails at startup outside development, so a misconfigured deployment cannot ship with effectively no limit. Trusted edge headers (fastly-client-ip, x-real-ip) take precedence over user-supplied X-Forwarded-For to stop spoofing.
  • DNSSEC, SPF/DKIM/DMARC, CAA on every zone. DMARC p=reject with strict alignment. Mail-sending DKIM keys are intentionally empty for the apex zones (we don't send from them), with the empty record hardening against unauthorised key publication. CAA pins issuance to the providers each edge uses; the iodef contact is security@glassbreak.io.
  • Audit log on every security-relevant action — failed logins, token issuance, share retrievals, decryption requests, approvals, impersonation, GDPR actions. Audit writes that fail are surfaced to the logger, not swallowed.
  • Multi-cloud redundancy across two fully isolated stacks today — AWS (Lambda + Neon Postgres) and Scaleway (Functions + Scaleway Serverless SQL) — with a third (Fly.io / GCP) on the way. No shared compute, network, or control plane. A full outage of any one provider takes out one stack; the others continue serving.

Coordinated disclosure

Email
security@glassbreak.io
PGP
Public key available on request to the address above.
Acknowledgement
Within 48 hours of receipt.
Disclosure window
Coordinated public disclosure targeted within 90 days of a fix shipping to production.
Bug bounty
No formal program yet. We will credit reporters by name with consent.
Policy
Full reporting scope and out-of-scope items in SECURITY.md.

What we don't claim

Security pages that only list strengths are not credible. Here is what Glassbreak is not, today.

  • ·Not SOC 2 or ISO 27001 certified. We have not committed to an audit timeline yet — when we do, this page will say so.
  • ·Not yet third-party penetration-tested. Internal threat modelling and code review feed every release; an external engagement is on the pre-launch checklist.
  • ·We do not store backups of your encryption keys. If you lose your team's quorum, the data is unrecoverable — by design. There is no support process that can restore it because no such key material exists on our side.
  • ·The web app currently stores JWTs in HttpOnly cookies; the previous localStorage fallback path is removed. CSRF middleware is mounted on every state-changing route.
  • ·Metadata (secret names, request reasons, audit timestamps) is stored in plaintext on the server so audit and notification flows can name the resource a row refers to. A future encrypted-metadata mode is on the roadmap; we will not pretend it ships today.

Stay Updated

Get product updates and security insights. No spam, unsubscribe anytime.

We respect your privacy. See our privacy policy.