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 v3VersionPlatform

PlatformState
Android 9+released, signed APK
Windows x64built, unsigned zip
Linuxproject files present, nothing built
Webbuilds, unfinished

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.


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

ComponentStateHow that was established
Signal protocol, X3DH + Double Ratchetworkinglibsignal_protocol_dart 0.8.2, the library Signal uses
Messages over the relayworkingend-to-end over the live relay
Nearby transport, no networkproven one direction325 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 directionnot provenneeds a second Android with BLE 5.0 — see Known gaps
Nearby-only switchworkingthe app opens no relay connection at all; test/core/nur_nahbereich_test.dart, test/nah/schalter_test.dart, test/nah/schalter_echt_test.dart
Attachmentsworkingseparate encrypted blob store, never through the relay
Update over an existing installworking1.5.0+11 → 1.5.1+12, identity and contacts survived
Recovery on a new deviceworking12 BIP39 words restore identity and contacts, not message history
Desktop clientbuilds, cannot be linkedWindows binary runs; it cannot join an existing identity — see Known gaps
Independent security auditnone

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

PlatformStateNearby over BLEPush wake-upLock factors offered
Android 9+released, signed APKAndroid 12+UnifiedPushapp password, fingerprint, device PIN, hardware key
Windows x64built, unsigned zipnono — the connection stays open insteadapp password
Linuxproject files present, nothing builtnonoapp password
Webbuilds, unfinishednonoapp 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.


Get it

Android

curl -fsSLO https://bitdm.net/bitdm-1.5.1.apk
sha256sum bitdm-1.5.1.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:

WhatValue
This file (sha256 of the APK)62027fc7cb2aceacd74d7150dcca1ff937861c30cc420fcd35f9055dc20ee72c
Signing key (certificate fingerprint)e325b01c08a1a679b6aac20ac9ae3ee255591b46b37dd92717f97085acc22063
apksigner verify --print-certs bitdm-1.5.1.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. Google Play and F-Droid submissions are prepared, not done.

Windows

secure-messenger/releases/bitdm-windows-1.5.1.zip — 15,419,797 bytes, unpack and run bitdm.exe.

WhatValue
This file (sha256 of the zip)08299da651e3d39e83c8c368727a9043795acd2174f6900be312527ff5187782

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 with the same 12 words does not join your identity, it takes it over.

An Inno Setup script for an installer exists at app/windows/bitdm.iss — it installs into %LOCALAPPDATA% without asking for admin rights and adds no autostart entry. No .exe from it is published yet.


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
  • Metadata at the relay, if you run your own

Not protected

  • Who talks to whom, at a relay you do not control. A relay cannot read content, but it necessarily sees which addresses connect when and how much they send. Two answers exist. Self-hosting, which costs a single command. And 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 your relay host would reveal. Run it yourself.

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.

The Windows output is a directory, not a single file. bitdm.exe is 90 KiB and useless alone: sqlite3mc.dll is the encrypted database, 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" windows\bitdm.iss

Tests:

cd secure-messenger/app && flutter test# 820 tests: app + crypto + nearbycd secure-messenger && py -m pytest server/test_relay.py server/test_blob.py -q # 35 + 25

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.

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 — 67 files, 22,471 lines Dart
main.dart 5,078 of them; the platform switches live here
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/ 73 files, 16,980 lines — 820 tests, all green
app/integration_test/ 1 file, 153 lines — attachment throughput on real hardware
app/android/…/kotlin/ 8 files, 2,574 lines — BLE, keystore, foreground service
(plus 2 test files)
app/windows/ CMake, runner, and bitdm.iss for the installer
app/linux/ app/web/ project files; no published build
app/tool/symbol/ 3 Python scripts that render and distribute the app icon
server/ Python relay + blob store, 4,508 lines including tests
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

  • No independent audit. Nobody outside this project has reviewed the cryptography, the protocol, or the implementation.
  • Nearby, second direction is unproven: the phone only transmits when it has something queued, and the ESP32 rig's contact stays pending because proving it needs libsignal-compatible X3DH on the rig. Blocked on a second Android with BLE 5.0.
  • One identity per device, and that is what limits the desktop. A second install with the same 12 words takes the address over instead of joining it: every install generates a fresh registration ID on purpose, and /register on the relay upserts the key bundle and drops the old one-time prekeys. So the Windows build starts with an empty conversation list, and the phone that had been using that address stops being reachable. Linked devices need the Sesame protocol, which is not built.
  • The web build is not finished. It compiles and the layout holds; it is not a client you should rely on.
  • Linux has no published build. The project files are there, nothing has been produced from them.
  • The Windows binary is unsigned, and the installer from bitdm.iss has not been built or published.
  • No group chats.
  • Google Play and F-Droid submissions are prepared, not done.

License

AGPL-3.0LICENSE.

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

Ende-zu-Ende verschlüsselter Messenger ohne Telefonnummer und ohne Username — die Adresse IST der öffentliche Schlüssel (Signal-Protokoll, AGPL-3.0)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

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 v3VersionPlatform

