|
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]
|
||
|---|---|---|
| .forgejo/workflows | ||
| android | ||
| crates/onionwire-sdk | ||
| docs | ||
| scripts | ||
| src | ||
| tests | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| LICENSE | ||
| README.md | ||
OnionWire
Lean Tor messenger: one Rust process (ratatui + Noise IK + sqlite + in-process Arti). No hosted server. Identity is a public key; the onion is only a locator.
v0.2 — Linux TUI. Two people add each other with an onionwire:v1:… invite string (F2 share / F3 paste). There is no QR graphic. Chat is Noise IK over your own v3 onion. Optional Monero invoices if you point at a local monero-wallet-rpc. Fail closed: peer down → send fails. Arti onion services are still experimental. Chat bodies are encrypted at rest; identity keys in sqlite are still plaintext.
You do not install a tor daemon, torrc, Prosody, or XMPP. OnionWire embeds Arti and publishes its own v3 onion.
Install
Pick one: a CI-built binary (fastest), or build from source.
Requirements
| Need | Notes |
|---|---|
| Linux | x86_64 or aarch64. TUI needs a real terminal (not a pipe). |
| Network | First run bootstraps Tor via Arti. Offline start will fail closed. |
| Not needed | tor package, torrc, Prosody, an XMPP account, a DHT. |
Arti onion services are still experimental. If the hidden service cannot come up, OnionWire exits — it does not fall back to C-tor.
Option A — download a release binary
CI (.forgejo/workflows/release.yml, Forgejo Actions on the Pi runner) builds the aarch64 binary on every v*.*.* tag and attaches it to the Forgejo release. There is no x86_64 runner on this instance, so the x86_64 asset is built and uploaded by scripts/build-release-local.sh on an x86_64 host — both land on the same release page.
x86_64 (most PCs / VMs):
curl -fL -O https://forgejo.siriusdevops.com/sirius/onionwire/releases/latest/download/onionwire-x86_64-unknown-linux-gnu
curl -fL -O https://forgejo.siriusdevops.com/sirius/onionwire/releases/latest/download/onionwire-x86_64-unknown-linux-gnu.sha256
sha256sum -c onionwire-x86_64-unknown-linux-gnu.sha256
chmod +x onionwire-x86_64-unknown-linux-gnu
./onionwire-x86_64-unknown-linux-gnu --version
aarch64 (Raspberry Pi, ARM servers):
curl -fL -O https://forgejo.siriusdevops.com/sirius/onionwire/releases/latest/download/onionwire-aarch64-unknown-linux-gnu
curl -fL -O https://forgejo.siriusdevops.com/sirius/onionwire/releases/latest/download/onionwire-aarch64-unknown-linux-gnu.sha256
sha256sum -c onionwire-aarch64-unknown-linux-gnu.sha256
chmod +x onionwire-aarch64-unknown-linux-gnu
./onionwire-aarch64-unknown-linux-gnu --version
Install onto PATH if you want onionwire as a command (keep the asset filename until after sha256sum -c):
mkdir -p ~/.local/bin
install -m 755 onionwire-x86_64-unknown-linux-gnu ~/.local/bin/onionwire
# ensure ~/.local/bin is on PATH, then:
onionwire
The binary is dynamically linked against glibc (Ubuntu 24.04 builders). If ./onionwire dies with GLIBC_… not found, use Option B on that machine.
Checksum files are sha256sum format (<hash> onionwire-<target>). Download both files with curl -O so the names match, then sha256sum -c.
Option B — build from source
Needs Rust 1.91+ (edition = "2024"; the pinned Arti 0.46 crates require rustc 1.91). A distro rustc is fine if rustc --version reports 1.91 or newer. Do not install tor for this app.
# rustup (if you do not already have cargo)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
rustc --version # must be 1.91 or newer
git clone https://forgejo.siriusdevops.com/sirius/onionwire.git
cd onionwire
cargo test --locked
cargo clippy --locked -- -D warnings
cargo build --release --locked
./target/release/onionwire --version
./target/release/onionwire
Install the binary:
cargo install --path . --locked --force
# lands in ~/.cargo/bin/onionwire
Build needs pkg-config and OpenSSL 3 headers (libssl-dev / openssl-devel) —
the released binaries link libssl.so.3 dynamically.
Two instances on one machine
Each process needs its own data dir (identity + onion + sqlite):
ONIONWIRE_HOME=/tmp/ow-a ./onionwire
ONIONWIRE_HOME=/tmp/ow-b ./onionwire
Then F2 on A (invite string), F3-paste on B (and the other way around). No QR.
First run
onionwire
or cargo run --release.
On start you are prompted for a store passphrase (echo off, like other CLI passwords) or set ONIONWIRE_STORE_PASSPHRASE. Empty passphrase is rejected; a wrong passphrase does not open chat. Then you should see onionwire: bootstrapping Arti… on stderr. Directory bootstrap is usually under a minute; the onion is ready once a probe connect works (combined Arti status may still say Bootstrapping). Fail closed at 360s. Data lives in ONIONWIRE_HOME if set, otherwise ~/.local/share/onionwire/ (onionwire.db + Arti state, mode 0700). First open creates an ed25519 identity key. That key is you.
Then F2 to share your invite, F3 to paste a friend’s. Mouse-select the onionwire:v1:… line to copy.
Friends are keys
A friend is an ed25519 pubkey (UNIQUE(pubkey)). The .onion on that row is only where they are reachable right now. Pasting the same k updates the locator; it never creates a second person.
Keys (TUI)
Focus starts on the composer so typing works immediately. Tab cycles panes; j/k and arrows only move the focused pane (roster or chat). 1/2/3 switch focus when you are not typing in the composer. ? opens help from every screen except paste (so a payload can contain ?).
| Key | Action |
|---|---|
Tab / Shift-Tab |
Cycle focus: roster → chat → composer |
1 / 2 / 3 |
Focus roster / chat / composer (when not typing) |
j k / ↑ ↓ |
Move or scroll the focused pane |
g / G |
Jump to top / bottom of the focused pane |
? |
Keybinding help (Esc closes) |
F2 |
Share invite (onionwire:v1:…) |
F3 |
Paste a friend’s invite |
F4 |
Rotate onion (locator only) |
F5 |
Selected friend’s profile (/who) |
| Enter | Run /wipe, /wipe-all, /profile, /who, /pay, /tip, /backup, /restore from the composer |
Esc |
Close overlay / back to Main / clear composer |
Ctrl-Q |
Quit: type CLEAR+Enter to wipe history, QUIT+Enter to leave it, Esc to stay |
F2 / F3 invite
There is no QR. F2 opens (o) invite with truncated fingerprint, current onion, and the full payload (wrapped, mouse-select to copy):
onionwire:v1:k=…:o=…:spk=…:sig=…
Give that string to a friend. They F3 paste it ((o) paste invite). Unknown k asks for approval. Same k already in the roster updates onion only.
F4 rotate onion
F4 is a locator change, not a new identity. Confirm by typing ROTATE (Enter alone does nothing).
- Your identity fingerprint stays the same.
- A new onion is published; the old one is hard-cut (no dual-host grace).
- Online friends get a signed
locframe. - Offline friends cannot find you until they F3-paste the new invite. There is no directory.
Fail closed
If a peer’s onion is down, send fails. v1 has no outbox, no retry queue, no DHT, no name server. There is still no hosted chat server.
Profile
/profile edits your friend-visible display name, bio, and optional Monero address (64 / 512 byte limits, no images). Enter saves and one-shot sends a signed prf frame to the selected friend. F5 or /who shows their last signed profile. There is no directory: unknown pubkeys are ignored.
Monero sidecar
OnionWire is not a wallet. Optional JSON-RPC to a user-hosted monero-wallet-rpc. The wallet must use --rpc-login; OnionWire refuses an open RPC (HTTP 200 without a Digest challenge) and refuses URLs with no credentials.
# monero-wallet-rpc --rpc-bind-ip 127.0.0.1 --rpc-bind-port 18083 --rpc-login onionwire:secret
export ONIONWIRE_WALLET_RPC=http://onionwire:secret@127.0.0.1:18083
# or keep the password out of the URL:
export ONIONWIRE_WALLET_RPC=http://127.0.0.1:18083
export ONIONWIRE_WALLET_RPC_LOGIN=onionwire:secret
Loopback only, HTTP Digest (RFC 2617, matching --rpc-login), 5s timeout, 1 MiB response cap. .onion RPC URLs are rejected (no Arti dial; do not point this at a remote wallet). Unset → chat still works; /pay and /tip say so. Do not log the RPC password.
/pay <xmr> [memo]— invoice (we want to receive). Uses a wallet subaddress if RPC is up, else the profilexmr_addr./tip <xmr> [memo]— pay the selected friend’s profile address, then send a signedrcp. Incoming receipts stay unverified until RPCget_transfersmatches.
Backup / restore
Losing identity_sk loses every friend relationship.
/backup /path— typeBACKUP, passphrase twice. Writesowbak1(Argon2id + ChaCha20-Poly1305). Onion is not in the file (locator is disposable; F4 after restore if needed)./restore /path— typeRESTORE, passphrase. Overwrites self keys. Does not rewrite the roster — you may become a different person talking to old friends.
Treat the backup file like the sqlite db.
Mixed versions
0.1.2 peers store unknown plaintext as chat. A 0.2 sender of prf / inv / rcp will leave a garbage line on an un-upgraded peer. Upgrade both sides. The Noise handshake is unchanged.
Wipe
Composer (bottom of the roster screen):
/wipe— confirm by typingWIPE. Chat and payments history gone (overwrite message bodies, droppayments,VACUUM, WAL checkpoint). Identity key and friends stay. Not a forensic erase (SSD wear-leveling)./wipe-allis the identity burn./wipe-all— confirm by typingWIPEALL. Deletes the data dir. Next start is a new person (new identity key). Same disk caveat. Esc cancels. Nothing is wiped without confirm.
Uninstall
# binary (whichever you used)
rm -f ~/.local/bin/onionwire ~/.cargo/bin/onionwire
# optional: destroy identity, friends, and message log
# (same as /wipe-all — you become a new person next run)
rm -rf ~/.local/share/onionwire
If you set ONIONWIRE_HOME, delete that directory instead.
Seized laptop
Chat bodies in sqlite are ChaCha20-Poly1305 (nonce || ciphertext in messages.plaintext) with AAD bound to friend_id, dir, and row id, wrapped by a passphrase-derived Argon2id key. A disk grep of onionwire.db must not yield the message log. Empty-AAD v0.2 blobs are rewrapped once on unlock.
Still plaintext on disk (unless you add OS/FDE):
- your identity secret key (
self.identity_sk) - friend public keys and current locators
The message key is not wrapped with identity_sk (that key is in the same file). sqlcipher is out of v1. /wipe deletes chat and payments history; it is not a forensic erase. Roster and identity stay. /wipe-all deletes the data dir (new identity). Ctrl-Q can clear history on the way out (CLEAR) without becoming a new person.
Threat model: docs/THREAT_MODEL.md.
Android (SDK + APK)
Same repository, same protocol, separate product from the Linux TUI. An Android peer and a Linux peer that exchange invites interoperate — one protocol, not two.
- SDK —
onionwire-sdk-<version>.aar. UniFFI Kotlin bindings over the same Rust crate the TUI runs on, pluslibonionwire_sdk.soforarm64-v8a. Another Android app depends on it directly. Seecrates/onionwire-sdk/README.md. - APK —
onionwire-<version>-android-arm64-v8a.apk. Kotlin + Jetpack Compose + Material 3 messenger that depends on the SDK and nothing else.
It is not a WebView, and src/tui.rs is untouched. Both are built on a host with
the Android SDK/NDK — the Pi runner is aarch64 Linux and cannot produce an APK:
# one-time: sdkmanager platform 36 / build-tools 36.0.0 / ndk 28.2.13676358,
# rustup target add aarch64-linux-android, cargo install cargo-ndk
scripts/build-android-local.sh # -> dist/, with .sha256 files
PUBLISH_TAG=v0.3.0 scripts/build-android-local.sh # + upload to that release
Every asset has a matching .sha256 so sha256sum -c works. The APK is
debug-signed and is for sideloading, not for Play. Full build notes, the
minSdk 26 rationale, the 16 KB page-size check and the signing story are in
android/README.md.
Releases (maintainers)
Tags trigger Forgejo Actions on the self-hosted Pi runner. The x86_64 asset has no runner on this instance, so it is built locally and attached to the same release.
# 1. version in Cargo.toml must match the tag without the leading v
cargo update --offline -p onionwire # keep Cargo.lock at the new version
git commit -am "chore: release vX.Y.Z" && git push origin main
# 2. tag and push — the release workflow builds + publishes aarch64
git tag -a vX.Y.Z -m "OnionWire vX.Y.Z"
git push origin vX.Y.Z
# 3. attach x86_64 from an x86_64 host (idempotent — safe to re-run)
FORGEJO_TOKEN=<pat> scripts/build-release-local.sh vX.Y.Z
Workflows (Forgejo Actions, .forgejo/workflows/ — jobs run on the docker
label of the Pi runner and do their Rust work in a rust:1.91-bookworm
sibling container):
.forgejo/workflows/ci.yml—cargo test --locked+cargo clippy --all-targets -- -D warningsonmainand PRs (ignored Tor-live tests are not run)..forgejo/workflows/release.yml— onv*.*.*tags: buildsaarch64-unknown-linux-gnunatively, strips, sha256, publishes to the release. Also runnable viaworkflow_dispatchwith a tag input to re-publish.
Scripts:
scripts/publish-release.sh— create-or-update a release and (re)upload assets via the Forgejo API. NeedsFORGEJO_TOKEN(repo secret in CI, env locally).scripts/build-release-local.sh— x86_64 build + strip + checksum + publish.scripts/build-android-local.sh— Android AAR + APK build, checksums, optional publish (see the Android section above).scripts/release-body.md— release notes template (@TAG@is substituted).
Do not run cargo publish; publish = false. CI needs the repo secret
FORGEJO_TOKEN (an instance user PAT with repo write) to publish releases.
Troubleshooting
| Symptom | What to do |
|---|---|
GLIBC_… not found |
Binary is newer than your libc. Build from source (Option B). |
sha256sum: FAILED |
Re-download both the binary and .sha256; run the check in the same directory. |
Hang on bootstrapping Arti… / hs status: Bootstrapping |
Outbound network required. Combined Arti status can stay Bootstrapping through a 5 min HsDir upload round; OnionWire probes the onion and proceeds once a connect works. Still fail closed after 360s if neither a probe nor DegradedReachable/Running — no C-tor fallback. |
onionwire: unknown argument |
No subcommands. Flags are --version / --help only, then the TUI. |
| Blank / broken TUI | Run in a real terminal emulator, not nohup / systemd without a TTY. |
| Two chats, same laptop | Separate ONIONWIRE_HOME per process. |
| Friend cannot find you after F4 | Expected if they were offline. They must F3-paste the new invite. Same k updates the row. |
Not in v1
Prosody, XMPP, s2s, MAM, carbons, outbox, multi-device, DHT / name server, sqlcipher, QR codes.
License
MIT