6.4 KiB
Android private storage (#90)
The platform supplies canonical Context.filesDir, noBackupFilesDir and
cacheDir facts off the UI thread. PrivateStorage validates existing, distinct
roots and opens directory capabilities. It derives all subordinate paths;
Kotlin never lists, reads or mutates repository contents. activate() installs
one immutable process path context so existing Rust configuration/onboarding,
Git, key-transfer and TOTP APIs use these roots. Android calls made before
activation fail instead of consulting HOME, cwd, XDG or public storage.
| Object | Placement | Update / cleanup policy |
|---|---|---|
| pass-format repository | noBackupFilesDir/ironstorage/vault |
Retained; existing storage/Git atomic mutation rules |
| GPG key material | noBackupFilesDir/ironstorage/keys |
Retained; Rust crypto import/export only |
| secret-free configuration | noBackupFilesDir/ironstorage/config.toml |
Atomic Rust writes; relative vault/key paths survive relocation |
| TOTP discovery catalog | noBackupFilesDir/ironstorage/totp-catalog.toml |
Valid catalogs survive updates and relocation; invalid/oversized/wrong-store catalogs are removed and rebuilt by discovery |
| version / device binding | noBackupFilesDir/ironstorage/layout.toml |
Atomic version 1 marker; unknown versions require a compatible migration |
| transactional scratch | noBackupFilesDir/ironstorage/scratch |
Per-owner file locks; startup removes orphaned transactions and skips live owners |
| disposable cache | cacheDir/ironstorage |
Only this capability is cleared by clearDisposableCache; vault/key/catalog data is retained |
| protected binding and credentials | Keystore AES-GCM keys / noBackupFilesDir/ironstorage/secrets (#93) |
No serialized token or passphrase in configuration or layout metadata |
filesDir |
OS root fact only | No durable password-store object is placed here |
Directory opens use safe Rust rustix handles with NOFOLLOW; names are single
components. Existing vault/key trees reject links and special files. Directories
are 0700; files are 0600, preserving an existing owner executable bit. These are
Unix permission checks inside the Android UID/SELinux sandbox, not promises
about physical media erasure or an untrusted OS. No application process or
handwritten unsafe FFI is introduced.
Configuration must refer to vault and key material below keys. A corrupt or
missing configuration with remaining data produces typed recovery, never a new
store or automatic repository deletion. Interrupted .local-setup-*
transactions are retained because they can contain the previous repository.
Owned orphan .ironstorage-config-* and .ironstorage-atomic-* files are removed only after taking their
file lock; active atomic writers are left alone. A directory gate protects the
creation-to-owner-lock interval from concurrent startup cleanup. Android atomic files use
private named capabilities and rename because SELinux denies anonymous-file
linking via /proc; safe rustix locks supply the semantics absent from the
pinned Rust standard library on Android. Missing layout metadata with existing
data is a recovery error, so a device binding cannot be silently discarded. Scratch/cache cleanup unlinks
links without following their targets and cannot descend into the vault.
verifySecureStorage uses the OS-mechanics callback from #91 and a storage-owned
ApplicationBinding locator. Rust creates an opaque 32-byte device record and
persists only its SHA-256 in layout metadata. A missing or mismatched protected
record for an already bound layout reports RestoredWithoutKeystore or
InvalidSecureBinding; it never creates a replacement that would conceal key
loss. Locked/unavailable storage has separate recovery kinds. #93 supplies the real Android Keystore adapter; startup verifies
the binding through it before presenting the shell. The contract is tested with
loss/mismatch fixtures.
Errors distinguish full storage, read-only storage, access denial, missing roots, concurrent initialization, corrupt configuration/layout and unsupported layout versions. Failed initialization or writes leave existing repository, key and configuration data intact. Free space or repair the reported condition and retry; destructive reset is not an update/recovery operation.
Normal APK/Play updates and process recreation retain no-backup app data. Uninstall deletes the sandbox and Keystore entries; recovery then requires the upstream Git repository and GPG key/passphrase or an explicit encrypted export. Backup/transfer is not an alternative recovery path. The manifest disables backup, and both legacy backup rules and cloud/device-transfer rules exclude all nine credential- and device-protected domains. No-backup and cache roots are intrinsically excluded by Android. See the Android backup contract.
Validation includes real GPG fixture byte preservation through updates and directory relocation, valid/corrupt TOTP catalogs, interrupted writes/setup, concurrent scratch ownership, link-safe cleanup, device-binding loss/mismatch, and typed storage-pressure/read-only I/O faults. A separate integration test activates isolated roots and uses existing local onboarding to create an actual Git repository and protected GPG key without environment/path overrides. Instrumented tests inspect installed backup XML/manifest resources and execute private initialization/recreation/cache clearing on persistent 4 KB and strict 16 KB Android emulators. No physical-device testing is required.
Delivery verification on 2026-10-08: all four required workspace gates passed (431 Rust tests), Android ktlint/unit/lint gates passed, and each persistent API 37 emulator (4 KB and strict 16 KB) passed eight native tests and two app tests covering all 25 shell state/tab combinations and installed backup policy. The native fixture creates a real protected GPG key and Git repository through Rust, persists a preference atomically, and survives subsequent APK/test-process updates. Debug and release install-over/startup tests preserve the layout on both emulators. All three ABI payloads pass the APK/AAB native audit. Swift private runtime/redaction tests and 588 shared contract entries pass; iPhone, Watch and AutoFill simulator builds pass. The persistent iPhone app also launches with its existing store intact. Backup verification reads installed policy and package flags; it does not export sandbox data to a transport.