Lean Tor messenger: Arti in-process, identity=pubkey, onion=locator. No XMPP.
Find a file
Sirius DevOps 9441bccf58
Some checks failed
ci / test (push) Successful in 5m46s
release / aarch64 (push) Failing after 5m46s
fix(release): tooling checkout so old tags can publish; never move a tag
The dispatched build of v0.1.1 kept failing at publish with
'scripts/publish-release.sh: No such file or directory' — the workflow is
read from the dispatch ref (main) but the workspace was the v0.1.1 tag,
whose tree predates scripts/ and .forgejo/.

- second checkout of the workflow's own ref into .ci-tools; publish runs
  .ci-tools/scripts/publish-release.sh (and .ci-tools/scripts/release-body.md)
- exclude ./.ci-tools from the build context tar
- target_commitish is now opt-in in publish-release.sh and only set for a
  push event, so a re-publish can never move an existing tag

Verified locally end-to-end against a throwaway tag: create, idempotent
re-run (stale assets deleted + replaced), public download, sha256sum -c,
--version. Test release and tag deleted afterwards.
2026-09-10 13:58:46 -04:00
.forgejo/workflows fix(release): tooling checkout so old tags can publish; never move a tag 2026-09-10 13:58:46 -04:00
docs Add /wipe, /wipe-all, and threat model docs. 2026-09-10 02:49:42 -04:00
scripts fix(release): tooling checkout so old tags can publish; never move a tag 2026-09-10 13:58:46 -04:00
src hs: wait 360s for HsDir publish; floor cbtmintimeout at 10s 2026-09-10 13:43:23 -04:00
tests fix(ci): clippy clean on tests/hs.rs + pin step cwd to GITHUB_WORKSPACE 2026-09-10 13:50:57 -04:00
.gitignore git: ignore /.worktrees/ (kanban worktrees live there) 2026-09-10 13:30:11 -04:00
Cargo.lock ci: Forgejo-native ci + release workflows 2026-09-10 13:10:56 -04:00
Cargo.toml ci: build on rust 1.91 — the locked Arti 0.46 crates require rustc >= 1.91 2026-09-10 13:14:55 -04:00
LICENSE Initial Cargo bin scaffold 2026-09-10 01:02:33 -04:00
README.md hs: wait 360s for HsDir publish; floor cbtmintimeout at 10s 2026-09-10 13:43:23 -04:00

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. 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, 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. 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.

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)
Enter Run /wipe or /wipe-all 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.

Wipe

Composer (bottom of the roster screen):

  • /wipe — confirm by typing WIPE. Overwrites the message log and VACUUMs. 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

# 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.

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.ymlcargo test --locked + cargo clippy --all-targets -- -D warnings on main and PRs (ignored Tor-live tests are not run).
  • .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:

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. Client bootstrap is minutes; HsDir descriptor upload can stay Bootstrapping up to ~6 min before DegradedReachable. Fail closed after that — 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