Fidelius — bundle-owned E2E identity
The chat bundle owns Fidelius identity bootstrap end-to-end. The SDK provides storage delegates and gets out of the way.
Fidelius is Snap's end-to-end encryption protocol. Message bodies and snaps are encrypted on the sender side with keys derived from a P-256 ECDH-based key-agreement protocol that runs in the chat-bundle WASM (e4fa90570c4c2d9e59c1.wasm, ~12 MB). Receiving plaintext requires running that WASM with the recipient's keys.
The current architecture: the bundle owns identity bootstrap, mint, registration, and persistence end-to-end. The SDK provides storage delegates for the bundle's pr() UDS wrapper and a gRPC transport for InitializeWebKey. The SDK has no first-class concept of "the user's Fidelius identity" — it's an internal detail of the MessagingSession and lands in DataStore as opaque wrapped bytes.
What changed
An earlier version of the SDK pre-minted a Fidelius identity during login, serialized it to the DataStore at indexdb_snapcap__fidelius__identity, and re-read it on warm boots. That code path is removed. There is no consumer-facing client.fidelius field — Fidelius identity is an internal detail of the bundle's messaging session (lazily brought up on the first client.messaging.on(...) call) and lands in the DataStore as opaque wrapped bytes under local_uds_uds.e2eeIdentityKey.shared.
What the bundle does on first login
The session bring-up lives in src/bundle/chat/standalone/session/setup.ts:setupBundleSession and is invoked from src/api/messaging/bringup.ts:bringUpSession. The phases that touch identity:
mintFideliusIdentity(src/bundle/chat/standalone/identity-mint.ts) is called purely to boot the chat-bundle WASM into the standalone realm and expose itsModulefor downstream slots. Its minted-identity return value is discarded; we only need the realm — the bundle does its own minting oncecreateMessagingSessionruns.En.createMessagingSessionis called with slot 11 (loadUserWrappedIdentityKeys) returning[]. An empty array signals to the bundle: "no cached identity, please mint and register one yourself."- The bundle runs
constructPostLogininternally. The C++ side callse2ee_E2EEKeyManager.generateKeyInitializationRequest(1)to mint a fresh P-256 keypair + RWK + identityKeyId locally (the WASM uses its own CSPRNG). - The bundle calls
InitializeWebKeyserver-side via the gRPC factorysetupBundleSessionregisters on the realm'sGrpcManager. This is the/snapchat.fidelius.FideliusIdentityService/InitializeWebKeycall in real traffic. The server returns a wrapped form of the identity — RWK-encrypted private key + identity metadata. - The bundle persists the wrapped result through slot 8 (
e2eeIdentityKeyStore) — the SDK's DataStore-backed UDS store under prefixlocal_uds.. The key it writes to islocal_uds.e2eeIdentityKey.shared(single.betweenudsande2eeIdentityKey; an earlier version usedlocal_uds_which produced a duplicate-prefix path — seesession/session-args.ts).
After step 5, the wrapped identity lives in the consumer's DataStore. The plaintext private key has never crossed back from the WASM — only the wrapped form is observable from JS.
What the bundle does on warm boot
When setupBundleSession runs on a process that already has a populated DataStore:
- The Sandbox boots, the
LocalStorageShimrehydrates from DataStore. setupBundleSessionruns,createMessagingSessionis called.- Slot 8's
getItem("uds.e2eeIdentityKey.shared")(resolved against thelocal_uds.prefix) returns the wrapped bytes the bundle persisted previously. - The bundle's C++ side unwraps and rehydrates the in-memory key state.
- No
InitializeWebKeyround-trip; no fresh mint.
Source map
| Concept | Code reference |
|---|---|
| Lazy session bring-up | src/api/messaging/bringup.ts:bringUpSession |
Slot 8 (e2eeIdentityKeyStore) UDS-backed store + slot 11 returning [] | src/bundle/chat/standalone/session/setup.ts:setupBundleSession (and session-args.ts for the slot table) |
| Standalone-WASM realm boot (mint discarded) | src/bundle/chat/standalone/realm.ts:getStandaloneChatRealm |
GrpcManager factory backing InitializeWebKey | src/bundle/chat/standalone/session/grpc-web-factory.ts |
Why this is better than what the SDK used to do
The earlier flow (pre-mint in JS, register, persist as JSON) had three problems:
- Re-registration on every boot. Anything that lost the cached blob (logout, machine swap, encrypted-rest key rotation that broke decryption) forced a fresh
InitializeWebKey. The server rate-limits this; do it too often and the account silently stops authorizing. - Plaintext private key in the DataStore. The SDK's serialized form held the cleartext private key + RWK as hex. Bundle-owned form is RWK-wrapped, with the wrapping key derived from server-side state we don't see.
- Drift from Snap's actual flow. The bundle's
constructPostLogindoes more than just mint — it sets up internal state machines, registers callbacks, and calls follow-up RPCs we'd otherwise have to enumerate ourselves. Letting the bundle drive means we never have to re-derive that.
The cost of the new flow is that the bundle must be brought up before any Fidelius-gated operation works. That's already true for the non-Fidelius-gated path in flight (sending/receiving messages via the bundle session), so the cost is amortized.
What's still in flight
- Snap-send via bundle session. The bundle's
getSnapManager()does outbound snaps — that includes Fidelius envelope encryption for kind 122. Migration off the legacy direct-gRPC path is in progress. - Outbound text/media send. Inbound is wired (
client.messaging.on("message", ...)fires plaintext today via the bundle's wrappedmessagingDelegate); the matching outbound surface still needs the right Embind class on the live session identified.
What this chapter used to say
Earlier drafts described an SDK-side mint + register flow with low-level details about the e2ee_E2EEKeyManager.generateKeyInitializationRequest proto shape, the FideliusEncryption / FideliusRecipientInfo / PHI / MediaKey / CEK proto definitions, Djinni proxy semantics, and an open-questions list around the KDF salt/info bytes.
That content is historical context only. The whole point of letting the bundle drive is that none of those details matter to the SDK — Snap can rotate the protocol and we don't have to chase. Don't treat the previous documentation as an action list.
The SSO bearer flow
WebLoginService gives you a long-lived cookie. It does not give you a Bearer token. To call any gRPC service on web.snapchat.
Why this works (and what doesn't)
The mobile pivot was abandoned in April 2026 after exhaustive validation — every emulator, every rooted phone, every Frida bridge ran into Snap's Argos / Play Integrity wall and got the universal "you…