Files
IronStorage/docs/android/architecture.md
T
hugo 66b1d4a4c4
Android ARM verification / rust (push) Successful in 50m6s
Android ARM verification / android (push) Successful in 1h33m57s
Dependency security audit / rustsec (push) Successful in 21s
Establish native Linux ARM64 Android CI and verification gates (#94)
2026-10-09 05:02:59 +02:00

16 KiB

Android architecture decision

Accepted implementation contract for #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. 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. Recheck stable SDK and Play requirements 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 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 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.20, KSP 2.3.12, Hilt 2.60.1 and JDK 25 (JVM output 17); its catalog and build instructions live in 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; 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
Compose BOM, Material 3, Adaptive, Activity Compose Native controls/layout and activity hosting Apache-2.0, AndroidX license; 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; check Kotlin compatibility and tagged release
Hilt/Dagger Dependency injection Apache-2.0, upstream license; verify KSP/AGP support
KSP Hilt code generation Apache-2.0, upstream license; verify compiler/AGP support
UniFFI Shared typed Swift/Kotlin bridge generation MPL-2.0, Mozilla license; pin workspace tool/library together
JNA Android AAR UniFFI native loading/calls Choose Apache-2.0 under JNA dual 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; 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; test-only, pin maintained 4.13.x patch
ktlint Kotlin formatting/static style MIT, upstream; 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 assigns every current non-Watch mobile capability and every Android issue to a Rust owner and Android API. Its pending rows are implementation obligations, not accepted gaps. #111 must replace pending status with evidence and have no unexplained gap before #112 distribution.

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.