Skip to content
Henner4746Public

About

End-to-end encrypted messenger without phone number, e-mail or username — your address is your public key. Signal protocol, Bluetooth offline mode, Tor. Android, Windows, Linux, Web. AGPL-3.0

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

This page in German.

BitDM

An encrypted messenger with no phone number, no username, and no account. Your address is your public key.

License: AGPL v3 Version Platform

Platform State
Android 9+ released, signed APK
Windows x64 released, unsigned installer and zip
Linux released, tar.gz; tested in a container under Xvfb, not yet on a real desktop
Web runs at bitdm.net/app, limited

Most messengers ask for your number first. BitDM asks for nothing. On first launch it generates a key pair; the 56-character address it shows you is the public half of it.

That has a consequence which is easy to miss and hard to overstate: no server can hand you the wrong key. To do that it would have to change the address, and you already have the address. There is no directory to poison, because there is no directory.

tayn-bret-xl2d-i6f6-i5h4-cleu-salt-5mr3-ed3a-m3yo-bafm-qwqf-wdwo-rd2p
└─ base32( pubkey[32] ‖ sha256(pubkey)[:3] ): 35 bytes, exactly 56 characters

Three checksum bytes rather than two, for a reason worth one line: 35 bytes divide evenly into base32, so there is never a = to strip and re-append. The side effect is 24 bits of typo detection instead of 16.

Warning

This software has not been independently audited. It sends real messages and the reasoning behind each part is written down, but no outside party has reviewed the result. Read Known gaps before you rely on it. If your safety depends on the encryption, for example as a journalist or under surveillance, use Signal until BitDM has had an independent audit.


How a message travels

There are three paths, and which one a message takes is decided in exactly one place: app/lib/core/nah/wegwahl.dart. Never two paths for the same message: that would be two encryptions of one text, and two key chains side by side in the Double Ratchet.

flowchart TD
    M["Alice sends"] --> T{"Text or attachment?"}

    T -->|text| P{"Nearby-only<br/>switch on?"}
    P -->|"yes: never touch a server"| BLE["Bluetooth LE<br/>no Wi-Fi, no mobile data,<br/>no data centre"]
    P -->|no| RCH{"Relay reachable?"}
    RCH -->|yes| RLY["Relay<br/>encrypted envelope over TLS"]
    RCH -->|"no, nearby enabled"| BLE
    RCH -->|"no, nearby off"| WAIT["Stays queued.<br/>Queued, not failed."]

    T -->|attachment| SPLIT["Split into 32 MiB chunks,<br/>one random AES-GCM key each"]
    SPLIT --> BLOB["Blob store<br/>sees ciphertext, size, time"]
    SPLIT --> RCP["Recipe holding the keys:<br/>an ordinary Signal message"]
    RCP --> RLY

    RLY --> BOB["Bob"]
    BLE --> BOB
    BLOB --> BOB
Loading

The attachment split is not caution for its own sake. AES-GCM is broken, not merely weakened, the moment one key meets one nonce twice; a fresh random key per chunk removes the counter that would otherwise have to survive every resume and abort. The chunk number travels in the authenticated data, so a store that swaps chunk 3 for chunk 5 produces a decryption failure instead of a quietly wrong file.


Three things that are actually different

1 · The address is the key. No registration, no lookup, no trust in a directory. You verify a contact by comparing 56 characters, not by trusting that a server matched a phone number to a key.

2 · It works with no server at all. Two phones next to each other exchange messages over Bluetooth LE. Measured on two devices with mobile data switched off: 325 bytes in 1.35 seconds, the one measurement on this page for which no log exists in the repository. The radio underneath was measured separately on the same two phones and is written down: 120 ms to find the peer, 700 bytes in 202 ms over GATT, and no system dialog at any point (docs/NAHBEREICH.md).

To find each other without broadcasting who they are, devices emit beacons instead of addresses: six bytes per contact, derived from the shared secret and the current 15-minute window.

beacon = HMAC-SHA256( shared_secret, "bitdm-nearby-v1" ‖ sender_pubkey ‖ be64(window) )[:6]
shared_secret = X25519( own_private_identity, their_public_identity )

