onionwire/crates/onionwire-sdk
apk-dev 672f2c9990
All checks were successful
ci / test (pull_request) Successful in 3m22s
feat(android): SDK AAR (UniFFI) + Compose APK, Arti rustls feature split
Adds the Android product alongside the existing Linux TUI, in one repo.

SDK — crates/onionwire-sdk is a UniFFI facade over the very same `onionwire`
crate the TUI runs on. Identity, invite, friend upsert, send/receive, rotate
and wipe all delegate; no protocol is reimplemented, so an Android peer and a
Linux peer interoperate. It is its own Cargo workspace because Arti's TLS
backends are non-additive: the TUI keeps native-tls (OpenSSL), Android needs
rustls + static-sqlite (no OpenSSL, no system libsqlite3 in the NDK).

android/ — Gradle project. :sdk produces the AAR (Kotlin bindings generated at
build time + libonionwire_sdk.so via cargo-ndk), :app is a Kotlin/Compose/M3
messenger depending on :sdk only. minSdk 26, targetSdk/compileSdk 36,
INTERNET-only, data in filesDir, backups excluded.

Root Cargo.toml grows `native-tls` (default) and `rustls` features so exactly
one Arti TLS backend is selected per build graph. The default build is
unchanged: same backend, ratatui still a normal dependency, src/tui.rs
untouched.

Also: Store::self_fingerprint/set_petname + Node wrappers (additive only),
scripts/build-android-local.sh, README sections, .gitignore for local SDK paths.
2026-09-10 18:20:19 -04:00
..
src feat(android): SDK AAR (UniFFI) + Compose APK, Arti rustls feature split 2026-09-10 18:20:19 -04:00
Cargo.lock feat(android): SDK AAR (UniFFI) + Compose APK, Arti rustls feature split 2026-09-10 18:20:19 -04:00
Cargo.toml feat(android): SDK AAR (UniFFI) + Compose APK, Arti rustls feature split 2026-09-10 18:20:19 -04:00
README.md feat(android): SDK AAR (UniFFI) + Compose APK, Arti rustls feature split 2026-09-10 18:20:19 -04:00
uniffi.toml feat(android): SDK AAR (UniFFI) + Compose APK, Arti rustls feature split 2026-09-10 18:20:19 -04:00

onionwire-sdk (Android)

Kotlin-facing UniFFI bindings over the same Rust crate the OnionWire Linux TUI runs on. An Android peer and a Linux peer that exchange invites interoperate — there is one protocol, not two.

This is not a WebView wrapper and not a client for a hosted service. Each install runs Arti in-process (arti-client 0.46, onion-service-client + onion-service-service), hosts its own v3 onion service, and dials friends' onions directly. There is no tor binary, no torrc, no Orbot as the pipe.

Locked protocol facts

Piece Rule
Identity ed25519 keypair. The pubkey is who you are.
Roster key friends.pubkey, UNIQUE. Petnames are local only.
Onion A locator, not an identity. It rotates.
Invite onionwire:v1:k=…:o=…:spk=…:sig=…
Crypto Noise IK (Noise_IK_25519_ChaChaPoly_BLAKE2s). spk is x25519.
Fail closed Peer onion down → the send fails. No outbox, no spool in v1.
At rest Chat bodies encrypted (Argon2id wrap + ChaCha20-Poly1305). Identity keys in sqlite are plaintext — that is honest, not an oversight.

Invite string

onionwire:v1:k=<ed25519 pubkey hex64>:o=<v3 onion address>:spk=<x25519 prekey hex64>:sig=<ed25519 sig hex128>
  • sig is a signature over the concatenation k ‖ o ‖ spk as raw ASCII bytes.
  • A malformed, unsigned, or tampered invite is rejected before anything is stored.
  • Same k never creates a second friend. It updates the stored locator on the existing row. That is the whole point of keying the roster on the pubkey.
  • The invite carries no display name and no Monero address — those arrive later over the wire as signed prf frames, not through the invite.

Adding the AAR to another Android app

The SDK ships as onionwire-sdk-<version>.aar on the Forgejo release, with a matching .sha256.

// settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        flatDir { dirs("libs") }   // or however you vendor the AAR
    }
}

// app/build.gradle.kts
dependencies {
    implementation(files("libs/onionwire-sdk-0.1.0.aar"))
}

Requirements:

  • minSdk >= 26. The AAR is built with the NDK against android-26.
  • abiFilters must include an ABI the AAR ships. The release AAR includes arm64-v8a. Add x86_64 at build time with -Ponionwire.abis=arm64-v8a,x86_64 if you need an emulator.
  • Keep uniffi.onionwire_sdk.** (see the AAR's consumer-rules.pro, applied automatically by AGP) if you enable R8.

Kotlin surface

Everything is a suspend fun — Arti bootstrap and onion publish take minutes, and a send retries the peer for up to 3 minutes before failing.

import uniffi.onionwire_sdk.*

// 1. Open (or create) the identity inside app-private storage.
//    `filesDir`, never external storage. Empty passphrase fails closed.
val wire: Wire = openWire(File(context.filesDir, "onionwire").absolutePath, passphrase)

// 2. Who you are, and where you are right now.
val fingerprint: String = wire.fingerprint()   // stable forever
val onion: String = wire.onion()               // changes on rotate

// 3. Invite out / invite in.
val mine: String = wire.invite()
val preview: InviteInfo = decodeInvite(pasted)  // verifies sig, stores nothing
val friend: FriendInfo = wire.addFriend(pasted)

// 4. Roster and chat.
val roster: List<FriendInfo> = wire.friends()
wire.setPetname(friend.pubkeyHex, "ada")
wire.send(friend.pubkeyHex, "hello wire")
val msgs: List<ChatMessage> = wire.messages(friend.pubkeyHex)

// 5. Locator rotation. Do NOT wire this to a single tap — the app that embeds
//    the SDK owns the typed confirmation.
val out: RotateOutcome = wire.rotateOnion()   // out.notified / out.friends

All peer references are lowercase hex strings so nothing Rust-shaped leaks into the AAR.

Building the bindings yourself

The Gradle task :sdk:uniffiBindgen runs, inside crates/onionwire-sdk:

cargo build --release --lib                       # host cdylib
cargo run --release --bin uniffi-bindgen -- generate \
  --library target/release/libonionwire_sdk.so \
  --language kotlin --out-dir <generated dir>

Do not hand-edit anything under android/sdk/build/generated/; it is a build output.

Why this crate has its own Cargo workspace

Arti's TLS backends are non-additive: exactly one of native-tls and rustls may be enabled in a given feature resolution. The Linux TUI keeps native-tls (OpenSSL, as it always has). Android has no OpenSSL in the NDK, so this crate selects rustls plus static-sqlite (no system libsqlite3 on Android). Two separate workspaces keep the two resolutions from colliding.