Skip to content

Wire Protocol

#[derive(Serialize, Deserialize)]
pub struct Envelope {
/// Protocol version (currently 1)
pub version: u8,
/// Message type (see MessageType enum)
pub msg_type: u8,
/// Sender's device public key
pub sender_id: [u8; 32],
/// Sync group identifier
pub group_id: [u8; 32],
/// Monotonic sequence number (assigned by relay)
pub cursor: u64,
/// Wall clock timestamp (informational only)
pub timestamp: u64,
/// Unique nonce for this message
pub nonce: [u8; 24],
/// E2E encrypted payload
pub payload: Vec<u8>,
}
TypeValueDirectionPurpose
HELLO0x01Client → RelayInitial connection, declare group
WELCOME0x02Relay → ClientConnection accepted, relay info
PUSH0x10Client → RelayUpload encrypted blob
PUSH_ACK0x11Relay → ClientBlob received, cursor assigned
PULL0x20Client → RelayRequest blobs after cursor
PULL_RESPONSE0x21Relay → ClientDeliver requested blobs
PRESENCE0x30Client → RelayHeartbeat, online status
NOTIFY0x31Relay → ClientNew blob available
DELETE0x40Client → RelayRemove blob (after all ACK)
REVOKE_DEVICE0x50Client → RelayRemove device from sync group
DEVICE_REVOKED0x51Relay → ClientNotify of device revocation
REGISTER_PUSH0x60Client → RelayRegister push notification token
UNREGISTER_PUSH0x61Client → RelayUnregister push token
CONTENT_REF0x70Client → RelayLarge content reference (iroh-blobs)
CONTENT_ACK0x71Client → RelayAcknowledge content transfer complete
ERROR0xFFEitherError with code and message
pub struct Hello {
pub version: u8,
pub device_name: String, // Human readable
pub group_id: [u8; 32],
pub last_cursor: u64, // 0 if first sync
}
pub struct Welcome {
pub version: u8,
pub relay_id: String,
pub max_cursor: u64, // Highest cursor for group
pub pending_count: u32, // Blobs waiting for this device
}
pub struct Push {
pub blob_id: [u8; 16], // Client-generated UUID
pub payload: Vec<u8>, // E2E encrypted
pub ttl: u32, // Seconds until auto-delete (0 = default)
}
pub struct PushAck {
pub blob_id: [u8; 16],
pub cursor: u64, // Assigned cursor
pub timestamp: u64, // Server timestamp
}
pub struct Pull {
pub after_cursor: u64, // Return blobs with cursor > this
pub limit: u32, // Max blobs (default 100)
}
pub struct PullResponse {
pub blobs: Vec<BlobEntry>,
pub has_more: bool,
pub max_cursor: u64,
}
pub struct BlobEntry {
pub blob_id: [u8; 16],
pub cursor: u64,
pub sender_id: [u8; 32],
pub payload: Vec<u8>,
pub timestamp: u64,
}
pub struct Notify {
pub blob_id: [u8; 16],
pub cursor: u64,
pub sender_id: [u8; 32],
pub timestamp: u64,
pub size: u32,
}
pub struct Error {
pub code: u32,
pub message: String,
}
/// Reference to large content stored via iroh-blobs.
/// The actual encrypted content is transferred P2P via iroh-blobs,
/// only this small metadata blob goes through the sync relay.
pub struct ContentRef {
/// BLAKE3 hash of CIPHERTEXT (iroh-blobs content address)
/// This is the hash of encrypted bytes, not plaintext
pub content_hash: [u8; 32],
/// XChaCha20-Poly1305 nonce used for encryption (24 bytes)
pub encryption_nonce: [u8; 24],
/// Original plaintext size in bytes
pub content_size: u64,
/// Ciphertext size in bytes (content_size + 16 byte auth tag)
pub encrypted_size: u64,
}

Note: ContentRef uses encrypt-then-hash: content key is derived via HKDF from GroupSecret + blob_id, making blob_id a derivation input rather than a struct field. Debug output redacts content_hash and encryption_nonce (F-016).

/// Acknowledge successful content transfer
pub struct ContentAck {
/// BLAKE3 hash of the acknowledged content
pub content_hash: [u8; 32],
}

Note: Content transfers bypass the sync relay entirely. The relay only sees the small ContentRef metadata blob. Actual encrypted content is transferred device-to-device via iroh-blobs (QUIC/P2P). See Large Content Transfer Protocol.

5.4 Security Audit Remediation Features (2026-02-05)

Section titled “5.4 Security Audit Remediation Features (2026-02-05)”

The following hardening measures were applied during security audit v1 + v2 remediation:

IDFeatureImplementation
F-001Random Argon2id salt16-byte salt via getrandom (not fixed/empty)
F-004File permissionsConfig files written with mode 0600 (Unix)
F-005Echo suppressionrpassword hides passphrase input on terminal
F-006HELLO timeoutConfigurable timeout on handshake (default 10s)
F-007Session limitsmax_concurrent_sessions caps relay connections
F-012Device name truncationUTF-8 aware truncation in Hello message
F-013Pull limit clampingServer clamps client-requested pull limit
F-015/F-016Debug redactionGroupSecret, GroupKey, ContentRef redact sensitive fields in Debug output
F-017Content size limitMAX_CONTENT_SIZE = 100 MiB
F-019Cursor gap capMaximum cursor gap = 10,000 blobs per pull
SR-001Global rate limiterAggregate rate limit across all clients (governor crate)
CL-001OWASP Argon2id floorLowest tier raised to 19 MiB / 2 iter
ST-001Message size limitMAX_MESSAGE_SIZE (1 MB) enforced at transport layer before deserialization
XC-001Zeroize key materialGroupSecret, GroupKey, ContentTransfer zeroize on drop

Why cursors instead of timestamps?

Problem with TimestampsSolution with Cursors
Device clocks driftRelay assigns monotonic cursor
No guaranteed orderingCursor guarantees: “after 500” = all > 500
Gap detection impossibleNo gaps: cursor 501 follows 500
Mobile clocks unreliableCursor independent of device clock

Timestamps are kept for logging/debugging only, never for ordering.