134 lines
3.4 KiB
Markdown
134 lines
3.4 KiB
Markdown
# clipsync Protocol v1 (MVP)
|
|
|
|
This document defines an implementation-friendly first protocol for clipboard sync.
|
|
|
|
## Transport
|
|
|
|
- Discovery: UDP broadcast on port `45821`
|
|
- Session: TCP on port `45821`
|
|
- Payload encoding: UTF-8 JSON frames
|
|
- Frame format on TCP: 4-byte big-endian length prefix + JSON bytes
|
|
|
|
## Versioning
|
|
|
|
Each message includes:
|
|
|
|
- `version`: protocol version string, initially `"1"`
|
|
- `type`: message type
|
|
- `request_id`: unique string per request/response pair where relevant
|
|
- `device_id`: sender stable ID
|
|
- `timestamp_ms`: unix epoch milliseconds
|
|
|
|
## Discovery message (UDP)
|
|
|
|
`DISCOVERY_ANNOUNCE`
|
|
|
|
```json
|
|
{
|
|
"version": "1",
|
|
"type": "DISCOVERY_ANNOUNCE",
|
|
"device_id": "desktop-uuid",
|
|
"device_name": "workstation",
|
|
"platform": "linux|windows",
|
|
"tcp_port": 45821,
|
|
"capabilities": ["clipboard:text"],
|
|
"timestamp_ms": 0
|
|
}
|
|
```
|
|
|
|
Android listens for these packets and shows peers.
|
|
|
|
## Pairing flow (TCP)
|
|
|
|
### 1) HELLO
|
|
|
|
Both peers exchange:
|
|
|
|
- protocol version
|
|
- device info
|
|
- ephemeral nonce
|
|
|
|
### 2) PAIR_REQUEST
|
|
|
|
Unpaired side sends pairing request including:
|
|
|
|
- public key
|
|
- pairing method (`"pin"` or `"qr"`)
|
|
|
|
### 3) PAIR_CHALLENGE
|
|
|
|
Responder sends short code metadata (or QR payload hash) and challenge nonce.
|
|
|
|
### 4) PAIR_CONFIRM
|
|
|
|
Requester returns challenge response.
|
|
|
|
### 5) PAIR_ACCEPT
|
|
|
|
Responder confirms trust and stores remote public key.
|
|
|
|
After `PAIR_ACCEPT`, both devices persist:
|
|
|
|
- `peer_device_id`
|
|
- `peer_public_key`
|
|
- `paired_at`
|
|
|
|
## Session messages
|
|
|
|
### `CLIP_UPDATE`
|
|
|
|
```json
|
|
{
|
|
"version": "1",
|
|
"type": "CLIP_UPDATE",
|
|
"request_id": "req-123",
|
|
"device_id": "android-uuid",
|
|
"timestamp_ms": 0,
|
|
"origin_device_id": "android-uuid",
|
|
"content_type": "text/plain",
|
|
"encoding": "utf-8",
|
|
"content_hash": "sha256:...",
|
|
"content": "clipboard text",
|
|
"sequence": 42
|
|
}
|
|
```
|
|
|
|
Rules:
|
|
|
|
- Only sync `text/plain` in MVP.
|
|
- Ignore messages with known `content_hash` from recent history.
|
|
- Ignore message if `origin_device_id` equals local device id (echo prevention).
|
|
|
|
### `ACK`
|
|
|
|
Acknowledges accepted `request_id`.
|
|
|
|
### `ERROR`
|
|
|
|
Contains error code and message (`UNPAIRED`, `UNSUPPORTED_VERSION`, `INVALID_MESSAGE`, `RATE_LIMITED`).
|
|
|
|
### `PING` / `PONG`
|
|
|
|
Keepalive every 15 seconds. Drop and reconnect after timeout.
|
|
|
|
## Security baseline
|
|
|
|
- Reject non-paired peers for clipboard writes.
|
|
- Encrypt session channel (TLS or Noise).
|
|
- Enforce max payload size (e.g., 1 MiB for MVP text).
|
|
- Apply rate limits per peer.
|
|
|
|
## Current daemon behavior (implemented)
|
|
|
|
- Server sends `HELLO` immediately on TCP connect.
|
|
- First client message must be `HELLO` (else `ERROR: INVALID_MESSAGE`).
|
|
- After `HELLO`, server accepts either:
|
|
- `PAIR_REQUEST` and responds with `PAIR_ACCEPT` (while persisting peer trust), or
|
|
- `CLIP_UPDATE` for already-paired peers.
|
|
- `CLIP_UPDATE` from unknown peers is rejected with `ERROR: UNPAIRED`.
|
|
- While connected, server polls local clipboard and emits `CLIP_UPDATE` to the connected paired peer when local text changes.
|
|
- Incoming paired `CLIP_UPDATE` (`content_type = text/plain`) is applied to local OS clipboard.
|
|
- Inbound `CLIP_UPDATE` messages are deduplicated using `content_hash` (with a fallback hash for missing values).
|
|
- Inbound `CLIP_UPDATE` messages are rate-limited per peer; over-limit updates return `ERROR: RATE_LIMITED`.
|
|
- Keepalive is implemented: server sends periodic `PING`, responds to inbound `PING` with `PONG`, and drops idle sessions after timeout.
|