Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions packages/protocol/src/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {};
Expand Down
15 changes: 9 additions & 6 deletions packages/protocol/src/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
11 changes: 6 additions & 5 deletions packages/protocol/src/wire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down