PlatformState
Android 9+released, signed APK
Windows x64built, unsigned zip
Linuxproject files present, nothing built
Webbuilds, unfinished

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.


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

ComponentStateHow that was established
Signal protocol, X3DH + Double Ratchetworkinglibsignal_protocol_dart 0.8.2, the library Signal uses
Messages over the relayworkingend-to-end over the live relay
Nearby transport, no networkproven one direction325 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 directionnot provenneeds a second Android with BLE 5.0 — see Known gaps
Nearby-only switchworkingthe app opens no relay connection at all; test/core/nur_nahbereich_test.dart, test/nah/schalter_test.dart, test/nah/schalter_echt_test.dart
Attachmentsworkingseparate encrypted blob store, never through the relay
Update over an existing installworking1.5.0+11 → 1.5.1+12, identity and contacts survived
Recovery on a new deviceworking12 BIP39 words restore identity and contacts, not message history
Desktop clientbuilds, cannot be linkedWindows binary runs; it cannot join an existing identity — see Known gaps
Independent security auditnone

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

PlatformStateNearby over BLEPush wake-upLock factors offered
Android 9+released, signed APKAndroid 12+UnifiedPushapp password, fingerprint, device PIN, hardware key
Windows x64built, unsigned zipnono — the connection stays open insteadapp password
Linuxproject files present, nothing builtnonoapp password
Webbuilds, unfinishednonoapp 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.


Get it

Android

curl -fsSLO https://bitdm.net/bitdm-1.5.1.apk
sha256sum bitdm-1.5.1.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:

WhatValue
This file (sha256 of the APK)62027fc7cb2aceacd74d7150dcca1ff937861c30cc420fcd35f9055dc20ee72c
Signing key (certificate fingerprint)e325b01c08a1a679b6aac20ac9ae3ee255591b46b37dd92717f97085acc22063
apksigner verify --print-certs bitdm-1.5.1.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. Google Play and F-Droid submissions are prepared, not done.

Windows

secure-messenger/releases/bitdm-windows-1.5.1.zip — 15,419,797 bytes, unpack and run bitdm.exe.

WhatValue
This file (sha256 of the zip)08299da651e3d39e83c8c368727a9043795acd2174f6900be312527ff5187782

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 with the same 12 words does not join your identity, it takes it over.

An Inno Setup script for an installer exists at app/windows/bitdm.iss — it installs into %LOCALAPPDATA% without asking for admin rights and adds no autostart entry. No .exe from it is published yet.


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
  • Metadata at the relay, if you run your own

Not protected

  • Who talks to whom, at a relay you do not control. A relay cannot read content, but it necessarily sees which addresses connect when and how much they send. Two answers exist. Self-hosting, which costs a single command. And 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 your relay host would reveal. Run it yourself.

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.

The Windows output is a directory, not a single file. bitdm.exe is 90 KiB and useless alone: sqlite3mc.dll is the encrypted database, 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" windows\bitdm.iss

Tests:

cd secure-messenger/app && flutter test# 820 tests: app + crypto + nearbycd secure-messenger && py -m pytest server/test_relay.py server/test_blob.py -q # 35 + 25

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.

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 — 67 files, 22,471 lines Dart
main.dart 5,078 of them; the platform switches live here
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/ 73 files, 16,980 lines — 820 tests, all green
app/integration_test/ 1 file, 153 lines — attachment throughput on real hardware
app/android/…/kotlin/ 8 files, 2,574 lines — BLE, keystore, foreground service
(plus 2 test files)
app/windows/ CMake, runner, and bitdm.iss for the installer
app/linux/ app/web/ project files; no published build
app/tool/symbol/ 3 Python scripts that render and distribute the app icon
server/ Python relay + blob store, 4,508 lines including tests
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

  • No independent audit. Nobody outside this project has reviewed the cryptography, the protocol, or the implementation.
  • Nearby, second direction is unproven: the phone only transmits when it has something queued, and the ESP32 rig's contact stays pending because proving it needs libsignal-compatible X3DH on the rig. Blocked on a second Android with BLE 5.0.
  • One identity per device, and that is what limits the desktop. A second install with the same 12 words takes the address over instead of joining it: every install generates a fresh registration ID on purpose, and /register on the relay upserts the key bundle and drops the old one-time prekeys. So the Windows build starts with an empty conversation list, and the phone that had been using that address stops being reachable. Linked devices need the Sesame protocol, which is not built.
  • The web build is not finished. It compiles and the layout holds; it is not a client you should rely on.
  • Linux has no published build. The project files are there, nothing has been produced from them.
  • The Windows binary is unsigned, and the installer from bitdm.iss has not been built or published.
  • No group chats.
  • Google Play and F-Droid submissions are prepared, not done.

License

AGPL-3.0LICENSE.

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

Ende-zu-Ende verschlüsselter Messenger ohne Telefonnummer und ohne Username — die Adresse IST der öffentliche Schlüssel (Signal-Protokoll, AGPL-3.0)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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 v3VersionPlatform

