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

88 lines
6.4 KiB
Markdown

# 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](keystore.md); 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](https://developer.android.com/identity/data/autobackup).
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.