A WhatsApp-style Flutter app with:
- End-to-end encryption per contact, via the Signal Protocol (X3DH + Double Ratchet)
- Encrypted voice/video calls via WebRTC (DTLS-SRTP)
- Screenshot & screen-recording protection (full block on Android, detect+react on iOS)
- Firebase for auth/signaling only — your server never sees plaintext or private keys
lib/
main.dart — app entry: global screenshot protection, crypto init on cold start
services/
crypto_service.dart — Signal Protocol: identity, prekeys, sessions, encrypt/decrypt
firestore_service.dart — all server I/O: prekey bundles, contacts, ciphertext relay, call signaling
security_service.dart — screenshot/recording block + detection
call_service.dart — WebRTC peer connection setup for calls
auth_service.dart — firebase_auth phone sign-in; bootstraps crypto on first sign-in
local_store.dart — SQLCipher-encrypted local chat history
models/models.dart — Contact, ChatMessage
screens/
login_screen.dart — phone number + SMS code
contacts_screen.dart — live contact list, incoming-call listener
add_friend_screen.dart — where Signal sessions get established
chat_screen.dart — encrypt on send, decrypt on receive, persists locally
call_screen.dart — WebRTC UI, handles both caller and callee signaling
android/app/src/main/AndroidManifest.xml — permissions
ios/Runner/Info-additions.plist.md — permission strings to merge in
This isn't just a UI restriction — it's structural:
- Each device generates a Signal identity key pair + prekeys (
CryptoService.init, called on sign-in and on every cold start viaAuthGateinmain.dart). - Public prekey bundles get published to Firestore (
AuthService._onSignedIn). - Adding a friend (
add_friend_screen.dart) fetches their bundle fromFirestoreService.fetchPrekeyBundleand runs the X3DH handshake, creating a session in the local Signal store. encryptMessage()/decryptMessage()require an existing session — there's no code path to encrypt or decrypt for someone you haven't added, because the session simply doesn't exist. Symmetrically, when a friend's first message arrives as aPreKeySignalMessage, decrypting it establishes your side of the session automatically (this is inherent to X3DH — no separate "add back" step is required to reply).
- Install Flutter (3.19+) and run
flutter create .in this directory once, to generate the platform boilerplate this repo doesn't include (ios/Runner/Info.plist,android/build.gradle, etc.) — merge, don't overwrite, since this repo'slib/,pubspec.yaml, andandroid/app/.../AndroidManifest.xmlare already written. - Firebase: create a Firebase project, enable Phone sign-in under
Authentication and Cloud Firestore, then run
flutterfire configureto generatelib/firebase_options.dart. Uncomment theFirebase.initializeAppline inmain.dart. - Firestore security rules: lock these down before shipping — at minimum,
a user should only be able to write their own
prekey_bundles/{uid}andusers/{uid}/contacts/*, and only append (not edit/delete) messages in achats/{chatId}they're a participant of. The scaffold's rules are permissive by default (whatever your Firebase project ships with) — don't run this in production without writing real rules. - iOS permissions: merge the keys from
ios/Runner/Info-additions.plist.mdintoios/Runner/Info.plist. flutter pub get- TURN server: add your TURN credentials in
call_service.dart(see caveat below) before testing calls across real networks. flutter run— test with two devices/accounts to exercise add-friend, messaging, and calling end-to-end.
| Piece | Status |
|---|---|
| Signal Protocol key generation, session building, encrypt/decrypt | Real, functional logic |
| Firestore signaling: prekey bundles, contacts, message relay, call offer/answer/ICE | Wired up (firestore_service.dart) |
| Phone auth (firebase_auth) | Wired up (auth_service.dart, login_screen.dart) |
| Encrypted text chat end-to-end (send → Firestore → decrypt → persist) | Wired up (chat_screen.dart) |
| Local encrypted message storage (SQLCipher) | Wired up (local_store.dart) |
| WebRTC offer/answer/ICE flow, both caller and callee paths | Wired up (call_screen.dart) |
| Incoming call ringing while not in an open chat | Wired up (contacts_screen.dart) |
| Screenshot/recording protection | Real plugin wiring |
| Firestore security rules | Not written — see setup step 3 |
| Screenshot notice sent to the peer (not just shown locally) | Not implemented — TODO in chat_screen.dart |
| Removing/blocking a contact, deleting message history | Not implemented |
| Push notifications for messages/calls while the app is backgrounded | Not implemented |
| Multi-device (same account on phone + desktop) | Not implemented — see caveat below |
- TURN server: STUN alone (in
call_service.dart) won't work for every network. You'll want a TURN server (e.g. coturn, self-hosted or a paid service) for reliable connections behind restrictive NATs — uncomment and fill in theturn:entry in_iceServers. - iOS screenshots can't be blocked, only detected — see the note in
Info-additions.plist.md. - One-time prekey reuse: this scaffold publishes a single prekey
(
preKeyId: 1) and never rotates it. In production, publish a batch of one-time prekeys and remove each one from the store after a friend consumes it — otherwise multiple people establishing sessions with you concurrently could reuse prekey material, weakening forward secrecy for that first message. - Mutual contact add:
FirestoreService.addContactadds the contact to both users' lists at once, so the person you add doesn't need a separate "accept" step to see the chat. A production app would probably want an explicit friend-request/accept flow instead. - Multi-device support (using the same account on phone + desktop)
adds real complexity to the Signal session model — this scaffold
assumes one device per user for simplicity (
deviceId: 1throughout). - This is a scaffold, not a security audit. Before handling real user data, get the crypto implementation and Firestore security rules reviewed by someone with Signal Protocol / Firebase security experience.