PlatformState
Android 9+released, signed APK
Windows x64built, unsigned zip
Linuxproject files present, nothing built
Webbuilds, unfinished

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.


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

ComponentStateHow that was established
Signal protocol, X3DH + Double Ratchetworkinglibsignal_protocol_dart 0.8.2, the library Signal uses
Messages over the relayworkingend-to-end over the live relay
Nearby transport, no networkproven one direction325 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 directionnot provenneeds a second Android with BLE 5.0 — see Known gaps
Nearby-only switchworkingthe app opens no relay connection at all; test/core/nur_nahbereich_test.dart, test/nah/schalter_test.dart, test/nah/schalter_echt_test.dart
Attachmentsworkingseparate encrypted blob store, never through the relay
Update over an existing installworking1.5.0+11 → 1.5.1+12, identity and contacts survived
Recovery on a new deviceworking12 BIP39 words restore identity and contacts, not message history
Desktop clientbuilds, cannot be linkedWindows binary runs; it cannot join an existing identity — see Known gaps
Independent security auditnone

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

PlatformStateNearby over BLEPush wake-upLock factors offered
Android 9+released, signed APKAndroid 12+UnifiedPushapp password, fingerprint, device PIN, hardware key
Windows x64built, unsigned zipnono — the connection stays open insteadapp password
Linuxproject files present, nothing builtnonoapp password
Webbuilds, unfinishednonoapp 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.


Get it

Android

curl -fsSLO https://bitdm.net/bitdm-1.5.1.apk
sha256sum bitdm-1.5.1.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:

WhatValue
This file (sha256 of the APK)62027fc7cb2aceacd74d7150dcca1ff937861c30cc420fcd35f9055dc20ee72c
Signing key (certificate fingerprint)e325b01c08a1a679b6aac20ac9ae3ee255591b46b37dd92717f97085acc22063
apksigner verify --print-certs bitdm-1.5.1.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. Google Play and F-Droid submissions are prepared, not done.

Windows

secure-messenger/releases/bitdm-windows-1.5.1.zip — 15,419,797 bytes, unpack and run bitdm.exe.

WhatValue
This file (sha256 of the zip)08299da651e3d39e83c8c368727a9043795acd2174f6900be312527ff5187782

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 with the same 12 words does not join your identity, it takes it over.

An Inno Setup script for an installer exists at app/windows/bitdm.iss — it installs into %LOCALAPPDATA% without asking for admin rights and adds no autostart entry. No .exe from it is published yet.


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
  • Metadata at the relay, if you run your own

Not protected

  • Who talks to whom, at a relay you do not control. A relay cannot read content, but it necessarily sees which addresses connect when and how much they send. Two answers exist. Self-hosting, which costs a single command. And 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 your relay host would reveal. Run it yourself.

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.

The Windows output is a directory, not a single file. bitdm.exe is 90 KiB and useless alone: sqlite3mc.dll is the encrypted database, 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" windows\bitdm.iss

Tests:

cd secure-messenger/app && flutter test# 820 tests: app + crypto + nearbycd secure-messenger && py -m pytest server/test_relay.py server/test_blob.py -q # 35 + 25

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.

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 — 67 files, 22,471 lines Dart
main.dart 5,078 of them; the platform switches live here
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/ 73 files, 16,980 lines — 820 tests, all green
app/integration_test/ 1 file, 153 lines — attachment throughput on real hardware
app/android/…/kotlin/ 8 files, 2,574 lines — BLE, keystore, foreground service
(plus 2 test files)
app/windows/ CMake, runner, and bitdm.iss for the installer
app/linux/ app/web/ project files; no published build
app/tool/symbol/ 3 Python scripts that render and distribute the app icon
server/ Python relay + blob store, 4,508 lines including tests
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

  • No independent audit. Nobody outside this project has reviewed the cryptography, the protocol, or the implementation.
  • Nearby, second direction is unproven: the phone only transmits when it has something queued, and the ESP32 rig's contact stays pending because proving it needs libsignal-compatible X3DH on the rig. Blocked on a second Android with BLE 5.0.
  • One identity per device, and that is what limits the desktop. A second install with the same 12 words takes the address over instead of joining it: every install generates a fresh registration ID on purpose, and /register on the relay upserts the key bundle and drops the old one-time prekeys. So the Windows build starts with an empty conversation list, and the phone that had been using that address stops being reachable. Linked devices need the Sesame protocol, which is not built.
  • The web build is not finished. It compiles and the layout holds; it is not a client you should rely on.
  • Linux has no published build. The project files are there, nothing has been produced from them.
  • The Windows binary is unsigned, and the installer from bitdm.iss has not been built or published.
  • No group chats.
  • Google Play and F-Droid submissions are prepared, not done.

License

AGPL-3.0LICENSE.

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

Ende-zu-Ende verschlüsselter Messenger ohne Telefonnummer und ohne Username — die Adresse IST der öffentliche Schlüssel (Signal-Protokoll, AGPL-3.0)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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 v3VersionPlatform

