diff --git a/packages/protocol/src/channel.ts b/packages/protocol/src/channel.ts index 9dfb94f..35e7a16 100644 --- a/packages/protocol/src/channel.ts +++ b/packages/protocol/src/channel.ts @@ -269,14 +269,15 @@ export class SecureChannel { /** * The routing header for a message, lifted out of its body. * - * The relay keeps the ordered log that lets a reconnecting client catch up — the - * daemon does not replay — and ordering a log means reading the order. These two - * fields are therefore mirrored in cleartext on the envelope, and bound into the - * AEAD so the relay can read them without being able to change them. + * These two fields are mirrored in cleartext on the envelope and bound into + * the AEAD, so a captured frame cannot be re-addressed to another session or + * position without the recipient rejecting it. Nothing between the endpoints + * reads them: the relay stores nothing, and the daemon itself replays a + * reconnecting client from its own ordered session log. * - * Shared by both transports so a message cannot be ordered on one path and not - * the other, which would show up as a phone that resumes correctly over Wi-Fi - * and loses history over the relay. + * Shared by both transports so a message carries the same header whichever + * path it takes, and a frame sealed for one cannot be made to pass for the + * other. */ export function envelopeHeader(message: unknown): { sid?: string; seq?: number } { if (typeof message !== "object" || message === null) return {}; diff --git a/packages/protocol/src/crypto.ts b/packages/protocol/src/crypto.ts index 9d102ec..66d9d5b 100644 --- a/packages/protocol/src/crypto.ts +++ b/packages/protocol/src/crypto.ts @@ -124,17 +124,20 @@ export function directionKey(rootKey: Uint8Array, direction: Direction): Uint8Ar /** * A sealed frame, as it travels over the wire. * - * `sid`, `seq` and `ctr` are deliberately readable. The relay keeps an ordered - * event log so a reconnecting phone can be caught up — the daemon does not - * provide replay itself — and ordering a log requires reading the order. They - * are bound into the AEAD as associated data, so the relay can *read* them but - * cannot alter them without every recipient rejecting the frame. + * `sid`, `seq` and `ctr` are deliberately readable. `ctr` must be: replay + * protection is checked against the per-sender window before decryption. The + * relay stores nothing and replays nothing — a reconnecting phone is caught up + * by the daemon, from its own ordered session log — so no hop needs to read + * the order. All three are bound into the AEAD as associated data: a frame + * cannot be re-addressed to another session or position without every + * recipient rejecting it, and the cleartext copy stays useful for diagnosing + * traffic without the key. */ export interface Envelope { t: "e"; /** Session this belongs to, or absent for connection-level frames. */ sid?: string; - /** Position within the session, for the relay's replay log. */ + /** Position within the session; readable for diagnosis, AEAD-bound against tampering. */ seq?: number; /** Monotonic per connection per sender. Replay protection. */ ctr: number; diff --git a/packages/protocol/src/wire.ts b/packages/protocol/src/wire.ts index 48e9052..9e2b43c 100644 --- a/packages/protocol/src/wire.ts +++ b/packages/protocol/src/wire.ts @@ -690,11 +690,12 @@ export const ErrorMessage = z.object({ /** * A sealed message. Everything with user content travels as one of these. * - * `sid` and `seq` are readable on purpose, and only because the relay keeps the - * ordered log that lets a reconnecting phone catch up — the daemon does not - * replay. They are bound into the AEAD as associated data, so the relay may - * *read* them to order its log but cannot alter them without every recipient - * rejecting the frame. + * `sid` and `seq` are readable on purpose and bound into the AEAD as + * associated data, so a captured frame cannot be re-addressed to another + * session or position without every recipient rejecting it. No hop reads them: + * the relay stores nothing, and replay on reconnect is the daemon's — it owns + * the ordered session log and answers the phone's cursors itself. The + * cleartext copy remains useful for diagnosing traffic without the key. * * The definitive shape lives in `crypto.ts`, which is what actually seals and * opens these; this mirror exists so a message can be validated on arrival