An encrypted messenger with no server. Peers find each other, carry each other's ciphertext, and hold each other's mail.
Nothing here is a service you have to run or pay for, no node can read a message,
and no node's disappearance kills the network. The full design, including what
that does not protect against, is in
.documentation/design.md — also published as an
artifact.
vega.guhhammer.dev offers the right file for whatever you are reading this on. Everything is also here:
| Platform | File |
|---|---|
| Linux (any distribution) | Vega-linux-x86_64.AppImage |
| Debian, Ubuntu | Vega-linux-amd64.deb |
| macOS (Intel and Apple silicon) | Vega-macos-universal.dmg |
| Windows | Vega-windows-x86_64-setup.exe |
| Android 7 and later | Vega-android-universal.apk |
Android is a sideload, and there is no store listing coming. A store entry
means a review queue, a developer account tied to a legal identity, and a
company whose rules can change under a messenger whose whole point is that no
third party sits in the middle. Open the .apk on the phone and allow the
install when asked; one file carries all three phone architectures.
Delivery there happens only while the app is open. The foreground service
that would hold the socket in the background is not written yet. When it is,
delivery with the screen off will be bursty rather than immediate: Doze
suspends network access for any app not exempted from it, and a foreground
service does not buy that exemption — only the user can, with one prompt. The
alternative is a push service, which is the third party this project exists
without. .documentation/android.md works through
what that costs and what it takes to write.
None of the builds is code-signed, so the first open will show a warning from
the operating system.
SHA256SUMS.txt on the
release lists the checksum of each file above — worth checking, and worth more
than a certificate.
Per-platform steps, including how to get past those warnings and where Vega
keeps its data, are in
.documentation/installing.md. There is nothing
to sign up for: the first launch generates an account on the device — and every
install generates its own. One account does not span two devices yet; see
a second device for what to do instead.
Nothing here has been audited. Read the security notes before trusting it with anything that matters.
crates/vega-core identity, sigchain, groups, message crypto — knows nothing about networking
crates/vega-net the transport ladder — moves opaque bytes, cannot read them
crates/vega-android the foreground service that stops Android killing the process
app/ Tauri desktop app (React frontend, Rust backend)
app/src-tauri the shell: commands, runtime, key storage
The split is load-bearing. vega-net handles envelopes it cannot open, which is
what makes it safe for a stranger's node to relay them.
Needs Rust (the pinned version in rust-toolchain.toml is fetched
automatically), Node 20 or newer, and the Tauri system dependencies for your
platform — tauri.app/start/prerequisites.
./make check # fmt, clippy, tests, typecheck — run this constantly
./make dev # desktop app against the vite dev server
./make dist # this machine's installers, staged in release/
./make node # headless bootstrap/relay/mailbox nodecargo build alone produces binaries that prove the code compiles but are not
applications — without the Tauri CLI there is no frontend embedded, so the
window opens on a connection error. Use dev or dist.
./make dist writes release/ with the same file names as the download table
above and a SHA256SUMS.txt beside them, so a local build and a released build
are interchangeable. Tauri links against the system webview and cannot
cross-compile, so each machine builds only its own platform; CI builds all
three on a v* tag. See
.documentation/releasing.md.
Start the app on both. They find each other over mDNS with no configuration.
Copy the invite from one (My invite) and paste it into the other
(Add contact), then send a message.
Run a seed somewhere reachable:
./make node -- --port 15000It prints its address. Put that in seeds.json in the app's data directory — a
plain JSON array of multiaddrs — and restart. No rebuild needed. A seed holds no
key that can read anything, and cannot withhold a message it never sees.
New group in the sidebar, once you have a contact or two. Everybody you pick
is told immediately — there is no invitation to accept, because there is no
server to hold one, and only people you have already exchanged invites with can
be picked.
A group message is an ordinary sealed message sent once to each member's device. There is no group key: the ratchet's forward secrecy and post-compromise security hold per member, and the price is that a group of eight is eight sends. Text only for the same reason — a file to eight people would be eight copies of the file.
Only whoever made the group can change who is in it. Anyone can leave. With no server to order two people's simultaneous edits, one writer is the alternative to inventing a consensus protocol, and a membership list two members disagree about is worse than a limitation.
By default a second device is a second account. The first launch generates an identity on the device and nothing imports an existing one, so installing Vega on a laptop and on a phone gives you two accounts rather than one. This is where the project is today, not a decision to keep it that way.
Until pairing exists, add yourself as a contact: paste the laptop's invite into the phone, and the phone's into the laptop. Messages and files then move between your own machines exactly as they do to anybody else, end to end encrypted the same way. What you do not get is one identity — a conversation opened on the laptop stays on the laptop, each device shows its own ten words, and a contact who wants to reach you on both has to hold both invites.
The crypto for the real thing is already there — Identity::adopt, and the
sigchain accepts device-signed additions. What is missing is the pairing flow
(SPAKE2 over a six-word code, per
.documentation/design.md), and a rushed one would
be worse than none.
- Encrypted with Olm — a separate ciphertext for every one of the recipient's devices, plus a copy to each of your own.
- Wrapped in a sealed-sender box, so an intermediary sees a rotating routing tag and nothing else.
- Handed to the cheapest transport that works: the LAN, then a direct internet connection, then a relay peer, then a mailbox peer if the recipient is gone.
| Identity, sigchain, device roster | done, tested |
| Olm sessions, sealed sender, replay defence | done, tested |
| Local storage, outbox, invites | done, tested |
| T0 LAN discovery and delivery | done, tested over real sockets |
| T2 DHT rendezvous — publish, look up, dial | done, tested over real sockets |
| T4 mailbox park/collect | done, tested over real sockets |
| T1 hole punching, T3 relay | wired end to end, but only two machines on two real networks can prove it |
| Delivery receipts | done, tested — the outbox clears when the recipient confirms decryption, not when a peer accepts bytes |
| Files, up to 10 MB | done, tested — sent as 96 KiB chunks, each an ordinary sealed message, reassembled and hash-checked on arrival. Not copied to the sender's own devices, and anything past ~3 MB needs both people online at once |
| Key storage | platform keyring (Secret Service / Keychain / Credential Manager), with a 0600 file where none exists |
| Device linking (second device on one account) | the crypto is there — Identity::adopt, and the sigchain accepts device-signed additions — but there is no pairing flow, so by default a second device is a second account (what to do instead) |
| Android | cross-compiles and ships a signed APK; delivery only while the app is open |
| Android background delivery | written in crates/vega-android — specialUse foreground service, Wi-Fi-tied multicast lock, battery-exemption prompt — but never run on a phone, so nothing above it has changed yet (see .documentation/android.md) |
| Groups | done, tested — a group is a name and a member list, and a message to one is an ordinary sealed message per member device. No group key: the ratchet's forward secrecy and post-compromise security hold per member, and the cost is that a group of eight is eight sends. Only the creator may change the membership; anyone may leave. Text only — a file would be sent once per member device |
| T5 offline mesh | not started, and not close. rust-libp2p has no BLE transport, so it is a custom transport plus a native plugin per platform, and BLE throughput makes it text-only |
Read the design document's non-goals section before trusting this with anything. Nothing here has been audited. vodozemac (the ratchet) has been; the code around it has not.
- Device linking is not implemented. An account is one device, so by default a second device is a second account, added to the first as a contact — see a second device. The crypto exists and the sigchain accepts device-signed additions; the pairing flow does not, and a rushed one would be worse than none.
- What is encrypted at rest is the content, not the metadata. Message bodies, contacts, chains, queued envelopes and received files are stored sealed under the device key. The database keys are not: which account ids you hold, how many messages are in each conversation, and when they arrived are all readable by anyone who can read the directory. And with no platform keyring the device key is a 0600 file beside the database, so on those systems the encryption is worth exactly what those permissions are.
- Where there is no keyring, the key is a 0600 file. Headless systems, containers, and Android take the fallback path. It protects against another user on the machine and a stolen backup, and nothing else. The app logs which backing it used at startup.
- Android has no background delivery in any released build. A message sent
to a phone with Vega closed arrives the next time it is opened. The foreground
service and multicast lock now exist in
crates/vega-android— aspecialUseservice, which unlikedataSynccarries no six-hour daily cap — but they have never run on a phone, and until they have, this stays on the list. Even once they work they cannot make delivery immediate: Doze suspends network access for unexempted apps, and a foreground service is not an exemption. Bursty is the ceiling unless the user turns off battery optimisation for Vega. - Two people's first message to the same person can collide. There is no server to hand each sender a different one-time prekey, so both pick from the same published list — spread by hashing the sender, but with a one-in-fifty chance of landing on the same key. The first to arrive consumes it and the second never opens, and since the outbox retries the same envelope, that first message never arrives at all. It affects a first message only; an established conversation is unaffected. The fix is for the sender to notice and re-establish with a different prekey, which is not written.
- A relay can still misroute. The routing tag is now bound into the sealed layer, so rewriting it is detected rather than silently obeyed — but a relay that drops or misdirects an envelope still denies delivery. That is inherent to asking a stranger to carry your traffic; the outbox retries.
- Prekey exhaustion. One-time keys were published once and never again, so after ~50 conversations everyone fell back to the reusable fallback key. They are now topped up when they run low, and the updated chain reaches contacts two ways that need no server: attached inside the next encrypted message, and carried in the rendezvous record a contact fetches to find us. The merge happens before sender authentication, so a device added since we last spoke is not locked out by the very check that needs the update it is carrying.
- Routing tag tampering. The tag stays readable — a relay cannot forward what it cannot see — but it is now bound into the sealed layer's associated data, so rewriting it produces a box that no longer opens.
- Unbounded replay set. Message ids are pruned on a two-week window. Safe because the ratchet already refuses replays inside a live session; this layer only catches the same message arriving over two tiers, seconds apart.
- Prekey exhaustion, receipts, and the conversation scan. All three are fixed; see the 0.1.0 entry in CHANGELOG.md.
- The lock was held across network I/O.
Runtimenow performs no I/O at all: it returns plans, and the caller executes them unlocked. A slow peer delays one delivery instead of every incoming message. - Sender impersonation. Anyone holding your invite could open a legitimate
Olm session and label the message as coming from a third party — decryption
proves possession of a key, not whose account it is. The claimed sender is
now checked against that account's signed device roster, using the identity
key recorded when the session was created rather than anything the envelope
asserts. Sessions are no longer filed before that check passes.
(
crates/vega-core/src/session.rs) - Forged self-copies. A contact could send a
SelfCopyand have it appear in your own outgoing messages. Now only your own account may send one. - Mail theft. Collecting parked mail required only the routing tag, which every relay sees — so any of them could drain a mailbox and destroy the mail. Collection now needs a token derived from the pairwise secret.
- Unbounded chains. An invite could contain an arbitrarily long chain (CPU) or claim unlimited devices (fan-out amplification). Both are capped.
- Misfiled incoming messages. Conversation was taken from a sender-chosen field, so incoming messages landed under the wrong contact and a contact could drop messages into an unrelated thread.
