onionwire/README.md
Sirius DevOps e902a9041d
All checks were successful
ci / test (push) Successful in 2m59s
fix(hs): probe onion reachability instead of waiting on Bootstrapping
Arti combined status stays Bootstrapping through a 5 min HsDir upload
round. Treat a successful connect as published, floor cbtinitialtimeout
with cbtmintimeout at 20s, and skip preemptive 80/443 circuits so IPT
and HsDir builds are not starved. PUBLISH_WAIT stays 360s fail-closed.
2026-09-10 16:14:10 -04:00

270 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# OnionWire
Lean Tor messenger: one Rust process (ratatui + Noise IK + sqlite + in-process Arti).
Two people scan a QR, then chat. No hosted server. Identity is a public key; the onion is only a locator.
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](https://forgejo.siriusdevops.com/sirius/onionwire/releases). 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):**
```bash
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):**
```bash
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`):
```bash
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](https://rustup.rs/) **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.
```bash
# 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
```
```bash
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:
```bash
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):
```bash
ONIONWIRE_HOME=/tmp/ow-a ./onionwire
ONIONWIRE_HOME=/tmp/ow-b ./onionwire
```
Then F2 on A, F3-paste on B (and the other way around).
## First run
```
onionwire
```
or `cargo run --release`.
On start 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.
## Friends are keys
A friend is an ed25519 pubkey (`UNIQUE(pubkey)`). The `.onion` on that row is only where they are reachable right now. Re-scanning 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: terminal QR + payload |
| `F3` | Paste a friends payload |
| `F4` | Rotate **onion** (locator only) |
| `F5` | Selected friends 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 |
## F2 QR
`F2` shows a terminal QR and the payload:
`onionwire:v1:k=…:o=…:spk=…:sig=…`
`F3` pastes a payload. 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 `loc` frame.
- Offline friends cannot find you until they rescan the new QR. There is no directory.
## Fail closed
If a peers 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`:
```bash
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 profile `xmr_addr`.
- `/tip <xmr> [memo]` — pay the selected friends profile address, then send a signed `rcp`. Incoming receipts stay unverified until RPC `get_transfers` matches.
## Backup / restore
Losing `identity_sk` loses every friend relationship.
- `/backup /path` — type `BACKUP`, passphrase twice. Writes `owbak1` (Argon2id + ChaCha20-Poly1305). Onion is **not** in the file (locator is disposable; F4 after restore if needed).
- `/restore /path` — type `RESTORE`, 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 typing `WIPE`. Overwrites the message log and `VACUUM`s. Identity key and friends stay.
- `/wipe-all` — confirm by typing `WIPEALL`. Deletes the data dir. Next start is a **new person** (new identity key). Esc cancels. Nothing is wiped without confirm.
## Uninstall
```bash
# 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
v1 stores **plaintext** on disk:
- message log (sqlite `messages.plaintext`)
- your identity secret key (`self.identity_sk`)
- friend public keys and current locators
Full-disk encryption plus `/wipe` / `/wipe-all` is the mitigation. There is no sqlcipher in v1.
Threat model: [`docs/THREAT_MODEL.md`](docs/THREAT_MODEL.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.
```bash
# 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`](.forgejo/workflows/ci.yml) — `cargo test --locked` + `cargo clippy --all-targets -- -D warnings` on `main` and PRs (ignored Tor-live tests are not run).
- [`.forgejo/workflows/release.yml`](.forgejo/workflows/release.yml) — on `v*.*.*` tags: builds `aarch64-unknown-linux-gnu` natively, strips, sha256, publishes to the release. Also runnable via `workflow_dispatch` with a tag input to re-publish.
Scripts:
- [`scripts/publish-release.sh`](scripts/publish-release.sh) — create-or-update a release and (re)upload assets via the Forgejo API. Needs `FORGEJO_TOKEN` (repo secret in CI, env locally).
- [`scripts/build-release-local.sh`](scripts/build-release-local.sh) — x86_64 build + strip + checksum + publish.
- [`scripts/release-body.md`](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 QR. Same `k` updates the row. |
## Not in v1
Prosody, XMPP, s2s, MAM, carbons, outbox, multi-device, DHT / name server, sqlcipher.
## License
MIT