Done in https://github.com/badbundle/vault-app/pull/612 (merged as ee9130b6): docs/on-device-encryption.md.
Verdict: feasible without compromises, but not with SwiftData.
- Field-level encryption leaves counts, types, dates, order, killphrase flags and the tag graph readable.
- A custom SwiftData
DataStoresilently ignoresdelete(model:where:)and unique upserts. - SwiftData in memory with an encrypted file is too slow for large vaults: 1.1 s to load 5,000 items.
The design:
- File: a single encrypted file of 16 equal-size slots, always present.
- While unlocked: the vault is held as plain records in an in-memory record store.
- Keys: each vault has a random data key, wrapped by an Argon2id-derived key.
- Key derivation: 64 MiB, with passes calibrated on the device to about 0.5 s when the file is created.
- Saving: every change is written as an atomic, verified replacement of the whole file.
- Users who never set a password: they keep today's SQLite store.
- Turning the password off: rewraps the key with a device key.
Measured on an M5 Max:
- Key derivation: about 170 ms.
- Loading 1,000 items: about 9 ms.
- Saving at 1,000 items: about 19 ms.
Decisions:
- Argon2id, vendored as C.
- Parameters calibrated on the device.
- N = 16 slots, L = 10 reserved for duress nesting.
- No option to exclude the vault from device backups.
- Restoring widgets and AutoFill after the password is turned off is in scope.
Implementation:
- VAULT-22: sub-issues VAULT-40 to VAULT-50.
- VAULT-23: VAULT-51.
- VAULT-34: VAULT-52.
Side findings, filed separately: VAULT-53 (backup fields), VAULT-54 (keyboard learning) and VAULT-55 (plain-store residue).