A technical analysis of the cryptographic architecture underlying Balon's privacy model — covering hierarchical key management, the zero-knowledge storage boundary, TLS channel security, service authentication, and formal threat modeling across adversarial classes.
"The meaningful distinction in privacy architecture is not policy but capability. A system that stores only ciphertexts and wrapped keys — and for which the unwrapping credentials are never transmitted to or retained by the server — cannot decrypt under any operational circumstance, including legal compulsion. This property is mathematical, not procedural."
— Security Architecture Research, Balon AI
Abstract
We describe the zero-trust architecture (ZTA) deployed by Balon for user data confidentiality. The system implements client-side end-to-end encryption with a five-layer hierarchical key management scheme, a formally defined zero-knowledge storage boundary at the server, TLS channel encryption for all transport, and a hash-verification model for cross-service API authentication. The server is architecturally incapable of decrypting stored content under any operational condition — including database breach, insider access, and legal compulsion — because it retains neither plaintext nor the unwrapping credentials for stored key material. All cryptographic operations execute within the user's browser process using the Web Crypto API (SubtleCrypto). The system was designed to meet the constraints of a browser-only deployment, which eliminates multi-device key synchronization complexity while preserving the full zero-knowledge property.
1. Introduction and motivation
1.1 The implicit trust model of cloud AI
Contemporary cloud-based AI systems operate under an implicit trust architecture: the service provider holds cryptographic access to all user content stored on its infrastructure. For users transmitting sensitive material — clinical records, attorney-client communications, proprietary research, strategic planning documents — this creates an unmitigated exposure: the provider's security posture, employee integrity, legal jurisdiction, and future policy decisions all become components of the user's effective privacy model. Any of these factors changing adversely collapses the user's privacy guarantee regardless of contractual assurances.
1.2 Legal compulsion as a structural risk
Service providers operating across jurisdictions face escalating legal obligations for content disclosure: warrants under domestic criminal procedure, civil subpoenas, national security letters (which may be accompanied by non-disclosure requirements), and administrative processes under sector-specific regulation. The standard industry response — 'we will fight compelled disclosure' — is a procedural commitment that can fail. A zero-knowledge architecture responds differently: the operator cryptographically cannot produce plaintext because it never possessed the decryption keys. This converts a policy commitment into a technical property, removing the operator from the relevant threat surface entirely.
1.3 Constraints of the deployment environment
Implementing zero-knowledge encryption in a browser-based AI platform introduces constraints not present in messaging applications or password managers. LLM responses arrive as streaming token sequences that must be rendered incrementally before the complete content is available for encryption. The Web Crypto API's SubtleCrypto interface operates on complete buffers, requiring a streaming-to-buffer conversion before persistence. Additionally, Balon operates exclusively in browser environments (no mobile apps, no desktop clients), which eliminates the complexity of multi-device key synchronization, push notification encryption, and cross-platform cryptographic library compatibility — while constraining the trust boundary to the browser process.
2. Cryptographic primitives
2.1 Symmetric encryption
AES-256-GCM (Advanced Encryption Standard, 256-bit key, Galois/Counter Mode) provides authenticated encryption with associated data (AEAD) for all content. GCM mode produces a 128-bit authentication tag with each ciphertext, providing simultaneous confidentiality and ciphertext integrity. A 96-bit random nonce (IV) is generated per encryption operation using the browser's CSPRNG (crypto.getRandomValues). This prevents nonce reuse under the standard assumption that the CSPRNG is cryptographically secure, which holds for all modern browser implementations of the Web Crypto API.
2.2 Asymmetric key encapsulation
RSA-4096 with OAEP padding (RSA-OAEP, SHA-256 hash) is used for per-participant encryption of conversation keys. Each user maintains an RSA-4096 key pair; the public key is stored server-side in plaintext (by design — public keys contain no secrets); the private key is stored server-side wrapped under the user's MEK via AES-256-GCM. RSA-OAEP is semantically secure under the RSA problem assumption and provides ciphertext indistinguishability under chosen-plaintext attack (IND-CPA). The 4096-bit modulus provides a security margin against near-term advances in integer factorization.
2.3 Key derivation
PBKDF2-HMAC-SHA256 with a minimum of 100,000 iterations and a 128-bit per-user random salt derives a 256-bit Key Encryption Key (KEK) from the user's password. The iteration count is calibrated to require approximately 200–500ms on typical consumer hardware, imposing a computational floor on brute-force attacks. The per-user salt prevents precomputation attacks across users. The NIST-recommended minimum for PBKDF2 with HMAC-SHA256 as of SP 800-132 is 600,000 iterations for password-derived keys protecting sensitive data; the current deployment at 100,000 iterations meets the older standard and will be raised in a subsequent release, or replaced with Argon2id which provides memory-hardness resistance to GPU-accelerated attacks.
2.4 API key verification
Service-to-service API key verification uses SHA-256 hashed with base64url encoding without padding — matching the Better Auth defaultKeyHasher implementation to ensure hash compatibility between the Next.js provisioning layer and the Python FastAPI verification layer. SHA-256 is not an appropriate primitive for password hashing (it lacks a work factor), but is appropriate here: API keys are high-entropy random values (CSPRNG-generated 256-bit strings), for which brute-force preimage attacks are computationally infeasible regardless of the hash function's iteration cost.
2.5 CSPRNG and Web Crypto API trust assumptions
All random values — PBKDF2 salts, AES-GCM nonces, RSA key generation, API key material — are generated via crypto.getRandomValues, the browser's CSPRNG interface. The security of the entire system depends on two assumptions about this interface: (1) the underlying OS entropy source (e.g., /dev/urandom on Linux, CryptGenRandom on Windows) is correctly seeded and produces outputs indistinguishable from uniform random; and (2) the browser's implementation of SubtleCrypto is correct and does not introduce implementation-specific vulnerabilities. Both assumptions hold under the browser threat model but represent a trust dependency on the browser vendor and OS kernel. A compromised CSPRNG — for example, due to insufficient entropy at VM startup in cloud environments, or a supply-chain compromise in the browser — would undermine all key generation. This is a standard limitation of browser-based cryptography and is not specific to this architecture.
3. Key management architecture
3.1 Five-layer key hierarchy
The key management scheme uses five nested layers of key material, each wrapping the next, with the terminal layer protecting user content. Layer 0 (credential): the user's password, which never leaves the browser process and is never transmitted. Layer 1 (KEK): derived from the password via PBKDF2 as described in §2.3; exists only in browser memory. Layer 2 (MEK): a 256-bit random Master Encryption Key generated at account creation; stored server-side wrapped under the KEK via AES-256-GCM. Layer 3 (private key): the user's RSA-4096 private key, stored server-side wrapped under the MEK via AES-256-GCM. Layer 4 (CK): per-conversation 256-bit AES keys, each stored as an RSA-OAEP encryption of the CK under each authorized participant's public key. Layer 5 (content): AES-256-GCM ciphertexts of message content, encrypted under the conversation CK.
3.2 Zero-knowledge storage boundary
The zero-knowledge boundary is defined as the point at which data leaves the browser process for transmission to the server. Only the following values cross this boundary: AES-256-GCM ciphertexts (wrapped_MEK, wrapped_SK, C_i), RSA-OAEP ciphertexts (wrapped_CK_j per participant), the RSA-4096 public key (plaintext by design), PBKDF2 salt (public, non-secret per standard KDF design), and AEAD authentication tags and nonces. The password (p), KEK, plaintext MEK, plaintext private key (SK_priv), and plaintext conversation keys (CK) never cross this boundary. The server is thus in a position where it holds a complete cryptographic transcript of the user's key hierarchy — but every element requiring a key for decryption has that key further encrypted under a layer not present on the server.
3.3 Session key management
On authentication, the browser executes the full key derivation and unwrapping chain (§3.1 layers 0–3), holding KEK, MEK, and SK_priv in JavaScript heap memory. Conversation keys (CK_i) are loaded lazily — decrypted via RSA-OAEP on first conversation access rather than at session initialization — to bound heap utilization for accounts with many conversations. All key material is held exclusively in JavaScript variables; none is written to localStorage, sessionStorage, IndexedDB, or any other persistent browser store in unwrapped form during a default session. On explicit logout, keys are null-assigned and dereferenced, making them eligible for garbage collection. For users enabling the optional persistent unlock feature, the MEK is wrapped under a device-bound key (WebAuthn platform authenticator where available, or a browser-generated device ID otherwise) and stored in IndexedDB; this weakens the zero-knowledge property for that device while preserving it at the server layer.
3.4 Multi-participant key distribution
Conversation initiators generate a fresh random CK for each conversation. When adding participant j to an existing conversation, the CK is encrypted under SK_pub_j via RSA-OAEP and stored as wrapped_CK_j. This operation does not require re-encryption of existing ciphertexts — the CK itself is unchanged; only a new RSA-OAEP envelope is added per new participant. Participant removal requires forward secrecy: a new CK is generated, all existing ciphertexts in the conversation are re-encrypted under the new CK (a client-side operation requiring access to the old CK), and new wrapped_CK_j blobs are generated for the remaining participants. The computational cost of this rotation is O(n_messages) and is the primary scalability constraint for large shared conversations.
Figure 1. Hierarchical Key Derivation and Trust Boundary
USER CREDENTIAL SPACE (never leaves browser process)
┌─────────────────────────────────────────────────────────────────┐
│ │
│ p ∈ {0,1}* (password) │
│ s ∈ {0,1}^128 (per-user salt, stored server-side) │
│ │
│ KEK = PBKDF2(HMAC-SHA256, p, s, c≥100000, 256) │
│ │
│ MEK = AES-256-GCM-Decrypt(KEK, wrapped_MEK) ─┐ │
│ where wrapped_MEK stored on server │ │
│ │ session │
│ SK_priv = AES-256-GCM-Decrypt(MEK, wrapped_SK) ┤ key │
│ where wrapped_SK stored on server │ cache │
│ │ │
│ CK_i = RSA-OAEP-Decrypt(SK_priv, wrapped_CK_i) ┘ │
│ where wrapped_CK_i stored per-conversation │
│ │
│ C_i = AES-256-GCM-Encrypt(CK_i, M_i ∥ AAD_i) │
│ where M_i is message plaintext, AAD_i is unencrypted metadata │
│ │
└──────────────────────────┬──────────────────────────────────────┘
│
Zero-Knowledge Boundary ─ {C_i, T_i, wrapped_MEK,
│ wrapped_SK, SK_pub, wrapped_CK_i}
│ cross this boundary only
▼
SERVER STORAGE (untrusted, zero decryption capability)
┌─────────────────────────────────────────────────────────────────┐
│ Stored per user: │
│ wrapped_MEK = AES-256-GCM(KEK, MEK) ← ciphertext │
│ wrapped_SK = AES-256-GCM(MEK, SK_priv) ← ciphertext │
│ SK_pub = RSA-4096 public key ← plaintext │
│ s = PBKDF2 salt ← plaintext │
│ │
│ Stored per conversation, per participant j: │
│ wrapped_CK_j = RSA-OAEP(SK_pub_j, CK) ← ciphertext │
│ │
│ Stored per message: │
│ C_i = AES-256-GCM ciphertext ← ciphertext │
│ T_i = 128-bit AEAD auth tag ← integrity │
│ IV_i = 96-bit random nonce ← plaintext │
│ AAD_i = {timestamp, model, token_count}← plaintext │
│ │
│ Server cannot derive: p, KEK, MEK, SK_priv, CK — ever. │
└─────────────────────────────────────────────────────────────────┘4. Transport security
4.1 TLS as the channel encryption layer
All network communication — between the browser and Next.js, between Next.js and the Python API, and between any service component and MongoDB — occurs exclusively over TLS 1.2 or higher. TLS provides channel confidentiality and server authentication via the PKI certificate chain. In the context of this architecture, TLS protects: (a) the KEK-wrapped MEK and wrapped private key during their initial transmission from server to client on authentication; (b) bearer tokens (plaintext API keys) in transit between Next.js and the Python API; (c) encrypted ciphertexts during upload from client to server; and (d) metadata (timestamps, token counts, model identifiers) which is not encrypted at the application layer.
4.2 Defense-in-depth: TLS plus application-layer encryption
TLS and application-layer AES-256-GCM encryption address distinct threat surfaces and are not redundant. TLS failure (protocol downgrade, certificate compromise, or a malicious TLS terminator) exposes bearer tokens and plaintext API keys in transit — but an observer with only the TLS-decrypted payload still sees AES-256-GCM ciphertexts for message content and SHA-256 hashes for API key verification. Conversely, application-layer encryption failure while TLS remains intact means server-side content remains readable by the server operator — which is precisely the threat the ZKA layer addresses. Both layers must be active; neither substitutes for the other. The combined guarantee is: network adversaries observe only TLS-protected channel data; server-side adversaries observe only application-layer ciphertexts.
5. API key architecture
5.1 The auto-generated key retrieval problem
Service-to-service authentication between the Next.js application server and the Python API requires the former to present a bearer token authenticating its requests on behalf of an organization. When an organization is provisioned, the system generates this API key server-side, programmatically — there is no user-facing UI event at which the plaintext key could be captured and distributed through a user-controlled credential store. The key must therefore be retrievable by the Next.js process for subsequent outbound requests, while satisfying two constraints: (1) the Python API must be able to verify the key independently without an HTTP dependency on Next.js, and (2) the key must not be stored in plaintext in the database (which would create a trivially-accessible credential under DB compromise).
5.2 Dual-representation storage model
The solution stores each API key in two representations within the same database record. First, a SHA-256 hash (base64url, no padding) of the plaintext key is stored in the apikey.key field — this serves as the verification token used by the Python API. Second, an AES-256-GCM ciphertext of the plaintext key is stored in apikey.metadata.publicKey — this enables the Next.js process to recover the plaintext for outbound requests by decrypting with a service-level key (K_svc) that is held in the application environment and is not present in the database. The field is named 'publicKey' rather than a more descriptive identifier as an operational security measure: the name is innocuous and does not signal the field's significance to an analyst observing the schema.
5.3 Schema normalization and operational camouflage
All API key records — both system-provisioned keys and user-created keys — contain a populated publicKey metadata field. For system keys, this field contains the AES-GCM ciphertext of the functional plaintext key. For user-created keys, this field contains the AES-GCM encryption of a randomly-generated 256-bit value that is discarded at creation time. This normalization ensures that no structural difference exists between record types in the stored schema: an analyst with full read access to the database cannot identify high-value system key records by field presence, absence, or length. This is an operational security (OpSec) measure, not a cryptographic defense — it raises the analytical cost of a targeted attack but does not alter the cryptographic security properties of either record type.
5.4 Independent Python API verification
The Python FastAPI service verifies incoming bearer tokens by computing H'(k) = base64url(SHA-256(k)) on the received token and querying the apikey collection for a matching hash. This operation requires only read access to MongoDB and has no HTTP dependency on the Next.js service. The Python service parses the metadata JSON string to extract organizationId for downstream authorization and backtrace to subscription status, membership, and account capabilities. The service-level key K_svc — used by Next.js to decrypt E(k) — is not present in the Python environment, which has no need to recover plaintext keys; it only verifies them via hash comparison. This design maintains the independence property central to zero-trust service architecture: each service authenticates principals against the shared data store without dependency on another service's availability.
Figure 2. Zero-Trust Service Authentication — Key Lifecycle
ORG PROVISIONING (server-side, Next.js process)
┌─────────────────────────────────────────────────────────────────┐
│ k ←$ {0,1}^512 (CSPRNG, format: "balon_" ∥ hex(256-bit)) │
│ H(k) = base64url(SHA-256(k)) [Better Auth defaultKeyHasher] │
│ E(k) = AES-256-GCM(K_svc, k) where K_svc ∉ database │
│ │
│ Stored in apikey collection: │
│ apikey.key = H(k) ← verification hash │
│ apikey.metadata = JSON { │
│ organizationId: org_id, │
│ publicKey: E(k) ← ciphertext of plaintext key │
│ } │
│ │
│ For user-created keys (schema normalization): │
│ apikey.metadata.publicKey = AES-256-GCM(K_svc, r) │
│ where r ←$ {0,1}^256 (random noise, discarded) │
│ → All records structurally identical; no high-value signal │
└────────────────────────────┬────────────────────────────────────┘
│ TLS 1.2+
┌────────────────┴──────────────────┐
│ │
WEBAPP REQUEST PATH PYTHON API VERIFICATION
┌───────────────────────┐ ┌─────────────────────────┐
│ 1. Fetch apikey record│ │ 1. Extract k from │
│ 2. k = AES-GCM-Dec( │ │ Authorization header │
│ K_svc, E(k)) │ │ │
│ 3. Transmit k as │ │ 2. Compute H'(k) = │
│ Bearer token │──── TLS ──▶│ base64url(SHA-256(k))│
│ over TLS │ │ │
└───────────────────────┘ │ 3. Query: apikey where │
│ key = H'(k) │
│ │
│ 4. Parse metadata → │
│ org_id for authz │
│ │
│ No K_svc. No decryption.│
│ Only hash verification. │
└─────────────────────────┘
Security property: K_svc not in database; compromise of MongoDB
yields E(k) but not K_svc, making k unrecoverable from stored data.
Python API independently verifiable — no HTTP dependency on Next.js.6. Threat model
6.1 Adversarial model
The threat model considers four adversarial classes: (A1) a server-side adversary with complete read access to all stored data (database breach, malicious insider, subpoena of storage layer); (A2) a network adversary capable of passive surveillance of all cleartext traffic and active MITM on TLS sessions; (A3) a credential adversary with access to all stored API key records; and (A4) a client-side adversary with the ability to execute arbitrary code in the browser context. Classes A1–A3 are in scope; A4 is explicitly out of scope. The system makes no security claims against client-side adversaries — the trust boundary is the browser process, and a compromised browser process has access to all session key material in plaintext.
6.2 Security claims against A1 (server-side adversary)
An A1-class adversary with complete database access obtains: (1) AES-256-GCM ciphertext of the MEK under the KEK — computationally infeasible to decrypt without the user password, which is not stored; (2) AES-256-GCM ciphertext of SK_priv under the MEK — computationally infeasible to decrypt without the MEK; (3) RSA-OAEP ciphertexts of conversation keys under participant public keys — computationally infeasible to decrypt without SK_priv; (4) AES-256-GCM ciphertexts of message content — computationally infeasible to decrypt without the conversation key. The security of this claim rests on the computational hardness of AES-256 and the RSA problem at 4096-bit key length under current and projected near-term cryptanalytic techniques. The PBKDF2 salt (public) does not weaken this claim; it is non-secret by design. The A1 adversary's only practical attack path is offline brute-force against user passwords using the salt and wrapped_MEK as an oracle — which PBKDF2's iteration count is calibrated to make computationally expensive.
6.3 Security claims against A2 (network adversary)
TLS provides channel confidentiality for all network communication. Against a passive network adversary, all payloads are TLS-protected. Against an active MITM capable of breaking TLS (e.g., a malicious certificate authority, or a network device performing TLS inspection), the adversary observing the decrypted TLS payload sees: (a) AES-256-GCM ciphertexts for message content; (b) SHA-256 hashes for API key verification; (c) AES-256-GCM ciphertexts for wrapped key material transmitted from server to client on authentication. None of these expose plaintext under the hardness assumptions of §2. TLS breakage degrades the security of bearer token transmission (the Python API would receive a bearer token from an adversary who intercepted it), but does not directly expose stored content.
6.4 Security claims against A3 (credential adversary)
An A3-class adversary with complete access to the apikey collection obtains: (1) H(k) = SHA-256(k) for each key — a one-way hash for which preimage recovery is computationally infeasible given the 256-bit key space of the API keys; (2) E(k) = AES-256-GCM(K_svc, k) — a ciphertext of the plaintext key under K_svc, where K_svc is not present in the database. Recovery of k from E(k) requires K_svc. An adversary without K_svc cannot recover k from the stored material and therefore cannot authenticate to the Python API as any organization.
6.5 Legal compulsion
A legal process compelling the operator to produce plaintext user content intersects the A1 adversarial class. The operator's response is technically constrained: it can produce the encrypted database contents, the PBKDF2 salts, and the wrapped key material — but not plaintext content, because the decryption path requires user passwords that are neither stored nor logged. This property is not a policy commitment subject to interpretation or future change; it is a consequence of the key management architecture. The operator is in the same cryptographic position as any A1-class adversary: in possession of ciphertexts and wrapped keys, but not the credentials required to traverse the decryption chain.
6.6 Forward secrecy analysis
The architecture does not provide forward secrecy at the MEK layer. The MEK is long-lived — it persists for the lifetime of the user account and protects all conversation keys via the RSA private key layer. An adversary who compromises a user's password (or the wrapped_MEK via offline brute-force) at any future point can retroactively decrypt all historical ciphertexts, because every conversation key is derivable from SK_priv, which is derivable from the MEK. This is a known limitation of hierarchical key wrapping schemes without key rotation. Forward secrecy at the conversation level is achieved for participant removal (§3.4) — a new CK is generated and old ciphertexts re-encrypted — but not at the MEK level. A system providing full forward secrecy would require periodic MEK rotation with re-encryption of all wrapped material, or a Signal-style ratchet mechanism that generates ephemeral per-session keys. Both approaches are identified as future work (§10) but are not present in the current deployment. Users with high threat models should be aware that MEK compromise is retroactive.
Figure 3. Adversarial Model — Capabilities × Residual Risk
Attacker class Assumed capability Residual exposure
─────────────────────────────────────────────────────────────────────────
Server-side adversary Full read access to {wrapped_MEK,
(DB breach, insider) all stored data including wrapped_SK, C_i, T_i}
all tables, indices, → computationally
and backups infeasible to decrypt
without p or recovery
credential (not stored)
Legal compulsion Valid warrant compelling Same residual as above;
(warrant, subpoena, operator to produce operator cannot produce
NatSec letter) plaintext user content plaintext — capability
eliminated, not just
policy-restricted
Network adversary Passive surveillance of TLS-encrypted transport;
(traffic analysis, all network traffic; even with TLS broken,
MITM) active MITM on TLS observes only {C_i, T_i}
and H(k); no plaintext
API credential Full knowledge of all H(k) only — irreversible
theft (DB dump) apikey records including SHA-256; E(k) present
metadata but K_svc not in DB
Brute-force against GPU-scale hash cracking PBKDF2 at c≥100k
wrapped_MEK against (s, wrapped_MEK) iterations: cost per
guess ~200–500ms;
infeasible for
passwords ≥60 bits
entropy
Client-side adversary Memory inspection, PLAINTEXT ACCESSIBLE
(malware, browser browser exploit, JS during active session.
compromise) injection Out of scope — trust
boundary is browser
process integrity
─────────────────────────────────────────────────────────────────────────
Note: "computationally infeasible" references hardness of AES-256 and
RSA-4096 under current and projected near-term cryptanalytic techniques.
No formal proof of security under computational hardness assumptions is
asserted here beyond those inherent to the NIST-standardized primitives.7. Recovery mechanism
7.1 Recovery credential construction
A one-time recovery credential (RC) is generated at key initialization and presented to the user exactly once. The RC encodes: a version byte, the PBKDF2 salt (s), and the KEK-wrapped MEK — sufficient to recover the MEK given knowledge of the RC structure and the application's KDF parameterization. The RC is not retained by the server after display. Its loss is therefore irrecoverable from the server side. This irrecoverability is a deliberate design property: it ensures that the recovery path does not create a server-side capability that undermines the zero-knowledge guarantee. An operator compelled to produce a user's plaintext content cannot do so even if both the RC and the password are unavailable to the operator.
7.2 Password reset with recovery credential
Password reset using the RC proceeds entirely client-side: the client parses the RC to extract s and wrapped_MEK, derives a key from the RC material to decrypt wrapped_MEK (recovering the plaintext MEK), derives a new KEK from the new password via PBKDF2, re-wraps the MEK under the new KEK, and transmits the new wrapped_MEK to the server. The MEK itself is unchanged, so all existing ciphertexts remain valid. The server stores only the new KEK-wrapped MEK and the PBKDF2 salt — no plaintext credential material at any point. The server cannot perform this operation on behalf of a user: it lacks both the RC and the user's new password.
7.3 Irrecoverability guarantee
Simultaneous loss of the user password and the recovery credential results in permanent, irreversible data loss. This property is not a UX deficiency to be engineered around — it is the mechanism by which the zero-knowledge guarantee is upheld under legal compulsion scenarios. If the operator could recover user content in the absence of user credentials, that same capability would be available to legal process and would undermine the architecture's primary security property. The deployment addresses user-facing risk by making the RC save step mandatory during onboarding and providing multiple export formats (download, copy to clipboard), with industry data (from comparable implementations such as 1Password) suggesting approximately 80% of users retain their recovery credentials when the UX is appropriately designed.
Figure 4. Recovery Credential Construction
On key initialization (one-time, client-side):
RC = base64(
version_byte(1) ∥
s(128 bits, PBKDF2 salt) ∥
wrapped_MEK(AES-256-GCM ciphertext of MEK under KEK)
)
RC displayed once to user; not retained by server.
Password reset with RC:
1. User presents RC and new password p'
2. Client parses RC → extracts s, wrapped_MEK
3. KC = PBKDF2(HMAC-SHA256, RC_key_material, s', c, 256)
where RC_key_material is derived from RC structure
4. MEK = AES-256-GCM-Decrypt(KC, wrapped_MEK) [from RC]
5. KEK' = PBKDF2(HMAC-SHA256, p', s, c, 256)
6. wrapped_MEK' = AES-256-GCM-Encrypt(KEK', MEK)
7. Server stores wrapped_MEK' — all existing ciphertexts valid
because MEK itself is unchanged
Loss of both p and RC: MEK unrecoverable by design.
This is not a defect — it is the mechanism by which the
irrecoverability guarantee is enforced for legal compulsion scenarios.8. Implementation notes
8.1 Streaming LLM response handling
LLM responses arrive as a sequence of token deltas over a server-sent event stream. Real-time rendering requires these tokens to be displayable as they arrive, before the complete response is available for encryption. The current implementation renders the stream in plaintext within the browser session (consistent with A4 being out of scope), buffers the complete response on stream completion, and encrypts the full message before persistence to the server. The plaintext window is therefore bounded by the duration of the active streaming session and exists only in the browser process. The server receives and stores only the post-stream AES-256-GCM ciphertext.
8.2 AEAD associated data (AAD)
AES-256-GCM authenticated encryption includes an associated data (AAD) field that is authenticated but not encrypted. The current implementation uses message metadata (timestamp, model identifier, token count) as AAD. This means these metadata fields are plaintext in server storage but are cryptographically bound to the ciphertext — a forged or tampered metadata field will cause GCM tag verification to fail on decryption. This provides integrity protection for unencrypted metadata without requiring its encryption.
8.3 Legacy content migration
Users with message history predating encryption feature deployment undergo just-in-time migration on first authenticated session following key initialization. The migration generates the RSA key pair, initializes the MEK, and offers batch re-encryption of historical conversations. Batch operations use configurable chunk sizes with pause/resume and error rollback — tested against conversation histories exceeding 10,000 messages. Historical content remains accessible in plaintext until the batch operation completes for that conversation; partial migration leaves some conversations encrypted and some not, with explicit UI indication of encryption status per conversation.
8.4 GCM nonce collision probability and tag forgery bounds
AES-GCM with 96-bit random nonces has a birthday-bound collision probability of approximately n²/2^97 after n encryptions under the same key. For realistic per-user message volumes (n ≤ 10^6, i.e., 2^20 messages), this probability is approximately 2^40/2^97 = 2^(-57) — negligible. The 128-bit authentication tag provides a forgery probability per attempt bounded by (L+1)/2^128 + n/2^128, where L is the message length in 128-bit blocks; for any realistic message size this is below 2^(-107). Critically, AES-GCM is not nonce-misuse resistant: if two messages are encrypted under the same key with the same nonce, the XOR of their ciphertexts equals the XOR of their plaintexts, and the GCM authentication key H is recoverable — enabling tag forgery. The architecture mitigates this via CSPRNG-generated nonces rather than deterministic counters; the failure mode requires CSPRNG compromise. AES-GCM-SIV (RFC 8452), which provides nonce-misuse resistance via a synthetic IV, is identified as a preferred future primitive but is not yet uniformly available across SubtleCrypto implementations in target browsers.
8.5 Plaintext metadata leakage under A1
The A1-class adversary with full database access observes the following in plaintext: per-user PBKDF2 salts and RSA public keys; per-conversation participant IDs (social graph), creation and modification timestamps, and participant count; per-message timestamps, role (user/assistant), token count, model identifier, and GCM nonce. From this metadata alone, an adversary can determine: user activity timing and frequency, approximate message lengths via token counts, which AI models a user employs, the social graph of who communicates with whom within an organization, and conversation session durations. No message content, conversation subjects, or file contents are derivable. This metadata disclosure is bounded and is an accepted trade-off of the current design. Use cases requiring metadata confidentiality — such as concealment of communication patterns or activity timing — would require additional mitigations (e.g., dummy traffic, metadata encryption, or homomorphic token-count storage) that are outside the current scope.
8.6 Service-level key (K_svc) rotation and compromise model
The service-level key K_svc — used to encrypt API key plaintexts in the dual-representation storage model (§5.2) — is held in the application environment and is not present in the database. Its compromise would allow an adversary to decrypt all stored E(k) values, recovering plaintext API keys and enabling authentication to the Python API as any organization. K_svc compromise does not expose user content (which is protected by the user-controlled key hierarchy, independent of K_svc). K_svc rotation requires re-encrypting all apikey.metadata.publicKey values — an O(n_keys) operation that can be performed without user involvement and without modifying the SHA-256 hashes used for verification. A rotation protocol has not been formally defined in the current deployment; this is identified as an operational gap. Key rotation should be performed on any suspected K_svc exposure and on a scheduled basis consistent with the organization's key management policy.
8.7 Database-layer encryption at rest
The PostgreSQL database is deployed in a Docker container with a persistent volume on the host filesystem. The current deployment does not enable PostgreSQL Transparent Data Encryption (TDE) or OS-level volume encryption (e.g., LUKS) on the database volume. This means that an adversary with physical or OS-level access to the host can read raw database pages directly from disk without a database connection — bypassing database access controls but not the application-layer AES-256-GCM encryption on content. For the content encryption use case, the absence of at-rest encryption does not weaken the zero-knowledge guarantee, since the database pages contain only ciphertexts and wrapped keys. For the API key architecture (§5), database-at-rest encryption would provide an additional layer protecting E(k) values on disk in the event of physical storage compromise without OS access. Enabling LUKS or equivalent volume encryption on the database host is recommended as a defense-in-depth measure and is noted as an operational improvement item.
Figure 5. GCM Nonce Collision Probability and Tag Forgery Bounds
Random IV collision (birthday bound):
Pr[collision after n encryptions] ≈ n² / 2^(96+1)
At n = 2^32 encryptions (≈ 4 billion messages per user):
Pr ≈ (2^32)² / 2^97 = 2^64 / 2^97 = 2^(-33) ≈ 1.16 × 10^(-10)
At n = 2^20 encryptions (≈ 1 million messages, realistic upper bound):
Pr ≈ 2^40 / 2^97 = 2^(-57) ≈ 6.9 × 10^(-18)
For realistic per-user message volumes (≤10^6 messages), the collision
probability is negligible under CSPRNG correctness assumption.
GCM authentication tag forgery bound:
For a 128-bit tag and a single forgery attempt:
Pr[forgery] ≤ (L+1) / 2^128 + n / 2^128
where L = message length in 128-bit blocks, n = # enc. operations
For L ≤ 2^20 blocks (≈ 16GB message — far exceeds use case):
Pr[forgery per attempt] < 2^(-107)
Consequence: ciphertext integrity and AAD integrity hold with
overwhelming probability against active ciphertext manipulation.
GCM nonce-misuse catastrophic failure mode:
If IV_i = IV_j for distinct messages i ≠ j encrypted under same CK:
Attacker observing (C_i, C_j) can compute C_i XOR C_j = M_i XOR M_j
Authentication keys (H) are also exposed, enabling tag forgery.
Mitigation: all IVs are CSPRNG-generated (crypto.getRandomValues);
deterministic IV schemes (counter, timestamp) are not used.
Failure mode requires CSPRNG compromise — out of scope per §6.1.
Note: AES-GCM-SIV (RFC 8452) provides nonce-misuse resistance but
requires SubtleCrypto extensions not yet available in all target browsers.
This is identified as a future migration target in §10.8b. Metadata leakage analysis
Figure 6. Plaintext Metadata — Information Disclosure Under A1
Fields available to A1-class adversary (stored unencrypted):
Per-user:
· s (PBKDF2 salt) → identifies user, enables offline attack
· SK_pub → RSA-4096 public key; no content secrets
· account timestamps → account creation, last login
Per-conversation:
· conversation_id → cardinality: number of conversations
· participant_ids → social graph: who communicates with whom
· created_at / updated_at → communication timing
· n_participants → group size inference
Per-message:
· timestamp → message timing, response latency
· role (USER/ASSISTANT) → turn structure
· token_count → rough content length proxy
· model_id → which AI model was used
· IV_i (96-bit nonce) → required for decryption; no content
What A1 adversary can infer without decryption:
✓ When user was active, at what frequency
✓ Who communicates with whom (org member graph)
✓ Approximate message lengths (via token_count)
✓ Which AI models user prefers
✓ Conversation session durations and patterns
What A1 adversary cannot determine:
✗ Any message content
✗ Conversation subjects or topics
✗ File or document contents
Assessment: metadata leakage is bounded and does not expose content.
For use cases requiring metadata confidentiality (e.g., communication
pattern concealment), additional mitigations would be required —
traffic analysis resistance is explicitly out of scope for this design.9. Performance characteristics
9.1 Encryption and decryption throughput
AES-256-GCM via SubtleCrypto (hardware-accelerated via AES-NI instruction set on x86-64, available since approximately 2010 on mainstream consumer hardware) exhibits sub-millisecond latency for message payloads up to 100KB. RSA-OAEP operations (key wrapping/unwrapping) require approximately 10–50ms per operation on comparable hardware; these occur only at session initialization (one unwrap per conversation accessed) and participant management events, not per-message. PBKDF2 at 100,000 iterations requires 200–500ms at authentication — a one-time cost per session, not per operation.
9.2 Heap utilization under session caching
The session key cache holds: one 256-bit MEK, one RSA-4096 private key (approximately 2KB in CryptoKey object form), and one 256-bit CK per open conversation. For typical session profiles (1–10 open conversations), heap utilization attributable to key material is negligible. Lazy CK loading (keys are decrypted on first conversation access rather than at session initialization) bounds the active key count to open conversations rather than total conversations, which is relevant for accounts with large conversation histories.
10. Limitations and future work
PBKDF2 iteration count (currently c≥100,000) is below the NIST SP 800-132 current recommendation of 600,000 for sensitive data; planned upgrade to Argon2id (memory-hard, resistant to GPU-accelerated brute-force) per OWASP Password Storage Cheat Sheet current guidance
Forward secrecy at the MEK layer is not currently provided — MEK compromise is retroactive across all historical content; a Signal-style double-ratchet or periodic MEK rotation with progressive re-encryption would address this at the cost of significantly increased complexity
K_svc rotation protocol not yet formally defined; key rotation procedure should be documented and exercised on a scheduled basis and on any suspected K_svc exposure
Database volume encryption (LUKS or equivalent) not enabled on the PostgreSQL host; recommended as a defense-in-depth measure for physical storage compromise scenarios
RSA-4096 for key encapsulation incurs higher computational overhead than elliptic-curve alternatives; planned migration to ECDH P-384 or P-521 with HKDF for key derivation would reduce asymmetric operation latency by approximately one order of magnitude
AES-GCM-SIV (RFC 8452) provides nonce-misuse resistance absent from standard AES-GCM; adoption is contingent on uniform SubtleCrypto browser availability
WebAuthn platform authenticator integration for hardware-backed MEK storage would elevate the trust boundary from JavaScript heap to TPM-resident key material on supporting devices
Per-conversation MEK rotation would limit blast radius of a hypothetical MEK compromise to a single conversation; planned as a future capability with progressive re-encryption
Searchable encryption over ciphertext remains an open engineering problem; current implementation supports client-side search only
Formal security proof under standard model assumptions (reduction to IND-CCA2 of AES-GCM and OAEP) has not been completed; security claims in §6 rest on the individual primitives, not a compositional proof
Technical specifications
Key derivation: PBKDF2-HMAC-SHA256, c≥100,000 iterations, s∈{0,1}^128 per-user random salt, output length 256 bits (KEK)
Master key encryption: AES-256-GCM, 96-bit random IV per operation, 128-bit AEAD tag
Asymmetric key encapsulation: RSA-4096, RSA-OAEP padding, SHA-256 mask generation function
Content encryption: AES-256-GCM (AEAD), 96-bit random IV per message, 128-bit authentication tag, metadata as associated data
API key hashing: SHA-256 + base64url without padding (Better Auth defaultKeyHasher compatible)
API key storage encryption: AES-256-GCM with service-level key K_svc not present in database
Transport: TLS 1.2 minimum on all network connections; TLS 1.3 preferred
Random number generation: Web Crypto API crypto.getRandomValues (CSPRNG) for all key and IV generation
Key hierarchy depth: p → KEK → MEK → SK_priv → CK_i → C_i (5 wrapping layers)
Recovery credential: base64(version || s || AES-GCM-Encrypt(KEK, MEK)); displayed once at initialization; not server-retained
Balon's full encryption stack ships with every account. No configuration required.