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. We can't reconstruct a team secret without a quorum.

    Team secrets are split with Shamir's Secret Sharing in GF(28), T-of-N where T >= 2 and N >= T. There is no server-held extra share, no escrow, no per-secret service key. A database CHECK constraint on the secrets table enforces the two-mode boundary at the storage layer: (T=1, N=1) personal or (T>=2, N>=T) team. Below the threshold, the shares carry zero information about the AES key — that is a property of the polynomial, not a policy decision.

  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).

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 a distributed store (Upstash Redis or Cloudflare KV in production). The in-memory backend hard-fails at startup outside development, so a misconfigured deployment cannot ship with effectively no limit. Trusted edge headers (cf-connecting-ip, fly-client-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 three fully isolated stacks (Cloudflare, Scaleway, Fly.io with PostgreSQL on each). No shared compute, network, or control plane. A full outage of any one provider takes out one stack; the other two 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.