2026-09-10 01:02:33 -04:00
# OnionWire
2026-09-10 02:49:42 -04:00
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.
2026-09-10 01:02:33 -04:00
2026-09-10 03:04:56 -04:00
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
2026-09-10 13:10:56 -04:00
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.
2026-09-10 03:04:56 -04:00
**x86_64 (most PCs / VMs):**
```bash
2026-09-10 13:10:56 -04:00
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
2026-09-10 03:05:44 -04:00
sha256sum -c onionwire-x86_64-unknown-linux-gnu.sha256
chmod +x onionwire-x86_64-unknown-linux-gnu
./onionwire-x86_64-unknown-linux-gnu --version
2026-09-10 03:04:56 -04:00
```
**aarch64 (Raspberry Pi, ARM servers):**
```bash
2026-09-10 13:10:56 -04:00
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
2026-09-10 03:05:44 -04:00
sha256sum -c onionwire-aarch64-unknown-linux-gnu.sha256
chmod +x onionwire-aarch64-unknown-linux-gnu
./onionwire-aarch64-unknown-linux-gnu --version
2026-09-10 03:04:56 -04:00
```
2026-09-10 03:05:44 -04:00
Install onto `PATH` if you want `onionwire` as a command (keep the asset filename until after `sha256sum -c` ):
2026-09-10 03:04:56 -04:00
```bash
mkdir -p ~/.local/bin
2026-09-10 03:05:44 -04:00
install -m 755 onionwire-x86_64-unknown-linux-gnu ~/.local/bin/onionwire
2026-09-10 03:04:56 -04:00
# 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.
2026-09-10 03:05:44 -04:00
Checksum files are `sha256sum` format (`<hash> onionwire-<target>` ). Download both files with `curl -O` so the names match, then `sha256sum -c` .
2026-09-10 03:04:56 -04:00
### Option B — build from source
2026-09-10 13:14:55 -04:00
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.
2026-09-10 03:04:56 -04:00
```bash
# rustup (if you do not already have cargo)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
2026-09-10 13:14:55 -04:00
rustc --version # must be 1.91 or newer
2026-09-10 03:04:56 -04:00
```
```bash
2026-09-10 13:10:56 -04:00
git clone https://forgejo.siriusdevops.com/sirius/onionwire.git
2026-09-10 03:04:56 -04:00
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
```
2026-09-10 13:10:56 -04:00
Build needs `pkg-config` and OpenSSL 3 headers (`libssl-dev` / `openssl-devel` ) —
the released binaries link `libssl.so.3` dynamically.
2026-09-10 03:04:56 -04:00
### 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).
2026-09-10 02:49:42 -04:00
## First run
2026-09-10 01:02:33 -04:00
2026-09-10 02:49:42 -04:00
```
2026-09-10 03:04:56 -04:00
onionwire
2026-09-10 02:49:42 -04:00
```
2026-09-10 03:04:56 -04:00
or `cargo run --release` .
2026-09-10 02:49:42 -04:00
2026-09-10 03:04:56 -04:00
On start you should see `onionwire: bootstrapping Arti…` on stderr. First bootstrap can take a minute. 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.
2026-09-10 02:49:42 -04:00
## 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.
2026-09-10 03:04:56 -04:00
## Keys (TUI)
tui: ASCII onion chrome, themed panes, and real keyboard navigation
- Hand-written wordmark banner (<=80 cols, auto-collapses to a compact
mark below banner width) plus onion glyph and themed screen headings
- Color-palette consts, stateful pane titles, highlighted selection row
(accent + bg), context-sensitive footer that always keeps '? help'
- Pane focus model (roster/chat/composer) with Tab/Shift-Tab, 1/2/3,
j/k + arrows, g/G, and a '?' help overlay; Esc closes any overlay
- New tests/tui_chrome.rs: banner widths, help overlay bindings, real
footer helper at 80 and 120 cols, 80x24 draw smoke test
- README keybinding table updated to match
Locked helper strings (fingerprint_mismatch_banner, rotate_screen_text,
wipe_screen_text, parse_slash) unchanged; UI-only (src/tui.rs).
2026-09-10 13:01:40 -04:00
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 `?` ).
2026-09-10 03:04:56 -04:00
| Key | Action |
|---|---|
tui: ASCII onion chrome, themed panes, and real keyboard navigation
- Hand-written wordmark banner (<=80 cols, auto-collapses to a compact
mark below banner width) plus onion glyph and themed screen headings
- Color-palette consts, stateful pane titles, highlighted selection row
(accent + bg), context-sensitive footer that always keeps '? help'
- Pane focus model (roster/chat/composer) with Tab/Shift-Tab, 1/2/3,
j/k + arrows, g/G, and a '?' help overlay; Esc closes any overlay
- New tests/tui_chrome.rs: banner widths, help overlay bindings, real
footer helper at 80 and 120 cols, 80x24 draw smoke test
- README keybinding table updated to match
Locked helper strings (fingerprint_mismatch_banner, rotate_screen_text,
wipe_screen_text, parse_slash) unchanged; UI-only (src/tui.rs).
2026-09-10 13:01:40 -04:00
| `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) |
2026-09-10 03:04:56 -04:00
| `F2` | Share: terminal QR + payload |
| `F3` | Paste a friend’ s payload |
| `F4` | Rotate **onion** (locator only) |
tui: ASCII onion chrome, themed panes, and real keyboard navigation
- Hand-written wordmark banner (<=80 cols, auto-collapses to a compact
mark below banner width) plus onion glyph and themed screen headings
- Color-palette consts, stateful pane titles, highlighted selection row
(accent + bg), context-sensitive footer that always keeps '? help'
- Pane focus model (roster/chat/composer) with Tab/Shift-Tab, 1/2/3,
j/k + arrows, g/G, and a '?' help overlay; Esc closes any overlay
- New tests/tui_chrome.rs: banner widths, help overlay bindings, real
footer helper at 80 and 120 cols, 80x24 draw smoke test
- README keybinding table updated to match
Locked helper strings (fingerprint_mismatch_banner, rotate_screen_text,
wipe_screen_text, parse_slash) unchanged; UI-only (src/tui.rs).
2026-09-10 13:01:40 -04:00
| Enter | Run `/wipe` or `/wipe-all` from the composer |
| `Esc` | Close overlay / back to Main / clear composer |
2026-09-10 03:04:56 -04:00
| `Ctrl-Q` | Quit |
2026-09-10 02:49:42 -04:00
## 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 peer’ s onion is down, send fails. v1 has no outbox, no retry queue, no DHT, no name server.
## 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.
2026-09-10 03:04:56 -04:00
## 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.
2026-09-10 02:49:42 -04:00
## 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.
2026-09-10 03:04:56 -04:00
Threat model: [`docs/THREAT_MODEL.md` ](docs/THREAT_MODEL.md ).
## Releases (maintainers)
2026-09-10 13:10:56 -04:00
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.
2026-09-10 03:04:56 -04:00
```bash
2026-09-10 13:10:56 -04:00
# 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
2026-09-10 03:04:56 -04:00
```
2026-09-10 13:10:56 -04:00
Workflows (Forgejo Actions, `.forgejo/workflows/` — jobs run on the `docker`
2026-09-10 13:14:55 -04:00
label of the Pi runner and do their Rust work in a `rust:1.91-bookworm`
2026-09-10 13:10:56 -04:00
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:
2026-09-10 03:04:56 -04:00
2026-09-10 13:10:56 -04:00
- [`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).
2026-09-10 03:04:56 -04:00
2026-09-10 13:10:56 -04:00
Do not run `cargo publish` ; `publish = false` . CI needs the repo secret
`FORGEJO_TOKEN` (an instance user PAT with repo write) to publish releases.
2026-09-10 03:04:56 -04:00
## 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. |
2026-09-10 13:43:23 -04:00
| Hang on `bootstrapping Arti…` / `hs status: Bootstrapping` | Outbound network required. Client bootstrap is minutes; HsDir descriptor upload can stay Bootstrapping up to ~6 min before `DegradedReachable` . Fail closed after that — no C-tor fallback. |
2026-09-10 03:04:56 -04:00
| `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. |
2026-09-10 02:49:42 -04:00
## Not in v1
Prosody, XMPP, s2s, MAM, carbons, outbox, multi-device, DHT / name server, sqlcipher.
2026-09-10 01:02:33 -04:00
## License
MIT