|
|
||
|---|---|---|
| .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 (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:
export ONIONWIRE_WALLET_RPC=http://127.0.0.1:18083
Loopback or .onion only, HTTP, 5s timeout. Unset → chat still works; /pay and /tip say so.
/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. Overwrites the message log andVACUUMs. Identity key and friends stay./wipe-all— confirm by typingWIPEALL. Deletes the data dir. Next start is a new person (new identity key). 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 overwrites message bodies and vacuums; /wipe-all deletes the data dir. 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