Pairing & Devices
8. Pairing Flow
Section titled “8. Pairing Flow”8.1 Overview
Section titled “8.1 Overview”Zero accounts. QR or short code pairing.
8.2 Create Sync Group (First Device)
Section titled “8.2 Create Sync Group (First Device)”1. User taps "Enable Sync"2. Generate: - group_id: random 32 bytes - group_secret: random 32 bytes - device_keypair (if not exists)3. Create invite payload (v3): { relay_nodes: ["primary-node-id", "secondary-node-id"], // NodeIds of sync-relays group_id: base64(group_id), group_secret: base64(group_secret), created_by: base64(device_pubkey), expires: unix_timestamp + 600 // 10 minutes }4. Display QR code or short code8.3 Join Sync Group (Second Device)
Section titled “8.3 Join Sync Group (Second Device)”1. User taps "Join Sync"2. Scan QR or enter short code3. Decode invite payload4. Store in keychain: - group_id - group_secret (→ derive Group Key via Argon2id) - relay NodeId (if present — Tier 1 uses iroh public network, no relay NodeId needed)5. Connect to relay6. Sync begins8.4 QR Code Format
Section titled “8.4 QR Code Format”URL: your-app://sync?invite=BASE64_PAYLOAD
Payload (before base64):{ "v": 1, "r": "sync-relay-node-id-or-discovery-url", "g": "base64(group_id)", "s": "base64(group_secret)", "c": "base64(creator_pubkey)", "e": 1705000000}
Note: For Tier 1 (iroh public network), the `r` field is omitted — peers discover each other via the public iroh relay network. For Tiers 2-6, `r` contains the sync-relay's NodeId or an HTTPS discovery URL (e.g., `https://sync.example.com/.well-known/iroh`) that resolves to a NodeId.Note: Replace your-app:// with your application’s custom URL scheme.
8.5 Short Code Format (Alternative)
Section titled “8.5 Short Code Format (Alternative)”16-character alphanumeric: XXXX-XXXX-XXXX-XXXX
Split:
- First 8 chars: lookup_key (sent to relay)
- Last 8 chars: decrypt_key (never sent)
Flow:
- Creator encrypts payload with decrypt_key
- Creator POSTs
{lookup_key, encrypted_payload}to relay - Joiner GETs
/invite/{lookup_key}from relay - Relay returns encrypted_payload and deletes it
- Joiner decrypts with decrypt_key
Relay never sees decrypt_key or plaintext invite.
8.6 Invite Security
Section titled “8.6 Invite Security”| Property | Mechanism |
|---|---|
| Time-limited | 10 minute expiry |
| Single-use | Deleted on first claim |
| Encrypted | Relay can’t read (for short codes) |
| Revocable | Creator can cancel |
15. Device Revocation
Section titled “15. Device Revocation”15.1 The Lost Device Problem
Section titled “15.1 The Lost Device Problem”Scenario: A user loses their phone. The phone never comes back online to ACK pending blobs. Under normal operation, those blobs remain on the relay until the 7-day TTL expires.
Problems:
- Storage waste on relay
- Blobs stuck in “pending delivery” state
- No way to remove the lost device from the sync group
15.2 Device Revocation Flow
Section titled “15.2 Device Revocation Flow”15.3 Message Specification
Section titled “15.3 Message Specification”REVOKE_DEVICE
Section titled “REVOKE_DEVICE”pub struct RevokeDevice { /// Device to revoke (public key) pub device_id: [u8; 32],
/// Reason (for audit log) pub reason: RevokeReason,
/// Also clear pending blobs for this device pub clear_pending: bool,}
pub enum RevokeReason { Lost, // Device lost/stolen Decommissioned, // User retired device Compromised, // Security concern}DEVICE_REVOKED (notification to other devices)
Section titled “DEVICE_REVOKED (notification to other devices)”pub struct DeviceRevoked { /// Revoked device pub device_id: [u8; 32],
/// Who revoked it pub revoked_by: [u8; 32],
/// When pub timestamp: u64,
/// Reason pub reason: RevokeReason,}15.4 Force Delete
Section titled “15.4 Force Delete”For blobs that are stuck due to offline devices (not just revoked), provide a force delete option:
pub struct Delete { pub blob_id: [u8; 16],
/// Normal delete: wait for all ACKs /// Force delete: delete immediately, clear pending ACKs pub force: bool,}Force Delete Rules:
- Only the blob creator can force delete
- Force delete is audited (logged)
- Other devices receive DELETE_NOTIFICATION so they can remove local copies
15.5 Relay Behavior Changes
Section titled “15.5 Relay Behavior Changes”On REVOKE_DEVICE:
-- 1. Mark device as revokedINSERT INTO revoked_devices (device_id, group_id, revoked_at, reason)VALUES (?, ?, ?, ?);
-- 2. Clear pending deliveries for revoked deviceDELETE FROM deliveriesWHERE device_id = ? AND delivered_at IS NULL;
-- 3. Delete blobs that only had this device pendingDELETE FROM blobsWHERE blob_id IN ( SELECT b.blob_id FROM blobs b LEFT JOIN deliveries d ON b.blob_id = d.blob_id WHERE d.blob_id IS NULL -- No remaining pending deliveries);On Connection (HELLO):
// Reject connections from revoked devicesif is_device_revoked(&hello.device_id, &hello.group_id) { return Err(Error::DeviceRevoked);}15.6 Client API Additions
Section titled “15.6 Client API Additions”impl SyncClient { /// List all devices in the sync group pub async fn list_devices(&self) -> Result<Vec<DeviceInfo>>;
/// Revoke a device (remove from sync group) pub async fn revoke_device( &self, device_id: DeviceId, reason: RevokeReason ) -> Result<()>;
/// Force delete a blob (skip waiting for ACKs) pub async fn force_delete(&self, blob_id: BlobId) -> Result<()>;}
pub struct DeviceInfo { pub device_id: DeviceId, pub device_name: String, pub last_seen: u64, pub pending_blobs: u32, pub is_online: bool,}15.7 Security Considerations
Section titled “15.7 Security Considerations”| Concern | Mitigation |
|---|---|
| Unauthorized revocation | Only devices in group can revoke |
| Revocation race condition | Atomic operation, idempotent |
| Revoked device reconnects | Checked on every HELLO |
| Key rotation after compromise | Optional but recommended |
Optional Key Rotation:
If a device is revoked due to compromise, the group key should be rotated:
- Generate new group_secret
- Distribute to remaining devices via existing E2E channel
- Old group key becomes invalid
- Compromised device cannot decrypt new blobs