Files
hugo b54636b7bc
Android ARM verification / rust (push) Failing after 50m14s
Android ARM verification / android (push) Failing after 8s
Dependency security audit / rustsec (push) Failing after 23s
Complete Android parity and security audit (#111)
2026-10-15 13:40:50 +02:00

207 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.