Version 1.0 | April 2026 Published by Inverted Software
WhiteStar Messenger is a production-grade, end-to-end encrypted instant messaging platform built for enterprise environments. It implements the Signal Protocol for message encryption, WebRTC for peer-to-peer voice and video calling, and a scalable real-time delivery infrastructure powered by ASP.NET Core, SignalR, and Redis. The system achieves complete server-side blindness: encrypted message payloads are opaque to the server, which functions purely as a routing and queuing layer. This whitepaper details every layer of the architecture, from cryptographic primitives to deployment topology.
WhiteStar Messenger consists of four major components:
All communication between clients and server uses TLS. Message content is always encrypted end-to-end before leaving the sender’s device.
The protocol layer uses the following algorithms:
| Algorithm | Purpose | Key/Output Size | Standard |
|---|---|---|---|
| X25519 | Elliptic-curve Diffie-Hellman | 32-byte keys, 32-byte shared secret | RFC 7748 |
| Ed25519 | Digital signatures | 32-byte keys, 64-byte signatures | RFC 8032 |
| HKDF-SHA256 | Key derivation | Variable output | RFC 5869 |
| HMAC-SHA256 | Message authentication | 32 bytes (truncated to 8 for wire format) | RFC 2104 |
| AES-256-CBC | Symmetric encryption (Double Ratchet) | 32-byte key, 16-byte IV | NIST |
| AES-256-GCM | Authenticated encryption (transport envelope) | 32-byte key, 12-byte nonce, 16-byte tag | NIST |
Ed25519 keys are converted to X25519 form for Diffie-Hellman operations using the standard Montgomery mapping: u = (1 + y) / (1 - y) mod p where p = 2^255 - 19.
Each user maintains the following key material, all generated and stored exclusively on their device:
Session establishment follows the X3DH protocol. When Alice wants to message Bob for the first time:
0xFF^32 || DH1 || DH2 || DH3 [|| DH4]"WhisperText"AD = IK_A_public || IK_B_public (64 bytes)The one-time pre-key (DH4) is consumed atomically on the server – once fetched, it is deleted and cannot be reused. This provides forward secrecy even if the signed pre-key is later compromised.
After X3DH establishes a shared secret, all subsequent messages use the Double Ratchet for forward secrecy and break-in recovery.
Ratchet State consists of:
Symmetric Ratchet (per message):
Each message derives unique keys from the current chain key using HMAC-SHA256:
This ensures that compromising one message key does not reveal any other message key.
DH Ratchet (per round-trip):
When a new DH public key is received from the peer, a DH ratchet step occurs:
This provides break-in recovery: even if an attacker compromises the current state, future messages are protected by new DH operations.
Message Authentication:
Each encrypted message carries an 8-byte MAC computed as HMAC-SHA256 over: associated_data || ratchet_public_key || counter || previous_counter || ciphertext, truncated to 8 bytes. MAC verification uses constant-time comparison to prevent timing attacks.
Out-of-Order Message Handling:
When messages arrive out of order (e.g., due to network reordering), the receiver stores skipped message keys in a bounded cache (maximum 1,000 entries). When the expected counter does not match, the receiver advances the chain and caches intermediate keys for later use.
SignalMessage (in-session, Double Ratchet encrypted):
Version (1 byte, value: 3)
Ratchet Public Key (32 bytes, X25519)
Counter (uint32, little-endian)
Previous Counter (uint32, little-endian)
Ciphertext (variable, AES-256-CBC output)
MAC (8 bytes, truncated HMAC-SHA256)
Timestamp (int64, Unix milliseconds)
PreKeySignalMessage (session establishment):
Version (1 byte, value: 3)
Registration ID (uint32)
Pre-Key ID (optional uint32)
Signed Pre-Key ID (uint32)
Base Key (32 bytes, Alice's ephemeral X25519 public)
Identity Key (32 bytes, Alice's Ed25519 public)
Nested SignalMessage (the first encrypted message)
MessageEnvelope (transport wrapper):
Id: GUID string
SenderAddress: string (username)
RecipientAddress: string (username)
Type: integer (1=PreKey, 2=Signal, 3=Receipt, 4=Typing, 5=Plaintext)
Payload: byte[] (AES-256-GCM encrypted serialized inner message)
Timestamp: int64 (Unix milliseconds)
The server sees only the MessageEnvelope. The Payload field is opaque – the server cannot decrypt it.
Pre-Key Replenishment:
Clients monitor their one-time pre-key count via the REST API. When the count drops below 20, the client generates a fresh batch (typically 100 keys) and uploads them. The server tracks remaining count and signals replenishment need.
Signed Pre-Key Rotation:
Clients periodically generate and upload new signed pre-keys. The server marks the previous key as non-current. A background maintenance service deletes non-current signed pre-keys older than 90 days.
Identity Persistence:
Identity keys are stored in a local SQLite database (MAUI) or localStorage (web). The private key never leaves the device. On first contact with a peer, their identity key is recorded (Trust On First Use). Subsequent sessions verify the peer’s identity matches the stored key; mismatches generate a security alert.
Registration creates a new user with username, email, display name, and password. The client generates an Ed25519 identity keypair and uploads the public keys. The server stores password hashes using ASP.NET Identity’s PBKDF2 implementation. Password requirements: minimum 10 characters, must include uppercase, digit, and non-alphanumeric character.
Login validates credentials and returns a JWT containing claims for user ID (NameIdentifier), username (Name), display name, and a unique token ID (Jti). Tokens are signed with HMAC-SHA256. Account lockout activates after 5 failed attempts, locking the account for 10 minutes.
Password Recovery uses a two-stage flow: an 8-character alphanumeric code (excluding ambiguous characters 0/O, 1/I/L) is emailed to the user with a 15-minute TTL. After code verification, a 32-byte random reset token (10-minute TTL, single-use) is issued for the actual password change.
The SignalR hub handles all real-time operations with the following configuration:
Connection Lifecycle:
On connect, the server registers the user’s SignalR connection ID in a Redis set (presence:{userId}), sets a short-lived online indicator (online:{userId} with 61-second TTL), flushes any pending offline messages, and broadcasts online status to all accepted contacts.
On disconnect, the connection ID is removed. If no connections remain, all presence keys are deleted and offline status is broadcast.
Message Routing:
When a message is sent, the server resolves the recipient username to a user ID, checks the bidirectional block list, and attempts live delivery via SignalR. If the recipient is offline or delivery fails, the message is enqueued in Redis with a 24-hour TTL. The sender receives a delivery status callback (“delivered” or “queued”) with timestamp.
Call Signaling:
The hub provides secure methods for WebRTC call signaling: SendCallOffer, SendCallAnswer, SendIceCandidate, SendCallHangup, SendCallBusy, and SendCallRinging. Every call signaling method:
Presence is tracked entirely in Redis with three keys per user:
| Key Pattern | Value | TTL | Purpose |
|---|---|---|---|
presence:{userId} |
Set of connection IDs | 5 minutes | Message routing |
online:{userId} |
“1” | 61 seconds | Quick online/offline detection |
callcap:{userId} |
“audio,video” | 61 seconds | Call capability advertisement |
Clients send a combined heartbeat every 50 seconds that refreshes all three keys in a single Redis batch operation. If a client stops sending heartbeats (crash, network loss), the online indicator expires after 61 seconds and a background monitor broadcasts offline status to contacts.
A dedicated PresenceMonitorService runs every 10 seconds to detect expired online indicators and broadcast status changes.
Messages for offline recipients are stored in Redis:
pending:{userId}:{messageId} (serialized MessageEnvelope, 24-hour TTL)pending:{userId}:index (sorted set ordered by timestamp, 24-hour TTL)When the recipient connects, all pending messages are flushed in chronological order. The client acknowledges each message, which triggers immediate deletion from Redis. Unacknowledged messages persist until the 24-hour TTL expires.
All endpoints are protected by fixed-window rate limiters (per IP address, no request queuing):
| Endpoint Category | Limit | Window |
|---|---|---|
| Authentication (login, register, password reset) | 10 requests | 1 minute |
| Key management (pre-key upload) | 30 requests | 1 minute |
| File upload | 10 requests | 1 minute |
| General API (search, connections, profile) | 60 requests | 1 minute |
Exceeded limits return HTTP 429 (Too Many Requests).
All responses include:
X-Content-Type-Options: nosniffX-Frame-Options: DENYX-XSS-Protection: 1; mode=blockReferrer-Policy: no-referrerPermissions-Policy: camera=(self), microphone=(self), geolocation=()Strict-Transport-Security: max-age=63072000; includeSubDomains (production)Calls are peer-to-peer using WebRTC. The server acts only as a signaling relay – call media (audio/video) flows directly between participants via DTLS-SRTP encrypted channels. The server never processes, records, or stores call media.
STUN servers are used for NAT traversal. The client fetches ICE server configuration from the backend (which can inject TURN credentials), with a fallback to public Google STUN servers. Configuration is cached for 5 minutes.
Audio:
Video:
setCodecPreferences API for cross-platform compatibility)Adaptive Bitrate:
A quality monitor runs every 5 seconds, checking packet loss statistics from RTCStatsReport. When packet loss exceeds 8%, the video bitrate is reduced. When loss drops below 2%, bitrate is restored. This prevents video degradation on constrained networks.
getUserMedia acquires local media streamsRTCPeerConnection establishes DTLS-SRTP encrypted media channelEach client advertises its call capabilities (none, audio, video, or audio+video) as part of the heartbeat. Contacts see available call types in the chat header and can only initiate supported call types.
Files are uploaded via a dedicated REST endpoint (POST /api/files/upload) using multipart/form-data. The upload is separate from the message system – only a file reference (ID, name, size, content type) is sent as a message through the encrypted channel.
Maximum file size: 20 MB. Files are stored in a temporary server directory with a 6-hour expiry. A background cleanup service runs every 30 minutes to delete expired files and their database records.
Every uploaded file passes through four layers of validation:
photo.jpg.exe by checking every extension segment.application/x-msdownload, application/x-executable, application/x-dosexec, and application/x-msi.Filenames are sanitized by stripping all characters except letters, digits, dots, underscores, and hyphens.
Only the sender and designated recipient can download a file. The server verifies the requesting user’s identity against the file’s sender and recipient fields before serving the file.
The MAUI application uses HybridWebView to host a JavaScript-based UI identical to the web client. Communication between JavaScript and .NET uses a bidirectional bridge:
JavaScript to .NET: bridge.invoke('namespace.action', { payload }) serializes a JSON message and sends it via HybridWebView.SendRawMessage. The .NET side deserializes, routes to the appropriate handler, and returns a JSON response via EvaluateJavaScriptAsync.
NET to JavaScript: Push events (incoming messages, call offers, presence changes) are forwarded from SignalR event handlers through EvaluateJavaScriptAsync, which invokes a window.__bridge_message_handler function that dispatches events to registered JS listeners.
The JavaScript UI layer is shared between web and MAUI with one critical exception:
On Windows, the application supports background operation. When the user closes the window, the app minimizes to the system tray rather than exiting. A native Win32 context menu (via TrackPopupMenuEx) provides “Show” and “Exit” options. The SignalR connection remains active, and Windows toast notifications alert the user to new messages while the window is hidden. This behavior is controlled by a user-facing setting (“Keep Running in Background”) that defaults to enabled.
On Android, a foreground service keeps the SignalR connection alive when the app is backgrounded. Android 13+ runtime permissions for notifications are requested after login.
| Entity | Key Fields | Purpose |
|---|---|---|
| ApplicationUser | Id (GUID), UserName, IdentityKeyHex, DhIdentityKeyHex | User identity and profile |
| SignedPreKeyRecord | UserId, KeyId, PublicKeyHex, SignatureHex, IsCurrent | Medium-term DH key with signature |
| OneTimePreKeyRecord | UserId, KeyId, PublicKeyHex | Single-use DH keys |
| ConnectionRequest | SenderId, ReceiverId, Status (Pending/Accepted/Rejected) | Contact relationships |
| BlockedUser | BlockerId, BlockedId | Bidirectional block list |
| FileTransfer | Id (GUID), SenderId, RecipientId, StoragePath, ExpiresAt | Temporary file metadata |
| PasswordResetCode | UserId, Code, Purpose, ExpiresAt, IsUsed | Password recovery flow |
| Key Pattern | Value Type | TTL | Purpose |
|---|---|---|---|
presence:{userId} |
Set of connection IDs | 5 min | SignalR routing |
online:{userId} |
String “1” | 61 sec | Online status |
callcap:{userId} |
String (capabilities) | 61 sec | Call capability advertisement |
pending:{userId}:{msgId} |
JSON (MessageEnvelope) | 24 hrs | Offline message queue |
pending:{userId}:index |
Sorted set (timestamps) | 24 hrs | Message ordering |
Threat: An attacker gains access to the server.
Mitigation: The server never possesses message plaintext. All message payloads are AES-256-GCM encrypted envelopes containing Double Ratchet ciphertext. Even with full server access, an attacker can only observe message metadata (sender, recipient, timestamp, size) but cannot read content. Private keys exist only on client devices.
Threat: An attacker intercepts the X3DH key exchange.
Mitigation: Signed pre-keys carry Ed25519 signatures verified against the peer’s known identity key. Safety numbers (derived from both identity keys) can be verified out-of-band. Identity key changes trigger alerts (Trust On First Use model).
Threat: An attacker obtains a user’s current session state.
Mitigation: Forward secrecy ensures past messages remain secure (each message uses a unique derived key that is deleted after use). Break-in recovery ensures future messages become secure after the next DH ratchet step (typically the next message round-trip).
Threat: An attacker resends previously captured messages.
Mitigation: Double Ratchet counters prevent replay – each message has a unique (ratchet key, counter) pair. One-time pre-keys are consumed atomically and cannot be reused. The skipped-key cache is bounded to 1,000 entries to limit resource exhaustion.
Threat: An attacker attempts to guess user passwords.
Mitigation: Passwords require minimum 10 characters with complexity (uppercase, digit, non-alphanumeric). Account lockout activates after 5 failed attempts (10-minute duration). Rate limiting restricts authentication endpoints to 10 requests per minute per IP. The web client enforces reCAPTCHA v3 on login and registration.
Threat: An attacker uploads executable files to compromise recipients.
Mitigation: Four-layer validation: extension blacklist (40+ types), MIME type blacklist, double-extension detection, and magic byte inspection (PE, ELF, shebang headers). Files expire after 6 hours with automatic cleanup.
Threat: An attacker intercepts voice/video call media.
Mitigation: WebRTC enforces DTLS-SRTP encryption on all media channels. Call media flows peer-to-peer and never passes through the server. ICE connectivity checks validate endpoint authenticity.
+------------------+
| Load Balancer |
| (TLS Termination)|
+--------+---------+
|
+------------+------------+
| |
+---------+--------+ +-----------+---------+
| Web Client | | API Server |
| (im.whitestar | | (imserver.whitestar|
| labs.com) | | labs.com) |
| | | |
| Static HTML/JS | | ASP.NET Core |
| Reverse proxy | | SignalR Hub |
| for /api/* | | REST Controllers |
+------------------+ +----+-----+-----+----+
| | |
+--------+ +--+--+ +--------+
| | | |
+----+----+ +----+----+ +----+-----+
|SQL Server| | Redis | | File |
| | | Backplane| | Storage |
| Users | | Presence | | (Temp) |
| Keys | | Queues | | |
| Contacts| | PubSub | | |
+---------+ +---------+ +----------+
Direct SignalR Connection: The web client connects its SignalR WebSocket directly to the API server, bypassing the web client’s reverse proxy. This avoids WebSocket proxy issues with IIS and ensures reliable real-time delivery.
Horizontal Scaling: The Redis backplane enables multiple API server instances. All presence, message queuing, and pub/sub operations go through Redis. API controllers are stateless. SignalR messages are broadcast across all nodes via Redis channels.
| Parameter | Value | Description |
|---|---|---|
| X25519/Ed25519 key size | 32 bytes | Asymmetric key length |
| AES-256-GCM nonce | 12 bytes | Transport encryption nonce |
| AES-256-GCM tag | 16 bytes | Authentication tag |
| AES-256-CBC IV | 16 bytes | Double Ratchet encryption IV |
| HMAC truncation | 8 bytes | Wire-format MAC length |
| Max skipped message keys | 1,000 | Out-of-order tolerance |
| Offline message TTL | 24 hours | Redis queue expiry |
| Presence set TTL | 5 minutes | Connection routing |
| Online indicator TTL | 61 seconds | Quick offline detection |
| Heartbeat interval | 50 seconds | Client-side |
| File upload max size | 20 MB | Per-file limit |
| File storage TTL | 6 hours | Auto-cleanup |
| Password min length | 10 characters | With complexity requirements |
| Account lockout | 10 minutes after 5 failures | Brute-force protection |
| JWT expiry | 10 years | Long-lived session |
| SignalR message limit | 64 KB | Per-message maximum |
| SignalR keep-alive | 15 seconds | Connection health |
| Video max bitrate | 500 kbps | Adaptive ceiling |
| Audio max bitrate | 32 kbps | Opus speech quality |
| Video resolution | 640x480 | SD quality |
| Video frame rate | 24 fps | Smooth motion |
| Audio sample rate | 16 kHz mono | Speech-optimized |
| Call ring timeout | 30 seconds | Before auto-decline |
| Signed pre-key max age | 90 days | Server-side cleanup |
| OTK replenishment threshold | 20 keys | Client-driven |
| Layer | Technology |
|---|---|
| Protocol | Custom Signal Protocol implementation (.NET) |
| Backend | ASP.NET Core (.NET 10) |
| Database | SQL Server + Entity Framework Core |
| Real-time | SignalR + Redis backplane |
| Cache/Queue | Redis (StackExchange.Redis) |
| Authentication | ASP.NET Identity + JWT (HS256) |
| Cryptography | .NET BCL (AES, HMAC, HKDF) + Curve25519/Ed25519 |
| Web Client | Vanilla JavaScript, HTML5, CSS3 |
| Desktop/Mobile | .NET MAUI with HybridWebView |
| Voice/Video | WebRTC (RTCPeerConnection, VP8/Opus) |
| System Tray | H.NotifyIcon.WinUI + Win32 API |
| Bot Protection | Google reCAPTCHA v3 (web only) |
WhiteStar Messenger demonstrates that enterprise-grade encrypted messaging can be achieved with modern .NET technologies while maintaining complete server-side blindness. The Signal Protocol provides mathematically proven forward secrecy and break-in recovery. The Redis-backed infrastructure ensures horizontal scalability and reliable offline message delivery. WebRTC enables peer-to-peer calling without server-side media processing. The cross-platform MAUI HybridWebView architecture maximizes code reuse while preserving native platform capabilities.
The system is designed for organizations that require private, auditable, and scalable communication infrastructure without compromising on usability or security guarantees.
WhiteStar Messenger is developed and maintained by Inverted Software. For more information, visit https://whitestarlabs.com