跳到正文

ni5arga

protestchat

Encrypted messaging with no internet, no cell tower, and no server. Phones relay for each other over Bluetooth mesh, so it keeps working during internet shutdowns and inside cellular jammers

README 已保存到本站,可直接阅读

Documentation snapshot

README 快照

本页保存的是公开项目资料快照,阅读过程不需要连接 GitHub。

protestchat

connected to a nearby phone — no internet, no tower, no server required.

Off-grid messaging for internet shutdowns and jammed protests. Phones talk directly to each other over Bluetooth — no cell tower, no ISP, no server, no account.

Built after Delhi Police deployed portable cellular jammers at Jantar Mantar on 20 July 2026 alongside an unpublished mobile internet restriction. Those jammers target 3G/4G/5G only. 2.4 GHz is untouched, so two phones standing next to each other were still perfectly capable of talking — there was just no user-friendly & acessible app to do it.

How it works

┌──────────────── shared TypeScript (~85%) ────────────────┐
│  UI · SQLite · sealing · dedup · relay · expiry          │
├──────────────────────────┬───────────────────────────────┤
│  Swift (iOS)             │  Kotlin (Android)             │
│  CoreBluetooth           │  BLE GATT                     │
└──────────────────────────┴───────────────────────────────┘
              BLE / Wi-Fi Direct — no tower, no ISP

The native layer is a dumb byte pipe: advertise, discover, connect, send bytes, receive bytes. It contains no chat logic, no storage, and no crypto, so there is exactly one implementation of the security-critical code to audit rather than three that drift apart.

Routing is epidemic, not addressed. There are no routing tables, because a routing table is a map of who talks to whom. Every phone carries every unexpired envelope it has seen and offers it to every peer it meets; the recipient is simply whoever can decrypt it. This costs battery and bandwidth and buys the property that a captured phone reveals nothing about who was talking to whom.

It also means a phone is a courier. Someone who walks out of a jammed zone carries queued messages with them and delivers them on the other side.

Four ways to send

Who reads itUse it for
Everyone nearbyAnyone in range, including police running this appCrowd warnings: “exit blocked at gate 4”
ChannelAnyone with the passphraseAn affinity group that needs to grow by word of mouth
GroupOnly the people you addedYour actual crew
DirectOne personEverything sensitive

Channels have no owner, no admin, no kick — a channel is a passphrase and nothing else. That is deliberate: BitChat’s channel commands were validated only by the issuing client, so any member could seize a channel or strip its encryption. A construct with no privileged operations has none to forge. The cost is that a leaked passphrase ends the channel; start a new one.

Groups are fan-out — one separately sealed copy per member, no shared group key, so there is no rekeying problem and no group cryptography to get wrong. Capped at 15 people because each message costs N transmissions over a radio Google documents as low-bandwidth.

Layout

src/lib/
  crypto-core.ts    sealing + channel keys — pure, no RN imports, fully tested
  crypto.ts         keystore wrapper around crypto-core
  protocol.ts       wire format, padding, expiry
  db.ts             SQLite: messages, contacts, channels, groups, envelope cache
  store.ts          MeshStore interface — lets the engine run against memory in tests
  mesh.ts           the engine — sealing, dedup, store-and-forward, relay
  transport.ts      radio abstraction (our BLE GATT today, LoRa/gateway later)
  conversation.ts   derives the mode and its warning, in ONE place
  app-state.tsx     React bindings
src/app/            home, chat/[id], add, verify/[id], join-channel, new-group, settings
modules/nearby-mesh/    the Swift + Kotlin native module
scripts/doctor.sh       checks your build toolchain and names the fix for anything missing
docs/THREAT-MODEL.md

mesh.ts takes its transport and store by injection, so the whole engine — relaying, dedup, hop limits, channels, fan-out — is exercised by npm test with no radio and no phone.

Build it

Expo Go will not work — it cannot load custom native Bluetooth code. You need a development build, which means Xcode and/or Android Studio.

npm install
npm run doctor      # checks the toolchain, names the fix for anything missing

npm test            # 72 tests — crypto, wire format, mesh logic. No device needed.
npm run typecheck

npm run android     # Android phone over USB
npm run ios         # iPhone over USB

npm run doctor exists because the two usual blockers — xcode-select pointing at CommandLineTools, and Android Studio never having downloaded its SDK — both fail with errors that do not name the fix.

The first native build takes several minutes. After that JavaScript hot-reloads; Swift or Kotlin changes need a rebuild.

Platform status

State
AndroidBuilds. Kotlin module compiles against play-services-nearby.
iOSBlocked. See below.

Why we do not use Google Nearby Connections

The obvious choice for cross-platform device-to-device is Google’s Nearby Connections. We built on it first and then removed it, for two independent reasons:

  1. On iOS it only ever brings up the Wi-Fi LAN medium — both phones must already be joined to the same Wi-Fi network. In a jammed square with no infrastructure that is not a degraded path, it is no path at all. Google lists iOS BLE as “in development.”
  2. It ships for iOS via Swift Package Manager only, with no CocoaPods support — an open request since May 2023, filed precisely because React Native and Flutter plugin systems depend on CocoaPods.

So even solving the packaging would have produced an iPhone that cannot reach an Android phone at a protest. We now own the radio outright in modules/ble-mesh/: CoreBluetooth on iOS, BLE GATT on Android, no third-party dependency on either side.

The migration is an upgrade rather than a workaround. Nearby gave us no control over the advertising identifier, and a stable BLE identifier is a tracking beacon — open problem #2 in the threat model. Owning the advertisement is the only way to rotate it, which we now do every 15 minutes from fresh CSPRNG bytes, with no name or key material advertised at all.