PlatformState
Android 9+released, signed APK
Windows x64built, unsigned zip
Linuxproject files present, nothing built
Webbuilds, unfinished

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.


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

ComponentStateHow that was established
Signal protocol, X3DH + Double Ratchetworkinglibsignal_protocol_dart 0.8.2, the library Signal uses
Messages over the relayworkingend-to-end over the live relay
Nearby transport, no networkproven one direction325 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 directionnot provenneeds a second Android with BLE 5.0 — see Known gaps
Nearby-only switchworkingthe app opens no relay connection at all; test/core/nur_nahbereich_test.dart, test/nah/schalter_test.dart, test/nah/schalter_echt_test.dart
Attachmentsworkingseparate encrypted blob store, never through the relay
Update over an existing installworking1.5.0+11 → 1.5.1+12, identity and contacts survived
Recovery on a new deviceworking12 BIP39 words restore identity and contacts, not message history
Desktop clientbuilds, cannot be linkedWindows binary runs; it cannot join an existing identity — see Known gaps
Independent security auditnone

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

PlatformStateNearby over BLEPush wake-upLock factors offered
Android 9+released, signed APKAndroid 12+UnifiedPushapp password, fingerprint, device PIN, hardware key
Windows x64built, unsigned zipnono — the connection stays open insteadapp password
Linuxproject files present, nothing builtnonoapp password
Webbuilds, unfinishednonoapp 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.


Get it

Android

curl -fsSLO https://bitdm.net/bitdm-1.5.1.apk
sha256sum bitdm-1.5.1.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:

WhatValue
This file (sha256 of the APK)62027fc7cb2aceacd74d7150dcca1ff937861c30cc420fcd35f9055dc20ee72c
Signing key (certificate fingerprint)e325b01c08a1a679b6aac20ac9ae3ee255591b46b37dd92717f97085acc22063
apksigner verify --print-certs bitdm-1.5.1.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. Google Play and F-Droid submissions are prepared, not done.

Windows

secure-messenger/releases/bitdm-windows-1.5.1.zip — 15,419,797 bytes, unpack and run bitdm.exe.

WhatValue
This file (sha256 of the zip)08299da651e3d39e83c8c368727a9043795acd2174f6900be312527ff5187782

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 with the same 12 words does not join your identity, it takes it over.

An Inno Setup script for an installer exists at app/windows/bitdm.iss — it installs into %LOCALAPPDATA% without asking for admin rights and adds no autostart entry. No .exe from it is published yet.


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
  • Metadata at the relay, if you run your own

Not protected

  • Who talks to whom, at a relay you do not control. A relay cannot read content, but it necessarily sees which addresses connect when and how much they send. Two answers exist. Self-hosting, which costs a single command. And 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 your relay host would reveal. Run it yourself.

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.

The Windows output is a directory, not a single file. bitdm.exe is 90 KiB and useless alone: sqlite3mc.dll is the encrypted database, 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" windows\bitdm.iss

Tests:

cd secure-messenger/app && flutter test# 820 tests: app + crypto + nearbycd secure-messenger && py -m pytest server/test_relay.py server/test_blob.py -q # 35 + 25

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.

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 — 67 files, 22,471 lines Dart
main.dart 5,078 of them; the platform switches live here
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/ 73 files, 16,980 lines — 820 tests, all green
app/integration_test/ 1 file, 153 lines — attachment throughput on real hardware
app/android/…/kotlin/ 8 files, 2,574 lines — BLE, keystore, foreground service
(plus 2 test files)
app/windows/ CMake, runner, and bitdm.iss for the installer
app/linux/ app/web/ project files; no published build
app/tool/symbol/ 3 Python scripts that render and distribute the app icon
server/ Python relay + blob store, 4,508 lines including tests
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

  • No independent audit. Nobody outside this project has reviewed the cryptography, the protocol, or the implementation.
  • Nearby, second direction is unproven: the phone only transmits when it has something queued, and the ESP32 rig's contact stays pending because proving it needs libsignal-compatible X3DH on the rig. Blocked on a second Android with BLE 5.0.
  • One identity per device, and that is what limits the desktop. A second install with the same 12 words takes the address over instead of joining it: every install generates a fresh registration ID on purpose, and /register on the relay upserts the key bundle and drops the old one-time prekeys. So the Windows build starts with an empty conversation list, and the phone that had been using that address stops being reachable. Linked devices need the Sesame protocol, which is not built.
  • The web build is not finished. It compiles and the layout holds; it is not a client you should rely on.
  • Linux has no published build. The project files are there, nothing has been produced from them.
  • The Windows binary is unsigned, and the installer from bitdm.iss has not been built or published.
  • No group chats.
  • Google Play and F-Droid submissions are prepared, not done.

License

AGPL-3.0LICENSE.

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

Ende-zu-Ende verschlüsselter Messenger ohne Telefonnummer und ohne Username — die Adresse IST der öffentliche Schlüssel (Signal-Protokoll, AGPL-3.0)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

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 v3VersionPlatform

