# Android parity, architecture and security audit (#111) Audit date: 2026-10-15. Scope is milestone 23, the current non-Watch iPhone surface and the Android release graph. [parity.md](parity.md) maps every capability to its Rust owner, concrete Android component and executable checks. [architecture.md](architecture.md) remains the support/threat-model contract. A test listed as a coverage pointer is not evidence that an opt-in journey ran. The current iPhone AutoFill extension controller has no implemented selection flow. The Android provider requirement is nevertheless fully implemented through Rust and the native OS services; the matrix does not attribute that capability to the empty iPhone controller. ## Findings corrected in this audit - First-run Preferences previously claimed that configuration had been lost while repository/key data remained. `PrivateStorage::authentication` now distinguishes the validated empty configuration from actual `ConfigurationMissingWithData`. Genuine recovery still retains its typed error and ciphertext. Rust regressions first failed against the old implementation and now pass for both cases. - Startup onboarding, binding verification and Home independently used the same nonblocking runtime gate. Brief legitimate overlap could display a storage failure on a cold start. Rust now tolerates up to one second of contention, checking the same capability-validated lock every 10 ms. Longer operations still fail with retry guidance. Scratch-owner locks remain nonblocking so startup cleanup never deletes active scratch. Tests cover transient release, a retained long-lived lock, unchanged encrypted bytes and successful retry. - Busy authentication is a typed `Conflict` titled `Private Storage Busy`, rather than a claim of corruption/recovery. The Home facade preserves the typed conflict. Setup also reports contention as `Private Storage Busy` while genuine setup corruption retains `Setup Recovery Required`. No frontend derives state from error strings. - API 26 exposed a test synchronization race in the TOTP disposal check: the baseline was read immediately after a click, before Compose removed the code view, and one legitimate pre-disposal tick could land afterward. The regression now asserts that the code node is absent and reads/checks the counter on the UI thread. It retains the full 1.2-second no-further-ticks assertion; no production timer or assertion was disabled to obtain a pass. The corrected focused check passed three isolated API 26 runs, then passed in the full 53-case app runner. The failed original attempt is not counted as passing evidence. - The release artifact gate now rejects `debuggable`, `testOnly` and `profileable` access in the packaged production manifest. Debug/profile/benchmark packages remain separately identifiable test artifacts. Manifest regression tests prove rejection and the release CI path invokes the gate on decoded APK contents. - Updated Android-reachable Rustls 0.23.45 to 0.23.46 (and raised its minimum), faster-hex 0.10.0 to 0.10.1, rqrr 0.10.1 to 0.11.0 / lru 0.18.2, and the yanked chacha20 0.10.1 to 0.10.2. QR fixtures and native decode tests exercise the decoder change; both release ABIs were rebuilt from the resulting lockfile. ## Boundary and lifetime review Reviewed the connected `crates/storage` mobile/authentication/onboarding/Git/QR/ credential/secret-store implementations, `crates/mobile-bridge`, `crates/android`, and Android app/core/features/platform/native-loader modules. Rust owns directory capabilities, repository operations, parsing, generation, OTP, QR transfer/decode, secret records, authentication, endpoint and caller/site policy. UniFFI exports mechanical values and OS callbacks. Kotlin maps typed state, schedules blocking calls off the main thread, collects input and presents Android UI/OS services. No Android-reachable application component launches a process. Project-authored Rust forbids unsafe code; generated/dependency JNI/JNA/Keystore FFI is outside that claim. `std::process::id()` obtains a transaction owner ID and does not launch a process. Build/release tools legitimately invoke host commands. The desktop CLI's external-editor support is outside the Android runtime graph and this audit; this report does not claim that every unrelated workspace executable avoids processes. NativeRuntime's `File`/`ZipFile` checks inspect installed APK/native-library facts, not the repository. Provider package-certificate hashing obtains OS identity facts; Rust compares caller/site identities and decides grants/matches. Kotlin never parses pass documents, `.gpg-id`, GPG packets, OTP payloads or Git transport errors. The source/dependency gate complements this connected-code review. Git endpoints are validated typed HTTPS on Android (SSH feature disabled), without embedded account/token userinfo. Repository remote configuration is authoritative. Tokens stay in Rust-owned sealed records with OS wrapping keys. TLS uses the Android platform verifier and redirects cannot forward credentials. There is no cleartext transport or certificate-validation bypass to make fixtures pass. Ordinary persistent state contains encrypted entries, encrypted private keys, configuration, encrypted secret envelopes and private TOTP metadata, not decrypted entry fields, passphrases, tokens or OTP seeds. Editor recovery is encrypted and revision/generation-bound. Android saved state retains non-secret selection/query facts only. Logs and reports use bounded typed diagnostics and sanitized metrics; there is no app crash-upload service. Kotlin/Java memory cannot promise complete erasure, and accessibility/IME/clipboard consumers can observe deliberately revealed or handed-off data under OS control. ## Threat-test coverage and limits | Threat | Enforcement and executable evidence | Interpretation | | --- | --- | --- | | Keystore record tamper, missing keys, restore/device binding | Rust Android backend/private-storage tests; real `AndroidKeystoreTest.deviceUnlockedRoundtripReplaceDeleteAndTamper` | Failure retains existing encrypted repository; no silent identity/key recreation for bound data | | Biometric cancellation, rejection, invalidation/lock removal | Rust authentication/backend tests; opt-in platform `AndroidKeystoreTest` and `AuthenticationLeaseTest` | OS-interactive checks require separate recorded runs; do not count standard-suite exclusions as passes or change enrollment on the retained app phone | | Backup/transfer and update | Packaged XML reference and all-domain exclusion gates, `BackupPolicyTest`, private-context/update Rust tests | Install over the same ID/signing lineage; emulator data retained. OEM transfer tools remain outside available emulator evidence | | Screen capture/recents, focus loss, logs and plaintext saved state | Secure-window/recents platform wiring; `PresentationSecurityTest`, source audit, generation/lease Rust tests | Computer-use visibility is test observation, not proof that an arbitrary compromised OS cannot capture pixels | | Clipboard and accessibility | OS ownership/expiry adapter, Rust copy policy, `PresentationSecurityTest`; manual hardening evidence | Deliberate reveal/copy is observable by authorized platform consumers. TalkBack focus/activation is verified; speech output and actual external switch scanning are not claimed | | Background, process death, dirty edits and stale callbacks | `AuthenticationLeaseTest`, `PresentationSecurityTest`, `NativeEntryViewerTest`; Rust lease/editor checkpoint/cancellation tests | No stored lease, no plaintext draft restore, no replay of a completed mutation | | Git dirty tree/conflict, TLS/auth/offline/cancellation | Rust `mobile_home::android_tests`, Git transport/embedded tests, Compose Home tests | Injected transports prove failure ordering without sending fixture tokens to external servers; runtime local Git journey is separate from public-server availability | | Camera interruption, malicious/oversized QR/image | `AndroidQrCameraTest`, Rust QR/key-transfer tests and bounded Y-plane adapter | Camera restart and real frame delivery are distinct from decoding known QR vectors in Rust | | Malicious providers, forged/stale/cancelled intents, preauth disclosure | Rust `mobile_credentials`/authentication tests, `ProviderIntentSecurityTest`, `NativeCredentialProviderTest`, system `provider-client` journeys | Paths/candidates may be listed while locked; secret fill requires exact live request, explicit consent/selection and valid lease | | Offline, doze/battery, memory/storage pressure | Rust fault/rollback tests; native environment/hardening checks | No physical disk filling or physical-device coverage is implied by the low-storage broadcast | | Root/debug assumptions | Non-debuggable packaged release gate, private capabilities/backup/Keystore policy | Rooted/fully compromised devices and local hostile timing observers are outside the accepted sandbox threat model; a debug APK is not the production security boundary | ## Dependency exceptions RustSec database: 1,329 advisories, commit `edb58f5974b83eb8302332821cb6d389777aa351`, updated 2026-10-14T22:18:32+02:00. After the updates, the Android normal/build dependency graph has two remaining findings, explicitly tracked outside this milestone in [#125](https://git.rfc1437.de/hugo/IronStorage/issues/125): - `pgp -> rsa 0.9.10`: [RUSTSEC-2023-0071](https://rustsec.org/advisories/RUSTSEC-2023-0071.html) has **no patched release**. OpenPGP decryption is local and follows explicit authorization; Git/HTTPS does not expose a remote RSA-decryption endpoint. This limits the ordinary remote oracle but does not establish constant-time RSA or eliminate local timing risk. Retaining existing pass RSA-key compatibility does not justify claiming the advisory fixed. Reassess an upstream-reviewed, compatible pure-Rust mitigation under #125. - `gix-protocol -> bisync 0.3.0`: yanked/deprecated, with no replacement release. Its [upstream notice](https://github.com/JM4ier/bisync) says the author retired the tiny utility to avoid maintenance/supply-chain responsibility. The existing lockfile checksum remains required; track a reviewed transport update removing it under #125 rather than hiding the warning or adding an unreviewed fork. Workspace `cargo audit` still exits nonzero: besides RSA it reports host/desktop packages outside the Android graph (including quinn-proto and ruzstd). They were not silently updated as Android work. No clean whole-workspace audit or unrestricted release approval is claimed. License/SBOM policy and vulnerability status are separate checks. ## Run record Required Rust gates passed: formatting, warnings-as-errors workspace/all-target check, Clippy with warnings denied, and the full workspace test run (524 passed, zero failed). The final Android verification/build passed debug/release/AAB, Lint including release, ktlint, JVM tests, both ARM native builds/metadata/16-KB package audits, decoded backup/transfer/manifest checks, separate native symbols, R8 mapping and deterministic CycloneDX/license policy (1,839 inventory components). The CI orchestration self-test run passed all 11 tests. The final API 37 / 4-KB ARM64 run passed app/core/bridge/platform instrumentation and five cold starts. The native core suite exercised the new 12-pair concurrent onboarding/Home regression along with real encrypted edit/mutation/OTP/provider and catalog/recovery tests. Native QR analysis was 84 ms, authenticated fill 1,249 ms, and 10,000-entry navigation/search were 90/273 ms. Debug cold starts were 840–1,220 ms; five native-load measurements were 40.8–58.4 ms. The final API 26 / 4-KB ARM64 run likewise passed all four instrumentation suites and five cold starts after the TOTP test synchronization fix. Native QR analysis was 80 ms, authenticated fill 1,210 ms, and 10,000-entry navigation/search were 76/227 ms. Debug cold starts were 210–249 ms. Computer use opened actual first-run Preferences and verified `Set Up Password Store` with local/HTTPS setup guidance, replacing the old false data-recovery claim without creating or deleting a store. API 26 release-equivalent Macrobenchmark passed five cold and five warm starts: initial display 149.8–171.0 ms cold and 44.1–95.4 ms warm; full cold display 189.7–219.5 ms. The measured artifact retained its app data and baseline profile. The explicit `biometricSecurity=true` real-OS fixture passed on the existing API 37 phone fingerprint enrollment. Computer use observed the CryptoObject prompts and cancellation result; emulator sensor events supplied matching/nonmatching synthetic fingerprints. OS counters confirmed one rejected crypto attempt before a matching unlock. The test asserted enrollment, shared reload while unlocked, manual lock, cancellation, real activity background interruption, fresh post-background unlock, immediate memory-pressure relock and biometric disable. Its namespace, public passphrase and generated store were isolated from the retained app. Fixture aliases and runner were cleaned up; device enrollment and app data were retained. Strict API 37 / 16-KB ARM64 passed all four suites and five cold starts with both compatibility paths disabled. Native QR analysis was 82 ms, authenticated fill 1,216 ms, and 10,000-entry navigation/search were 82/251 ms. Cached TOTP, warm refresh and provider preparation p95 were 5,349/8,241/4,195 us. Five debug cold starts were 832–971 ms and native loads 66.4–75.8 ms. Computer use verified the actual retained six-commit store and clean worktree loaded after the final cold starts, with no startup busy/recovery failure. Strict API 37 release-equivalent Macrobenchmark also passed five cold and five warm starts: initial display 192.3–335.9 ms cold and 77.7–134.5 ms warm; full cold display 399.0–510.1 ms. Computer use completed real framework Autofill and Android Credential Manager selection/consent/unlock against the separate provider client; both returned the exact expected public fixture username/password. The initial requests expired during slow inspection; fresh requests passed within the unchanged request lifetime. Pre-authentication selectors exposed entry paths, not the secret fields. Real API 35 ARM64 Keystore enrollment-change invalidation passed in an isolated platform test app: the current-set biometric key became `Invalidated`, while a separate DeviceUnlocked key still decrypted its public token. The first attempt timed out while driving OS setup and is not counted as a pass; the fresh test completed in 124.4 seconds, within the unchanged 180-second fixture window. Computer use observed the encryption prompt, OS enrollment and return to the fixture. Only synthetic enrollment on that dedicated device changed; no ordinary IronStorage app was installed there. Test keys were deleted by the fixture. The device was shut down gracefully afterward. Its existing synthetic PIN was retained; no data or enrollment reset was used to recover the cold boot. The updated fold emulator also loaded real first-run setup and Preferences via computer use, with readable adaptive navigation and the corrected setup guidance. The final connected-code review checked the diff, callers, error mappings, fixtures and acceptance matrix. No issue is closed on an expired/failed run. After the final setup-contention title correction, all four Rust gates passed again (524 tests) and the full Android build/audit passed again for both ARM ABIs. The final native viewer regression, including concurrent onboarding/Home, passed on API 37 in 54.9 seconds. A fresh app cold launch took 743 ms and computer use verified the retained three-commit vault and clean worktree. ARM64 emulator runtime evidence stays distinct from ARMv7 packaging and historical per-issue evidence.