PoolChat
End-to-end encrypted chat for iOS/macOS with Curve25519 key agreement, AES-256-GCM, voice messages, polls, and reactions.
Install / Use
/learn @Olib-AI/PoolChatREADME
PoolChat
End-to-end encrypted mesh chat for iOS and macOS by Olib AI
Used in StealthOS - The privacy-focused operating environment.
PoolChat is a Swift package that provides fully encrypted, serverless group and private messaging over a local mesh network. It sits on top of ConnectionPool and adds Curve25519 key agreement, AES-256-GCM message encryption, Trust-On-First-Use identity verification, rich message types, encrypted history persistence, and ready-made SwiftUI views. No internet connection, no servers, no accounts -- just devices talking directly to each other with end-to-end encryption.
Why PoolChat Exists
Because chat should not require trusting a third party. Every mainstream messenger routes your messages through corporate servers, where they can be stored, analyzed, or handed over on request -- even when "end-to-end encrypted." PoolChat removes the server entirely. Messages travel directly between devices over a local mesh network, encrypted before they leave the sender and decryptable only by the intended recipient. There is no metadata to harvest because there is no central point to collect it.
Features
Encryption & Security
- End-to-end encryption -- Curve25519 ECDH key agreement, HKDF-SHA256 key derivation, AES-256-GCM authenticated encryption
- Trust-On-First-Use (TOFU) -- Automatically records peer identities on first contact and alerts on key changes
- Key fingerprint verification -- Human-readable fingerprints for out-of-band MITM detection
- Encryption downgrade prevention -- Unencrypted messages rejected by default (configurable)
- Image metadata stripping -- EXIF, GPS, and all metadata stripped from images before transmission
- Encrypted storage -- Chat history persisted through an injectable
SecureStorageProvider(AES-256-GCM) - Relay-aware key exchange -- E2E encryption works across relay hops in the mesh network
- Session teardown -- Cryptographic material securely cleared when sessions end
Messaging
- Rich message types -- Text, images, voice notes, emoji, polls, and system messages
- Message reactions -- Quick-react with emoji on any message, synced across all peers
- Polls -- Create polls with multiple options, optional vote-change policy, live vote counts
- Replies -- Reply to specific messages with preview context
- @Mentions -- Mention peers by name with autocomplete support and notification triggers
- Group and private chat -- Switch between group conversation and 1-on-1 private messaging
- Message status tracking -- Sending, sent, delivered, read, and failed states
Infrastructure
- Works over ConnectionPool -- Peer discovery, connection management, and message routing handled by the mesh layer
- Chat history sync -- Host sends encrypted history to newly joined peers (configurable)
- Local notifications -- Background message notifications with deep link support, reply actions, and thread grouping
- Notification bridge -- Notifications work even when the chat window is closed
- Voice recording -- AVFoundation-based recording with playback, seek, and progress tracking
- Configurable logging -- Inject your own logger or use the built-in
os.Loggerfallback - Cross-platform -- iOS and macOS from a single codebase with platform-adaptive SwiftUI views
- Swift 6 strict concurrency -- No data races, proper actor isolation,
Sendablethroughout
Architecture
graph TD
subgraph YourApp["Your App"]
PoolChatView["PoolChatView\nSwiftUI, cross-platform"]
PoolChatVM["PoolChatViewModel\nMessages, UI state, chat mode\npolls, reactions, mentions\nimage/voice send"]
PoolChatView --> PoolChatVM
PoolChatVM --> ChatHistory
PoolChatVM --> ChatEncryption
PoolChatVM --> VoiceRecording
ChatHistory["ChatHistoryService\nEncrypted persistence"]
ChatEncryption["ChatEncryptionService\nCurve25519 + AES-256-GCM"]
VoiceRecording["VoiceRecordingService\nAVFoundation record/playback"]
ChatHistory --> SecureStorage["SecureStorageProvider"]
ChatEncryption --> ConnectionPool["ConnectionPool\nMesh network transport"]
VoiceRecording --> ConnectionPool
end
Message flow (send):
- User composes a message in
PoolChatView PoolChatViewModelcreates aRichChatMessageand strips image metadata if applicable- Message is serialized to
RichChatPayload(orPrivateChatPayloadfor DMs) ChatEncryptionServiceencrypts the payload with the recipient's shared AES-256-GCM key- Encrypted payload is wrapped in
EncryptedChatPayloadand sent viaConnectionPoolManager ChatHistoryServicepersists the message throughSecureStorageProvider
Message flow (receive):
ConnectionPoolManagerdelivers an incomingPoolMessagePoolChatViewModelunwraps theEncryptedChatPayloadChatEncryptionServicedecrypts using the sender's shared key- Decrypted payload is deserialized into a
RichChatMessageand displayed - If the chat window is closed,
ChatNotificationBridgesends a local notification
Security
End-to-End Encryption
Every chat message is encrypted before it leaves the sending device. The encryption pipeline:
- Key Agreement -- Each peer generates an ephemeral Curve25519 key pair on session start. Public keys are exchanged over the mesh network.
- Shared Secret -- Curve25519 ECDH produces a shared secret between each pair of peers.
- Key Derivation -- HKDF-SHA256 derives a 256-bit symmetric key from the shared secret. The salt is the SHA-256 hash of both public keys (sorted lexicographically), ensuring both peers derive the same key regardless of who initiated the exchange.
- Encryption -- AES-256-GCM encrypts the message payload. Each message gets a unique nonce. The sealed box (nonce + ciphertext + authentication tag) is transmitted.
- Decryption -- The recipient uses the same derived symmetric key to open the AES-GCM sealed box. Authentication tag verification prevents tampering.
Trust-On-First-Use (TOFU)
PoolChat implements a TOFU model similar to SSH:
- First contact: The peer's public key is recorded as the "known" key. A
newPeerTrustedevent is emitted with the key fingerprint. - Subsequent contacts: The presented key is compared against the stored key. If it matches, the connection proceeds silently.
- Key change detected: If a peer presents a different public key, a
peerKeyChangedevent is emitted with both old and new fingerprints. This may indicate a MITM attack or legitimate key regeneration. - Explicit verification: Users can verify fingerprints out-of-band (in person, phone call) and mark peers as explicitly trusted. Verified status is cleared if the key changes.
Limitation: TOFU does not protect against MITM during the very first contact. Users who require stronger guarantees should verify fingerprints through a separate channel.
Key Fingerprint Verification
Both public key fingerprints and shared key fingerprints are available for out-of-band verification:
// Your public key fingerprint (share with peers)
let myFingerprint = ChatEncryptionService.shared.publicKeyFingerprint
// e.g., "A3:4F:B2:19:CC:87:D1:E6"
// Shared key fingerprint with a specific peer (both sides should match)
let sharedFingerprint = ChatEncryptionService.shared.sharedKeyFingerprint(for: peerID)
If both peers see the same shared key fingerprint, no MITM interception occurred during key exchange.
Encryption Downgrade Prevention
By default, PoolChat rejects unencrypted messages:
// Default: unencrypted messages are silently dropped
PoolChatConfiguration.rejectUnencryptedMessages = true
// Migration period only: accept with warning marker
PoolChatConfiguration.rejectUnencryptedMessages = false
Setting this to false is an encryption downgrade vector and should only be used during migration periods when legacy clients are still in the network.
Image Metadata Stripping
Before any image is sent, PoolChat strips all EXIF metadata, GPS coordinates, camera information, and other embedded metadata. The image is re-encoded as a clean JPEG/PNG with no identifying information.
Encrypted Storage
Chat history is persisted through the SecureStorageProvider protocol. The host application injects its own implementation (e.g., AES-256-GCM encrypted file storage). PoolChat never writes plaintext messages to disk.
Media (images, voice notes) is stored separately from message metadata with independent encryption keys, and referenced by opaque storage keys.
What Relay Nodes Can See
In a mesh network, messages may travel through relay nodes to reach non-adjacent peers. Here is what relay nodes can and cannot observe:
| Data | Visible to Relay? | |------|-------------------| | Message content | No (AES-256-GCM encrypted) | | Sender/receiver peer IDs | Yes (routing metadata) | | Message type (chat, reaction, poll) | Yes (envelope metadata) | | Message size | Yes (encrypted blob size) | | Timing | Yes (when message transits) | | Public keys during exchange | Yes (but cannot derive shared secret without private keys) |
Relay nodes forward encrypted blobs. They cannot decrypt content, forge messages, or modify payloads without detection (GCM authentication tag verification will fail).
Installation
Swift Package Manager
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/Olib-AI/PoolChat.git", from: "1.3.0")
]
Then add the dependency to your target:
