|
All checks were successful
ci / test (pull_request) Successful in 3m30s
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]
|
||
|---|---|---|
| .. | ||
| 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.