Threnody is a private messenger that works without a company in the middle. There is no server that holds your account, sees your contact list, or logs who talks to whom and when. Your phone and computers talk to your contacts' devices directly, over the internet, the local network, Bluetooth or Wi-Fi Direct, and through your own contacts when no direct path exists.
Website: https://larrydewey.github.io/threnody/
Status: 1.0, not audited. Threnody works day to day on Android and Linux, but nobody outside the project has reviewed its security yet. Don't rely on it where your safety depends on it.
- No servers that see who talks to whom. There is no Threnody server at all. Messages go straight to your contact, or through relays that are your own approved contacts or volunteers. Whenever enough relays are available, they take onion circuits through two of them by default, so no single relay sees both ends.
- Works with no internet nearby. Phones find approved contacts over Bluetooth LE and connect without any taps, and a chat can move to Wi-Fi Direct for speed. Devices on the same network find each other on their own. A phone with only a Bluetooth link can reach contacts that its contacts reach over the internet.
- Hides when you talk, not just what you say. Messages are padded so short ones all look the same size, and each connection sends a steady stream of padded frames with filler in the gaps, so an observer can't tell when you send (a long message or a file still stands out by its size; see the trade-offs). Photos lose their location and camera details before they're sent. All of this is on unless you switch it off.
- Post-quantum encryption. Connections, messages and groups use hybrid encryption (X25519 with ML-KEM-768), designed to resist attacks by future quantum computers as well as today's. The core protocols have machine-checked proofs (Tamarin).
- No phone number, no email. Your identity is a key on your device. You add contacts with a QR code, a link (which finds you on mobile data and IPv6-only networks too), or by tapping phones, or, if you choose to make one, a short handle like
larry-7K3M-QX9Pthat people can type. You can compare safety numbers to be sure it's them. - Anonymous identities. Make a separate identity for one conversation, with its own keys, contacts and history. Nothing links it to you unless you choose to reveal it, and it can delete itself after a day, a week or four weeks.
- Voice and video calls between two people, and group calls of up to eight in a group, carried inside the same encrypted connections. A call rings every device of your contact's account.
- Groups using MLS, with no server. Owners and admins manage members, every member enforces their roles, and an owner can hand a group over or name a successor. Members who can't reach each other directly pass messages through a member who can.
- Several devices, one account. Link a laptop to your phone with a one-time code. Devices are equal: any of them can add or remove the others, and they share your contacts, history and delivery ticks. Files a contact sends show on all of them, whichever device they reached. Recent photos and voice messages follow you to your other devices on Wi-Fi; other files come over when you tap them.
- WireGuard tunnels. Approved contacts can get private network tunnels between their devices, with keys from the post-quantum session.
- Messages for offline contacts are sealed so only the recipient can open them, and held by mutual contacts until the recipient comes back. Their delivered and read ticks come back the same way, even if you are offline by then.
- Open source, under Apache-2.0 or MIT.
All downloads are on the latest release.
- Android (10 or newer): download
threnody-<version>-android.apkand open it on your phone. It isn't in an app store yet. - Linux desktop: clone this repository and run
apps/linux/install.sh. It builds the app (you need Rust, GTK 4 and libadwaita; see the Linux app's README) and adds it to your app menu. - Command line for Linux, macOS and Windows:
threnody-<version>-<platform>.tar.gz(or.zip). See Quick start.
Every release has a SHA256SUMS file, and every file carries signed build provenance: gh attestation verify <file> -R larrydewey/threnody checks that this repository's release workflow built it. There's no iOS release yet; the iOS app builds from source in apps/ios.
A short comparison by threat, as fairly as we can put it. The other apps change too; corrections are welcome.
| Threnody | Signal | Briar | SimpleX Chat | |
|---|---|---|---|---|
| A central service carries your messages and can see who talks to whom | No servers; direct, or onion-routed through contacts and volunteer relays | Yes; Signal's servers relay all messages (sealed sender hides the sender of most messages from them) | No; peer to peer over Tor | Relays carry messages but see no user IDs; you can choose or run your own |
| Needs a phone number | No | Yes, to register (usernames can hide it from contacts) | No | No |
| Works offline over Bluetooth or Wi-Fi | Yes: Bluetooth LE, Wi-Fi Direct, local network | No | Yes: Bluetooth, Wi-Fi | No |
| Post-quantum encryption | Yes: handshake, messages and groups | Yes, for key agreement | No | Yes, in direct chats |
| Several devices on one account | Yes, all equal | Yes: one phone plus linked devices | No | Limited: a desktop can use a phone's profile |
| Groups | Yes (MLS) | Yes | Yes | Yes |
| Independent security audit | Not yet | Widely reviewed | Yes | Yes |
- It costs data. Hiding when you send means sending all the time. Each connected contact costs about 560 MB a day on Wi-Fi and about 110 MB a day on mobile data, both ways. You can turn cover traffic off.
- Size still shows for long messages and files. With cover traffic on, messages of up to about 4 KB of text are all padded to one size, the size of the filler. Longer ones, and every photo, voice message and file, are padded up to the next size step (8 KiB, 16 KiB, 32 KiB, 64 KiB, then steps of 64 KiB), so an observer can tell that something larger was sent and roughly how large. Without cover traffic the steps start at 256 bytes.
- Your contacts need Threnody too, and a relay is only as available as the contact or volunteer running it.
- Finding a contact across the internet uses the public BitTorrent DHT as a meeting point, for contacts and for invite links and handles alike. What's stored there is encrypted so only people who hold the key can read it, under keys that change, but DHT nodes see your IP address.
- VPNs can get in the way. Two devices behind NAT connect by punching a hole through it, which often fails behind symmetric NAT, as many VPNs are, especially when both people use one. Relaying then needs a contact you already share, so a brand-new contact with both of you behind VPNs may not connect until one of you turns the VPN off.
- Optional extras can reach outside services. GIF search, for example, contacts GIPHY, and only after you agree to it.
- The apps are young. Android and Linux only, with rough edges. The Linux app doesn't do Bluetooth or Wi-Fi Direct yet (the command line does).
For the longer story of why this exists, see Why Threnody. To see how it works in more depth, read How it works on the website, or the protocol specification and its appendices below.
Threnody is the Rust reference implementation of the Threnody Protocol Specification, an encrypted, metadata-resistant messaging protocol with post-quantum hybrid cryptography.
Status: milestone 6. Two devices can connect over IP and authenticate with an X-Wing (X25519 + ML-KEM-768) handshake. They can then chat and send files over a post-quantum double ratchet with encrypted headers, and mutually approve each other. Mutually approved devices find each other automatically on the local network through private beacons, and they get post-quantum-hybrid WireGuard tunnels. MLS groups use an X-Wing ciphersuite and need no server. Approved nodes relay end-to-end sessions over up to three hops. Messages to offline contacts are sealed to their prekeys and held by mutual contacts until the recipient returns. Onion circuits through two or more relays keep any single relay from seeing both ends, through approved contacts or through volunteer relays listed by directories you subscribe to, each paid with an anonymous token. Zero-knowledge credentials (BBS, with post-quantum-signed issuance) let someone prove chosen attributes another person vouched for, and nothing else. Several devices can share one account, linked with a one-time code; any device can add or remove others. The KEM matches the official X-Wing test vectors, the handshake, ratchet, credentials and relay tokens are machine-checked in Tamarin, every parser is mutation-tested, and identity keys can be protected with a passphrase. Apps can embed a node through generated Kotlin, Swift and Python bindings. Over Bluetooth LE, approved contacts recognise each other's private beacons and connect without any taps. A Pixel 8a and a laptop have chatted this way, and the phone keeps its sessions while in the background. Relays work across transports, so a phone with only a Bluetooth link can reach peers that its contacts reach over IP. Sessions over Bluetooth can be upgraded to Wi-Fi Direct for bandwidth. Across the internet, contacts find each other with no server: sessions run over QUIC as well as TCP, routers are asked to forward the port, and encrypted records in the public BitTorrent DHT lead to a hole punch. Invite links and handles meet the same way, so an invite made on mobile data still finds its maker. Contacts can talk in voice and video calls inside the session.
For a graphical messenger on Linux, run apps/linux/install.sh and start Threnody from your app menu (details). It shares its identity with the CLI. The rest of this section uses the CLI.
cargo build --release
alias threnody=target/release/threnody
# Device B
threnody --home ~/.thr-b init
threnody --home ~/.thr-b invite 192.0.2.7:7450 # prints a link + QR code
threnody --home ~/.thr-b run --listen 0.0.0.0:7450
# Device A
threnody --home ~/.thr-a init
threnody --home ~/.thr-a run --no-listen -c 'threnody://<fingerprint>@192.0.2.7:7450?r=<key>'An invite link is threnody://<fingerprint>[@<addr>,…][?r=<key>]: the inviter's fingerprint, up to four addresses (invite takes several, comma-separated, with IPv6 addresses in brackets and best first), and a key naming a meeting place in the DHT where the running node keeps its current addresses. A finder tries onion circuits, then every address, then the meeting place, then relays through contacts you share. Both sides need 0.17.1 or later for the key; see Appendix S.
Inside run, any line you type goes to the current peer. The available commands are below. Groups use /group new|invite|accept|decline|remove|leave|delete, roles /group admin|unadmin|owner|successor|claim|coordinate, /groups and /g <group> <text>. /relay <contact|fingerprint|invite> reaches a peer through approved relays, and /connect falls back to relays when a direct dial fails. Text sent to a contact who isn't connected is sealed and left with mutual contacts, who deliver it when that contact returns. /ble scan finds nearby Threnody devices over Bluetooth LE, and /ble connect <n> opens a session over the radio (Linux, BlueZ). run --ble makes a Linux machine advertise a private beacon, accept Bluetooth sessions, and connect to approved contacts it hears. The Android app advertises on its own once it has the Bluetooth permissions, and it can scan and dial. With run --wifi-direct, /wifi-direct request asks the current (nearby, approved) peer to host a Wi-Fi Direct group, and the laptop joins it through NetworkManager. The session then moves to the faster link.
/onion <peer> [min-relays] builds an onion circuit (two relays by default) so no single relay learns both ends.
Volunteer relays and directories. Without enough approved contacts to relay through (a new user, or an anonymous identity), circuits can go through volunteer relays instead. /dir add <threnody-dir://…> subscribes to a relay directory. The client fetches its signed list of relays and anonymous tokens over a link that shows the directory an address, never who you are. When contacts can't make a path, contacts are then reached through two volunteer relays, each paid with a token it can't link to you or to your other circuits; /vonion <peer> does it on demand. A relay is used only if enough of your directories list it (--directory-threshold). run --volunteer-relay <public host:port> volunteers this machine as a relay, and run --serve-directory <public host:port> runs a directory and prints its link. A directory lists a relay only after its operator approves it with /directory list <relay>; --no-review-relays lists every relay that proves reachable. See Appendix P.
Credentials. Anyone can vouch for attributes of someone else with a zero-knowledge credential: /cred offer <peer> <schema> key=value…. The holder later proves just the attributes it chooses, /cred present, when a verifier asks with /cred ask <peer> <schema> <keys>. Proofs can't be linked to each other or to the issuance, except that each verifier sees a stable pseudonym of its own. See Appendix O.
/history [peer] [n] shows recent messages, numbered and stored encrypted. /search <words> finds messages in every conversation, and /search @<peer|group> <words> in one; every word must appear (ignoring case, in the text or a file name), and "quoted words" match as a phrase. Search decrypts history in memory, so no index of your messages is ever written to disk. Results of a search in one chat are numbered like /history's. /del <n> deletes message n of that list on all your devices, and /del <n> all deletes one of your own messages for everyone too. /edit <n> <text> edits one of your own messages everywhere. /reply <n> <text> replies quoting message n, and /pin <n> pins it to the chat (/pins lists them). /react <n> <emoji> adds a reaction (several per message are fine; the same command again takes it back). Messages stay unless you choose a timer. /disappear 1h (or 30s, 10m, 1d, 1w, off) sets one for the current chat, and the peer adopts it. --disappear-default <time> sets one for chats that haven't chosen their own. /clear [peer] deletes your messages with someone but keeps them as a contact; /forget <peer>, or threnody forget <peer>, deletes the contact and the conversation. Both apply on all your devices.
threnody backup <file> saves your identity, contacts and history to a file sealed under a passphrase you choose. After losing a device, threnody restore <file> puts them on a new one; the apps do the same from their menus (Account & Identity on Android, Preferences on Linux). Anonymous identities and received files' contents aren't in a backup.
threnody wipe erases this device's identity, contacts, history, received files and anonymous identities, and their keyring keys. It asks you to type erase (or takes --yes), refuses while a node runs on the data directory, and keeps any files in it that Threnody didn't write. Nothing is sent: contacts and your other devices aren't told, and your other devices keep listing this one until you remove it there. The apps do the same from "Erase all Threnody data", and can lock behind a PIN, passphrase or the phone's own lock, with an optional duress PIN that erases everything when typed on the lock screen.
To add a device to your account, run /device add [host:port] on a device you already have. It prints a one-time code (and a QR code) with this machine's LAN address, or the one given. Then run threnody link '<code>' on the new device. /devices lists your devices, /device rename <name> <new name> renames one, and /device remove <name> revokes one. Your contacts see device changes, and refuse removed devices. Messages to a contact go to all of their devices. Your devices share your message history, and a new device gets it all when linked.
/connect <invite|handle|contact|host:port> /handle [name|off] /to <peer> /peers /contacts
/name <peer> <name> /approve [peer] /revoke [peer]
/safety [peer] /verify [peer] /file [-s] <path>... [| caption] /drop [peer]
/policy anyone|contacts|approved /status /quit
Message requests. Messages from someone you haven't accepted are kept apart as requests. You accept someone by approving them, dialing their invite, or writing to them. /requests lists them, /accept <peer> takes them in, /block <peer> refuses that sender from then on and deletes the conversation, and /delete <peer> discards the request. Strangers can't wear a node down: it holds at most 64 sessions with people you haven't accepted, 4 from one network, and closes each after 5 idle minutes. A request that finds the node full waits with its sender, whose node tries again later (Appendix A, Limits). A new device you're linking still gets in while they're full.
You can also manage contacts outside a session with threnody contacts | name | approve | revoke | verify | forget. While a node runs on the same data directory these refuse (it would write its own contacts over the change); use threnody ctl or the app instead. Only one node runs on a data directory at a time.
Metadata protection is on by default. Every message is padded. Each session also sends one padded frame every 2 s, with cover traffic in the gaps, so an observer can't tell when you send. Every frame up to the size of an onion cell's is padded to it, about 6.5 kB, so a link looks the same whether it carries short messages, onion circuits or nothing (longer messages and files still show their size). That's roughly 560 MB a day per connected contact, both ways (about 110 MB on mobile data, where the interval is 10 s). Contacts are also reached through two-relay onion circuits first, whenever approved relays make one possible, including when the node reconnects them by itself (after a network change, through the DHT, on the LAN). The first contact back after start-up or a network change (two, if the first is slow to answer) may be dialed directly, so that the circuits have a first hop; so may a contact found on the LAN. Messages left in mailboxes for offline contacts go over such circuits too, so the mailbox doesn't learn who sent them. --constant-rate-ms N changes the interval, --no-cover turns cover traffic off, and --no-onion dials directly. Photos lose their location and camera details before they're sent. HEIC, AVIF and TIFF photos, whose details the node can't remove, aren't sent until converted to JPEG; --keep-metadata sends photos as they are. --policy approved only accepts mutually approved contacts.
Anonymous identities. threnody persona new <label> [--burn-after 7d] makes a separate identity, and threnody --persona <label> run uses it. It has its own keys, contacts and history, and nothing links it to you. /profile set name … and /share <peer> name choose what each contact sees of your profile; nothing is shared by default. As a persona, /reveal <peer> <host:port> proves to that peer who you are, with a signature that can't be taken back. threnody persona burn <label> deletes a persona and everything it kept.
While listening, nodes send private UDP beacons that only mutually approved peers can recognise, and reconnect to each other automatically. Use --no-discover to turn this off. See Appendix E.
threnody init seals the identity key (Argon2id) with a random key kept in the system keyring: the Secret Service on Linux (GNOME Keyring, KWallet, KeePassXC), the Keychain on macOS, or the Credential Manager on Windows. A copy of the data directory is then useless without your unlocked keyring. Without a keyring (say, on a headless server), or with --no-keyring, the key is stored unprotected. threnody keyring on|off|status moves an existing identity into or out of the keyring. Alternatively, threnody init --passphrase seals it with a passphrase you type, and threnody passphrase adds, changes or removes it. $THRENODY_PASSPHRASE supplies it non-interactively, and so does a file named by $THRENODY_PASSPHRASE_FILE or a systemd credential named threnody-passphrase.
Running as a service. threnody run --daemon runs without a terminal (under systemd, say). It takes commands over an owner-only control socket in the data directory ($THRENODY_CONTROL to put it elsewhere) instead of stdin: threnody ctl /peers, threnody ctl /approve <peer>, and so on. Its log never shows message text or the answers to ctl. Running an always-on relay node uses this to give phones a relay that's always reachable, wherever they are.
--tunnel [PORT] builds WireGuard tunnels to mutually approved peers. Each pair's preshared key comes from its Threnody session, which makes the tunnels post-quantum hybrid. The node keeps <home>/wireguard/thr0.conf current: bring the interface up with sudo wg-quick up …, then add --wg-apply to push changes live. Tunnel peers survive restarts. See Appendix D.
| Crate | Contents |
|---|---|
threnody-core |
Sans-IO protocol: identity and fingerprints, the hybrid KEM, the handshake, the ratchet, the CBOR wire format (via const-cbor), padding, and the contact/identity store |
threnody-net |
Stream framing, the async handshake driver, and Node (sessions, trust policy, approval exchange, constant-rate mode, relays, onion circuits, mailboxes, internet reachability, handles, calls). Sessions run over TCP and QUIC, and the session driver works over any byte stream (Bluetooth, Wi-Fi Direct). |
threnody-groups |
MLS groups on openmls with the X-Wing ciphersuite. Credentials are bound to Threnody identities, owners and admins agree on roles in the MLS group context, one owner device commits what they propose, and messages travel as sans-IO fan-out over the 1:1 sessions |
threnody-media |
Voice and video call media (WebRTC), carried over a Threnody call |
threnody-tun |
A TUN interface from a file descriptor, for userspace WireGuard on Android |
threnody-ffi |
UniFFI bindings (Kotlin, Swift, Python) for embedding a node in apps; see its README |
threnody-cli |
The threnody binary |
threnody-desktop |
A graphical messenger for the Linux desktop (GTK 4, libadwaita) in apps/linux; see its README |
The normative details that the spec deferred are written up in docs/:
- Appendix A: handshake
- Appendix B: ratchet
- Appendix C: CBOR schemas
- Appendix D: WireGuard tunnels
- Appendix E: private LAN discovery
- Appendix F: MLS groups
- Appendix G: relay circuits
- Appendix H: offline delivery
- Appendix I: onion circuits
- Appendix J: accounts and multiple devices
- Appendix K: Bluetooth LE transport
- Appendix L: Wi-Fi Direct link upgrade
- Appendix M: anonymous identities and selective disclosure
- Appendix N: reaching contacts across the internet (0.3.0)
- Appendix O: zero-knowledge credentials (0.4.0)
- Appendix P: volunteer relays and directories (0.4.0)
- Appendix Q: voice and video calls (voice and video between two people, and group calls of up to eight)
- Appendix R: waking a sleeping device with UnifiedPush (opt-in)
- Appendix S: handles
- Appendix T: adding contacts by NFC tap
- Test vectors, regenerated and checked by
cargo test - Tamarin proofs of the handshake, ratchet, sealed messages, onion hops and device linking
| Spec | Status |
|---|---|
| §3.2 Hybrid KEM X25519 + ML-KEM-768 | ✅ X-Wing (draft-11), passes the official vectors |
| §3.2 Ed25519, ChaCha20-Poly1305 + AES-256-GCM, domain-separated KDF | ✅ BLAKE3 KDF; both AEADs negotiated |
| §3.3 FS / PCS ratchet with hybrid PQ updates | ✅ KEM double ratchet with header encryption |
| §3.4 Formal verification | ✅ Tamarin, 61 lemmas: handshake, ratchet, sealed messages, onion hops, device linking and account chains, credentials, relay tokens, and meeting places for invites and handles; proofs/check.sh |
| §4.2 Pseudonymous identity, §4.4 fingerprint | ✅ 32-character Crockford Base32 |
| §4.1 Anonymous mode, §4.3 selective disclosure | ✅ anonymous identities (personas) with their own keys, contacts and history, unlinkable to the main identity, optionally burning themselves, revealed only by a signed proof; profiles shared per contact, nothing by default (Appendix M); zero-knowledge credentials: BBS selective disclosure with per-verifier pseudonyms, blind issuance, ML-DSA-signed issuer keys and receipts (Appendix O) |
| §4.5 Multi-device | ✅ signed device chains (equal peers, threshold-ready), link codes, revocation, own-device contact sync and history sync (with a backfill for newly linked devices), account-wide offline keys and group invites |
| §5 TOFU, out-of-band invites with pinned fingerprint, QR, safety numbers, mutual approval and revocation | ✅ invite links with several addresses and a DHT meeting place, handles that need no directory (Appendix S), relay directories (Appendix P), adding contacts by NFC tap (Appendix T): Android both ways, iPhone and Linux (PC/SC reader) tapping an Android phone; web-of-trust not yet |
| §6.1 1:1 text + files | ✅ voice and video messages, played in the chat; photos and albums with captions, a sensitive flag that shows them covered until opened, and metadata (location, camera) stripped before sending by default; including disappearing messages (the timer travels with each message); end-to-end acknowledgements, with resending after a dropped session or a restart |
| §6.2 MLS groups | ✅ openmls with the X-Wing ciphersuite; text, files, photos, voice and video messages; owners and admins, with roles every member enforces, a successor and fork repair; encrypted persistence; members forward and hold messages for members who can't be reached directly |
| §6.3 / §11 CBOR, versioning, unknown-field tolerance | ✅ |
| §6.4 Local-first store | ✅ encrypted identity, contacts, groups and message history, including file transfers, synced across an account's devices; passphrase-sealed backups that restore onto a new device (Appendix C) |
| §7 Transports | ✅ TCP/IP and QUIC, reaching contacts across the internet with no server (router port mapping, DHT rendezvous, hole punching, fast recovery after a network change; Appendix N), Bluetooth LE (L2CAP, tested phone ↔ laptop), private LAN discovery with auto-connect, multi-hop relay circuits (≤ 3 relays), store-and-forward mailboxes, Wi-Fi Direct upgrade (Android hosts or joins; Linux joins); opt-in UnifiedPush wake-ups instead of an always-on connection on Android: content-free, paced, padded with cover wake-ups, sent by the contacts holding messages (Appendix R) |
| §8 WireGuard full-mesh tunnels | ✅ kernel WireGuard on desktops, userspace WireGuard (boringtun) through a VPN interface on Android; PQ PSK from the session; gated on mutual approval; peers kept, encrypted, across restarts |
| §9 Metadata layers | ✅ on by default: padding, constant-rate cover traffic, onion-first routing (≥ 2 relays, fixed-size cells), onion-routed mailbox deposits, each only off when switched off; volunteer relays from subscribed directories (k-of-n listing, Ed25519 + ML-DSA signed), paid with unlinkable rate-limited tokens, entered without revealing who you are (Appendix P) |
| §10 Status indicators | ✅ /status |
| §3.1 Platform keystore | ✅ Android: the identity is sealed under an Android Keystore key (StrongBox or TEE). iOS: under a Secure Enclave key (a this-device-only Keychain item without one). Desktop: sealed with a key in the system keyring by default, or a passphrase; 0600 files without either |
| §15 Test vectors | ✅ docs/test-vectors/v1.txt |
- Internet reachability (remaining). 0.3.0 reaches contacts across the internet with no server: sessions over QUIC, rendezvous in the public BitTorrent DHT under keys only the two contacts share, hole punching, and fast recovery that reconnects in under a second when a phone changes network (see Appendix N). Since then: router port mapping (UPnP, PCP, NAT-PMP), an always-on relay that a device keeps a session with (
run --anchor, Always-on relay in the apps; see Running an always-on relay node), and invite links and handles that meet in the DHT. Still to do: tests between two phones, and a way through for two people who are both behind symmetric NAT (many VPNs) and share no contact to relay through. - Hardening (remaining). An external audit.
- Groups (remaining). Member key updates for post-compromise security.
- Tunnels (remaining). The userspace data plane on iOS and unprivileged desktops (Android has it).
- Mesh (remaining). Upgrading to Wi-Fi Direct automatically for large transfers, and a phone-to-phone Wi-Fi Direct test.
- Metadata (remaining). Relays that hold a connection for a device behind NAT, as onion services do (Appendix N, "Later"), and cover traffic inside circuits.
- Mobile (remaining). An Android app is in
apps/android, with conversations, chat, files, groups, invites by QR code and link, approval and safety numbers. Its transports have been tested on a Pixel 8a, and it keeps sessions alive in the background with a foreground service, or, if the user chooses, sleeps and is woken through UnifiedPush by the contacts holding its messages (Appendix R). An iOS app is inapps/ios(SwiftUI) with Android's features except calls, tunnels, Bluetooth and Wi-Fi Direct links, and recording voice and video messages; it receives only while open, as iOS push would need a server that sees every wake-up (why). Still to do: calls on iOS, a field test of push wake-ups between two phones, and sending wake-ups through circuits so the push server doesn't see contacts' addresses.
cargo test # unit, vector, mutation and TCP integration tests
cargo clippy --all-targets
cargo +nightly fuzz run envelope # see fuzz/README.md
crates/threnody-ffi/bindings.sh # generate bindings, run the Python binding testEvery push and pull request runs formatting, clippy, the full test suite, the Python binding test, Android builds with and without calls (the second as F-Droid builds it), an iOS simulator build with its unit tests and a dependency advisory check (.github/workflows/ci.yml). It also builds and tests on macOS and Windows. Fuzzing and the Tamarin proofs run weekly, and the proofs also run whenever they change.
To release, bump version in the workspace Cargo.toml and the matching versionCode and versionName literals in apps/android/app/build.gradle.kts (the Android build fails if they differ), add the Android changelog fastlane/metadata/android/en-US/changelogs/<versionCode>.txt (500 characters at most) and set the version in the F-Droid recipe (apps/android/fdroid/org.threnody.app.yml), then push a matching tag:
git tag -a v0.2.0 -m "Threnody 0.2.0" && git push origin v0.2.0The release workflow runs CI, then publishes a GitHub Release with:
- CLI binaries for Linux (x86_64, aarch64), macOS (Apple silicon, Intel) and Windows;
- the Android APK;
- FFI libraries with Python, Kotlin and Swift bindings, including the Android libraries;
- a
SHA256SUMSfile.
Every file carries signed build provenance, so you can check that it was built by this repository's release workflow: gh attestation verify <file> -R larrydewey/threnody.
A tag with a suffix (v0.2.0-rc.1) makes a pre-release. The APK is signed when the ANDROID_KEYSTORE_BASE64, ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_ALIAS and ANDROID_KEY_PASSWORD secrets are set; otherwise it is published unsigned.
Apache-2.0 OR MIT, at your option (LICENSE-APACHE, LICENSE-MIT).