diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..b6345e4 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,86 @@ +# clipsync + +LAN clipboard sync between Android and Linux/Windows devices. A device is a +participant in a sync mesh; paired devices exchange clipboard changes over an +authenticated session. + +## Identity & Trust + +**Device ID** +A stable, install-time unique identifier for a device. Immutable for the life +of the install and used as the echo/loop key for clipboard updates. +_Avoid_: device name, peer id (transient) + +**Identity key** +A device's long-term key pair. The public half is exchanged at pairing and +pinned in the trust record; it authenticates every session. +_Avoid_: public key (ambiguous — could mean ephemeral) + +**Pairing** +The one-time process that establishes mutual trust between two devices: an +authenticated key-exchange handshake plus verification of a short +authentication string derived from the handshake. Produces a pinned trust +record on both sides. +_Avoid_: linking, connecting + +**SAS (Short Authentication String)** +A human-comparable code both devices derive from the handshake transcript. The +user confirms both sides show the same string to defeat a man-in-the-middle. +_Avoid_: PIN, pairing code, OTP + +**Trust record** +The persisted per-peer entry proving a completed pairing: peer device ID, +pinned identity public key, paired-at time, SAS-verified flag, display name, +last-seen time. +_Avoid_: credential, token + +**Peer** +A device for which a trust record exists (i.e., already paired). +_Avoid_: client, node, endpoint + +**Session** +A single authenticated channel between two paired peers over one TCP +connection. Re-established on every (re)connect using the pinned identity keys. + +## Sync Propagation + +**Origin device** +The device that first produced a clipboard change. Immutable as the update is +fanned out; used for echo prevention. +_Avoid_: sender (ambiguous — forwarders also send) + +**Sequence** +A persistent, monotonic per-device counter tagging each clipboard update. +Together with origin device ID it forms the loop-prevention key. +_Avoid_: nonce, index + +**Fan-out** +The propagation rule: a device sends each local clipboard change to every +currently-connected trusted peer. + +**Loop prevention** +Dropping a clipboard update when its origin device ID equals the local device +ID, or when its (origin device ID, sequence) pair is not newer than the +last-seen pair for that peer. + +**Ordering** +The deterministic total order `(timestamp_ms, origin device ID)` used to decide +which clipboard update wins when changes conflict; bounded by a clock-skew +guard. + +## Protocol & Discovery + +**CLIP_UPDATE** +A message carrying one clipboard change: origin device ID, content, content +type, content hash, sequence, timestamp. Text only in MVP; content type is +extensible. + +**Discovery** +How unpaired devices find each other: one side announces over UDP and the +other listens, with manual IP:port entry as a fallback when announcement is +blocked. + +**Clipboard backend** +The component that reads and writes the operating-system clipboard on the +desktop side; reaches the clipboard through the platform's sanctioned API +rather than raw display-server access. diff --git a/docs/adr/0001-noise-transport.md b/docs/adr/0001-noise-transport.md new file mode 100644 index 0000000..47a4cc7 --- /dev/null +++ b/docs/adr/0001-noise-transport.md @@ -0,0 +1,8 @@ +# Transport security uses Noise, not TLS + +We secure the session with the Noise protocol (an `XX` handshake) using each +device's long-term identity key, rather than mutual TLS with per-device certs. +Noise avoids a CA/cert machinery on a peer-to-peer LAN, maps cleanly onto the +SAS pairing flow, and is light enough for the Odin daemon and Android +(Conscrypt). The pinned trust record and the session key are the *same* key, +simplifying the model. diff --git a/docs/adr/0002-fanout-topology.md b/docs/adr/0002-fanout-topology.md new file mode 100644 index 0000000..c5ac50c --- /dev/null +++ b/docs/adr/0002-fanout-topology.md @@ -0,0 +1,7 @@ +# Sync topology is fan-out to all trusted peers + +A clipboard change is sent to every currently-connected paired peer, not just +one. The trust store already models a list of peers, and restricting to 1:1 +later would be a painful product reversal. Fan-out makes the system +many-to-many by construction; correctness then rests entirely on loop +prevention (see ADR-0003). diff --git a/docs/adr/0003-loop-prevention-key.md b/docs/adr/0003-loop-prevention-key.md new file mode 100644 index 0000000..7a8854c --- /dev/null +++ b/docs/adr/0003-loop-prevention-key.md @@ -0,0 +1,9 @@ +# Loop prevention keys on (origin device ID, sequence), not content hash + +The original protocol draft used `content_hash` recent-history dedup as the +loop breaker. That is wrong for many-to-many: two different devices pasting +the same text are falsely dropped as echoes, and the hash is optional so the +gate is bypassable. The loop breaker is now the immutable origin device ID plus +a per-peer monotonic sequence: ignore updates whose `(origin, sequence)` is not +newer than the last-seen pair, and drop any update whose origin is the local +device. `content_hash` is retained only for integrity/diagnostics. diff --git a/docs/adr/0004-key-mismatch-repair.md b/docs/adr/0004-key-mismatch-repair.md new file mode 100644 index 0000000..fddae0a --- /dev/null +++ b/docs/adr/0004-key-mismatch-repair.md @@ -0,0 +1,8 @@ +# Pinned-key mismatch forces a full re-pair + +If a peer reconnects with a static key different from the pinned one, the +session is rejected and the peer is treated as unpaired; a fresh handshake plus +SAS verification is required before the pinned key is updated. Silently +accepting a changed key would void the entire purpose of SAS verification (a +LAN MITM who knows the peer's device ID could swap the key). Reinstalling an +app therefore means a new identity and a new pairing. diff --git a/docs/adr/0005-android-accessibility-read.md b/docs/adr/0005-android-accessibility-read.md new file mode 100644 index 0000000..0dcdc3a --- /dev/null +++ b/docs/adr/0005-android-accessibility-read.md @@ -0,0 +1,8 @@ +# Android reads the background clipboard via AccessibilityService + +Android 10+ blocks background apps from reading the primary clip, which breaks +the "send local copy to desktop" half of bidirectional sync. We use an +AccessibilityService to read the clipboard in the background; if the user denies +it, the app degrades gracefully to receive-only plus manual send (share/quick +tile). Applying a received clip via `setPrimaryClip` needs no extra permission +and always works. diff --git a/docs/adr/0006-desktop-portal-clipboard.md b/docs/adr/0006-desktop-portal-clipboard.md new file mode 100644 index 0000000..c235ab8 --- /dev/null +++ b/docs/adr/0006-desktop-portal-clipboard.md @@ -0,0 +1,9 @@ +# Desktop clipboard uses the xdg-desktop-portal Clipboard API + +The desktop daemon reaches the clipboard through `xdg-desktop-portal` rather +than native X11 calls. Wayland — the default session on modern Fedora/Ubuntu — +exposes no direct clipboard access, so an X11-only daemon is broken for most +Linux users. Consequence: on Wayland the daemon cannot passively poll; an active +portal session started by a user gesture is required, so "enable clipboard +sync" is an explicit action (trivial no-op grant on X11). Windows uses the +Win32 clipboard API.