Permissions

prebuild pulls RECORD_AUDIO, SYSTEM_ALERT_WINDOW and legacy storage permissions in from Expo defaults. A protest app requesting microphone access is bad optics and real attack surface, so app.json blocks them via android.blockedPermissions. Re-check after any prebuild — run npm run doctor and inspect android/app/src/main/AndroidManifest.xml for tools:node="remove".

INTERNET is still requested because Metro needs it in development. A release build should drop it: an app that physically cannot open a socket is a much stronger claim than one that promises not to.

Test it — the milestone that matters

What is automated

LayerAutomatedHow
Crypto, channel keys, wire formatyesnpm test
Mesh logic — relay, dedup, hop limits, fan-out, store-and-forwardyesnpm test, engines wired through a fake transport
Kotlin compilesyesnpm run android (or cd android && ./gradlew :app:assembleDebug)
Swift compilesblockedsee the iOS gap above
App installs and launchesyesnpm run android / npm run ios
The radio actually finding a peernotwo physical phones, by hand

The mesh cannot be tested in a simulator or emulator. Neither has a real Bluetooth radio — the transport will initialise and then discover nothing, forever. There is no emulator trick for this. Everything below needs two phones in the same room.

The manual part

The whole point is one thing working, so test exactly that first.

Setup. Install the build on two phones (one iPhone and one Android is the interesting case). Grant the Bluetooth permissions on Android (and Location on Android 11 and below); Bluetooth on iOS. Keep the app in the foreground on both — background BLE is an unsolved problem, see the threat model.

1. Cut the network for real.

Airplane mode on both phones, then manually turn Bluetooth back on. Wi-Fi can stay off entirely — this transport is BLE only. Confirm you genuinely have no connectivity: open a browser, load anything, watch it fail.

2. Introduce the two phones.

  • Phone A: Add a person → My code
  • Phone B: Add a person → Scan theirs, point it at A’s screen
  • Repeat in the other direction so each has the other

Do this standing next to each other. This in-person step is the entire trust model — there is no server to vouch for anyone.

3. Verify.

Open the chat, tap the amber Not verified yet strip, and check both phones show the same 15 digits. Mark verified. If they differ, someone is in the middle.

4. Send.

The banner at the top of the home screen should read Connected to 1 phone. Send a message. It should arrive in a second or two, with no internet on either device.

5. Now test the parts that actually matter.

TestExpected
Walk out of range, send, walk backShows “Waiting for someone in range”, then delivers on reconnect
Force-quit and reopenHistory intact, radio comes back up on its own
Send while the other phone is fully offQueues; delivers when it returns
Three phones, A and C never in range of each otherB relays. This is the mesh working.
Both phones join channel gate4 with the same passphraseMessages appear for both, with sender names
One phone joins gate4 with the wrong passphraseSees nothing. It relays the traffic but cannot read it
Send in “Everyone nearby”Every phone with the app sees it, contact or not
Make a group of 3, send onceAll members receive it; non-members see nothing
Leave a phone idle 6 hoursEnvelopes AND decrypted messages expire and disappear
Settings → panic wipeEverything gone, new identity, radio back up

Row 4 is the real proof — it is the difference between a mesh and a Bluetooth demo. Rows 5–8 cover the modes added after the first build.

The channel test worth doing deliberately: join gate4 on two phones with the same passphrase and on a third with the wrong one. The third should see nothing while still relaying the traffic — that demonstrates confidentiality and relaying are genuinely independent, which is the core claim of the design.

What is not built yet

  • Background relaying (iOS suspends BLE aggressively — the biggest open problem)
  • Double Ratchet / post-compromise security (per-message FS via OTKs is in for direct/group — see docs/FORWARD-SECRECY.md)
  • Images
  • Argon2id for channel passphrases (currently scrypt N=2^14, see the threat model)
  • Any passphrase strength enforcement
  • Synchronised group membership — your member list is local only
  • Sybil / flood resistance
  • Duress PIN and decoy mode
  • Reproducible builds
  • Any independent security audit

Contributing

Most useful right now, in order:

  1. Applied cryptographers — review the OTK/SPK FS construction (docs/FORWARD-SECRECY.md) and Double Ratchet feasibility under store-and-forward
  2. Anyone who has shipped cross-platform BLE GATT — especially iOS peripheral role and Android OEM quirks
  3. Anyone who has been in a shutdown or jammed protest — what would you actually have used?
  4. Anyone who wants to co-write the threat model

Security findings get published with credit. We would much rather hear it from you than read it in a paper written after people relied on this — which is exactly what happened to Bridgefy, which protesters in Hong Kong and during the CAA protests used while it was comprehensively broken.

Licence

MIT. Use it, fork it, ship it, build it into something better — no permission needed and no strings attached.

The point is that this gets into as many hands as possible. Anyone should be able to take this, rename it, and hand it to people in a shutdown without asking anyone.

One consequence worth stating plainly, since this is security software: MIT permits closed-source forks, so a modified build could ship without its source. Only trust a build you compiled yourself, or one from a source you have reason to trust. That is true of any app, and reproducible builds — still on the open-problems list — are what would let you verify it rather than take it on faith.

Official distribution

获取与安装

暂未发现可确认的官方软件包地址

当前 README 快照没有出现 npm、PyPI、Crates.io、pub.dev 等官方包页链接。本站不会根据仓库名称猜测下载地址。

本站不托管项目文件;需要安装时,请以项目维护者发布的官方文档为准。

使用前核验

本站保存公开资料用于阅读,不代表安全审计或功能背书。安装前请核对许可证、依赖来源和发布签名,不要直接运行来源不明的二进制文件或高权限脚本。