BrightChain Crypto Sessions: Server-Side Key Custody for End-to-End Encrypted Suites
Authors: Digital Defiance Version: 0.1.0 (draft) Date: 2026 License: MIT
Abstract
End-to-end encrypted productivity suites face a recurring usability tax: every operation that must touch ciphertext requires the user to re-supply credentials sufficient to derive their private key. State-of-the-art deployments such as Proton, Tutanota, and Apple Advanced Data Protection mitigate this by holding the unlocked key in client memory for the lifetime of a browser session, which trades server trust for cross-site-scripting (XSS) exposure. This paper describes BrightChain’s alternative: a sliding-TTL, in-process server-side crypto session that holds the unlocked BackendMember (secp256k1 private key plus wallet) bound to the JWT identity. We present the threat model, the API surface (useSessionEstablish, useSessionUnlock), the binding rules that prevent session-id replay across accounts, the disposal protocol that zeroes key material on revocation or expiry, and the migration path to a sealed Redis-backed multi-node deployment. The design is purely additive: existing per-request authenticateCrypto flows continue to work unchanged.
Keywords: end-to-end encryption, secp256k1, ECIES, session management, key custody, sliding TTL, zero-knowledge, threat model
I. Introduction
The BrightChain suite (BrightCal, BrightDB, BrightMail, BrightPass, BrightChat, BrightHub, BrightLedger, BrightChart, Digital Burnbag) shares a single cryptographic identity per user: a BIP-39 mnemonic that deterministically derives a secp256k1 keypair via @digitaldefiance/node-ecies-lib. Every encrypted artifact in the suite — calendar event payload, mailbox blob, password vault entry, ledger transcript line — is wrapped under that key (directly for self-only data, or via per-recipient ECIES envelopes for shared data).
The naïve integration requires the user to re-enter their mnemonic on every API call that needs to decrypt or sign. This is unacceptable for an interactive productivity suite, where a single page load may issue tens of requests across multiple products.
The classic mitigation — keeping the unlocked key in browser memory — has well-known downsides. A stored-XSS, malicious browser extension, or compromised CDN-hosted dependency can exfiltrate the live private key. Subresource integrity, content security policy, and isolated workers reduce but do not eliminate this risk class.
BrightChain Crypto Sessions take the opposite trade-off: the unlocked key never leaves the server. The client receives an opaque, HttpOnly, SameSite=Strict cookie. Every API request that needs cryptographic capability resolves the cookie against an in-process session map, and the resolved BackendMember is attached to the request handler’s req.eciesUser. The cookie is meaningless to a JavaScript attacker — its only privilege is “ask the BrightChain backend, while authenticated as user X, to perform a crypto operation on user X’s behalf.” This is the same shape of authority a successful CSRF would already grant; the additional XSS exposure surface for key material is zero.
The contributions of this paper are:
- A clear articulation of the trade-off space between client-memory and server-memory key custody for E2EE suites, and an argument for the latter under BrightChain’s threat model.
- The concrete
CryptoSessionStoredata structure and lifecycle, including sliding TTL, absolute cap, and per-user concurrent-session ceiling. - The
useSessionEstablish/useSessionUnlockmiddleware contract and its interaction with the existingauthenticateCryptoandcleanupCryptomiddlewares. - A threat model and a deployment guide covering single-node, sticky-session, and Redis-sealed configurations.
II. Threat Model
We adopt the following threat model.
In scope:
- Network adversary on any path between the client and the BrightChain origin (TLS-protected; assumes a sound HTTPS deployment).
- Cross-site request forgery from arbitrary third-party origins.
- Stored or reflected XSS in any product surface that ships browser code (client-rendered React).
- Theft of a single client-side artifact (cookie jar, localStorage, IndexedDB).
- Theft of an opaque session id by an authenticated peer attempting to act as a different user.
- Process crash / restart leaking key material.
Out of scope (explicit non-goals):
- A malicious BrightChain server operator with root and live RAM access. BrightChain is not zero-knowledge with respect to the server; it is zero-knowledge with respect to block storage and cold storage. Users who require zero-server-trust must run the open-source backend themselves.
- Compromise of the user’s mnemonic at rest (paper, KeePass, etc.).
- Side-channel attacks against the host OS kernel.
Security goals:
- The user’s mnemonic is presented to the server at most once per session (typically once per workday).
- A stolen client cookie is unusable after the sliding TTL elapses or the absolute cap is reached, whichever comes first.
- A stolen cookie used by a session whose JWT belongs to a different account is rejected before any crypto operation runs.
- Logout, password rotation, and account-compromise notification can revoke every live session in O(sessions-per-user) time.
- At process shutdown all live private-key material is zeroed.
III. Architecture
A. Components
Client Server
────── ──────
┌──────────────────────────┐
│ CryptoSessionStore │
│ Map<sid, Entry> │
│ Map<userId, Set<sid>> │
│ sweep(): every 60s │
└──────────┬───────────────┘
│ owns
▼
┌──────────┐ POST /auth/session/establish ┌──────────────────┐
│ Browser │ ──────────────────────────────▶ │ useSessionEstab │
│ │ { mnemonic | password } │ ─ verify creds │
│ │ │ ─ unlock Member │
│ │ ◀────────────────────────────── │ ─ store.estab() │
│ │ Set-Cookie: bc_session=… │ ─ set cookie │
│ │ └──────────────────┘
│ │
│ │ GET /api/calendars (Cookie) ┌──────────────────┐
│ │ ──────────────────────────────▶ │ useSessionUnlock │
│ │ │ ─ store.touch() │
│ │ │ ─ req.eciesUser │
│ │ └──────────────────┘
└──────────┘
B. Session Establishment (useSessionEstablish)
The establish endpoint is protected by the JWT middleware and the body must carry either the user’s mnemonic or password. The middleware:
- Loads the user via
application.authProvider.findUserByIdand verifiesaccountStatus === Activeandid === req.user.id. - Calls
authenticateWithMnemonicorauthenticateWithPasswordon the provider, obtaining anICryptoAuthResultcontaining the unlockedBackendMember. - Calls
store.establish(userId, member), receiving an opaque base64url-encoded 256-bit session id. - Sets
Set-Cookie: bc_session=<sid>; HttpOnly; Secure; SameSite=Strict; Path=/and theX-BC-Session: <sid>response header for non-browser clients (CLI, mobile WebView). - Calls
next()so that the upstream login response is sent normally.
C. Per-Request Resolution (useSessionUnlock)
The unlock middleware is mounted globally after JWT auth and before any controller. For every request:
- If
req.useris absent, pass through (anonymous routes are never crypto-bound). - Read the session id from
cookies.bc_sessionorX-BC-Sessionheader. If absent, pass through. - Call
store.touch(sid, req.user.id). The store rejects mismatched user ids in constant time and returnsundefined. On hit, the entry’sexpiresAtis updated tomin(now + slidingTtl, absoluteExpiresAt). - Attach the resolved
BackendMembertoreq.eciesUserand tag the request with the symbolCRYPTO_SESSION_OWNED.
The tag is consumed by cleanupCrypto, which now skips disposal whenever it is present — the session store, not the request, owns the lifecycle of session-resolved members.
D. Session Lifecycle
Each CryptoSessionEntry records:
| Field | Purpose |
|---|---|
sessionId | 256-bit cryptographic random, base64url-encoded. |
userId | The string id of the bound JWT identity. |
member | The unlocked BackendMember<TID> (private key in process memory). |
createdAt | Absolute start time, used by the per-user concurrency cap. |
expiresAt | Sliding deadline, refreshed on every successful touch. |
absoluteExpiresAt | Hard deadline, never extended. |
Defaults:
- Sliding TTL: 15 minutes.
- Absolute cap: 8 hours.
- Per-user concurrent sessions: 10 (oldest by
createdAtevicted first). - Sweep cadence: 60 seconds (
unref‘d so it never keeps a process alive on its own).
A process shutdown() method clears the sweep interval and disposes every live entry. Servers should call this from their SIGTERM handler.
E. Key Disposal
BackendMember.dispose() zeroes the wallet seed, the secp256k1 private scalar, and any cached symmetric subkeys. Disposal happens in three places, and only three:
- Inside
CryptoSessionStore.destroyEntry()when a session expires, is revoked, or is evicted. - Inside
CryptoSessionStore.shutdown()for every live entry. - Inside
cleanupCryptoforBackendMemberinstances that were not attached by the session middleware (e.g. the legacyauthenticateCryptoper-request unlock path).
Disposing a session-owned member from the request middleware would corrupt every subsequent request on the same session. The CRYPTO_SESSION_OWNED symbol exists precisely to prevent that bug class.
IV. Comparison to Prior Art
| System | Key residence | Re-auth cadence | XSS exposure of key | Server trust required |
|---|---|---|---|---|
| Proton Mail | Browser memory | Per browser session | High (stored-XSS exfil.) | None for ciphertext |
| Tutanota | Browser memory | Per browser session | High | None for ciphertext |
| Apple ADP | Device Secure Enclave | Per device boot | None (HW-protected) | None for ciphertext |
| Signal Desktop | OS keychain | Per device install | Low (Electron, no DOM) | None |
| 1Password 8 | OS keychain | Per device install + lock | Low (native app) | None for blob |
| Matrix (Element web) | Browser IndexedDB | Per device, megolm rotation | High | None for E2EE rooms |
| BrightChain Crypto Session | Server RAM | Per sliding TTL | None (HttpOnly cookie) | Medium (live RAM) |
BrightChain accepts a higher server trust requirement than the zero-knowledge ideal. The trade is deliberate: BrightChain users self-host or trust the BrightChain Foundation operator under a published threat model. In exchange, the suite gains immunity to a class of stored-XSS attacks that has repeatedly compromised webmail vendors operating under the zero-knowledge banner.
V. Deployment Topologies
Single node (default). The store is a Map in a single Node.js process. No external dependencies. Restart loses every session — users must re-establish.
Multi-node, sticky. The L7 load balancer is configured to pin clients to a single backend by bc_session cookie. Every session is still in-process; failure of the pinned node forces re-establish on the user’s next action. This is the recommended production configuration.
Multi-node, Redis-sealed (future). A drop-in RedisCryptoSessionStore wraps each BackendMember serialization in an ECIES sealed-box keyed to a per-process boot key, which itself lives only in process memory. A node that doesn’t hold the boot key cannot read the key material; the session is effectively pinned but failover is possible if and only if the new node holds the same boot key (via a sealed key-distribution channel). This is left as future work.
VI. Failure Modes and Mitigations
| Failure | Mitigation |
|---|---|
Process killed (SIGKILL) leaves keys in core dump | Run with setrlimit(RLIMIT_CORE, 0) on production hosts. |
| Heap snapshot exfiltration | Production builds disable --inspect; container hardening blocks ptrace. |
| Session id leaked via referrer / log | Cookie is HttpOnly + SameSite=Strict; X-BC-Session header is never logged at proxy layer. |
| Session id replayed by different user | store.touch(sid, req.user.id) enforces JWT-binding. |
| User re-establishes from a new device | Old sessions remain valid up to the per-user cap; on cap-exceed, oldest is evicted. |
| Catastrophic key compromise | revokeAllForUser(userId) on password rotation invalidates every live session and disposes all member instances. |
| Long-lived login (laptop left open) | Absolute cap (default 8 hours) forces re-unlock once per day. |
VII. Extensions and Future Work
brightchain-keywrap. A universalEncryptedEnvelopelibrary that wraps an AES-256-GCM data-encryption key under an ECIES envelope per recipient. The crypto session is the natural unwrap context —req.eciesUseris exactly whatkeywrap.unwrapneeds.- Redis-sealed multi-node store. As above.
- Hardware-backed unlock. WebAuthn / passkey resident credentials can replace the mnemonic / password for
useSessionEstablishonce theIAuthenticationProvidergrows anauthenticateWithAttestationmethod. - Cross-tab session inheritance. Today each tab inherits the cookie automatically. A
BroadcastChannelheartbeat is a candidate optimization to avoid every tab triggering its own slide-touch. - Audit log.
establish,touch,revoke,evict, andexpireare all observable inflection points. A pluggable observer hook is a natural addition.
VIII. Conclusion
BrightChain Crypto Sessions deliver per-session crypto-grade UX without holding the user’s private key in the browser. The design is intentionally simple: an in-memory map, two middlewares, an explicit ownership symbol, and a sliding-with-cap TTL. It composes cleanly with the existing JWT and authenticateCrypto middlewares — neither is changed, and a single deployment can mix per-request and session-based crypto unlock as needs evolve. The threat model is honest about its server-trust assumption, and the deployment guide is explicit about the configurations under which that assumption holds.
Appendix A — Public API
// services/crypto-session-store.ts
class CryptoSessionStore<TID extends PlatformID = Buffer> {
constructor(options?: CryptoSessionStoreOptions);
establish(userId: string, member: BackendMember<TID>): string;
touch(sessionId: string, expectedUserId: string): BackendMember<TID> | undefined;
revoke(sessionId: string): void;
revokeAllForUser(userId: string): void;
shutdown(): void;
readonly size: number;
}
// middlewares/session-unlock.ts
function useSessionEstablish<TID, TStatus>(
application: IApplication<TID>,
options: SessionMiddlewareOptions<TID>,
): RequestHandler;
function useSessionUnlock<TID>(
options: SessionMiddlewareOptions<TID>,
): RequestHandler;
Appendix B — Default Constants
| Constant | Default |
|---|---|
slidingTtlMs | 15 × 60 × 1000 |
absoluteTtlMs | 8 × 60 × 60 × 1000 |
sweepIntervalMs | 60 × 1000 |
maxSessionsPerUser | 10 |
| Cookie name | bc_session |
| Header name | X-BC-Session |
| Cookie attributes | HttpOnly; Secure; SameSite=Strict; Path=/ |
References
- ECIES-Lib: Cross-Platform ECIES Library —
docs/papers/ecies-lib.md. - BrightChain Platform Paper —
docs/papers/brightchain.md. - OWASP Application Security Verification Standard v4.0.3, §3 (Session Management) and §6 (Stored Cryptography).
- Proton Technologies AG. Mail Security Architecture. 2023.
- Tutao GmbH. Tutanota Encryption Whitepaper. 2022.
- Apple Inc. Advanced Data Protection for iCloud. 2022.