Files
hugo 1abd75282e
Dependency security audit / rustsec (push) Failing after 23s
Implement Rust-owned Android Keystore secure storage (#93)
2026-10-08 16:22:57 +02:00

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.