Some checks are pending
ci / test (pull_request) Waiting to run
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.
122 lines
4.8 KiB
Markdown
122 lines
4.8 KiB
Markdown
# 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](../../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`.
|
|
|
|
```kotlin
|
|
// 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.
|
|
|
|
```kotlin
|
|
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.
|