Skip to content

Peer Sync Security

Kenneth LaCroix edited this page Apr 13, 2026 · 1 revision

Peer Sync Security

MoodHaven Journal syncs encrypted journal entries directly between devices on the same LAN — no cloud server, no relay, no accounts required. This page covers the security design for contributors and security reviewers.

For the general overview → README


Overview

┌─────────────────────────────────────────────────────────────┐
│  Layer 4: Encrypted Sync Engine (TCP, port 44000–44999)     │
│  [4-byte length][12-byte nonce][AES-256-GCM(JSON payload)]  │
├─────────────────────────────────────────────────────────────┤
│  Layer 3: Secure Pairing                                    │
│  6-digit PIN + QR code → mutual device trust                │
├─────────────────────────────────────────────────────────────┤
│  Layer 2: Peer Discovery                                    │
│  mDNS/DNS-SD: _moodhaven._tcp.local                          │
│  Tauri events: peer:discovered, peer:lost                   │
├─────────────────────────────────────────────────────────────┤
│  Layer 1: Device Identity                                   │
│  Ed25519 key pair in device.json + peer_key.bin             │
│  deviceId = first 16 hex chars of SHA-256(publicKey)        │
└─────────────────────────────────────────────────────────────┘

Threat Model

Threat Mitigation
Passive eavesdropper on LAN All sync payloads AES-256-GCM encrypted on the wire
Unknown device attempting sync Only trusted (paired) devices accepted; others disconnected at HELLO
Device ID spoofing Transport key derived from both public keys; cannot be derived without genuine private key
Replay attack Per-session nonce (12 bytes, random) included in every encrypted frame
Compromised peer reads journal Journal content is encrypted end-to-end; sync engine moves ciphertext, not plaintext
Password mismatch between devices Blobs sync correctly; frontend decrypt fails gracefully — no data corruption
MitM during pairing PIN must be verified out-of-band (user reads PIN off Device A and enters it on Device B)

Layer 1 — Device Identity

ed25519-dalek: generate_keypair() → (private_key, public_key)

private_key  →  {app_data_dir}/peer_key.bin      (0600 permissions)
public_key   →  {app_data_dir}/device.json
deviceId     =  hex(SHA-256(public_key))[0..16]

The private key never leaves the device. If device.json is deleted, the deviceId changes and all peers must re-pair.

Layer 2 — Peer Discovery (mDNS)

Broadcast announces: service type _moodhaven._tcp.local, port, TXT records device_id + device_name.

Not exposed on the network: public key, journal content, password.

Layer 3 — Secure Pairing

Device A                              Device B
  peer_generate_pairing_token()
  → generates 6-digit PIN
  → starts pairing server (HTTP, random port)
  → shows QR: { host, port, deviceId, pin }
                                        peer_accept_pairing(host, peer_id, pin)
                                        → connects to Device A's pairing server
                                        → exchanges public keys
                                        → verifies PIN
                                        → stores Device A in trusted_devices.json
  receives Device B's public key
  verifies PIN
  stores Device B in trusted_devices.json
  closes pairing server

Trusted device storage ({app_data_dir}/trusted_devices.json):

[{
  "deviceId": "9a4f0b2c...",
  "deviceName": "Ken's MacBook",
  "publicKey": "base64url-ed25519-public-key",
  "pairedAt": "2026-03-18T10:00:00Z"
}]

PIN is 6 digits, valid only for the current pairing session, communicated out-of-band. Trust revocation: peer_revoke_device(deviceId) removes the entry — the device is rejected at HELLO on the next connection.

Layer 4 — Encrypted Sync Engine

Sync Port

port = 44000 + (parseInt(deviceId[0..4], 16) % 1000)  // range: 44000–44999

Wire Format

[4 bytes: BE uint32 payload length]
[12 bytes: random AES-GCM nonce]
[N bytes: AES-256-GCM ciphertext of JSON payload]

Transport Key Derivation

Both sides derive the same key without transmitting it:

transport_key = SHA-256(
    "moodhaven-sync-v1:" +
    sort_lexicographic([base64(pubKeyA), base64(pubKeyB)])
)

Protocol Sequence

Device A (client)                    Device B (server)
  HELLO (plaintext) ─────────────▶   { deviceId, protocolVersion }
  ◀───────────────── HELLO (plain)   { deviceId, protocolVersion }

  [Both verify peer is in trusted_devices.json]
  [Both independently derive transport_key]

  MANIFEST (encrypted) ──────────▶  { entries: [{id, updated_at}] }
  ◀──────────── MANIFEST (encrypted) { entries: [{id, updated_at}] }

  [Each side computes what the other is missing]

  ENTRIES (encrypted) ───────────▶  [{id, content (ciphertext), mood, …}]
  ◀─────────────── ENTRIES (encrypted)

  DONE (encrypted) ──────────────▶
  ◀──────────────────── DONE_ACK

  [Both update peer_sync_state (last_sync_at)]

Conflict Resolution

Last-write-wins (LWW) by updated_at timestamp. Conservative but safe — no data is deleted, but concurrent edits on two devices result in one version being kept. Peer sync is designed for "merge missing entries," not collaborative real-time editing.

What Crosses the Wire

Plaintext (intentional): entry ID, updated_at, mood (1–5), privacy_mode, book_id, pinned.

Ciphertext (opaque blobs): entry content ({ iv, data, salt }).

Not synced: settings, 2FA config, WebDAV credentials, Oura PAT, device identity/pairing data, media attachments.

Auto-Sync Triggers

  1. peer:discovered event for a trusted device, if 30+ seconds since last sync
  2. Immediately after a new device is paired

Security Review Scope

File What to review
src-tauri/src/commands/peer_sync_engine.rs TCP server, protocol, encryption/decryption
src-tauri/src/commands/peer_pairing.rs PIN generation, key exchange, trust storage
src-tauri/src/commands/peer_identity.rs Key generation, persistence
src-tauri/src/commands/peer_discovery.rs mDNS broadcast content
src/lib/peerSyncEngineService.ts Frontend orchestration
src-tauri/capabilities/default.json ACL for sync commands

Full reference: docs/peer-sync-security.md

Clone this wiki locally