Skip to content

Repository files navigation

Vega

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.

Download

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.

Layout

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.

Building it

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 node

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

Using it

Two machines on one network

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.

Across the internet

Run a seed somewhere reachable:

./make node -- --port 15000

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

A group

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.

A second device

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.

How a message travels

  1. Encrypted with Olm — a separate ciphertext for every one of the recipient's devices, plus a copy to each of your own.
  2. Wrapped in a sealed-sender box, so an intermediary sees a rotating routing tag and nothing else.
  3. 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.

What is built

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-androidspecialUse 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

Security notes

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.

Known weaknesses, not yet fixed

  • 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 — a specialUse service, which unlike dataSync carries 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.

Fixed during review

  • 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. Runtime now 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 SelfCopy and 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.

About

Vega is an encrypted messenger where the infrastructure is the people using it. Peers find each other and communicate.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages