Files
IronStorage/docs/android/architecture.md
T
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

221 lines
16 KiB
Markdown

# Android architecture decision
Accepted implementation contract for [#88](https://git.rfc1437.de/hugo/IronStorage/issues/88)
in milestone 23, **06 - Implement an Android version of the software**.
Reviewed against the checkout's iPhone app, AutoFill extension, and shared Rust
mobile APIs on 2026-10-08. The milestone goal is full native Android feature
parity. This decision records ownership and release requirements; it does not
claim that the Android implementation or device audit already exists.
## Language and presentation
Use Kotlin, Gradle Kotlin DSL, one activity, Jetpack Compose, Material 3 and
Material 3 Adaptive. Use Navigation 3: its [stable release is 1.2.0](https://developer.android.com/jetpack/androidx/releases/navigation3).
Screen ViewModels expose immutable StateFlow state and accept typed events.
Compose observes with collectAsStateWithLifecycle. Hilt supplies dependencies;
KSP runs its processors. Blocking UniFFI calls run on a bounded background
coroutine dispatcher. Cancellation cancels the Rust operation as well as the
coroutine; a late result must not restore a revoked lease or stale screen.
Rust owns repository layout, config, keys, OpenPGP, Git, conflicts, documents,
generation, OTP, QR payloads, identities, external access permissions and
secure-secret orchestration. Kotlin collects input, renders Rust state, and
adapts OS mechanics. No app component launches a subprocess. No Kotlin feature
reads repository files, parses entries/URIs or error labels, generates passwords
or codes, or independently decides credential matches or authentication policy.
Use typed semantic icons/actions, stable IDs, generation/revision tokens,
view-ready records and typed errors. #91's `crates/mobile-bridge` removes SF Symbol
and Watch assumptions from shared phone contracts and generates Swift and Kotlin
from one source. `crates/apple` opts into Watch; `crates/android` packages the
phone surface through #92's native build gates. A thin Android bridge may adapt platform
callbacks; it must contain no password-store decisions or handwritten unsafe
Rust. Generated/dependency FFI stays isolated from project-authored safe Rust.
## Modules and allowed dependencies
| Module | Responsibility | Allowed project dependencies |
| --- | --- | --- |
| android/app | Activity, composition root, navigation, manifest and service registration | features, core, platform |
| android/features | Related Compose workflows, screen ViewModels, accessibility | core only |
| android/core | Android-facing facade, immutable presentation contracts, coroutine scheduling, bridge mapping | rust-bridge |
| android/platform | Keystore, BiometricPrompt, clipboard, camera, paths and OS request translation | core contracts |
| android/rust-bridge | Generated UniFFI Kotlin and ABI native packaging | shared Rust bridge via build tasks |
| Shared Rust bridge | Mechanical records, lifetime/callback/error conversion | crates/storage |
| crates/storage | All domain behavior and trust-boundary validation | audited Rust dependencies |
Start with these five Gradle modules; split feature groups only when useful.
Platform implementations are injected at app composition. Feature code cannot
import generated UniFFI/JNA types or depend directly on platform/bridge modules.
OS services delegate to core; exported service input is untrusted. Only Rust
holds repository capabilities. Gradle dependency checks, source-boundary checks,
and release manifest inspection are gates in #89/#94/#111, not substitutes for
review. Generated bindings and native/build outputs are not checked in.
## Support and build policy
Production package, namespace and application ID: **de.rfc1437.ironstorage**.
Updates retain this ID and signing lineage. Tests use derived IDs; a development
variant may not masquerade as the production signed installation.
Minimum SDK: **26** (Android 8, including Autofill). Compile/target SDK: **37**
(Android 17) at this decision, following the [SDK instructions](https://developer.android.com/about/versions/17/setup-sdk).
Recheck stable SDK and [Play requirements](https://support.google.com/googleplay/android-developer/answer/11926878)
at implementation and release. Never select preview APIs to satisfy a release
policy. The installed android-37.0 SDK reports an empty codename and
PreviewSdkInt=0 (revision 2); verify the same package metadata in CI before
pinning the build, rather than relying on preview wording in older instructions.
Release ABIs: **arm64-v8a** and **armeabi-v7a**, corresponding to
**aarch64-linux-android** and **armv7-linux-androideabi**. On 2026-10-08 the user
removed x86_64 Android from the support and CI requirements and specified
emulator-only testing for every issue. This supersedes the original three-ABI
decision in #88; historical test evidence remains historical.
All shipped native dependencies, including JNA, must pass ELF LOAD
alignment, APK ZIP alignment and actual 16 KB runtime checks. Do not depend on
compatibility mode. [Android's page-size guidance](https://developer.android.com/guide/practices/page-sizes)
requires auditing dependencies as well as the app library.
At #89 pin a mutually supported stable AGP/Gradle/Kotlin/KSP/Hilt set, JDK,
SDK build tools and NDK, plus wrapper checksum. The current [AGP 9.4 contract](https://developer.android.com/build/releases/agp-9-4-0-release-notes)
requires Gradle 9.6.0 and JDK 17 and supports API 37. This is a candidate build
baseline, subject to actual artifact availability and plugin compatibility.
The #89 scaffold pins AGP 9.4.1, Gradle 9.8.1, Kotlin 2.4.21, KSP 2.3.12,
Hilt 2.60.1 and JDK 25 (JVM output 17); its catalog and build instructions live
in [android/README.md](../../android/README.md). These pins are verified by the
scaffold build gates rather than inferred from minimum-version requirements.
No dynamic versions, snapshots or automatic toolchain substitutions. #92 pins
cargo-ndk, Rust targets/toolchain and the workspace UniFFI version and builds all
ABIs from the locked Cargo graph. Retain separate native symbols and R8 mappings.
Support phones, tablets, foldables, resizing/multi-window, desktop windowing,
portrait/landscape, system/light/dark themes, hardware keyboard, large fonts,
TalkBack and process recreation. Preserve non-secret navigation/filter state;
never serialize decrypted fields, passphrases, tokens, QR key payloads or a lease
into SavedStateHandle, bundles, preferences, crash reports or test artifacts.
Rotation/process death must not replay a mutation. In-flight mutations resolve
through Rust revision/transaction state before a retry.
Apple Watch, watchOS and Watch Connectivity are excluded. Android Auto, TV,
Wear OS and XR-specific experiences are outside the product; Wear is not a
substitute for Watch. Do not show disabled Watch controls on Android.
## Approved dependencies and review policy
These are approved purposes, not permission to add every library immediately.
#89 pins only scaffold dependencies; later owners add their required artifacts.
Exact versions live in one version catalog, Cargo.lock and reviewed toolchain
pins. Keep Gradle dependency verification metadata and locks where supported;
review checksums independently before accepting a new artifact. For each addition
and upgrade, record exact coordinates/version, upstream release/maintenance
check, SPDX license and bundled notices, transitives, security advisories and
compatibility results. Approval of a library family does not approve every
transitive version. #94 builds the resolved SBOM/license gate; #112 bundles
attributions and available source obligations.
| Library/API | Single purpose | License review and maintenance source |
| --- | --- | --- |
| Kotlin and Compose compiler plugin | Kotlin compilation | Apache-2.0, [JetBrains license](https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt); check matching stable compiler/plugin releases |
| Gradle, AGP and Android Lint | Reproducible packaging and Android static checks | Apache-2.0 tool distribution notices; [AGP compatibility/release notes](https://developer.android.com/build/releases/about-agp) |
| Compose BOM, Material 3, Adaptive, Activity Compose | Native controls/layout and activity hosting | Apache-2.0, [AndroidX license](https://github.com/androidx/androidx/blob/androidx-main/LICENSE.txt); check each resolved artifact's stable AndroidX release |
| Navigation 3 | Typed adaptive navigation/back stack | AndroidX Apache-2.0; stable release source above |
| Lifecycle ViewModel/Runtime Compose | Lifecycle-aware screen state collection | AndroidX Apache-2.0; stable AndroidX release notes |
| kotlinx.coroutines and coroutines-test | Bounded async calls, Flow and deterministic scheduling tests | Apache-2.0, [upstream](https://github.com/Kotlin/kotlinx.coroutines); check Kotlin compatibility and tagged release |
| Hilt/Dagger | Dependency injection | Apache-2.0, [upstream license](https://github.com/google/dagger/blob/master/LICENSE.txt); verify KSP/AGP support |
| KSP | Hilt code generation | Apache-2.0, [upstream license](https://github.com/google/ksp/blob/main/LICENSE); verify compiler/AGP support |
| UniFFI | Shared typed Swift/Kotlin bridge generation | MPL-2.0, [Mozilla license](https://github.com/mozilla/uniffi-rs/blob/main/LICENSE); pin workspace tool/library together |
| JNA Android AAR | UniFFI native loading/calls | Choose Apache-2.0 under [JNA dual license](https://github.com/java-native-access/jna/blob/master/LICENSE); verify Android ABI/16 KB binaries per version |
| cargo-ndk and NDK | ABI cross compilation/packaging | MIT OR Apache-2.0 for [cargo-ndk](https://github.com/bbqsrc/cargo-ndk); retain NDK component notices and review stable toolchain |
| AndroidX Biometric | OS authentication prompt | AndroidX Apache-2.0; choose stable API supporting CryptoObject and required auth policy |
| CameraX Preview/ImageAnalysis | Camera capture and frame delivery | AndroidX Apache-2.0; check stable artifacts and device support; Rust decodes/parses frames |
| AndroidX Credentials provider | Android 14+ password-provider integration | AndroidX Apache-2.0; verify provider support and stable artifact API |
| AndroidX Test and Compose UI Test | Instrumented/system and Compose behavior checks | AndroidX Apache-2.0; align with Compose BOM and test runner |
| JUnit | JVM unit tests | EPL-1.0 for [JUnit 4](https://github.com/junit-team/junit4); test-only, pin maintained 4.13.x patch |
| ktlint | Kotlin formatting/static style | MIT, [upstream](https://github.com/pinterest/ktlint); pin CLI/rules together and review Kotlin compatibility |
| Macrobenchmark/Baseline Profiles | Measure startup and optimize measured bottlenecks | AndroidX Apache-2.0; add only with reproducible measurements in #105 |
| WorkManager | Explicitly approved durable noninteractive work | AndroidX Apache-2.0; add only when an operation survives process loss safely |
| Android framework | Keystore, KeyguardManager, AutofillService, clipboard, FLAG_SECURE, private paths, backup rules, connectivity and permissions | OS APIs; no extra bundled crypto/storage implementation; behavior audited on supported APIs/OEMs |
DataStore is allowed only for presentation-only preferences if necessary.
Shared preferences use Rust configuration. Room, Retrofit/OkHttp, JGit,
Bouncy Castle/OpenPGP, Kotlin OTP/password/parser libraries, ML Kit payload
interpretation and a competing QR format are prohibited. Reuse existing Rust
crypto/Git/QR code. Do not schedule interactive unlock, secret display or repository
mutations as background WorkManager tasks.
## Threat model and state lifetime
Protect the pass repository/key material, passphrases, HTTPS application tokens,
OTP secrets/codes, decrypted fields, credential selections and identity grants.
Untrusted inputs include remote endpoints/data, repository entries and policies,
QR/images, OS provider callers, restored files, and concurrent/stale UI requests.
Rust validates transports before any repository mutation or connection and uses
repository remotes as authoritative. Android starts HTTPS-only with SSH disabled;
SSH endpoints still parse to typed unsupported errors. Adding SSH requires an
explicit audited support decision while preserving storage-owned transport policy.
The sandbox and Keystore protect against ordinary cross-app access and accidental
backup, not a fully compromised/rooted device. Debuggable builds, logs, screenshots,
recents thumbnails, clipboard history, accessibility, keyboard suggestions and
crash telemetry are disclosure paths. #100 uses FLAG_SECURE, redaction, background
relock, bounded clipboard ownership/expiry and memory cleanup; never claim complete
Kotlin/JVM memory erasure. Clear platform references and revoke Rust generations.
Keystore holds non-exportable AES-GCM wrapping keys. Only sealed envelopes reach
private storage. Rust decides secret type, identity, enrollment/invalidation,
authentication policy, lease and recovery. Kotlin translates OS error facts.
BiometricPrompt authorizes the required CryptoObject; device credential fallback
must match the Rust policy/API level and must not silently weaken it. Changed
biometrics, lock-screen removal, restored-without-key data, cancellation and a
stale callback fail closed and require explicit recovery. Provider candidates
are non-secret until authorized user/system selection plus a valid lease.
## Paths, backup, update and recovery
#90 receives canonical filesDir, noBackupFilesDir and cacheDir from Android once;
Rust creates scoped capabilities and owns all contents. Never infer HOME or use
public/shared storage. Place repository, keys, config, TOTP discovery catalog,
Keystore envelopes and transactional durable scratch under noBackupFilesDir.
Disposable camera/image buffers may use cacheDir only when cleanup is safe;
never use the cache for the only copy of a repository or credential.
Set allowBackup=false and explicit legacy backup and Android 12+ data-extraction
rules excluding all sensitive roots from cloud backup and device transfer. Test
OEM transfer behavior; no claim that one manifest flag defeats every OEM tool.
No automatic key/repository transfer. The TOTP catalog contains only canonical
path, ciphertext SHA-256 and is_totp; treat even that metadata as private.
An APK/Play update installs over the same ID/signing lineage and retains valid
private state and Keystore aliases. Migrations are Rust-owned, versioned and
atomic; migration failure retains prior recoverable data. Process death revokes
in-memory authentication, preserves valid completed cache checkpoints and leaves
atomic transactions recoverable. Low storage/read-only/corruption produce typed
errors without deleting unrelated data. Uninstall/reset deletes app data and can
make keys unrecoverable; reinstall/restore is not an update. Recovery needs the
user's externally retained encrypted key material/passphrase and, when configured,
a remote clone. Do not seed app data or commit credentials to prove recovery.
## Verification and acceptance
The [parity matrix](parity.md) assigns every current non-Watch mobile capability
and every Android issue to a Rust owner, current Android component and executable
checks. The [#111 audit](audit.md) records current results, runtime limits and the
unpatched/deprecated upstream dependency findings tracked outside this milestone
in [#125](https://git.rfc1437.de/hugo/IronStorage/issues/125). This traceability does
not claim that cargo-audit is clean or grant unrestricted release approval.
Every issue runs the AGENTS.md Rust gates before commit. Android changes also
run build/lint/unit/instrumented checks appropriate to the issue and computer-use
validation on the existing emulator. Update by installation over the existing
app, never uninstall/reset. If unlocking needs an unavailable user, label the
issue untested and document exactly what could not be checked. The user clarified on 2026-10-08 that validation for every milestone issue is
emulator-only; no physical devices are included in testing. Record emulator/API
limitations explicitly and use untested for an unavailable required emulator
check. This does not change production security behavior or claim Play approval.
Apple changes require XcodeGen and affected simulator builds and UI checks;
never re-enroll an already enrolled iPhone simulator in ordinary validation.
#94 owns reproducible CI and test boundaries, #105 accessibility/performance,
#111 the complete architecture/security/parity audit, and #112 emulator
install/update evidence and Play release preparation. No release approval is implied by this ADR.