E2E Encryption
End-to-end encryption protocol — ECDH key exchange, AES-256-GCM, counter-based nonces, and verification codes.
All terminal data in TermPod is end-to-end encrypted. The relay forwards opaque ciphertext — it cannot decrypt, inspect, or store terminal content.
Key Exchange
TermPod uses ECDH P-256 for key exchange between desktop and viewer:
- Desktop generates an ephemeral P-256 key pair
- Desktop sends its public key to the viewer via
key_exchangemessage - Viewer generates its own P-256 key pair
- Viewer responds with its public key via
key_exchange_ack - Both sides compute the shared secret using ECDH
Key Derivation
From the ECDH shared secret, session keys are derived using HKDF-SHA256:
ikm = ECDH shared secret (raw bytes)
salt = empty
info = "termpod-e2e-{sessionId}" (UTF-8)
output = 256-bit AES keyThis produces a unique AES-256-GCM key per session.
Encrypted Frame Format
[0xE0][nonce:12 bytes][ciphertext + 16-byte GCM auth tag]- Channel byte:
0xE0identifies E2E encrypted frames - Nonce: 12 bytes — 8-byte counter (big-endian) + 4 bytes zero padding
- Ciphertext: AES-256-GCM encrypted payload
- Auth tag: 16-byte GCM authentication tag (integrity + authenticity)
The inner plaintext is a standard binary frame ([0x00][terminal data] or [0x01][resize]).
Counter-Based Nonces
Each peer maintains a send counter and a receive counter:
- Send counter: Incremented after each encrypted frame sent
- Receive counter: Tracks the highest nonce received
- Replay protection: Frames with nonces at or below the receive counter are rejected
- Counter format: 8-byte big-endian integer, zero-padded to 12 bytes
Verification Code
Both peers derive a 6-digit verification code from the shared secret for MITM detection:
ikm = ECDH shared secret
salt = empty
info = "termpod-verify-{sessionId}" (UTF-8)
output = 6-digit numeric codeUsers can compare verification codes out-of-band to confirm no intermediary is intercepting the key exchange.
E2E Session State
interface E2ESession {
key: CryptoKey; // AES-256-GCM derived key
sendCounter: number; // Monotonic send nonce counter
recvCounter: number; // Highest received nonce
sessionId: string; // Session ID (used in HKDF info)
verificationCode: string; // 6-digit MITM detection code
}Implementation Files
| Platform | File | API |
|---|---|---|
| Desktop | packages/protocol/src/crypto.ts | Web Crypto API |
| iOS | apps/ios/TermPod/Services/CryptoService.swift | CryptoKit |
Both implementations must produce identical output for the same inputs. Key functions:
generateKeyPair()— ECDH P-256 ephemeral key pairderiveSessionKey(privateKey, peerPublicKey, sessionId)— HKDF-SHA256encryptFrame(session, data)— AES-256-GCM with counter noncedecryptFrame(session, data)— Decrypt + verify + counter check
Scrollback Encryption
When a new viewer connects mid-session, the desktop delivers historical scrollback:
- Key exchange completes (ECDH + HKDF)
- Desktop reads local scrollback buffer
- Each scrollback chunk is encrypted with the session key
- Sent as
encrypted_scrollback_chunkJSON messages - Viewer decrypts and feeds to terminal
The relay never sees plaintext scrollback.