Hold the secret and you recognise your contact. Don't, and you see six bytes of noise that change every quarter hour. The sender's own key is inside the input, so what Alice emits is not what Bob emits; a recorded beacon cannot be replayed to impersonate the other side. Three windows are checked when scanning, not one, because two phones never share a clock and a device that vanishes for a minute every quarter hour arrives at the user as "it works sometimes".

3 · You can run the whole thing yourself. The relay is one Python file and a systemd unit. One command sets it up:

curl -fsSL https://bitdm.net/install.sh | sudo bash

Piping an unread script into a shell is a question of trust, and the right answer to it is suspicion. Download it, read it, then run it. Or see every step first without changing anything:

curl -fsSL https://bitdm.net/install.sh | sudo bash -s -- --dry-run

Status

Component State How that was established
Signal protocol, PQXDH + Double Ratchet working (from 2.0.0) Signal's official libsignal (Rust, version 0.105) through the Dart binding libsignal 8.0.0. Session setup is PQXDH with Kyber-1024; a session without Kyber is impossible. Up to 1.9.x BitDM used libsignal_protocol_dart, an unaudited pure-Dart port; see docs/LIBSIGNAL-UMSTIEG.md
Messages over the relay working end-to-end over the live relay
Nearby transport, no network proven one direction 325 B / 1.35 s between two devices, both offline (the one figure here with no log in the repository); independently witnessed by an ESP32 rig acting as a foreign device
Nearby, second direction working 26.09.2026: a Galaxy S25 Ultra in nearby-only mode sent Signal-encrypted messages on its own to a PC running the real BitDM core over Bluetooth 5.4; the PC decrypted, replied over Bluetooth, and delivery and read receipts came back the same way (app/test_manuell/pc_kontakt_test.dart, tools/pc_kontakt/)
Nearby-only switch working the app opens no relay connection at all; test/core/nur_nahbereich_test.dart, test/nah/schalter_test.dart, test/nah/schalter_echt_test.dart
Attachments working separate encrypted blob store, never through the relay
Update over an existing install working 1.5.0+11 → 1.5.1+12, identity and contacts survived
Recovery on a new device working 12 BIP39 words restore identity and address, not contacts or message history; those only come back from the encrypted backup. Since 2.0.0 the new device also needs a code from a device that is still signed in; if none is left, it signs out all the others
Desktop client runs, as an additional device the Windows binary joins with the same 12 words as another device of the same address and sees everything new from then on, but no old history. Since 2.0.0 a device already signed in has to confirm it with a code (see Known gaps)
Signing out a device working (from 2.0.0) signed device list per identity, checked by the relay and by every contact. Relay: section "Geraeteabmeldung" in server/test_relay.py; app against the real relay: test/net/geraeteabmeldung_test.dart; contacts against a lying relay: test/net/geraeteliste_stufe2_test.dart; see docs/GERAETEABMELDUNG.md
Independent security audit none n/a

The nearby proof used an ESP32 as the counterpart on purpose. Two BitDM phones talking to each other only prove that the same code agrees with itself. A foreign device that knows nothing but the wire format is a real witness.

Along the way that rig also measured something the specs do not make obvious: Bluetooth 4.2 cannot receive extended advertising. Both sides need BLE 5.0.

Platforms

Platform State Nearby over BLE Push wake-up Lock factors offered
Android 9+ released, signed APK Android 12+ UnifiedPush app password, fingerprint, device PIN, hardware key
Windows x64 released, unsigned installer and zip no no (the connection stays open instead) app password
Linux released, tar.gz; tested in a container under Xvfb, not yet on a real desktop no no app password
Web runs at bitdm.net/app, limited no no app password

Nearby, push and three of the four lock factors sit behind an Android platform channel written in Kotlin. On the desktop they are not shown at all rather than shown and broken: the first Windows build offered four factors of which three could not work, and one of them said "the PIN, pattern or password of this phone" on a PC.

Attachments went the other way, and only in 2.0.0. Until 1.9.x "Attach file" was visible on the PC and did nothing: the file picker was Android-only, and the call failed silently. Since 2.0.0 the Windows and Linux runners carry the same channel (windows/runner/datei_kanal.cpp, linux/runner/datei_kanal.cc), so choosing, sending and opening files works there too. In the web build the entry is greyed out and says why.

