Codex raised three P1s on the first revision. I checked each against the code before touching anything; all three are real, and the first two are the same mistake in two costumes — one predicate asked two different questions. 1. An announced suffix is not a UI decoration. `sealApplies` stripped a trailing `#abcd` from whatever it was given, including the LIVE announced nickname. A peer announces whatever string it likes, and `#` plus four hex is a legal thing to announce — this app's own `splitSuffix` exists because announced names carry them. So the single predicate was wrong in both directions at once: a key pinned as `medic` could rename to `medic#cafe` and keep its seal, and a key honestly verified as `medic#cafe` lost its seal without renaming at all. There are now two functions, `sealAppliesToAnnounced` and `sealAppliesToRendered`, because there are two questions: what a peer claims, and what a row shows after the list may have decorated it. The rendered one tries the raw name before undecorating, so a name that genuinely ends in a suffix still matches itself. The iOS patch has exactly this split and I collapsed it here. 2. The check was not reactive. `PeopleSection` caches `isPeerVerified` with `remember(verifiedFingerprints, peerFingerprints, connectedPeers)`, and the per-peer sheet with `remember(peerID, verifiedFingerprints)`. My gate reads the announced nickname inside those lambdas, and a rename changes none of those keys — so the NAME repainted while the cached `true`, and the seal, survived until something unrelated invalidated the cache. That is the connected peer still showing a seal after renaming: the live impersonation, and the single case this patch exists to stop. `peerNicknames` is now a key at both sites. 3. The watch was never patched at all. There is a `:wear` module in settings.gradle.kts with its own `WearPeerIdentityState`, its own verify button and its own seal, and it calls `setVerifiedFingerprint` without a nickname while deriving `isVerified` from the fingerprint set alone. Every peer verified on a watch failed open for ever. I missed it because I audited `app/` and assumed that was the app — I never read settings.gradle.kts. It now pins the announced name on verify and applies the binding in `snapshot`. 15 binding cases, up from 13. Mutation-checked: put the stripping back into the announced check and the new case fails. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bitchat for Android
A decentralized peer-to-peer messaging app with dual transport architecture: local Bluetooth mesh networks for offline communication and internet-based Nostr protocol for global reach. No accounts, no phone numbers, no central servers.
This is the Android implementation of bitchat, fully protocol-compatible with the iOS version for cross-platform mesh communication.
See it in action
| Offline mesh conversation | Geohash globe picker |
|---|---|
![]() |
![]() |
License
This project is released into the public domain. See the LICENSE file for details.
Features
- Dual Transport Architecture: Bluetooth LE mesh for offline messaging, Nostr relays for internet-based messaging
- Location-Based Channels: Geographic chat rooms using geohash coordinates over Nostr relays
- Intelligent Message Routing: Automatically chooses the best transport, with queuing and retry when a peer is unreachable
- End-to-End Encryption: Noise Protocol (XX pattern, X25519 + ChaCha20-Poly1305) for private messages over the mesh
- Decentralized Mesh Network: Automatic peer discovery and multi-hop relay over Bluetooth LE (max 7 hops)
- Wi-Fi Aware Transport: Higher-bandwidth local mesh on supported devices
- Channel Chats: Topic-based group messaging with optional password protection (Argon2id + AES-256-GCM)
- IRC-Style Commands: Familiar
/join,/msg,/whostyle interface - Tor Support: Built-in Tor (Arti) for private internet connectivity
- Emergency Wipe: Triple-tap to instantly clear all data
- Cross-Platform: Binary protocol compatible with bitchat on iOS and macOS
Technical Architecture
Bluetooth Mesh Network (Offline)
- Direct peer-to-peer within Bluetooth range, multi-hop relay through nearby devices
- Noise Protocol sessions with forward secrecy; peer identities derived from static keys
- Compact binary packet format with fragmentation, TTL routing, and deduplication
- Adaptive duty cycling and connection limits for battery efficiency
- Foreground service keeps the mesh alive within Android background execution limits
Nostr Protocol (Internet)
- Global reach via public relays, geohash-based location channels
- Private messages fall back to Nostr for mutual favorites when the mesh is unavailable
- Ephemeral keys per geohash area
Android Stack
- Kotlin, Jetpack Compose (Material 3), MVVM
- Coroutines and Flow for all networking and state
- Core components:
MeshForegroundService(persistent connectivity),BluetoothMeshService/WifiAwareMeshService(transports),UnifiedMeshService(transport selection),NoiseSessionManager(encryption sessions),MessageRouter(mesh/Nostr routing with outbox retry)
Building
Requires Android Studio and the Android SDK (API 26+).
git clone https://github.com/permissionlesstech/bitchat-android.git
cd bitchat-android
./gradlew assembleDebug
Install on a connected device:
adb install -r app/build/outputs/apk/debug/app-debug.apk
The app requests Bluetooth, location (required for BLE scanning), and notification permissions at runtime.
Release APKs and the Android App Bundle can be rebuilt byte-for-byte in the pinned Linux container. Maintainers should follow the Android release guide. See Reproducible builds for the build trust model and public GitHub/Google Play verification procedures.
Testing
# Unit tests
./gradlew test
# Lint
./gradlew lint
# Instrumented tests (requires a device or emulator)
./gradlew connectedAndroidTest
Note that BLE mesh behavior is difficult to emulate; protocol and session logic is covered by unit tests, while radio-level behavior needs real devices.


