RED. The APK builds, installs and opens, but the first rustls config built
from the process default panics one call deep inside Arti's rustls backend:
Could not automatically determine the process-level CryptoProvider from
Rustls crate features.
Call CryptoProvider::install_default() before this point ...
rustls 0.23 picks its provider from its own 'ring' / 'aws-lc-rs' crate
features, not from which crypto crates happen to be linked. Arti reaches
rustls via tor-rtcompat with default-features = false, so neither provider
feature is on and rustls compiles with zero providers. 'ring' does appear in
cargo tree, pulled in by snow for Noise -- unrelated and irrelevant.
This commit adds rustls as an explicit dependency with no provider feature
(no behaviour change: it is already in the graph that way) so the test can
name the type, plus the regression test. On the host it reproduces the
device's panic verbatim.
The SDK lockfile is resynced with the root Cargo.toml on main (onionwire
0.2.1, rpassword, md-5, rtoolbox) in the same commit so the test build is
against a consistent lock.
|
||
|---|---|---|
| .. | ||
| src | ||
| tests | ||
| Cargo.lock | ||
| Cargo.toml | ||
| README.md | ||
| uniffi.toml | ||
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>
sigis a signature over the concatenationk ‖ o ‖ spkas raw ASCII bytes.- A malformed, unsigned, or tampered invite is rejected before anything is stored.
- Same
knever 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
prfframes, 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 againstandroid-26.abiFiltersmust include an ABI the AAR ships. The release AAR includesarm64-v8a. Addx86_64at build time with-Ponionwire.abis=arm64-v8a,x86_64if you need an emulator.- Keep
uniffi.onionwire_sdk.**(see the AAR'sconsumer-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.