A secure Android chat client with end-to-end encryption, built in Kotlin with Jetpack Compose as a project for the Information Security course at UFRPE (Computer Engineering).
This repository contains the mobile client. It connects to the Secure Chat Python server through a custom JSON-over-TCP protocol, performs a cryptographic session handshake, authenticates users with asymmetric signatures, and establishes independent end-to-end encrypted sessions per contact.
Tip
Looking for the Python server? → secure-chat-server
Yann Leão — Computer Engineering, UFRPE
Advisor: Prof. Ygor Amaral
| Splash / Connection | Chat |
|---|---|
![]() |
![]() |
Android clients do not open direct sockets to each other — the server is responsible for routing. Message contents are protected with end-to-end encryption, so the server only ever sees an opaque e2e_payload.
[Android Client A] ─────────────────────── [Android Client B]
│ logical E2E encrypted channel │
│ │
└──────────── [Python Server] ──────────────┘
routes only
cannot read E2E content
- Connects to the server using a persistent TCP socket
- Performs an ECDH-based cryptographic handshake on connect
- Wraps all post-handshake traffic in a secure envelope (AES-256 + HMAC)
- Authenticates via ECDSA challenge-response — no password transmitted after registration
- Manages independent E2E encrypted sessions per contact
- Encrypts and authenticates chat messages before routing them through the server
- Stores conversation history encrypted at rest via Android Keystore
- Accepts, authenticates, and routes TCP client connections
- Distributes public keys between contacts
- Forwards E2E payloads without decrypting them
- Stores offline messages until the recipient reconnects
- Broadcasts user status and typing events
| Layer | Mechanism | Purpose |
|---|---|---|
| Client-server key exchange | Ephemeral ECDH + HKDF-SHA256 | Derives unique session keys per connection |
| Client-server envelope | AES-256-CBC + HMAC-SHA256 | Protects all traffic after handshake |
| Session expiration | Randomized time (30–60 min) and message count (50–100) | Forward secrecy; hides rekeying pattern |
| Identity | ECC key pair stored in Android Keystore | Represents the user's cryptographic identity |
| Authentication | ECDSA challenge-response | Proves identity without transmitting a password |
| Public key distribution | Server-mediated key lookup | Lets contacts discover each other's identity keys |
| E2E key exchange | Ephemeral ECDH per contact + HKDF-SHA256 | Derives chat keys unknown to the server |
| E2E message protection | AES-256-CBC + HMAC-SHA256 | Encrypts and authenticates chat payloads |
| Local storage | AES-256-GCM via Android Keystore | Encrypts message history at rest |
Plaintext message
│
▼
E2E encrypt (AES-256) + HMAC
│
▼
{ iv, ciphertext, hmac } → e2e_payload
│
▼
Wrapped in client-server secure envelope
│
▼
Server routes opaque payload (cannot read content)
│
▼
Recipient verifies HMAC → decrypts E2E payload → plaintext
- Registration, login, and existing-device authentication via asymmetric signature
- New-device login with password and automatic key rotation
- Contact list with real-time online/offline status
- Terminal-inspired dark UI built with Jetpack Compose
- Real-time chat with double-layer encrypted payloads
- Offline message delivery on reconnect
- Typing indicators
- Local encrypted message history (Room + Android Keystore)
- Logcat traces for protocol demonstration
- Android Studio (latest stable)
- JDK 17 or the bundled JDK from Android Studio
- Android device or emulator with API 26+
- A running instance of the Secure Chat Python server
- Network access between the client and the server
Project stack:
- Kotlin + Coroutines
- Jetpack Compose + Material 3
- Room (local encrypted storage)
- Kotlinx Serialization
- Bouncy Castle
- Android Keystore
git clone <client-repository-url>Open the project folder in Android Studio and let Gradle sync complete.
Start the Python server before launching the app. The client expects the server to be reachable at the configured host and port.
Important
Emulator: use Android's built-in alias 10.0.2.2 to reach localhost on the host machine.
Physical device: the phone and the server must be on the same network. Use the server machine's local IP address.
Locate the socket configuration in the client code and update the host and port to match your server.
Run directly from Android Studio, or build via the command line:
# macOS / Linux
./gradlew :app:assembleDebug
# Windows
.\gradlew.bat :app:assembleDebugRecommended demo flow:
- Start the Python server.
- Launch the app on two clients — for example, one emulator and one physical device.
- Register two different users (or authenticate with pre-existing accounts).
- Open a conversation from the contact list — the E2E handshake happens automatically.
- Exchange messages while both users are online.
- Send a message with one user offline, then reconnect to demonstrate offline delivery.
Useful Logcat filters for following the cryptographic protocol live:
E2EProtocol
Crypto
LOGOUT
The E2EProtocol tag is especially useful for demonstrations. It shows:
- Plaintext before E2E encryption
- Generated E2E payloads (
iv,ciphertext,hmac) - Received E2E payloads before decryption
- Decrypted plaintext after HMAC verification
- E2E session fingerprints (
aesFp,hmacFp) for comparing both clients
Tip
If E2E decryption fails, compare the aesFp and hmacFp values on both clients — they must match for the same conversation. Mismatched fingerprints indicate a handshake or key derivation issue.
The app communicates with the server using newline-delimited JSON over TCP:
{"type": "message_type"}\nAfter the initial cryptographic handshake, all messages are wrapped in a secure envelope:
{
"type": "secure_envelope",
"iv": "HEX_STRING",
"ciphertext": "HEX_STRING",
"hmac": "HEX_STRING"
}Chat messages carry a second encryption layer inside the envelope:
{
"type": "message",
"from": "alice",
"to": "bob",
"e2e_payload": {
"iv": "HEX",
"ciphertext": "HEX",
"hmac": "HEX"
}
}The server decrypts only the outer envelope to route the message. The e2e_payload remains opaque.
For the full specification — all message types, cryptographic flows, and E2E protocol — see PROTOCOLS.md.