PlatformState
Android 9+released, signed APK
Windows x64built, unsigned zip
Linuxproject files present, nothing built
Webbuilds, unfinished

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.


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

ComponentStateHow that was established
Signal protocol, X3DH + Double Ratchetworkinglibsignal_protocol_dart 0.8.2, the library Signal uses
Messages over the relayworkingend-to-end over the live relay
Nearby transport, no networkproven one direction325 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 directionnot provenneeds a second Android with BLE 5.0 — see Known gaps
Nearby-only switchworkingthe app opens no relay connection at all; test/core/nur_nahbereich_test.dart, test/nah/schalter_test.dart, test/nah/schalter_echt_test.dart
Attachmentsworkingseparate encrypted blob store, never through the relay
Update over an existing installworking1.5.0+11 → 1.5.1+12, identity and contacts survived
Recovery on a new deviceworking12 BIP39 words restore identity and contacts, not message history
Desktop clientbuilds, cannot be linkedWindows binary runs; it cannot join an existing identity — see Known gaps
Independent security auditnone

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

PlatformStateNearby over BLEPush wake-upLock factors offered
Android 9+released, signed APKAndroid 12+UnifiedPushapp password, fingerprint, device PIN, hardware key
Windows x64built, unsigned zipnono — the connection stays open insteadapp password
Linuxproject files present, nothing builtnonoapp password
Webbuilds, unfinishednonoapp 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.


Get it

Android

curl -fsSLO https://bitdm.net/bitdm-1.5.1.apk
sha256sum bitdm-1.5.1.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:

WhatValue
This file (sha256 of the APK)62027fc7cb2aceacd74d7150dcca1ff937861c30cc420fcd35f9055dc20ee72c
Signing key (certificate fingerprint)e325b01c08a1a679b6aac20ac9ae3ee255591b46b37dd92717f97085acc22063
apksigner verify --print-certs bitdm-1.5.1.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. Google Play and F-Droid submissions are prepared, not done.

Windows

secure-messenger/releases/bitdm-windows-1.5.1.zip — 15,419,797 bytes, unpack and run bitdm.exe.

WhatValue
This file (sha256 of the zip)08299da651e3d39e83c8c368727a9043795acd2174f6900be312527ff5187782

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 with the same 12 words does not join your identity, it takes it over.

An Inno Setup script for an installer exists at app/windows/bitdm.iss — it installs into %LOCALAPPDATA% without asking for admin rights and adds no autostart entry. No .exe from it is published yet.


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
  • Metadata at the relay, if you run your own

Not protected

  • Who talks to whom, at a relay you do not control. A relay cannot read content, but it necessarily sees which addresses connect when and how much they send. Two answers exist. Self-hosting, which costs a single command. And 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 your relay host would reveal. Run it yourself.

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.

The Windows output is a directory, not a single file. bitdm.exe is 90 KiB and useless alone: sqlite3mc.dll is the encrypted database, 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" windows\bitdm.iss

Tests:

cd secure-messenger/app && flutter test# 820 tests: app + crypto + nearbycd secure-messenger && py -m pytest server/test_relay.py server/test_blob.py -q # 35 + 25

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.

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 — 67 files, 22,471 lines Dart
main.dart 5,078 of them; the platform switches live here
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/ 73 files, 16,980 lines — 820 tests, all green
app/integration_test/ 1 file, 153 lines — attachment throughput on real hardware
app/android/…/kotlin/ 8 files, 2,574 lines — BLE, keystore, foreground service
(plus 2 test files)
app/windows/ CMake, runner, and bitdm.iss for the installer
app/linux/ app/web/ project files; no published build
app/tool/symbol/ 3 Python scripts that render and distribute the app icon
server/ Python relay + blob store, 4,508 lines including tests
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

  • No independent audit. Nobody outside this project has reviewed the cryptography, the protocol, or the implementation.
  • Nearby, second direction is unproven: the phone only transmits when it has something queued, and the ESP32 rig's contact stays pending because proving it needs libsignal-compatible X3DH on the rig. Blocked on a second Android with BLE 5.0.
  • One identity per device, and that is what limits the desktop. A second install with the same 12 words takes the address over instead of joining it: every install generates a fresh registration ID on purpose, and /register on the relay upserts the key bundle and drops the old one-time prekeys. So the Windows build starts with an empty conversation list, and the phone that had been using that address stops being reachable. Linked devices need the Sesame protocol, which is not built.
  • The web build is not finished. It compiles and the layout holds; it is not a client you should rely on.
  • Linux has no published build. The project files are there, nothing has been produced from them.
  • The Windows binary is unsigned, and the installer from bitdm.iss has not been built or published.
  • No group chats.
  • Google Play and F-Droid submissions are prepared, not done.

License

AGPL-3.0LICENSE.

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

Ende-zu-Ende verschlüsselter Messenger ohne Telefonnummer und ohne Username — die Adresse IST der öffentliche Schlüssel (Signal-Protokoll, AGPL-3.0)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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 v3VersionPlatform

