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 ────► DecryptSign (Ed25519) Verify (Ed25519)
Group key: per-group, shared among members via invite ECDHUser key: per-device, never leaves keychainThe 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_profilesand data migration are deferred.