This page in German.
An encrypted messenger with no phone number, no username, and no account. Your address is your public key.
| 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.
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
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.
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 bashPiping 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| 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.
| 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).
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.
curl -fsSLO https://bitdm.net/bitdm-2.0.0.apk
sha256sum bitdm-2.0.0.apkAndroid 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.apkThe 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.
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.
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.
https://bitdm.net/app/: the same app compiled for the browser, served without any third-party CDN. See the limits under Known gaps.
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.
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.issTests:
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 storeHow 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 deletebuild/. Do not kill processes wholesale;adbdies with them and takes a connected phone with it.Package dev.flutter.plugins.integration_test does not exist: a previous device test rewroteGeneratedPluginRegistrant.java. Onlyflutter cleanhelps;flutter pub getregenerates the same file.
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 bashDebian 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 | sha256sumNote
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.
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.
- 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
libsignal8.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.
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.