PlatformState
Android 9+released, signed APK
Windows x64built, unsigned zip
Linuxproject files present, nothing built
Webbuilds, unfinished

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.


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

ComponentStateHow that was established
Signal protocol, X3DH + Double Ratchetworkinglibsignal_protocol_dart 0.8.2, the library Signal uses
Messages over the relayworkingend-to-end over the live relay
Nearby transport, no networkproven one direction325 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 directionnot provenneeds a second Android with BLE 5.0 — see Known gaps
Nearby-only switchworkingthe app opens no relay connection at all; test/core/nur_nahbereich_test.dart, test/nah/schalter_test.dart, test/nah/schalter_echt_test.dart
Attachmentsworkingseparate encrypted blob store, never through the relay
Update over an existing installworking1.5.0+11 → 1.5.1+12, identity and contacts survived
Recovery on a new deviceworking12 BIP39 words restore identity and contacts, not message history
Desktop clientbuilds, cannot be linkedWindows binary runs; it cannot join an existing identity — see Known gaps
Independent security auditnone

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

PlatformStateNearby over BLEPush wake-upLock factors offered
Android 9+released, signed APKAndroid 12+UnifiedPushapp password, fingerprint, device PIN, hardware key
Windows x64built, unsigned zipnono — the connection stays open insteadapp password
Linuxproject files present, nothing builtnonoapp password
Webbuilds, unfinishednonoapp 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.


Get it

Android

curl -fsSLO https://bitdm.net/bitdm-1.5.1.apk
sha256sum bitdm-1.5.1.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:

WhatValue
This file (sha256 of the APK)62027fc7cb2aceacd74d7150dcca1ff937861c30cc420fcd35f9055dc20ee72c
Signing key (certificate fingerprint)e325b01c08a1a679b6aac20ac9ae3ee255591b46b37dd92717f97085acc22063
apksigner verify --print-certs bitdm-1.5.1.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. Google Play and F-Droid submissions are prepared, not done.

Windows

secure-messenger/releases/bitdm-windows-1.5.1.zip — 15,419,797 bytes, unpack and run bitdm.exe.

WhatValue
This file (sha256 of the zip)08299da651e3d39e83c8c368727a9043795acd2174f6900be312527ff5187782

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 with the same 12 words does not join your identity, it takes it over.

An Inno Setup script for an installer exists at app/windows/bitdm.iss — it installs into %LOCALAPPDATA% without asking for admin rights and adds no autostart entry. No .exe from it is published yet.


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
  • Metadata at the relay, if you run your own

Not protected

  • Who talks to whom, at a relay you do not control. A relay cannot read content, but it necessarily sees which addresses connect when and how much they send. Two answers exist. Self-hosting, which costs a single command. And 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 your relay host would reveal. Run it yourself.

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.

The Windows output is a directory, not a single file. bitdm.exe is 90 KiB and useless alone: sqlite3mc.dll is the encrypted database, 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" windows\bitdm.iss

Tests:

cd secure-messenger/app && flutter test# 820 tests: app + crypto + nearbycd secure-messenger && py -m pytest server/test_relay.py server/test_blob.py -q # 35 + 25

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.

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 — 67 files, 22,471 lines Dart
main.dart 5,078 of them; the platform switches live here
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/ 73 files, 16,980 lines — 820 tests, all green
app/integration_test/ 1 file, 153 lines — attachment throughput on real hardware
app/android/…/kotlin/ 8 files, 2,574 lines — BLE, keystore, foreground service
(plus 2 test files)
app/windows/ CMake, runner, and bitdm.iss for the installer
app/linux/ app/web/ project files; no published build
app/tool/symbol/ 3 Python scripts that render and distribute the app icon
server/ Python relay + blob store, 4,508 lines including tests
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

  • No independent audit. Nobody outside this project has reviewed the cryptography, the protocol, or the implementation.
  • Nearby, second direction is unproven: the phone only transmits when it has something queued, and the ESP32 rig's contact stays pending because proving it needs libsignal-compatible X3DH on the rig. Blocked on a second Android with BLE 5.0.
  • One identity per device, and that is what limits the desktop. A second install with the same 12 words takes the address over instead of joining it: every install generates a fresh registration ID on purpose, and /register on the relay upserts the key bundle and drops the old one-time prekeys. So the Windows build starts with an empty conversation list, and the phone that had been using that address stops being reachable. Linked devices need the Sesame protocol, which is not built.
  • The web build is not finished. It compiles and the layout holds; it is not a client you should rely on.
  • Linux has no published build. The project files are there, nothing has been produced from them.
  • The Windows binary is unsigned, and the installer from bitdm.iss has not been built or published.
  • No group chats.
  • Google Play and F-Droid submissions are prepared, not done.

License

AGPL-3.0LICENSE.

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

Ende-zu-Ende verschlüsselter Messenger ohne Telefonnummer und ohne Username — die Adresse IST der öffentliche Schlüssel (Signal-Protokoll, AGPL-3.0)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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 v3VersionPlatform

