Security and trust boundaries for SuchConfig desktop device-to-device sync on a local network only (Wi‑Fi or wired LAN). This is the authoritative public security reference for the P2P feature. Delivery maturity: public-alpha-roadmap.md. Parallel durability path: trusted-folder-sync.md.
Product promise: vault configuration and secrets never traverse SuchConfig-hosted infrastructure. LAN sync is opt-in, local-only (no internet relay), and explicitly paired before any replication bytes move between machines.
Public Alpha: pairing + Handoff are dogfood-verified; incremental deltas and some transport hardening remain open.
| Principle | SuchConfig behavior |
|---|---|
| Local-first sovereignty | Each device keeps a full encrypted SQLite + Loro store; LAN is an acceleration layer, not the source of truth (data sovereignty). |
| No vendor vault server | Discovery and transport are direct between user-owned desktops on the subnet; no upload path to SuchConfig. |
| Trust before transport | Pairing pins Ed25519 device keys out-of-band (QR / signed JSON) before mDNS sessions or delta exchange are accepted (Step 2 shipped). |
| Rust owns the wire | mDNS, QUIC/TCP, pairing crypto, and framing live in src-tauri; one writer to vault rows remains Elixir + Vault.Crdt NIF. |
| Same merge contract as archives | Loro bytes applied through the same import/audit paths as .suchvault and Trusted Folder — no second ad-hoc merge format. |
| Defense in depth | Device signatures (authentication) + AES-256-GCM session frames + vault-unlock gate + at-rest passkey encryption; passkey never on the wire; plaintext SQLite never leaves the process. Transit confidentiality against passive capture is pending the key-agreement fix — see Current confidentiality limitation. |
In scope
- Rogue or curious devices on the same LAN (coffee-shop Wi‑Fi, shared office subnet).
- Stale or replayed pairing payloads.
- User mistake (pairing with the wrong machine).
- Corporate AP isolation or VPN blocking device-to-device traffic (availability, not confidentiality breach).
Out of scope / not mitigated by LAN P2P alone
- Compromise of a already-paired device (attacker has full local vault access).
- Physical access to an unlocked app session.
- Malware on the host with user privileges (same trust boundary as any local password manager).
- Nation-state interception outside the LAN segment.
Assumptions
- Users only enable WiFi / LAN sync on networks they treat as trusted enough for device pairing (home, team office), consistent with Anytype local-only posture.
- Global vault encryption (passkey / master key) remains required for decrypt at rest.
SuchConfig’s P2P design follows patterns common in local-first, password-manager, and mesh sync products:
| Practice | Standard / precedent | SuchConfig implementation |
|---|---|---|
| Out-of-band pairing | Signal safety numbers; QR / short-code device link patterns | Signed pairing offer/response over QR or manual JSON paste; 6-character short code for human verification |
| Asymmetric device identity | TLS certs, SSH host keys, WireGuard public keys | Ed25519 long-lived device keypair per install (RFC 8032) |
| Mutual authentication | mTLS, Noise IK, SPAKE2 | Offer and response signed by initiator/responder keys; initiator verifies response before storing peer |
| Short-lived enrollment | OAuth state, pairing TTL | Pairing sessions expire after 10 minutes (SESSION_TTL_SECS) |
| Pinning trusted peers | Certificate pinning, SSH known_hosts |
peers.json stores pinned Ed25519 public keys per paired device_id |
| Encrypted replication | E2EE sync (Anytype, Syncthing-class) | All frames AES-256-GCM via the session cipher. Handoff snapshots keep item values as passkey-encrypted crdt_snapshot_encrypted blobs (metadata is plaintext in the JSON envelope); incremental deltas carry plaintext Loro updates protected only by the session layer. Transport key agreement (X25519/QUIC) is the open item — see Current confidentiality limitation |
| Least privilege on network | Bind to LAN, no cloud fallback | LAN-only sync plane; no SuchConfig relay; user-operated Trusted Folder as parallel path |
| Audit without leakage | SOC2-friendly activity logs | vault_merge_audit_events records counts and metadata, not secret bodies (tracking causality) |
| Explicit consent | Opt-in sync toggles | Pairing wizard requires Confirm on responder; initiator completes with response verification |
Status: implemented in src-tauri/src/p2p/ (June 2026). Does not yet move vault CRDT bytes over the network — it establishes trust for Steps 3–5.
- Initiator generates ephemeral
session_id, 6-char short code, and a signed pairing offer (kind: "pairing_offer", versionv: 1). - Responder validates offer signature, expiry, and short code; user confirms device name.
- Responder signs pairing response (
kind: "pairing_response") and stores initiator as a pinned peer. - Initiator verifies response signature and session binding, then stores responder as a pinned peer.
Canonical string formats for signatures (domain-separated to prevent cross-protocol replay):
suchconfig-pair-offer-v1|{session_id}|{device_id}|{public_key_b64}|{expires_at}
suchconfig-pair-response-v1|{session_id}|{initiator_device_id}|{responder_device_id}|{public_key_b64}
| File | Content | Permissions |
|---|---|---|
{app_data}/p2p/device.json |
Device UUID, display name, Ed25519 public + secret key (base64) | 0600 on Unix |
{app_data}/p2p/peers.json |
Map of paired device_id → pinned public key, paired_at, pinned: true |
0600 on Unix |
Secret keys are generated with OsRng (CSPRNG). Pairing rejects self-pairing, duplicate peers, invalid signatures, and expired sessions.
These are two separate keys and the distinction is deliberate:
| Key | Scope | Lifetime | Gates |
|---|---|---|---|
Ed25519 device key (device.json) |
Identifies this install on the LAN; signs pairing + handshake | Generated once at install with OsRng; persists |
Network trust (who may connect) |
| Vault master key (passkey / Argon2id) | Decrypts vault contents at rest | Per unlock session | Data access (whether bytes can be read/applied) |
The device key is not derived from the vault/passkey unlock — it exists before any vault is unlocked so discovery and pairing can work, and it survives lock/unlock cycles. Confidentiality is preserved by a second, independent gate: LAN sync only moves vault bytes when the vault is unlocked on both peers (the handshake authenticates the device; the unlock gate authorizes data). A compromised device key alone therefore grants LAN reachability and trust, not plaintext vault access.
Note: earlier wording in p2p-wifi-sync-best-practices.md §4 implied the device identity is "derived from the passkey unlock flow." That is a target-architecture aspiration, not the shipped model — the shipped model is the two-key split above. Treat this table as authoritative.
On every successful mutual handshake, both peers log the other's pinned Ed25519 public-key fingerprint (SHA-256, first 8 bytes, hex) — never the key bytes themselves. This makes wrong-peer and impersonation attempts auditable from [suchconfig-p2p] logs and lets a tester compare the logged fingerprint against the value shown in Settings → WiFi / LAN devices. A fingerprint that does not match the pinned peer is treated as a handshake failure even if the cryptographic exchange completes (the resolved endpoint is not the paired device).
| Crate | Version (manifest) | Role |
|---|---|---|
| ed25519-dalek | 2 |
Ed25519 signing and verification for device identity and pairing payloads |
| rand | 0.8 |
OsRng for key generation (via ed25519-dalek) and short-code entropy |
| uuid | 1 |
Version-4 device and session identifiers |
| serde / serde_json | 1 |
Versioned, typed pairing offer/response JSON |
| base64 | 0.22 |
Key and signature encoding in JSON (standard alphabet) |
| qrcode | 0.14 |
QR encoding of offer/response JSON for OOB transfer |
| image | 0.25 (PNG only) |
QR rasterization to PNG for LiveView display |
| parking_lot | 0.12 |
In-memory pairing session map (mutex) |
| chrono | 0.4 |
ISO8601 paired_at timestamps |
| hostname | 0.4 |
Default human-readable device name |
Why Ed25519: widely deployed (SSH, Signal, TLS 1.3 ecosystem), fast, compact keys, and well-audited Rust implementations. It is the same curve family recommended for modern device identity when X.509 ceremony is unnecessary at pairing time.
Status: not shipped; security requirements are fixed here so implementation can be reviewed against this doc.
| Crate (planned) | Role |
|---|---|
| mdns-sd | Multicast DNS advertise/query for _suchconfig._tcp.local on the LAN |
Rules: mDNS exposes only paired or in-progress session metadata (device id, ephemeral port) — never vault content. Anonymous mDNS responders are not trusted for delta exchange until mutual pairing and session crypto complete.
| Crate (planned) | Role |
|---|---|
| tokio | Async runtime for network I/O (already in Cargo.toml) |
| quinn | QUIC over UDP — encrypted, multiplexed streams; industry-standard datagram security |
Rules:
- Bind listeners to LAN interfaces only; no SuchConfig-hosted relay or internet fallback.
- Mutual authentication using pinned peer keys from Step 2 (TLS client/server certs or equivalent derived from device identity — final wire format TBD).
- Rate limits and framed messages; fuzzing and soak tests before RC.
LAN replication carries the same encrypted Loro snapshot bytes already used elsewhere in SuchConfig:
| Crate (existing) | Role |
|---|---|
| aes-gcm | AES-256-GCM for .loro.enc and archive envelopes |
| argon2 | Argon2id key derivation (Trusted Folder / vault master key) |
| sha2 | SHA-256 sidecars for backup integrity |
| keyring | OS keychain integration for vault wrapped keys (macOS / Linux / Windows) |
Merge semantics stay in suchconfig_vault_core / Loro via the Elixir NIF — Rust P2P must not apply CRDT merges directly (project-vault-CRDT-usage.md).
Severity: high — must fix before any release beyond two-device, single-owner dogfood. Documented openly so it is reviewed, not assumed away.
What is solid today: authentication. The mutual Ed25519 challenge/response over pinned keys means an on-LAN attacker cannot impersonate a paired device or man-in-the-middle the session — they cannot sign the challenge nonce without a pinned private key, and unpaired devices are rejected (HandshakeReject).
What is not solid today: confidentiality against a passive eavesdropper. The session key is computed as:
session_key = SHA-256( SESSION_KEY_DOMAIN ‖ sorted(signature_initiator, signature_responder) )
Both signatures are sent in cleartext handshake_response frames before the cipher exists (the key is derived from them afterward). An attacker who records the full session therefore has both signatures and can recompute session_key, then decrypt every AES-256-GCM frame. Practical exposure by path:
| Path | Exposure if a recorded session is decrypted |
|---|---|
| Handoff (Step 4) | Item metadata (titles, kinds, folder structure) — values stay passkey-encrypted (crdt_snapshot_encrypted) |
| Incremental delta (Step 5) | Plaintext secret/config values (Crdt.diff_from runs on decrypted Loro snapshots) |
This is a classic "derived a key from transmitted material instead of agreeing on a secret" mistake. It does not weaken authentication or the at-rest passkey encryption, and the passkey is never on the wire.
Remediation (one of):
- Ephemeral X25519 ECDH inside the signed handshake (near-term, keeps raw TCP): each side sends an ephemeral X25519 public key, signs it with its device Ed25519 key; the session key is the ECDH shared secret, which is never transmitted and gives forward secrecy. Add constant-time comparisons and fingerprint binding at the same time.
- iroh / QUIC transport swap (Step 8): TLS 1.3 supplies authenticated key agreement + forward secrecy natively, retiring the bespoke derivation.
Until one lands, treat LAN sync as opt-in dogfood on a trusted network you own (its current default-off posture). See public-alpha-roadmap.md.
flowchart TB
subgraph user["User trust actions"]
P[QR / confirm pairing]
T[Opt-in LAN sync toggle]
end
subgraph wire["Rust src-tauri — transport"]
M[mDNS discovery]
Q[QUIC + TLS mutual auth]
end
subgraph app["Application boundary"]
E[Elixir Vault.Crdt NIF — sole writer]
A[vault_merge_audit_events]
end
subgraph at_rest["Already shipped"]
K[Passkey / Argon2id master key]
C[AES-GCM encrypted Loro blobs in SQLite]
end
P --> M
T --> M
M --> Q
Q -->|"encrypted Loro bytes"| E
E --> A
K --> C
E --> C
- Pair only on networks you control or trust; treat short codes like pairing PINs.
- Remove stale peers from Settings when a device is decommissioned.
- Prefer Trusted Folder for off-LAN or air-gapped backup; LAN P2P complements it, does not replace it.
- If AP isolation blocks sync, use archive export or Trusted Folder — not a bypass through SuchConfig cloud (none exists).
Manual pairing / LAN checks are founder UAT scripts (not published in Public Alpha docs).
Still open for Public Alpha / later tags:
- Fuzz pairing JSON and future framing parsers.
- Replay resistance review for LAN delta batches (nonces / frontiers tied to Loro
oplog_vv). - Downgrade attack tests (reject unauthenticated plaintext sync).
- Authenticated key agreement (confidentiality fix — see Current confidentiality limitation): replace today's derive-key-from-signatures scheme with an ephemeral X25519 ECDH exchange inside the signed handshake (device Ed25519 key signs the ephemeral pubkeys). The session key becomes a shared secret that is never transmitted — closing the passive-eavesdropper gap and giving forward secrecy as a side effect. The QUIC/iroh transport swap (Step 8) delivers TLS 1.3 key agreement + forward secrecy for free and is the alternative path.
- Constant-time comparison: verify challenge/nonce and any MAC/tag checks with constant-time equality (
subtle::ConstantTimeEqor equivalent), never==on byte slices, to avoid timing oracles on the handshake path. - Handshake fingerprint binding: assert the connected peer's Ed25519 fingerprint equals the pinned
peers.jsonvalue before deriving the session key (defends against endpoint reuse / impersonation; see Handshake fingerprint logging above). - Sleep/wake and multicast failure soak tests; background-listener survival under macOS App Nap / hardened runtime, Windows power throttling, Linux session lock.
- External security review before public LAN sync RC.
- trusted-folder-sync.md — parallel encrypted backup path
- public-alpha-roadmap.md — Public Alpha maturity
- SECURITY.md — repo-root security policy