Files
IronStorage/docs/android/mobile-contract.md
T
hugo 874c8139a4
Dependency security audit / rustsec (push) Failing after 23s
feat(android): implement private Rust storage runtime (#90)
2026-10-08 14:36:05 +02:00

8.3 KiB
Raw Blame History

Shared mobile contract (#91)

crates/mobile-bridge is the single UniFFI source for phone records, enums, conversions, objects, operations and OS callbacks. crates/android packages it without features. crates/apple packages it with the explicit watch feature. Neither packaging crate copies converters. The component namespace is ironstorage_mobile; the native library names remain ironstorage_android and ironstorage_apple. Swift source/header names follow the shared namespace.

Public workflow inventory

Storage owner Shared phone exports Identity / concurrency boundary
mobile shell, fixtures, tab selection typed tabs/states/semantic icons
mobile_home / git Home page, commits/changes/actions, controlled operations/progress typed actions, repository operation serialization and cancellation
mobile_passwords browse, search, folder/entry rows storage entry paths and row IDs
mobile_authentication / authentication unlock, reveal/copy, preferences, identity/appearance/timeout, lifecycle generation-scoped authentication handles, monotonic expiry
mobile_entry / document / generate structured fields, editor sessions, OTP image import, generation, save/discard session u64 and field IDs; original ciphertext/conflict checks
mobile_mutation / mutation preparation, request, commit, outcome opaque revision token validated at execution; explicit overwrite decisions
mobile_totp / otp discovery/progress/cancel, cached search, detail/code/copy storage-owned catalog, Unix-time inputs and expiry
mobile_onboarding / config server discovery, clone and local setup, token replacement typed transport/phase errors, controlled cancellation
mobile_key_transfer / presentation export/import, frames, progress, key selection storage-owned QR envelopes and bounded decoding
mobile_watch Apple opt-in only: preferences, selection, snapshots/status/receipts no Watch enum, field, method or control in the Android contract

MobileIcon expresses intent, never an asset name. UIKit maps it to SF Symbols in MobileIcons.swift. Android's core maps it exhaustively to its presentation enum; Compose maps navigation intents to vector drawables. Asset names and selected variants belong to the native UI. Actions already use typed enums.

OS boundaries

  • MobilePlatformPaths accepts absolute private data and cache roots, rejects traversal and overlapping roots, and lets storage derive config/vault/key paths. This lightweight validator does not create or migrate files. #90's MobilePrivateRuntime opens the three canonical OS directory capabilities, activates the shared configuration context, and owns initialization, permissions, relocation, backup and cache policy; see private storage.
  • MobileSecretBackend performs create/retrieve/replace/delete/lock/unlock on a typed locator and protection supplied by storage. Values are opaque encoded storage records. OS errors are enums. It cannot select accounts, protection, enrollment recovery, cache lifetime or authentication policy. Its adapter is usable by mobileAuthenticationWithPlatform; #93 implements Android Keystore. Preferences use that same injected backend, never a hidden native fallback.
  • MobileClock returns elapsed monotonic milliseconds from a stable origin. It cannot mint or refresh leases. Rust uses the existing authentication clock and generation checks. Unix timestamps required by TOTP/Git remain explicit inputs, not values parsed from rendered countdowns.
  • MobileLifecycleEvent reports foreground, background, device lock, real user activity, or cancellation. Storage revokes leases/editors on background/lock, checks expiry on foreground, and extends activity only for an active lease.
  • MobileClipboard reads/writes/clears OS text and waits for a requested timeout or cancellation. Rust's existing manager restores the prior text or clears only if the copied value is still current; newer clipboard content survives. Call off the UI thread; the facade serializes access to one OS clipboard.
  • MobileCameraFrame supplies luminance bytes, dimensions and row/pixel stride. Storage validates bounds (4096 pixels per dimension, 32 MiB plane, pixel stride 1–4) and decodes QR payloads. Native UI never interprets envelopes.
  • MobileCredentialRequest carries an opaque request ID, OS-asserted package, signing-certificate SHA-256 and web origins. Rust validates/canonicalizes the facts. A validated request is not an authorization grant. #109 implements matching, selected-candidate authentication and scoped grants in storage.

Ownership, cancellation and diagnostics

UniFFI objects are reference-counted Rust owners. Operation objects retain their authentication owner while work runs. Kotlin close() / Swift deinitialization release native handles; the final storage authentication owner destroys the lease and locks its store. Frontends cancel the exact operation on navigation or coroutine cancellation before releasing it. MobileAuthentication.cancel revokes its generation and discards editor state; it is not interchangeable with cancelling an individual discovery/Git operation. Late results are discarded by facade revision IDs. Polling, repaint and progress never count as user activity. Editor IDs and field IDs are stable within their owning session; revision tokens are opaque and must be returned unchanged to storage.

Secrets and payloads are owned values, not persisted view state. Rust secret buffers zeroize on drop. Swift/Kotlin cannot promise complete managed-memory erasure, so callers release revealed/copy/OTP/frame values when their operation ends and never log or save them. Generated records, associated-value enums and errors have redacted diagnostic rendering; typed fields remain available for deliberate UI presentation. Unexpected native/callback errors redact their payloads. Mechanical generator hardening is version-checked and preserves wire layouts and every UniFFI contract/API checksum.

Contract gates

Every Android binding generation generates both Swift and Kotlin from the compiled phone component. The build tool verifies all records/fields, types, functions and operation checksums against metadata, writes inventory.tsv, and rejects any Watch item in the phone surface. The Apple opt-in build generates both languages too. compare-contract compares complete shared signatures; only Watch items and the opt-in Watch inputs to preferences differ.

cargo build -p ironstorage-apple --lib
cargo run -p ironstorage-android-build -- contract-bindings \
  target/debug/libironstorage_apple.dylib android/uniffi.toml /tmp/ironstorage-watch-contract watch
cargo run -p ironstorage-android-build -- compare-contract \
  android/rust-bridge/build/generated/bindings-raw/debug/inventory.tsv \
  /tmp/ironstorage-watch-contract/inventory.tsv

On non-macOS build hosts, use the host shared-library extension. Apple generation copies the verified/hardened Swift source and header into apple/Generated. Contract tests exercise typed errors, checksum failure, callback ownership, cancellation/concurrency, camera bounds, large foreign payloads and redaction. All app validation uses persistent emulator devices; no physical-device gate is part of this milestone's testing scope.

The native Swift probe in tools/android-build/tests/mobile_contract.swift runs against the generated source and the host Apple library:

xcrun swiftc -swift-version 5 -module-cache-path /tmp/ironstorage-swift-cache \
  -I apple/Generated \
  -Xcc -fmodule-map-file=apple/Generated/ironstorage_mobileFFI.modulemap \
  -L target/debug -lironstorage_apple \
  -Xlinker -rpath -Xlinker "$PWD/target/debug" \
  apple/Generated/ironstorage_mobile.swift \
  tools/android-build/tests/mobile_contract.swift -o /tmp/ironstorage-swift-contract
/tmp/ironstorage-swift-contract

The Android native probe is MobileContractTest: run it with the existing NativeRuntimeTest on persistent 4 KB and strict 16 KB ARM64 emulators. UI and native instrumentation suites run sequentially to preserve input focus. The storage integration test exercises a real encrypted fixture with the injected secret backend/clock, activity renewal, background revocation, editor invalidation and final-owner cleanup without global configuration.