Skip to content

feat(server,platform): retain and expose session identity material - #403

Open
johnthecat wants to merge 2 commits into
mainfrom
feat/session-identity-material
Open

feat(server,platform): retain and expose session identity material#403
johnthecat wants to merge 2 commits into
mainfrom
feat/session-identity-material

Conversation

@johnthecat

@johnthecat johnthecat commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Intent

The v2 pairing handshake carries three values the core consumed and then discarded: the identity chat X25519 private key, the answering device's encryption public key, and the peer statement account id. A host adopting core pairing could not reach any of them, so the only workaround was keeping a second duplicate pairing alive purely to harvest them — the user pairs twice and two sessions exist for one identity.

Refs #384

Solution

Retain, don't derive. The wallet chooses the chat key and peers already address that identity by its public half, so a host-side derivation would name an identity nobody can reach. The pairing host cannot recompute it regardless: the derivation needs raw BIP-39 entropy that only a signing host holds.

Read paths split by sensitivity:

  • Public material rides SessionUiInfo (chat_public_key, device_enc_public_key, peer_statement_account_id), so a host that only addresses the identity never touches a secret.
  • Secrets sit behind explicit CoreAdmin calls, because SessionUiInfo rides every AuthState broadcast to all registered AuthPresenters. Both are reachable over wasm and UniFFI.

The responder also gets its own random, persisted device encryption key. It previously advertised the SSO channel key, which makes every device sharing an identity indistinguishable.

Verified field-for-field against the Android provider and the host-papp consumer: handshake layout, the X25519 basepoint derivation of the chat public key, and the statement-signer origin of the peer account id all match.

Why two fields touch 26 files

SessionInfo and SessionUiInfo are contracts, not plain structs:

  • SessionInfo is the persisted AuthSession SCALE blob. Field order is the storage layout.
  • SessionUiInfo crosses three bindings that must agree byte-for-byte — SCALE, generated TS, and the UniFFI Swift converters. Adding a field regenerates the codegen golden, the TS client, and the Swift read/write sequences; a mismatch there silently corrupts every AuthState::Connected crossing the FFI.
  • Every CoreAdmin method is codegen'd into the TS interface, which then needs hand-written plumbing across the worker protocol, the worker runtime, and the main-thread provider, plus a wasm export and a UniFFI export.
  • device_enc_public_key was meaningless until the responder stopped aliasing it to the SSO key, which pulled in a new CoreStorageKey and install-scoped storage in the CLI host.

Upgrade note

SessionInfo gains fields mid-struct and the blob is unversioned, so sessions persisted by an earlier build no longer decode. Existing users re-pair once.

@johnthecat
johnthecat requested a review from a team August 14, 2026 15:34
johnthecat and others added 2 commits August 14, 2026 17:59
The v2 pairing handshake carries three values the core consumed and then
discarded: the identity chat X25519 private key, the answering device's
encryption public key, and the peer's statement account id. A host adopting
core pairing had no way to reach them, so it had to keep a second duplicate
pairing alive purely to harvest them.

Retain rather than derive. The wallet chooses the chat key and every peer
already addresses that identity by the matching public key, so a value
derived host-side would name an identity nobody can reach. The pairing host
provably cannot recompute it either: the derivation needs raw BIP-39 entropy,
which only a signing host holds.

Split the read paths by sensitivity. Public material rides SessionUiInfo
(chat_public_key, device_enc_public_key, peer_statement_account_id) so a host
that only addresses the identity never touches a secret. The secrets sit
behind explicit CoreAdmin calls, because SessionUiInfo rides every AuthState
broadcast to all registered AuthPresenters and would push them to hosts that
never asked. Both secrets are reachable over wasm and UniFFI.

Give the responder its own device encryption key. It previously advertised
the SSO channel key as device_enc_pub_key, which makes every device sharing
an identity indistinguishable. The key is now random and persisted under
CoreStorageKey::DeviceEncryptionKey, matching the provider, and the CLI host
anchors that slot to its bootstrap directory so a user switch cannot
regenerate the identity peers address this install by.

Verified field-for-field against the Android provider and the host-papp
consumer: the handshake layout, the X25519 basepoint derivation of the chat
public key, and the statement-signer origin of the peer account id all match.

SessionInfo gains fields mid-struct, so its SCALE layout changes. The blob is
unversioned and decode rejects short input, so sessions persisted by an
earlier build no longer decode and those users re-pair once.

Refs #384

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Move the device-key serialization guard from SigningHost onto RuntimeServices
and bundle it with the read. The key is reachable from both roles now, so a
guard owned by one of them left the CoreAdmin and pairing-runtime paths free
to race a first-time read against pairing, generate a second secret, and
overwrite the one peers were told to address. `device_encryption_secret` is
the only way to reach the key, so no caller can forget to hold it.

Reject instead of resolving with an empty key when the worker host runtime is
disposed. Callers encrypt with whatever comes back, so a zero-length secret
is the one answer that must never be handed out.
@johnthecat
johnthecat force-pushed the feat/session-identity-material branch from 9017396 to 8b8d333 Compare August 14, 2026 16:01
@johnthecat
johnthecat enabled auto-merge August 14, 2026 16:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant