# Orb text encryption (Web3 testnet prototype)

> Current status reviewed September 29, 2026. See [the release index](README.md) for shipped versus planned features, network, wallet signing and deployment boundaries. Older dated experiments below are retained as history.

New direct-message text uses E2EE. Server channels, including trading rooms, deliberately store new messages without E2EE and display a visible privacy disclosure. This is a channel policy, not a fallback after encryption fails: members do not need chat keys to participate. Existing encrypted channel messages remain encrypted and are still decrypted locally with their original keys; they are never rewritten as plaintext. Normal channel permissions still apply.

Connecting an owned embedded Orb Wallet starts chat-key setup automatically once the normal login wallet is linked to the Orb profile. A domain-separated wallet signature attests the chat public key. This signature authorizes no transaction, allowance, or spending. Existing valid attestations are reused without another signature; publication transport failures retry the same signed body at most twice. The client verifies the saved signature before declaring setup complete.

Chat encryption uses a separate P-256 key pair, not the wallet's transaction key. IndexedDB persists the non-extractable private key under the Orb person ID and embedded-wallet address. Setup uses a browser lock across tabs. Old unscoped keys are migrated only when their public key matches the authenticated server record, preserving existing history. A failed storage write blocks publication and keeps the legacy copy.

## Implemented protections

- The publication route checks the Orb cookie session, origin, real P-256 point, signature, timestamp, Privy ownership, normal login-wallet association, and exclusive embedded-wallet/profile binding.
- Competing device registrations cannot replace an existing key. Repeating a successful publication is idempotent.
- Clients cryptographically verify wallet attestations against the current origin and pin peer-key fingerprints. Changed or unverified peer keys block encrypted sends.
- DM text sends fail closed when a peer key is missing, changed or invalid. New server-channel messages do not use this encryption path; visibility, roles and bans still control channel access.
- New direct messages carry a separate encrypted copy for the sender, so both participants can reopen their own history.
- Decrypted text stays in mounted-chat memory, scoped to account and key revision. Incremental updates reuse matching ciphertext results; encrypted rows never use a server-supplied plaintext body for display.
- A local SQLite database uses WAL and a bounded busy timeout. Public-key publication has bounded retries; routine failures no longer leave an endless preparation state.

## Limits before a public rollout

- **Legacy history is not retroactively recoverable.** Keys made before migration 0031 were non-exportable and had no sealed backup. They stay archived in the original browser after an explicit migration. Only messages addressed to the new recoverable key become readable on other devices. Clearing original-browser storage still loses legacy history. Do not promise that old history has been synchronized.
- Only newly encrypted **message text** is covered. Attachments, GIF URLs, feed posts, metadata, membership, timestamps and existing plaintext messages are outside this layer. Voice/video has its own transport and is not covered by this chat-key work.
- Earlier outgoing v2 DMs without a sender copy cannot be retroactively decrypted by their sender. Existing local legacy keys are preserved where possible.
- Previously encrypted channel history requires the original recipient keys. Removing the key requirement for new channel messages does not recover, synchronize or decrypt that history on the server.
- Key fingerprints use trust on first use. Wallet signatures do not independently authenticate a person's wallet on a compromised first lookup. Key transparency or independently verified identities would strengthen this.
- This is not a Double Ratchet / MLS implementation and does not establish forward secrecy against later compromise of long-term keys. There is no independent cryptographic audit. XSS or a compromised client can access plaintext and invoke non-extractable keys.
- Use one canonical origin and configure `ORBITA_PUBLIC_ORIGIN` to match it. An origin change requires renewed attestations; do not trust an arbitrary server-provided origin in the browser.
- Search/mentions and reply snippets do not yet index or display encrypted history like plaintext history.


## Wallet recovery (migration 0031)

New identities generate a random P-256 key in client memory, seal its PKCS#8 bytes with AES-256-GCM, then import the operational key as non-exportable before storing it in IndexedDB. The encryption key uses HKDF-SHA-256 with a random salt and a **private, domain-separated wallet signature**. It is NOT the public chat attestation signature. The recovery signature never goes in an Orb request body, database, URL, log, or persistent browser storage. Account, wallet, origin and public key are authenticated by the encrypted envelope.

