Some checks are pending
ci / test (pull_request) Waiting to run
Wire format lives in docs/PROTOCOL.md so the handshake, invite, and frames can be read without the Rust. Threat model points at it. The in-house audit notes are a finding-status table, not a third-party audit. SDK README no longer claims the legacy concat invite signature is what we encode.
138 lines
5.2 KiB
Markdown
138 lines
5.2 KiB
Markdown
# OnionWire for Android
|
|
|
|
Two artifacts out of one Gradle build:
|
|
|
|
| Module | Output | What it is |
|
|
|---|---|---|
|
|
| `:sdk` | `sdk-release.aar` | The reusable library. UniFFI Kotlin bindings + `libonionwire_sdk.so`. Any Android app can depend on this. |
|
|
| `:app` | `app-release.apk` / `app-debug.apk` | A Jetpack Compose + Material 3 messenger that depends on `:sdk` and nothing else. |
|
|
|
|
The Linux TUI in the repository root is a **separate product** with a separate
|
|
Cargo workspace. Nothing here turns it into a WebView, and nothing here imports
|
|
its TUI code. Same wire protocol: [docs/PROTOCOL.md](../docs/PROTOCOL.md).
|
|
|
|
## Requirements
|
|
|
|
* JDK 17+ (built and tested on JDK 21)
|
|
* Android SDK with **platform 36** and **build-tools 36.0.0**
|
|
* NDK **28.2.13676358** (NDK r28c) — also what Rust's Android link step uses
|
|
* Rust with the `aarch64-linux-android` target and `cargo-ndk`
|
|
* `ANDROID_HOME` set, or `sdk.dir` in `android/local.properties` (gitignored)
|
|
|
|
One-time setup:
|
|
|
|
```bash
|
|
sdkmanager --install "platform-tools" "platforms;android-36" \
|
|
"build-tools;36.0.0" "ndk;28.2.13676358"
|
|
rustup target add aarch64-linux-android x86_64-linux-android
|
|
cargo install cargo-ndk
|
|
```
|
|
|
|
## Build
|
|
|
|
```bash
|
|
cd android
|
|
./gradlew :sdk:assembleRelease :app:assembleDebug # or :app:assembleRelease
|
|
```
|
|
|
|
The Rust cross-compile and the UniFFI Kotlin bindings are wired into the Gradle
|
|
tasks — there is no manual codegen step. `:sdk:preBuild` depends on them.
|
|
|
|
Emulator / x86_64 slice:
|
|
|
|
```bash
|
|
./gradlew :app:assembleDebug -Ponionwire.abis=arm64-v8a,x86_64
|
|
```
|
|
|
|
Or from the repo root, with checksums and an optional publish step:
|
|
|
|
```bash
|
|
scripts/build-android-local.sh # -> dist/onionwire-<v>-android-arm64-v8a.apk
|
|
PUBLISH_TAG=v0.3.0 scripts/build-android-local.sh
|
|
```
|
|
|
|
## Decisions you should know about
|
|
|
|
### `minSdk = 26` (Android 8.0)
|
|
|
|
The floor is not NDK-imposed — Rust's `aarch64-linux-android` std and NDK 28
|
|
both happily target API 21. We take 26 deliberately:
|
|
|
|
* it is the first API level where adaptive icons and notification channels are
|
|
unconditional, so there is one icon path instead of two;
|
|
* it keeps a single `arm64-v8a` APK inside Play's currently-supported range
|
|
without carrying legacy ART/JIT workarounds for pre-O devices;
|
|
* the crypto stack (`ring`, `rustls`, `argon2`, `chacha20poly1305`) is
|
|
exercised on 4.4+ kernels here, which is what we actually test.
|
|
|
|
**Devices cut:** Android 7.1 and older. If you need those, the change is
|
|
`-Ponionwire.api=24` **and** a real device test — do not bump it silently.
|
|
|
|
### 16 KB page size
|
|
|
|
The NDK r28 link step emits `max-page-size=16384` for 64-bit Android, which is
|
|
what Play requires for new apps. Verify with:
|
|
|
|
```bash
|
|
readelf -l android/sdk/build/rustJniLibs/arm64-v8a/libonionwire_sdk.so | grep LOAD
|
|
```
|
|
|
|
### Permissions
|
|
|
|
`INTERNET` only, declared at first launch. No camera (invites are pasted, not
|
|
scanned), no contacts, no storage permission — the data directory is
|
|
`context.filesDir`, which is already app-private. There is **no**
|
|
`FOREGROUND_SERVICE` and no keep-alive notification in v1: the node lives only
|
|
while the process does, and the UI says so. Adding one is a product decision
|
|
(battery, Play's FGS type declarations), not a silent fix.
|
|
|
|
### Data at rest
|
|
|
|
`context.filesDir/onionwire/` (mode 0700), holding `onionwire.db` and the Arti
|
|
state dir. `android:allowBackup="false"` plus explicit `data_extraction_rules`
|
|
exclusions, because an auto-backup of `onionwire.db` would hand the wrapped
|
|
message key *and* the plaintext identity keys to Google Drive. Chat bodies are
|
|
encrypted at rest; identity keys are not — that is the locked v1 posture.
|
|
|
|
### Signing
|
|
|
|
Both `debug` and `release` are signed with the **standard debug keystore**
|
|
(`~/.android/debug.keystore`, auto-created by AGP). This is what makes the
|
|
release APK installable for sideloading. It is **not** a Play upload key;
|
|
nothing here should be uploaded to Play. When a real upload key exists, put it
|
|
in `~/.gradle/gradle.properties` or CI secrets and reference it from
|
|
`signingConfigs` — never in this repository.
|
|
|
|
### Release builds are not minified
|
|
|
|
`isMinifyEnabled = false` for release. Keeps for the UniFFI/JNI surface already
|
|
exist in `sdk/consumer-rules.pro`, but enabling R8 without a mapping-file upload
|
|
path and an on-device test of the JNI surface is how you ship an
|
|
`UnsatisfiedLinkError` nobody can read. Turn it on together with crash
|
|
deobfuscation, not before.
|
|
|
|
## CI
|
|
|
|
`.forgejo/workflows/release.yml` runs on an aarch64 Linux container on the Pi.
|
|
It cannot build an APK. Android artifacts are built on a host with the SDK/NDK
|
|
via `scripts/build-android-local.sh`, exactly like the x86_64 Linux binary is
|
|
built via `scripts/build-release-local.sh`.
|
|
|
|
## Layout
|
|
|
|
```
|
|
android/
|
|
settings.gradle.kts
|
|
build.gradle.kts # plugin aliases only
|
|
gradle/libs.versions.toml # version catalog
|
|
sdk/ # -> AAR
|
|
build.gradle.kts # cargo-ndk + uniffi-bindgen wiring
|
|
consumer-rules.pro
|
|
src/main/AndroidManifest.xml
|
|
app/ # -> APK
|
|
build.gradle.kts
|
|
src/main/java/com/siriusdevops/onionwire/
|
|
MainActivity.kt # NavHost
|
|
WireViewModel.kt # owns the single Wire node
|
|
ui/ # Compose screens (M3)
|
|
```
|