Chat E2E - UNITRONIX/BetterDesk GitHub Wiki
Chat E2E Encryption
BetterDesk Chat uses end-to-end encryption with ECDH key exchange and AES-256-GCM. No server can read the messages.
Protocol Overview
User A Server User B
| | |
|--- key_exchange (pubkey_A) -->|--- key_exchange (pubkey_A) -->|
|<-- key_exchange (pubkey_B) ---|<-- key_exchange (pubkey_B) ---|
| | |
| [ECDH: shared_secret = dh(privA, pubB)] |
| [HKDF-SHA256: key = derive(shared_secret, salt)] |
| | |
|--- encrypted message -------->|--- encrypted message -------->|
|<-- encrypted message ---------|<-- encrypted message ---------|
Key Exchange
Algorithm: ECDH P-256
- Each client generates an ECDH P-256 key pair using WebCrypto API
- Public keys are exchanged via the
key_exchangemessage through the server - Shared secret is derived via ECDH Diffie-Hellman
- AES-256 encryption key is derived from the shared secret via HKDF-SHA256
- Key pairs persist in
localStorageacross sessions
Key Rotation
Keys rotate automatically when either condition is met:
- 24 hours since last rotation
- 1000 messages sent with current key
On rotation, a new key exchange is performed seamlessly.
Message Encryption
Algorithm: AES-256-GCM
Each message is encrypted with:
- Algorithm: AES-256-GCM
- Key: 256-bit derived key from ECDH + HKDF
- IV: 12-byte random nonce (unique per message)
- Authentication tag: 128-bit GCM tag
Encrypted Message Format
{
"type": "message",
"encrypted": true,
"iv": "<base64-encoded IV>",
"data": "<base64-encoded ciphertext>",
"tag": "<included in GCM ciphertext>"
}
The server relays the data field without decryption. Only the intended recipient can decrypt.
File Encryption
Files up to 50 MB can be sent with E2E encryption:
Algorithm
Same AES-256-GCM with separate IVs for metadata and file data.
Packed Format
[metaIV (12 bytes)] [metaLen (4 bytes)] [encryptedMeta] [dataIV (12 bytes)] [encryptedData]
Encrypted Metadata
{
"filename": "report.pdf",
"size": 1048576,
"timestamp": 1711929600000
}
Message Types
The chat protocol supports these E2E-aware message types:
| Type | Direction | Encrypted | Description |
|---|---|---|---|
key_exchange |
Both | ❌ | Public key exchange (plaintext) |
message |
Both | ✅ | Text message |
file_share |
Both | ✅ | File metadata + encrypted file |
read_receipt |
Both | ❌ | Array of read message IDs |
typing |
Both | ❌ | Typing indicator |
presence_update |
Both | ❌ | Online/away/busy status |
hello |
Server→Client | ❌ | Server greeting |
welcome |
Server→Client | ❌ | Server capabilities confirmation |
status |
Server→Client | ❌ | Connection status update |
Server Capabilities
The server announces capabilities in the welcome message:
{
"type": "welcome",
"capabilities": [
"e2e_encryption",
"read_receipts",
"typing",
"presence",
"file_share"
],
"server_time": 1711929600000
}
Implementation
Client Side (chatCrypto.js)
// Key generation
const keyPair = await crypto.subtle.generateKey(
{ name: 'ECDH', namedCurve: 'P-256' },
true, ['deriveBits']
);
// Key derivation
const sharedBits = await crypto.subtle.deriveBits(
{ name: 'ECDH', public: peerPublicKey },
keyPair.privateKey, 256
);
const aesKey = await crypto.subtle.deriveKey(
{ name: 'HKDF', hash: 'SHA-256', salt: salt, info: info },
baseKey, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']
);
// Encryption
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: iv },
aesKey, plaintext
);
Server Side (chatRelay.js)
The server:
- Receives
key_exchangeand forwards to the peer (plaintext relay) - Receives encrypted messages and forwards without decryption
- Never has access to private keys or plaintext messages
- Stores no messages (relay only)
Security Properties
| Property | Status |
|---|---|
| Forward secrecy | ✅ Key rotation every 24h or 1000 messages |
| Authentication | ✅ Key pairs bound to authenticated sessions |
| Integrity | ✅ GCM authentication tag on every message |
| Confidentiality | ✅ AES-256-GCM with unique IVs |
| Replay protection | ✅ Unique IVs prevent replay |
| Server-blind | ✅ Server relays ciphertext only |
Limitations
- Key exchange requires both parties online simultaneously
- No offline message queuing (relay model)
- File size limit: 50 MB per transfer
- Browser WebCrypto API required (no IE11 support)
- Key pairs stored in localStorage (cleared on browser data wipe)