Share the platform-neutral Rust mobile contract (#91)
Dependency security audit / rustsec (push) Failing after 22s

This commit is contained in:
hugo committed 2026-10-08 13:25:09 +02:00
1 parent fcb36a0684
commit 180851087b
52 files changed
+8932 -4263

No files matched your search

+4 -4
View File
@@ -25,10 +25,10 @@ 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 removes SF Symbol and Watch assumptions
from shared phone contracts. Keep Swift compatibility through mechanical
adapters. #92 generates Kotlin and Swift from one shared UniFFI surface rather
than copying crates/apple conversions. A thin Android bridge may adapt platform
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.
+127
View File
@@ -0,0 +1,127 @@
# 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.
+1 -1
View File
@@ -36,7 +36,7 @@ each row. Every issue linked here belongs to milestone 23.
| iPhone AutoFill credential selection (CredentialProviderViewController) and shared identity/access domain | desktop/read/document/secret_store today; storage-owned provider contract to add | AndroidX CredentialProviderService (API 34+) and framework AutofillService (API 26+) | #109; typed caller/site identity, non-secret candidates, explicit selection/auth, forged/stale requests, permissions and cross-process lifetime |
| Camera permissions, private paths, secret-store mechanics, clipboard, scene/process events, screen privacy | Rust repository/config/secret_store/authentication/presentation; platform callbacks contain facts only | Android framework adapters, FLAG_SECURE, backup rules, ClipboardManager | #90/#93/#98/#100; backup inspection, sandbox paths, pressure/update, screen/clipboard/lifecycle tests |
| Accessible labels/actions, large text, keyboard, responsive layout and localization | Typed Rust state/actions; UI accessibility is Kotlin | Compose semantics, TalkBack, adaptive layout, resources | #105; measured keyboard/TalkBack/large-font/multi-window/RTL/rotation coverage |
| Watch pairing/install state in Settings; TOTP Watch selection, snapshots, receipts, transfer and watchOS detail | mobile_watch; Watch fields currently in mobile_totp/mobile_authentication and Apple bridge | Excluded from Android records and UI by #91 | Deliberate Watch-only exclusion; no unsupported placeholder controls or Wear implementation |
| Watch pairing/install state in Settings; TOTP Watch selection, snapshots, receipts, transfer and watchOS detail | mobile_watch; shared bridge's explicit `watch` feature enabled only by crates/apple | Absent from Android records and UI (#91) | Deliberate Watch-only exclusion; no unsupported placeholder controls or Wear implementation |
Name search matches current mobile behavior; CLI/TUI decrypted grep is not an
iPhone parity claim. Android has no separate filesystem-based search. The