Keys, identity, and sessions
The composite identity tuple, the four lifetimes of key material, what a session actually is, and why trust-on-first-use is a decision your users re-run on every phone upgrade.
"The key" is not one thing. An application built on this SDK holds four kinds of key material with four lifetimes, four scopes, and four different failure consequences. Conflating them is a common design mistake.
The composite identity
The JavaScript profile uses one composite identity per (userId, identityType), provisioned across linked devices.
CompositeIdentityV1 =
version
|| X25519 algorithm tag
|| X25519 public key
|| Ed25519 algorithm tag
|| Ed25519 public keyThe complete canonical tuple, not either component by itself, is the identity trust object. The SDK locally derives a domain-separated commitment for comparison and transcript binding.
This distinction matters. An X25519 key alone is not the identity. Neither is an Ed25519 key alone. The concatenation, with its version byte and algorithm tags, is the identity. The SDK evaluates every trust decision against the whole tuple. This rule stops an adversary from swapping only the signing half or agreement half and inheriting an existing trust relationship.
The /keys subpath exports createCompositeIdentityV1, generateIdentityKeyPair, and the CompositeIdentityV1 type. On a live client, client.getIdentityPublicKey() returns the local public identity material.
First contact
First contact establishes an explicitly unverified trust-on-first-use record. It does not claim authenticated identity. Trust starts at UNVERIFIED_TOFU and stays there until a safety number or application trust mechanism authenticates it. generateCompositeSafetyNumber(...) from /safety produces numeric, emojis, and scannable forms. The client's verify(remoteUserId) and confirmSafetyNumber(confirmation) carry the decision into protocol state.
Existing trust
After the application pins a composite identity:
- replacing either component fails against the trusted commitment.
- the SDK rejects cached or supplied commitment mismatches before mutation.
- rotation requires an explicit trust decision.
- rollback to an older tuple is detectable.
The SDK promotes only the exact current tuple to VERIFIED. A verification that covered last month's tuple does not carry forward through a rotation, and it should not. The Threat model explains what a hostile relay can and cannot do against this rule.
TOFU is not a one-time decision
Trust-on-first-use is usually described as a single moment: you meet a peer, you accept their key, done. That was never accurate for multi-device systems, and Keybase reframed it usefully as TADA: Trust After Device Additions. Every device a user adds re-runs the decision. Every phone upgrade re-runs it, for every contact, forever.
So identity change is routine and high-frequency, not an incident. Treat every change as a red alarm and users learn to click through it. Hide the change and you remove the only signal that distinguishes a phone upgrade from an interception. The SDK creates comparison data. The application owns QR rendering, scanning, the verification experience, and storage of the trust decision. Identity change and safety numbers is where that work lives.
The four lifetimes
Identity keys are long-lived and per user. The provisioning flow copies the composite tuple above unchanged across a user's linked devices. The SDK selects Sesame's per-user identity-key model. Losing the private half loses the identity. Rotating it means every peer sees a change and must make a trust decision.
Registration IDs, prekeys, and signed prekeys are device-specific. The relay publishes this material so a stranger can contact you while you are offline. keyRefreshIntervalMs defaults to 172800000 (2 days) and maxPreKeyAgeMs to 1209600000 (14 days). One-time prekeys upload in batches of 100, and the client-config preKeyLowThreshold defaults to 50. syncToServer() replenishes. checkPreKeyStatus() reports. rotateEcSignedPreKey() and rotateKyberPreKey() rotate explicitly.
Session state is per remote address and holds the Double Ratchet. More below.
Message keys are per message and deleted after use. That deletion is the forward secrecy property: a device compromised today does not yield yesterday's messages, because yesterday's keys are gone. Bounded state exists for out-of-order delivery, maxSkip 1000, maxMessageKeysStored 1000, keyExpirationMs 604800000 (7 days), and those bounds make TOO_MANY_SKIPPED_MESSAGES a real error code rather than an unbounded memory sink.
What a session actually is
A session is not a connection or login. It is persistent Double Ratchet state for one (local device, remote address) pair. It contains the root key, chain keys, ratchet key pairs, counters, and a bounded set of skipped message keys.
What makes sessions difficult is that decryption mutates state. Advancing the ratchet consumes a chain key and derives the next. This is not a cache you can rebuild. If a successful decrypt does not persist the session record, in-memory and on-disk state diverge. The next message then uses the wrong chain.
Session records must therefore persist atomically. A partial write or a concurrent decrypt on the same address corrupts state irrecoverably. Records use version: 4. The SDK rejects and resets older formats instead of migrating them. This choice prevents the ratchet from using state written under different rules.
The SDK is 0.1.x. Public APIs and persisted formats may change before 1.0, and session records are among the formats most likely to change.
Next
- Local-first and offline-first: why this state cannot live anywhere but the device.
- Identity change and safety numbers: the ceremony, in code.
- Local storage: atomic persistence and adapter selection.
- Key rotation: schedules, thresholds, and what to alert on.
How E2EE changes your architecture
End-to-end encryption converts a server-authoritative CRUD application into a device-authoritative one, and the real cost is the inventory of logic that has to move.
Local-first and offline-first
Why the local store is a required adapter and the relay is optional, what changes when the device is the authoritative copy, and the parts of local-first that are still genuinely unsolved.