Features beyond the core: replies, reactions, edits, delete for everyone, polls, groups (up to 20), voice messages (Android), scheduled and pinned messages, note to self, typing indicator, search, encrypted backup, panic password. Where each comes from, what is deliberately missing and the known limits: docs/FUNKTIONSVERGLEICH.md (German).


Get it

Important

2.0.0 does not work together with 1.9.x. Both sides have to update. Until a contact has updated, your messages to them wait and the chat says why; they go out by themselves afterwards. Your address, chats and contacts stay, and every session is set up anew. If you use several devices, the first one you update creates the device list, and each further device joins it once with a code.

Android

curl -fsSLO https://bitdm.net/bitdm-2.0.0.apk
sha256sum bitdm-2.0.0.apk

Android will warn you when installing. It warns about every app whose certificate Google has not seen, and that warning says nothing about this file. These two values do, and they are not the same kind of thing:

What Value
This file (sha256 of the APK) 6354bc7d14eadbf6c2ebbb556f260a792fe2a88bf93eb033faa2fe448cac7b9d
Signing key (certificate fingerprint) e325b01c08a1a679b6aac20ac9ae3ee255591b46b37dd92717f97085acc22063
apksigner verify --print-certs bitdm-2.0.0.apk

The first value covers only this file. The second covers every future version: Android accepts an update only if it carries the same signing key. If that second value ever changes, it is no longer the same app.

Requires Android 9 or newer. Nearby mode needs Android 12; below that the app refuses it with a plain reason instead of asking for the location permission a BLE scan would otherwise require.

