207 lines
16 KiB
Markdown
207 lines
16 KiB
Markdown
# 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.
|