128 lines
8.1 KiB
Markdown
128 lines
8.1 KiB
Markdown
# 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. Validation does not create or migrate files. #90 owns initialization,
|
||
permissions, relocation, backup and cache policy.
|
||
- `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.
|
||
|
||
```sh
|
||
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:
|
||
|
||
```sh
|
||
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.
|