Automatic updates: Obtainium fetches every new version straight from the GitHub releases, the same file as above, checked by Android against the same signing key. Add BitDM to Obtainium (the link goes through Obtainium's own redirect page, because GitHub strips obtainium:// links; on bitdm.net the button opens Obtainium directly).

F-Droid and Google Play: BitDM is in neither official catalogue. You can add our own F-Droid repository instead (address and fingerprint on bitdm.net); it serves the same APK. That APK is still built reproducibly with F-Droid's own tooling (fdroid build, workflow .github/workflows/android-fdroid.yml); F-Droid's test build of 1.9.1 came out bit-identical to it.

Windows

Installer bitdm-windows-setup-2.0.0.exe (14,421,001 bytes) or bitdm-windows-2.0.0.zip (17,161,850 bytes, unpack and run bitdm.exe). Both are also attached to the GitHub release.

What Value
Installer (sha256) 6af884c40c2e0ff68cbefeb4e14b6bd0c49f93351c20969940387b7b5c2ee635
Zip (sha256) 87d1b5056434e5d86b128d173d9c5ef279700b807642facb187b8cb17611d6ea

SmartScreen will warn about it, and that cannot be fixed here: Windows code signing needs an Authenticode certificate from a commercial CA, and the Android key is no substitute. The answer is the same one as for Play Protect: publish the hash, so the warning becomes something you can check rather than something you have to believe.

Read Known gaps first if you already use BitDM on a phone. A second install joins your identity as another device. It needs your 12 words and a code from the phone (Settings › My ID › Manage devices › Add device). It sees everything new from then on, but no old history, and the phone stays signed in until you sign it out.

The installer comes from app/windows/bitdm.iss. It installs into %LOCALAPPDATA% without asking for admin rights and adds no autostart entry.

Linux

bitdm-linux-x64-2.0.0.tar.gz on the GitHub release, built by .github/workflows/linux.yml on Ubuntu 22.04 from the tagged source.

What Value
tar.gz (sha256) e7bf90ba3359b25ba059f9b91c911ed3e20cae63ba559faf33014c2b001e2842

The release also carries a build attestation: gh attestation verify bitdm-linux-x64-2.0.0.tar.gz -R Henner4746/BitDM checks that the file came out of that workflow for the tag v2.0.0.

Unpack and run ./bitdm. It needs GTK 3, libsecret and an unlocked keyring behind it (GNOME Keyring or another Secret Service). Without one, 2.0.0 shows a message with "Try again" instead of an empty window; nothing is deleted or created.

Tested on 17.10.2026, in a Debian 13 container under Xvfb, not on a real desktop. The 1.9.1 archive, with an unlocked GNOME Keyring: it starts, creates an identity, sets an app password, comes back locked after a restart, rejects a wrong password, opens with the right one and still shows a note to self written before the restart. The test kept it away from relay.bitdm.net, so 1.9.1 has not yet sent a message on Linux. Two Linux instances of a newer development build did exchange messages over a local relay: contact request, accept, both directions, and again after a locked restart.

Web

https://bitdm.net/app/: the same app compiled for the browser, served without any third-party CDN. See the limits under Known gaps.


What it protects, and what it does not

Being specific here matters more than sounding strong.

Protected

  • Message content, against anyone including the relay operator: end-to-end, forward-secret
  • Key substitution by a server: the address is the key
  • Traffic correlation in nearby mode: rotating beacons, not identifiers
  • Attachment content against the blob store: it holds ciphertext and never sees a key

Not protected

  • Who talks to whom, and when. A relay cannot read content, but it necessarily sees which addresses connect when and how much they send. Running your own relay does not remove that record. It changes who holds it: you instead of us, and your hosting provider still sees the connections. The one way around the relay is the nearby-only switch, which opens no relay connection at all: no registration, no polling, no push endpoint, no blob store. Its limit is part of the design: it forbids the server, it does not build a second path. Turn it on without Bluetooth and messages stay queued. It is also not a substitute for airplane mode; it speaks for BitDM only.
  • A compromised device. Keys live in the Android keystore; root or a malicious keyboard defeats any messenger, including this one.
  • The fact that you use BitDM. BLE advertising is visible as some device advertising, and TLS to a relay is visible as a connection.
  • Your presence in a room, in nearby mode. The beacon protects your identity, not the fact that a device is transmitting. Android rotates the Bluetooth address by itself, but someone standing in the same room long enough can still tell devices apart.
  • Anything a court order to the relay operator would reveal: which addresses connected when, and how much they sent. Run the relay yourself and that order goes to you or your hosting provider instead.

Nearby mode has no presence display, deliberately: the component that knows who is in range does not pass that on to the interface, and a message shows how it travelled, never who was around. What does exist is a per-contact switch: you can go invisible to one person in particular. Off means no beacon is sent to that contact and none is expected from them; both directions together, because the other arrangement would stop you finding them while still showing them where you are. Messages to that contact then always take the relay.


Build it yourself

The point of an open messenger is that you do not have to take the binary on faith.

cd secure-messenger/app
flutter build apk --release        # → build/app/outputs/flutter-apk/app-release.apk
flutter build windows --release    # → build/windows/x64/runner/Release/

Needs Flutter with Dart SDK ^3.12.2 and JDK 17. Your APK will not match the published hash byte for byte: it will be signed with your key, not ours. A differing hash is normal here.

Since 2.0.0 the build also needs Signal's libsignal. On your own machine the build hook of the libsignal package downloads the prebuilt library from that package's GitHub release. The published APK compiles it from source instead (fdroid/libsignal-quellbau.sh, details in docs/LIBSIGNAL-UMSTIEG.md).

The Windows output is a directory, not a single file. bitdm.exe is 90 KiB and useless alone: sqlite3mc.dll is the encrypted database, libsignal_frb.dll (since 2.0.0) and webcrypto.dll the crypto, and the actual Dart code lives in data/app.so. Ship the whole folder. For an installer instead of a zip:

& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" /DVersion=2.0.0 windows\bitdm.iss

Tests:

cd secure-messenger/app && flutter test      # app + crypto + nearby
cd secure-messenger && py -m pytest server/test_relay.py server/test_blob.py -q   # relay + blob store

How many tests each side has is listed once, under Layout.

The relay tests do not just check that delivery works. They check the attacks: registration without proof of possession, wrong signatures, overwriting someone else's bundle, prekey drain, oversized envelopes, double delivery.

Two traps when building the Android APK on Windows: a locked build/ directory, and a rewritten GeneratedPluginRegistrant.java

Each one is worth three failed attempts if you hit it cold.

  • Unable to delete directory … mergeReleaseAssets: a Gradle daemon is holding files. Fix: cd android && ./gradlew --stop, then delete build/. Do not kill processes wholesale; adb dies with them and takes a connected phone with it.
  • Package dev.flutter.plugins.integration_test does not exist: a previous device test rewrote GeneratedPluginRegistrant.java. Only flutter clean helps; flutter pub get regenerates the same file.

Run your own relay

A relay carries encrypted envelopes between devices that cannot reach each other directly. It cannot read them.

curl -fsSL https://bitdm.net/install.sh | sudo bash

Debian or Ubuntu. It asks for a domain and an email, checks that the domain actually resolves to the machine, installs what is missing, and sets up the certificate, nginx and the service. No config file to touch. Running it again is safe, and it refuses to take over an existing relay without asking.

It installs one fixed release, not whatever is on main right now: the script names a tag and a commit (REF, REF_COMMIT) and fetches exactly that commit; git checks while fetching that the content matches it. Whether bitdm.net serves the same file as the repository can be compared over two separate routes:

curl -fsSL https://bitdm.net/install.sh | sha256sum
curl -fsSL https://raw.githubusercontent.com/Henner4746/BitDM/main/secure-messenger/website/install.sh | sha256sum

Note

Everyone involved needs the same relay. There is no federation between instances. Two people on different relays cannot message each other.

Attachments need a second, separate blob store. Set up the relay and forget the store and attachments fail with an error that looks like a network problem. See docs/ZWISCHENLAGER.md.


Layout

secure-messenger/
  app/lib/               Flutter app: 113 files, 44,040 lines Dart
    main.dart              218 of them: start, BitApp, relay targets
    oberflaeche/home.dart  731: Home with all its state, lifecycle and lock clean-up
    oberflaeche/home/      21 parts of Home, 86 to 605 lines each: lock, access,
                           emergency, identity, chats, settings, nearby …
    core/crypto/           libsignal sessions, identity, address encoding, BIP39
    core/nah/              nearby: beacons, BLE radio, chunking, route choice
    core/anhang/           attachments: chunk crypto, recipe, blob store client
    core/store/            encrypted local database
  app/test/              144 files, 36,334 lines: 1,517 tests (18.10.2026)
  app/integration_test/  3 files, 791 lines on real systems: attachment throughput,
                         Windows platform, file dialog
  app/android/…/kotlin/  9 files, 3,062 lines: BLE, keystore, foreground service
                         (plus 2 test files)
  app/windows/           CMake, runner, and bitdm.iss for the installer
  app/linux/             runner; the release tar.gz comes from .github/workflows/linux.yml
  app/web/               shell for bitdm.net/app, built as in deploy/webapp/README.md
  app/tool/symbol/       3 Python scripts that render and distribute the app icon
  app/tool/*.py          checks for the main.dart split: pure move, name collisions
  server/                Python relay + blob store, 5,106 lines; tests 5,568 lines,
                         206 tests (18.10.2026)
  docs/                  operations, threat notes, measurements
  website/               bitdm.net
LICENSE                  AGPL-3.0
PLAN.md                  design log, older than the code in places

core/nah/ is where the interesting problem lives. Android hands each role its own random BLE address: advertising, a second advertiser, and an outgoing GATT connection are three different addresses as far as the peer is concerned. Two separate bugs in this project had that single root cause.


Known gaps

  • Most of this code was written by an AI. BitDM was built by one person working with Claude (Anthropic), and the AI wrote the large majority of the code, including the cryptographic plumbing. Every design decision was made deliberately and is documented, the test suite is extensive, and the reviews so far were thorough, but they were reviews by the same AI that wrote the code, which can share its own blind spots. Treat that as part of the risk, not as a footnote.
  • No independent audit. Nobody outside this project has reviewed the cryptography, the protocol, or the implementation. An internal review on 25.09.2026 found and fixed ~80 issues (CHANGELOG 1.8.1), and an AI-run security audit on 16.10.2026 found 63 more, two of them high severity in the Dart port of libsignal (gone with 2.0.0, see below). The way to an external audit is in docs/AUDIT.md.
  • The Dart binding to libsignal has a single maintainer. Since 2.0.0 the cryptography is Signal's own libsignal (Rust). It reaches Dart through the package libsignal 8.0.0 (flutter_rust_bridge, AGPL-3.0), which is young, unaudited and maintained by one person. BitDM pins the exact version. The published Android APK compiles the Rust library from source; the Windows, Linux and web builds still use the prebuilt library that the package downloads, checked only against a checksum file from the same GitHub release. Up to 1.9.x BitDM used a pure-Dart port (libsignal_protocol_dart) in which the audit of 16.10.2026 found two high-severity state-handling bugs. That is why it was replaced.
  • Post-quantum session setup (PQXDH), not yet a post-quantum ratchet. Every new session uses Kyber-1024 (PQXDH). Whether the post-quantum ratchet Signal added in 2025 (SPQR) is active through this binding is not verified yet.
  • Nearby mode reveals that a BitDM user is near. The Bluetooth adverts carry fixed service identifiers; the content is encrypted, but presence is visible to any scanner. Rotating identifiers need testing on real phones first.
  • Remote wipe only acts on an unlocked, connected app. A locked phone has no database key in memory and cannot read the requests; that is the lock doing its job, and it means a seized, locked phone is not wiped remotely.
  • Windows installs per user (no admin rights), so any program running as the same user could modify the files; together with the missing signature there is no integrity check after installation.
  • Nearby between two phones has been measured phone→phone once (29.07.2026) and phone↔PC in both directions (26.09.2026, the PC running the real core with a Windows Bluetooth bridge). Two current phones against each other have not been re-measured since 1.8.x.
  • Devices can be signed out, but whoever knows the 12 words can take over. A second install with the same 12 words joins the address rather than taking it over: the relay keeps up to five devices per address (BITDM_GERAETE_MAX), each with its own key bundle. Since 2.0.0 every identity keeps a signed device list. Contacts check the list themselves and encrypt only to the devices on it, whatever a relay claims. A new device needs the 12 words plus a code from a device already on the list (Settings › My ID › Manage devices › Add device). A lost device is signed out there; with no other device left, choose "Sign out all other devices" on the new one. The signed-out device receives nothing more, cannot sign back in, and forgets the 12 words as soon as it connects again; its chats stay on it, encrypted. The limit: anyone who knows the 12 words can do the same, sign out all your devices and take over the address. If a phone was stolen unlocked or the words are not safe, only a new identity whose address you give to your contacts helps. Signing out needs a relay from 2.0.0 on; against an older relay the app runs without a device list, and nothing can be signed out. A new device sees no old history; read and delivery ticks are not synchronised between devices (specification, §9; signing out: docs/GERAETEABMELDUNG.md).
  • The web build runs at https://bitdm.net/app/, but limited. Identity, app password, unlock after reload, restore from the 12 words and text messages between two browsers work (tested headless against a local relay). The database sits encrypted in IndexedDB (SQLite3MultipleCiphers as WASM). Without an app password the identity lives in memory only and is gone on reload. On purpose, because the browser's key store would keep the key right next to the data. No attachments, voice messages, nearby, push or screenshot protection. The relay itself sends no CORS headers; nginx in front of relay.bitdm.net allows exactly the origin https://bitdm.net, so a web build hosted anywhere else cannot register there.
  • Linux has only been tested in a container. Debian 13 under Xvfb (17.10.2026): start, identity, app password and lock work; a development build also sent and received messages over a local relay. The file dialog for attachments passed its test under Xvfb too. Not tested: a real desktop (Wayland, GPU drivers), other distributions, and a released build against relay.bitdm.net. The app sets up no notifications on Linux.
  • The Windows binaries are unsigned, installer and zip alike.
  • Groups are limited: up to 20 members, fan-out over one-to-one sessions (no sender keys; reasoning in docs/FUNKTIONSVERGLEICH.md). As long as one member is still on 1.9.x, every group message stays pending, and the group chat does not say who has to update.
  • Not in Google Play or the official F-Droid catalogue. Our own F-Droid repository on bitdm.net serves the same APK.

License

AGPL-3.0 (LICENSE).

Not GPL, deliberately. The GPL triggers on distributing a program. Someone who modifies the relay and merely operates it distributes nothing and would never have to publish the change. And the relay is precisely the component that sees who is online when. The AGPL closes that gap, which is what keeps "run it yourself" enforceable.

For a piece of software whose entire claim is that you do not have to trust the operator, readability is part of what makes that claim checkable.

About

End-to-end encrypted messenger without phone number, e-mail or username — your address is your public key. Signal protocol, Bluetooth offline mode, Tor. Android, Windows, Linux, Web. AGPL-3.0

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages