onionwire/crates/onionwire-sdk
Sirius DevOps d2d3b0c827
All checks were successful
ci / test (pull_request) Successful in 3m30s
fix(sdk): enable rustls' ring provider and install it in the init path
GREEN. rustls 0.23 selects its CryptoProvider from its own 'ring' /
'aws-lc-rs' crate features. Arti reaches rustls through tor-rtcompat with
default-features = false and nothing in this workspace turned a provider
feature on, so rustls was compiled with zero providers and the first TLS
config built from the process default panicked:

    Could not automatically determine the process-level CryptoProvider from
    Rustls crate features. ...

That is the 'Failed to start' screen on the unlock path. The 'ring' crate
being present in the graph (via snow, for Noise) never had anything to do
with it.

'ring', not 'aws-lc-rs': aws-lc-rs wants CMake and a C toolchain for the
NDK and fights the cross-compile.

Belt and braces: open_wire() now calls install_crypto_provider() before
Node::start_with_passphrase, a OnceLock-guarded
ring::default_provider().install_default() with the already-installed Err
ignored. That keeps the cdylib correct regardless of how a consumer's
feature graph resolves rustls.

aws-lc-rs is absent from the lock, so there is no two-provider conflict to
report.

Evidence:
  cargo test --release --test provider_resolution
    before: panicked at rustls-0.23.44/src/crypto/mod.rs:249 (device message)
    after:  1 passed
  cargo tree -f '{p} [{f}]' -p rustls
    rustls v0.23.44 [log,logging,ring,std,tls12]
2026-09-10 22:03:40 -04:00
..
src fix(sdk): enable rustls' ring provider and install it in the init path 2026-09-10 22:03:40 -04:00
tests fix(sdk): enable rustls' ring provider and install it in the init path 2026-09-10 22:03:40 -04:00
Cargo.lock fix(sdk): enable rustls' ring provider and install it in the init path 2026-09-10 22:03:40 -04:00
Cargo.toml fix(sdk): enable rustls' ring provider and install it in the init path 2026-09-10 22:03:40 -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.