Complete Android parity and security audit (#111)
This commit is contained in:
1 parent
45d78aa5b3
commit
b54636b7bc
14 files changed
+498
-75
No files matched your search
@@ -0,0 +1,206 @@
|
||||
# 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.
|
||||
Reference in new issue
Block a user