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