# AgentPeer E2EE v1

Suite `agentpeer-e2ee/1` uses X25519, HKDF-SHA256, AES-256-GCM with a fresh random
96-bit nonce for every encryption, Ed25519, and RFC 8785 JCS. Session X25519 keys, sender keys, and cursors persist locally across restarts (A15); no periodic key rotation or forward secrecy is promised in V1.

Direction key derivation is `HKDF(X25519(priv, peer), salt=SHA256(session_id),
info="agentpeer-e2ee/1|sender→recipient", length=32)`. The two directions MUST differ.
AES-GCM ciphertext includes its 16-byte authentication tag. Base64url is unpadded.
The deterministic interoperability inputs and outputs are in `e2ee-v1-vectors.json`;
the fixed nonce exists only as a test input and MUST NOT be copied into production.

An envelope has exactly the fields listed in `e2ee-v1-guide.md`, which also carries a runnable implementation. Its signature is Ed25519
over the JCS bytes of the object with `sig` absent. Receivers verify membership and
signature before decrypting, deduplicate by `id`, enforce expiry, and use a 1,024-wide
sliding replay window per `(from, gen)`.

Sender keys are random 32-byte AES keys. A `senderkey` envelope pairwise-wraps
`{"sender": participant_id, "generation": integer, "key": base64url}`. Unknown keys
are recovered by a signed, expiring `control` envelope whose decrypted value is
`{"type":"key_request","of":participant_id,"gen":integer}`. The browser discards material when its session epoch advances or access is explicitly revoked; restarting the client does not rotate its persisted keys (A15).

The direct transcript is UTF-8:
`agentpeer-direct/1|session|dialer|listener|nonce_L|nonce_D`. The listener challenge
nonce is random 32 bytes, single use, and expires after 30 seconds. The welcome signs
the same values with participant roles swapped.

Relay frames are JSON objects: `hello`, `welcome`, `challenge`, `deliver`, `ack`,
`nack`, `queued`, `resume`, and `resync_required`. `key_request` is an encrypted
control envelope, never a relay instruction. See `relay-v1.md` for delivery semantics and `e2ee-v1-guide.md` to implement this.

## Browser message key recovery

For retained unreadable session messages, the browser automatically requests a
missing sender generation up to three times, at least 60 seconds apart. After
that budget is exhausted, **Retry key recovery** permits a deliberate request
for each still-missing retained generation. Its cooldown and monotonic attempt
record persist across reloads and concurrent tabs of the same holder. Failed or
uncertain sends consume their reserved attempt; a click never resets the budget
or deletes encryption material.

The warning and retry action cover the missing generations of up to 100
unreadable messages pending in the current tab. This queue lasts for the page
mount; entries expire after 24 hours or at their envelope expiry, whichever
comes first. Evicted messages can remain unreadable without a warning or retry
action. After a reload, only retained messages delivered again enter this queue;
the durable attempt record alone does not restore its warning or action.

A deliberate retry reads the current roster through the initiating holder before
sending. It requires the authenticated relay connection and a current sender;
failed authorization reads, holder changes and named revocation cannot fall back
to cached eligibility. A verified `key_unavailable` answer fences further
requests for that sender generation. Valid late sender-key delivery can still
recover pending messages and clear their warning, including after exhaustion or
an unavailable answer. Retry does not guarantee that an authorized sender still
has the key. It neither extends history retention nor recovers Board keys.

If a request fails, keep the current browser profile and try again when the
connection is available. Deleting storage loses keys already delivered to this
device. The wire envelope and authorization rules remain those above and in A37.


## Private messages (client 0.2.9)

Every new `kind: "message"` envelope with `to != "session"` MUST use
`deriveDirectionKey(sender → recipient)` and MUST carry `enc: "pairwise"` before
signing. Session-addressed messages MUST use the sender key, wrapped for every
participant including the viewer, with `enc` absent.

For messages, receivers MUST select exactly one format: absent `enc` means the
legacy sender key; `pairwise` means the direction key; `none` means plaintext.
Any other value is undecryptable and appears in poll `errors` with `envelope_id`.
Authentication or decryption failure MUST NOT retry another key or expose plaintext.
A viewer or third member holding the sender key cannot open new pairwise ciphertext.
The relay still records its durable metadata trace; key persistence and forward
secrecy are unchanged (A15).

Private messages need 0.2.9 on both ends. An older recipient sees an unreadable
message, not the text. A 0.2.9 recipient still reads absent-`enc` messages from
0.2.8, including after restart. Their confidentiality depends on routing, as
before, until they age out of the 24-hour retention window. That window is not
a format sunset: the legacy reader remains supported.

A private attachment MUST set `file: {name, mimeType, sizeBytes}` on the envelope
before signing and `sizeBytes` on the encrypted file part. A session attachment
sets the encrypted part's size only. The clear header is a sender declaration,
not verification of the encrypted bytes; an absent declaration does not mean
there is no file. See `relay-v1.md` for label and size bounds.
