Skip to content

Epic: Multi-Device Sync with End-to-End Encryption

Epic: Multi-Device Sync with End-to-End Encryption

Status: Delivered
CAS: CAS-171
Delivered: 2026-04-25
Sub-task docs: CAS-460 · CAS-468 · CAS-496 · CAS-515 · CAS-583


What shipped

Casaconomy can now organise financial data across multiple devices and participants — a household partner, a shared trip group, a recurring family budget — while keeping all data end-to-end encrypted. The relay server that carries changesets between devices never sees plaintext. Each device holds its own keys; no key material ever transits the server.

The epic landed in five sequential phases, each building on the last:

Phase 1 — Profiles as first-class entities (CAS-460)

Financial identities moved from a flat JSON file into a proper profiles SQL table with UUIDs, names, initials, and payment-share metadata. This is the foundation that group membership, group-scoped encryption, and multi-device onboarding all sit on. No user-visible behaviour changed in Phase 1 — the data model upgrade was backward-compatible.

Phase 2 — Groups (CAS-468)

Named buckets organise transactions, members, and encryption keys by context. Every household starts with a “Home” group; additional groups can be created for trips, events, or shared budgets. Existing transactions were silently backfilled into Home.

Two-tier encryption governs visibility: group changesets use a shared group key generated per group, while sensitive fields (account numbers, cardholder references) and private-scope changesets continue to use each user’s own private key. The legacy household key migrated automatically to a KeyId::Group(<default-uuid>) on startup.

Phase 3b — Real AEAD encryption + OS keychain (CAS-496)

The placeholder mock-crypto shim was replaced with XSalsa20-Poly1305 (from libsodium) for authenticated symmetric encryption and Ed25519 for device signatures. Keys live in the OS keychain (macOS Keychain; Secure Enclave-eligible on Apple Silicon) and never touch disk unprotected. A compile-time guard prevents mock-crypto from reaching a release binary. 33 integration tests cover roundtrips, nonce freshness, tamper detection, and sign/verify.

Phase 4 — Group-scoped rules and drift detection (CAS-515)

Automatic categorisation rules can now be scoped to a specific Group. When a group with event dates (start/end) is created or edited, a date-window rule is generated automatically and kept in sync with the group’s dates. If the dates later change and the rule falls out of step, a drift notification surfaces in the dashboard and group detail view with a one-click resolution. Rules manually edited by the user are marked user_edited and excluded from auto-updates.

Phase 5 — Invite codes and device pairing (CAS-583)

Adding a second device or a new group member requires no manual key export. The issuing device generates a short Crockford-Base32 invite code and broadcasts it over the relay as an X25519 ephemeral public key, signed with its long-term Ed25519 key. The accepting device verifies the signature, completes the ECDH handshake, derives a session key via HKDF-SHA256, and decrypts an encrypted identity bundle (XSalsa20-Poly1305 AEAD) that delivers all necessary key material. The relay is treated as an untrusted message bus throughout — it carries ciphertext only.


How to use it

Groups (Settings → Groups):

  • Your existing “Home” group is pre-created.
  • Add groups for trips or shared budgets. Set event dates to get auto-generated date-window rules in the categorisation engine.
  • Add members to a group; they will receive a group key via the invite flow.

Assigning transactions to a group:

  • When more than one active group exists, a Group picker appears in transaction views.
  • New transactions default to the currently active group.

Invite a second device or new participant:

  • Open the invite screen and tap Generate invite.
  • Share the short code or QR with the joining device.
  • The joining device enters the code or opens the deep link; pairing completes automatically.

Encryption:

  • Fully automatic. You may see a single macOS “allow keychain access” prompt on first launch after the Phase 3b update; click Always Allow.
  • If data is ever tampered with, decryption fails with an authentication error rather than returning garbage.

Architecture summary

Device A Relay (zero-knowledge) Device B
─────────────────────────────────────────────────────────────────────────
Changeset (plaintext)
Encrypt (XSalsa20-Poly1305) ──── ciphertext only ────► Decrypt
Sign (Ed25519) Verify (Ed25519)
Group key: per-group, shared among members via invite ECDH
User key: per-device, never leaves keychain

The EncryptionProvider trait abstracts the crypto backend. The real libsodium implementation is selected by the real-crypto Cargo feature (default); tests use mock-crypto. A static_assertions! guard panics at compile time if mock-crypto is enabled in a release profile.


Known limitations / follow-on work

  • End-to-end smoke test across two local instances — deferred to CAS-591.
  • Group join UI (Flow B) — the backend transport is in place but the UI for a new user joining an existing group is not yet wired up.
  • Key rotation — if a device key is compromised, existing encrypted data cannot currently be re-keyed without re-importing from CSV.
  • Multi-device rule sync — group-scoped rules exist on the device that created them; sync of rule state across devices is deferred.
  • Group picker in main transaction feed — the active group filter is not yet wired into the main transaction list; filtering works via get_transactions_by_group.
  • Relay hardening — the Cloudflare Worker relay lacks production-grade rate-limiting and failover.
  • iOS / Windows keychain backends — the release keychain path targets macOS only in the current build.
  • Profile data migration — the frontend still reads payers from the legacy JSON path; the cutover to get_profiles and data migration are deferred.