Skip to content

feat(ffi): extend at-rest encryption to iOS/macOS - #322

Merged
kafkade merged 1 commit into
mainfrom
kafkade-ios-web-at-rest-encryption
Jul 27, 2026
Merged

feat(ffi): extend at-rest encryption to iOS/macOS#322
kafkade merged 1 commit into
mainfrom
kafkade-ios-web-at-rest-encryption

Conversation

@kafkade

@kafkade kafkade commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Description

Extends local-at-rest encryption from the CLI to every native platform. The shared Rust FFI previously opened the working store (vault.db) as a plain SQLite file, so on iOS/iPadOS/macOS account names, transaction descriptions, amounts, and commodities were readable from a locked device's filesystem or an unencrypted backup. This routes all working-store opens through SQLCipher, keyed by a subkey derived from the already-unlocked vault key, and migrates any existing plaintext store in place.

What's included

  • FFI keyed open (crates/ldgr-ffi/) — a shared open_encrypted_db helper applies the SQLCipher key via a raw-hex PRAGMA key (derived through HKDF-SHA256 from the vault key) and forces a page read so a wrong key fails fast. All three open sites (create, open, open-with-session-key) now go through it. SQLCipher is enabled per target: bundled-sqlcipher-vendored-openssl for iOS/macOS, plain SQLite on watchOS (which never opens a vault).
  • Explicit, verified migration — legacy plaintext vaults are upgraded via needs_migration() + migrate()/migrateWithSessionKey(): it re-materializes the store as SQLCipher, verifies the schema version and every table's row count before swapping, and keeps the original as vault.db.plaintext.bak for backout. Password unlock migrates with the password; biometric unlock migrates with the cached session key so the user is never forced to re-enter it.
  • iOS unlock wiring (apps/ios/, bindings/swift/)LdgrClient gains async needsMigration/migrate/migrateWithSessionKey wrappers, and UnlockView runs the upgrade inline (brief "Encrypting…" state, no separate screen) before opening.
  • Web at-rest verification (apps/web/) — the web app is already encrypted at rest by design (sql.js runs only in memory and is sealed into the AES-256-GCM vault container before it is written to IndexedDB); this adds a regression test asserting the persisted blob is ciphertext (no SQLite header, no known plaintext tokens) and that Web Storage holds no vault secrets. No SQLCipher/rusqlite is added to the WASM graph.
  • Build + docs — the XCFramework build script now exports iOS 16 / macOS 13 / watchOS 10 deployment targets (required so vendored-OpenSSL objects link). Adds a Swift byte-level at-rest test, a new architecture decision record documenting the per-platform at-rest model, and a CHANGELOG entry.

Related Issues

Closes #315
Relates to #295

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Documentation update
  • Refactoring (no functional changes)
  • CI / infrastructure
  • Other (describe below)

Component

  • crates/ldgr-core/ — Core library (crypto, accounting, storage)
  • crates/ldgr-cli/ — CLI tool
  • crates/ldgr-server/ — Sync server
  • bindings/swift/ — UniFFI Swift bindings
  • apps/ios/ — iOS / iPadOS / watchOS app
  • apps/web/ — Web app (Next.js + WASM)
  • docs/ — Documentation

Primary change is in crates/ldgr-ffi/ (the shared Rust FFI working-store layer), which has no dedicated checkbox above.

Privacy Checklist

  • No plaintext financial data is sent to or stored on the server
  • No new metadata exposure introduced (or documented if unavoidable)
  • Any new external service interaction is documented in the trust boundary model — N/A, no new external service; server and sync transport are untouched
  • Crypto changes use audited RustCrypto crates — no custom primitives (HKDF-SHA256 key derivation reused from the CLI; encryption via SQLCipher; no custom primitives)
  • Key material is never exposed in error messages, logs, or Debug output (the derived key type zeroizes on drop and redacts its Debug output; only PRAGMA key is set)

Checklist

  • I have read CONTRIBUTING.md
  • Tests pass (cargo test --workspace) — 743 passed / 0 failed; web npm test 32 passed; Swift swift test 3 passed (incl. at-rest test on a real macOS SQLCipher slice)
  • Clippy passes (cargo clippy --workspace -- -D warnings)
  • Formatting passes (cargo fmt --check)
  • I have updated documentation (if applicable) — new at-rest ADR + CHANGELOG

@kafkade
kafkade enabled auto-merge (squash) July 27, 2026 23:25
@kafkade
kafkade merged commit c1efa89 into main Jul 27, 2026
14 checks passed
@kafkade
kafkade deleted the kafkade-ios-web-at-rest-encryption branch July 27, 2026 23:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

M0/0f: Extend at-rest encryption to iOS (FFI) and web/WASM

1 participant