PlatformState
Android 9+released, signed APK
Windows x64built, unsigned zip
Linuxproject files present, nothing built
Webbuilds, unfinished

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.


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

ComponentStateHow that was established
Signal protocol, X3DH + Double Ratchetworkinglibsignal_protocol_dart 0.8.2, the library Signal uses
Messages over the relayworkingend-to-end over the live relay
Nearby transport, no networkproven one direction325 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 directionnot provenneeds a second Android with BLE 5.0 — see Known gaps
Nearby-only switchworkingthe app opens no relay connection at all; test/core/nur_nahbereich_test.dart, test/nah/schalter_test.dart, test/nah/schalter_echt_test.dart
Attachmentsworkingseparate encrypted blob store, never through the relay
Update over an existing installworking1.5.0+11 → 1.5.1+12, identity and contacts survived
Recovery on a new deviceworking12 BIP39 words restore identity and contacts, not message history
Desktop clientbuilds, cannot be linkedWindows binary runs; it cannot join an existing identity — see Known gaps
Independent security auditnone

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

PlatformStateNearby over BLEPush wake-upLock factors offered
Android 9+released, signed APKAndroid 12+UnifiedPushapp password, fingerprint, device PIN, hardware key
Windows x64built, unsigned zipnono — the connection stays open insteadapp password
Linuxproject files present, nothing builtnonoapp password
Webbuilds, unfinishednonoapp 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.


Get it

Android

curl -fsSLO https://bitdm.net/bitdm-1.5.1.apk
sha256sum bitdm-1.5.1.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:

WhatValue
This file (sha256 of the APK)62027fc7cb2aceacd74d7150dcca1ff937861c30cc420fcd35f9055dc20ee72c
Signing key (certificate fingerprint)e325b01c08a1a679b6aac20ac9ae3ee255591b46b37dd92717f97085acc22063
apksigner verify --print-certs bitdm-1.5.1.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. Google Play and F-Droid submissions are prepared, not done.

Windows

secure-messenger/releases/bitdm-windows-1.5.1.zip — 15,419,797 bytes, unpack and run bitdm.exe.

WhatValue
This file (sha256 of the zip)08299da651e3d39e83c8c368727a9043795acd2174f6900be312527ff5187782

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 with the same 12 words does not join your identity, it takes it over.

An Inno Setup script for an installer exists at app/windows/bitdm.iss — it installs into %LOCALAPPDATA% without asking for admin rights and adds no autostart entry. No .exe from it is published yet.


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
  • Metadata at the relay, if you run your own

Not protected

  • Who talks to whom, at a relay you do not control. A relay cannot read content, but it necessarily sees which addresses connect when and how much they send. Two answers exist. Self-hosting, which costs a single command. And 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 your relay host would reveal. Run it yourself.

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.

The Windows output is a directory, not a single file. bitdm.exe is 90 KiB and useless alone: sqlite3mc.dll is the encrypted database, 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" windows\bitdm.iss

Tests:

cd secure-messenger/app && flutter test# 820 tests: app + crypto + nearbycd secure-messenger && py -m pytest server/test_relay.py server/test_blob.py -q # 35 + 25

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.

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 — 67 files, 22,471 lines Dart
main.dart 5,078 of them; the platform switches live here
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/ 73 files, 16,980 lines — 820 tests, all green
app/integration_test/ 1 file, 153 lines — attachment throughput on real hardware
app/android/…/kotlin/ 8 files, 2,574 lines — BLE, keystore, foreground service
(plus 2 test files)
app/windows/ CMake, runner, and bitdm.iss for the installer
app/linux/ app/web/ project files; no published build
app/tool/symbol/ 3 Python scripts that render and distribute the app icon
server/ Python relay + blob store, 4,508 lines including tests
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

  • No independent audit. Nobody outside this project has reviewed the cryptography, the protocol, or the implementation.
  • Nearby, second direction is unproven: the phone only transmits when it has something queued, and the ESP32 rig's contact stays pending because proving it needs libsignal-compatible X3DH on the rig. Blocked on a second Android with BLE 5.0.
  • One identity per device, and that is what limits the desktop. A second install with the same 12 words takes the address over instead of joining it: every install generates a fresh registration ID on purpose, and /register on the relay upserts the key bundle and drops the old one-time prekeys. So the Windows build starts with an empty conversation list, and the phone that had been using that address stops being reachable. Linked devices need the Sesame protocol, which is not built.
  • The web build is not finished. It compiles and the layout holds; it is not a client you should rely on.
  • Linux has no published build. The project files are there, nothing has been produced from them.
  • The Windows binary is unsigned, and the installer from bitdm.iss has not been built or published.
  • No group chats.
  • Google Play and F-Droid submissions are prepared, not done.

License

AGPL-3.0LICENSE.

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

Ende-zu-Ende verschlüsselter Messenger ohne Telefonnummer und ohne Username — die Adresse IST der öffentliche Schlüssel (Signal-Protokoll, AGPL-3.0)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

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 v3VersionPlatform