Setup checks two signatures over the identical recovery message for consistency. A signer that produces non-deterministic signatures is rejected; recovery errors never generate or publish a replacement identity. This needs real Privy acceptance testing in addition to the deterministic fixture-wallet tests. The design depends on the wallet signing implementation, Privy recovery and client integrity. Anyone able to invoke arbitrary personal_sign for this wallet can reproduce the recovery secret: never grant an automatic-trading server an unrestricted message-signing delegation. The reserved-order keeper is retired. Existing Arena sponsorship uses an operator gas wallet and a bounded move session; it is not an unrestricted user-wallet message-signing delegation.

Only the current person's key response includes the sealed envelope. Peer and historical responses do not include it. Publication retains cookie/origin/Privy/exclusive-wallet ownership checks. Backup replacement is rejected; migration from a legacy key requires a separate wallet proof covering previous key, next key, account, wallet, origin and issuance time. Competing publication is atomic. Key setup stages the non-exportable identity and encrypted backup locally so publication retries reuse them. It activates only after reading and verifying the server's publication.

On a new browser, the client verifies the public attestation, obtains the private recovery signature, decrypts the envelope, proves that private and public keys match, and persists the recovered non-exportable key. Nothing is broadcast onchain; this operation has no gas, approvals or transfers. A normal reload reuses the local key without another recovery signature. Wallet MFA can require interaction; it is not bypassed.

For a legacy identity, **Enable wallet recovery** opens an inline explanation and confirmation. The migration preserves its old key locally and activates a new backed-up key for future messages. Contacts see the existing key-change warning and must verify it. Historical public attestations allow old messages to remain authenticated, while decryption tries the preserved local keys. A new browser cannot decrypt that old history. No silent migration or deletion is performed.

Backups are origin-bound. Use the same canonical HTTPS origin for browsers and the desktop app. Copying a database to a different origin does not transfer recoverability automatically. These keys are independent of Monad mainnet/testnet: changing the trading chain does not rotate chat keys. For E2EE DMs and previously encrypted channel history, database compromise alone exposes ciphertext and metadata. New server-channel messages are stored as plaintext. Compromised served JavaScript, wallet signing access, or a compromised device remains capable of exposing E2EE plaintext. This remains a prototype without an independent cryptographic audit.

Run `node scripts/test-e2ee-recovery.mjs`, `node scripts/test-e2ee-api.mjs` and `node scripts/test-e2ee-onboarding.mjs`. The suites cover authenticated encryption and context tampering, independent-browser restoration, wrong/forged proofs, backup immutability, account isolation, explicit migration, historical sender verification and retention of both sent/received legacy messages. Use `node scripts/migrate.mjs` against the isolated local database before running the preview.

## Validation

`node scripts/test-e2ee-api.mjs` runs the real route and all migrations against disposable SQLite. Only Privy token verification is mocked; authentication cookies, database ownership, signatures, permissions, bans, contention and atomic publication use real code.

`node scripts/test-e2ee-onboarding.mjs` runs React in a real headless Chromium browser with fixture wallets and intercepted API responses. It covers StrictMode, automatic setup, bounded retries, reload, two tabs, legacy migration, account isolation, both sides of DM history, room decryption, invalid attestations, storage blocking, preservation of another device's key and incremental rendering.

Neither suite signs a real user's proof nor submits a blockchain transaction. End-to-end validation with two real Privy accounts remains a separate acceptance check. Orb Web3 is publicly deployed as a testnet prototype. This test evidence still does not establish production cryptographic assurance.

## October 3: separate portable archive identity

The new [portable chat protocol](orb-portable-chat.md) binds a separate P-256 key
and sealed backup to wallet, chain 10143 and archive contract, instead of the
hosted account/origin. Its private recovery signature is never published. The
same wallet in another trusted client can recover that new key and decrypt
confirmed personal archives directly from Monad. Original hosted keys are not
rotated; readable old text is copied only with explicit consent and re-encrypted
locally. Historical community snapshots do not migrate live rights or attest
copied authorship. Attachments/calls and unreadable legacy text remain outside it.
Ciphertext and traffic are permanent/public; no ratchet or independent audit is
claimed. Public EOA rehearsal passed; actual Privy recovery remains an acceptance
gate. The local six-hour option needs the visible unlocked panel and active
session, and publication needs a confirmed receipt or configured operator keeper.
