Decentralized. Encrypted. Ephemeral.
A serverless, peer-to-peer encrypted messaging app for Android — built on Yggdrasil mesh networking and the Double Ratchet protocol.
Ember is an Android messaging application with no servers, no accounts, and no metadata. Messages travel directly between devices over the Yggdrasil mesh network, encrypted end-to-end using the Double Ratchet algorithm — the same cryptographic protocol that powers Signal.
Every message is encrypted before it leaves your device. No central authority can read, store, or intercept your conversations.
- Double Ratchet Protocol — P-256 ECDH key agreement with HKDF-SHA256 key derivation. Every single message is encrypted with a unique key, derived fresh via symmetric and DH ratcheting
- AES-256-GCM — Authenticated encryption with 256-bit keys, 12-byte random nonces, and 16-byte authentication tags
- Perfect Forward Secrecy — Compromise of current keys reveals nothing about past messages
- Future Secrecy — Regular DH ratchet steps mean key compromise heals automatically
- Encrypted at Rest — SQLCipher encrypts the entire local database; EncryptedSharedPreferences (backed by Android Keystore) protects identity keys and settings
- Biometric Lock — Optional fingerprint authentication on app resume
- Screen Security — Optional
FLAG_SECUREprevents screenshots and screen recording
- Yggdrasil P2P — Pure peer-to-peer over IPv6 mesh networking. No central relay, no STUN servers, no infrastructure to subpoena
- Direct TCP — Messages sent over raw TCP sockets with buffered I/O (Okio)
- Automatic Retry — Exponential backoff up to 5 attempts when peers are temporarily unreachable
- Foreground Service — Continuous background listener so messages are never missed
- Connection Pooling — Active peer connections reused when possible
- Ephemeral Messages (TTL) — Every message can have a time-to-live: 5 min, 10 min, 30 min, 1 hour, or 1 day. Expired messages are purged automatically by WorkManager
- Delivery Status — Sent (✓), Delivered (✓✓), and Read (✓✓ highlighted) indicators
- No Message Previews — Conversation list never exposes message content
- Real-Time Receive — Messages appear instantly when app is open
- QR Code Pairing — Scan a contact's QR to exchange display name, Yggdrasil address, port, and identity public key in one step
- Manual Fallback — Hex key entry for air-gapped or CLI scenarios
- Contact Verification — Mark contacts as verified after out-of-band fingerprint comparison
- Zero Discovery — No contact syncing, no phone number lookup, no directory
Identity Layer
└─ P-256 ECDH key pair (generated once, stored in EncryptedSharedPreferences)
└─ Public key shared via QR code on contact exchange
Session Initialization
├─ Alice (initiator) — initAlice()
│ ├─ Generates ephemeral DH key pair
│ ├─ ECDH(ephemeral_private, contact_identity_public)
│ └─ Derives (RootKey, ChainKey_send) via HKDF-SHA256
│
└─ Bob (recipient) — initBob()
├─ ECDH(identity_private, alice_ephemeral_public) → (RK₁, ChainKey_recv)
├─ Generates own ephemeral DH pair
├─ ECDH(ephemeral_private, alice_ephemeral_public) → (RK₂, ChainKey_send)
└─ Full ratchet state established
Per-Message Encryption
├─ Symmetric ratchet: HMAC-SHA256 advances chain key, derives message key
├─ DH ratchet: triggered on each new ephemeral key from sender
├─ Message key used once for AES-256-GCM, then discarded
└─ Skipped message keys cached (up to 100) for out-of-order delivery
Wire Format (EmberEnvelope)
└─ JSON over TCP
├─ version, conversationId, sender, timestamp, ttlSeconds
├─ ciphertext (Base64), nonce (Base64)
└─ ratchetHeader: { dhPublicKey (hex), pn, n }
| Layer | Technology |
|---|---|
| Language | Kotlin (100%) |
| UI | Jetpack Compose + Material 3 |
| Architecture | MVVM + Repository + Hilt DI |
| Async | Kotlin Coroutines + Flow |
| Database | Room + SQLCipher |
| Preferences | EncryptedSharedPreferences |
| Networking | Raw TCP sockets + Okio |
| Serialization | kotlinx.serialization |
| Crypto | Java Security (P-256 EC) + javax.crypto (AES-GCM) |
| Background | WorkManager (TTL cleanup) + Foreground Service |
| QR Codes | ZXing Android Embedded |
| Min SDK | API 26 (Android 8.0) |
| Target SDK | API 35 (Android 15) |
First-launch setup: enter a display name, detect your Yggdrasil IPv6 address, and configure the listening port. Your P-256 identity key pair is generated and persisted here.
Conversation list with contact avatars, names, and relative timestamps. No message content is ever shown in previews.
Full contact list with verification badges. Tap any contact to view their details, fingerprint, and QR code, or start a conversation.
Edit your display name, Yggdrasil address, and port. Toggle biometric lock and screen security. View version and encryption details.
Full conversation view with gradient sent-bubbles (orange → red) and dark received-bubbles. TTL chip selector, a gradient send button, and real-time delivery status icons.
Enter contact details manually or scan their QR code. Shows your own identity public key and QR for them to scan back. Contact exchange is bidirectional — both parties must add each other.
Ember requires Yggdrasil to be installed and running on your device (or accessible via your network). Yggdrasil provides the IPv6 address that Ember uses as your peer identity on the network.
- Install: yggdrasil-network.github.io/installation.html
- Your Yggdrasil address begins with
2(e.g.,200:e961:79f:ff83:...) - Ember auto-detects this address during onboarding
- Android 8.0+ (API 26)
- Camera (for QR scanning)
- Internet access (for Yggdrasil peer connectivity)
# Clone
git clone https://github.com/TheFruggg/Ember-5.0.git
cd Ember-5.0
# Set JAVA_HOME (Android Studio JBR recommended)
export JAVA_HOME=/path/to/android-studio/jbr
# Debug build
./gradlew assembleDebug
# APK output
# app/build/outputs/apk/debug/app-debug.apk| Variant | Debug Logging | ProGuard | App ID Suffix |
|---|---|---|---|
| Debug | Enabled | Disabled | .debug |
| Release | Disabled | Enabled | — |
| Permission | Purpose |
|---|---|
INTERNET |
Yggdrasil P2P communication |
CAMERA |
QR code scanning |
POST_NOTIFICATIONS |
Incoming message alerts |
FOREGROUND_SERVICE |
Background message listener |
WAKE_LOCK |
Keep listener alive |
ACCESS_NETWORK_STATE / ACCESS_WIFI_STATE |
Yggdrasil address detection |
| Property | Status |
|---|---|
| Central server | None |
| Account registration | None |
| Phone number required | No |
| Metadata logging | None |
| Messages stored server-side | Never |
| Contact discovery | Manual only |
| Cloud backup | None (by design) |
| Encryption in transit | AES-256-GCM (Double Ratchet) |
| Encryption at rest | SQLCipher + EncryptedSharedPreferences |
app/src/main/java/com/example/ember/
├─ data/
│ ├─ crypto/ # DoubleRatchet, MessageKeyStore
│ ├─ db/ # Room entities, DAOs, database
│ └─ network/ # YggdrasilClient, ListenerService, MessageBridge
├─ domain/
│ └─ ChatRepository # Single source of truth for all data operations
├─ ui/
│ ├─ screens/ # Compose screens (Home, Chat, Contacts, Add, Details)
│ ├─ theme/ # EmberComponents, color palette, typography
│ └─ viewmodel/ # MVVM ViewModels per screen
└─ work/
└─ TtlCleanupWorker # WorkManager job for expired message deletion
- Group messaging UI (data layer already implemented)
- In-app fingerprint comparison tool
- Message reactions
- Contact blocking
- Full-text message search
- Voice notes
Ember — because every conversation should burn after reading.