PlatformState
Android 9+released, signed APK
Windows x64built, unsigned zip
Linuxproject files present, nothing built
Webbuilds, unfinished

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.


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

ComponentStateHow that was established
Signal protocol, X3DH + Double Ratchetworkinglibsignal_protocol_dart 0.8.2, the library Signal uses
Messages over the relayworkingend-to-end over the live relay
Nearby transport, no networkproven one direction325 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 directionnot provenneeds a second Android with BLE 5.0 — see Known gaps
Nearby-only switchworkingthe app opens no relay connection at all; test/core/nur_nahbereich_test.dart, test/nah/schalter_test.dart, test/nah/schalter_echt_test.dart
Attachmentsworkingseparate encrypted blob store, never through the relay
Update over an existing installworking1.5.0+11 → 1.5.1+12, identity and contacts survived
Recovery on a new deviceworking12 BIP39 words restore identity and contacts, not message history
Desktop clientbuilds, cannot be linkedWindows binary runs; it cannot join an existing identity — see Known gaps
Independent security auditnone

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

PlatformStateNearby over BLEPush wake-upLock factors offered
Android 9+released, signed APKAndroid 12+UnifiedPushapp password, fingerprint, device PIN, hardware key
Windows x64built, unsigned zipnono — the connection stays open insteadapp password
Linuxproject files present, nothing builtnonoapp password
Webbuilds, unfinishednonoapp 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.


Get it

Android

curl -fsSLO https://bitdm.net/bitdm-1.5.1.apk
sha256sum bitdm-1.5.1.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:

WhatValue
This file (sha256 of the APK)62027fc7cb2aceacd74d7150dcca1ff937861c30cc420fcd35f9055dc20ee72c
Signing key (certificate fingerprint)e325b01c08a1a679b6aac20ac9ae3ee255591b46b37dd92717f97085acc22063
apksigner verify --print-certs bitdm-1.5.1.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. Google Play and F-Droid submissions are prepared, not done.

Windows

secure-messenger/releases/bitdm-windows-1.5.1.zip — 15,419,797 bytes, unpack and run bitdm.exe.

WhatValue
This file (sha256 of the zip)08299da651e3d39e83c8c368727a9043795acd2174f6900be312527ff5187782

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 with the same 12 words does not join your identity, it takes it over.

An Inno Setup script for an installer exists at app/windows/bitdm.iss — it installs into %LOCALAPPDATA% without asking for admin rights and adds no autostart entry. No .exe from it is published yet.


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
  • Metadata at the relay, if you run your own

Not protected

  • Who talks to whom, at a relay you do not control. A relay cannot read content, but it necessarily sees which addresses connect when and how much they send. Two answers exist. Self-hosting, which costs a single command. And 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 your relay host would reveal. Run it yourself.

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.

The Windows output is a directory, not a single file. bitdm.exe is 90 KiB and useless alone: sqlite3mc.dll is the encrypted database, 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" windows\bitdm.iss

Tests:

cd secure-messenger/app && flutter test# 820 tests: app + crypto + nearbycd secure-messenger && py -m pytest server/test_relay.py server/test_blob.py -q # 35 + 25

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.

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 — 67 files, 22,471 lines Dart
main.dart 5,078 of them; the platform switches live here
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/ 73 files, 16,980 lines — 820 tests, all green
app/integration_test/ 1 file, 153 lines — attachment throughput on real hardware
app/android/…/kotlin/ 8 files, 2,574 lines — BLE, keystore, foreground service
(plus 2 test files)
app/windows/ CMake, runner, and bitdm.iss for the installer
app/linux/ app/web/ project files; no published build
app/tool/symbol/ 3 Python scripts that render and distribute the app icon
server/ Python relay + blob store, 4,508 lines including tests
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

  • No independent audit. Nobody outside this project has reviewed the cryptography, the protocol, or the implementation.
  • Nearby, second direction is unproven: the phone only transmits when it has something queued, and the ESP32 rig's contact stays pending because proving it needs libsignal-compatible X3DH on the rig. Blocked on a second Android with BLE 5.0.
  • One identity per device, and that is what limits the desktop. A second install with the same 12 words takes the address over instead of joining it: every install generates a fresh registration ID on purpose, and /register on the relay upserts the key bundle and drops the old one-time prekeys. So the Windows build starts with an empty conversation list, and the phone that had been using that address stops being reachable. Linked devices need the Sesame protocol, which is not built.
  • The web build is not finished. It compiles and the layout holds; it is not a client you should rely on.
  • Linux has no published build. The project files are there, nothing has been produced from them.
  • The Windows binary is unsigned, and the installer from bitdm.iss has not been built or published.
  • No group chats.
  • Google Play and F-Droid submissions are prepared, not done.

License

AGPL-3.0LICENSE.

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

Ende-zu-Ende verschlüsselter Messenger ohne Telefonnummer und ohne Username — die Adresse IST der öffentliche Schlüssel (Signal-Protokoll, AGPL-3.0)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages