onionwire/crates/onionwire-sdk/README.md
Sirius DevOps 22b3a0062a
Some checks are pending
ci / test (pull_request) Waiting to run
docs: add PROTOCOL.md and retire the stale audit writeup
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.
2026-09-11 13:15:51 -04:00

4.8 KiB

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 ed25519 over onionwire-invite-v1 + 0x00-separated k, o, spk (hex/onion as they appear in the string). Decode still accepts the legacy raw concatenation so old invites verify. See docs/PROTOCOL.md.
  • 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.