Skip to main content

End-to-End Encryption

Zeq OS provides transparent end-to-end encryption between browser clients and the API server. Once enabled, all HTTP requests and responses are encrypted with AES-256-GCM — the server and client negotiate a shared key via a PBKDF2 handshake, and all subsequent traffic is encrypted automatically.

Architecture

ComponentImplementationPurpose
CipherAES-256-GCMAuthenticated encryption with 128-bit tags
Key DerivationPBKDF2-SHA256100,000 iterations from shared secret + nonces
Wire Formativ(12) + authTag(16) + ciphertextSame format as HITE
Content Typeapplication/x-zeq-e2eSignals encrypted body
Session TTL1 hourAutomatic cleanup every 10 minutes

Handshake Flow

 Browser                                          API Server
| |
| POST /api/e2e/handshake |
| { clientNonce: <32 bytes hex> } ──────────> |
| |
| Server generates serverNonce |
| Derives session key via PBKDF2 |
| Stores session with 1-hour TTL |
| |
| <──────── { sessionId, serverNonce } |
| |
| Client derives same key via PBKDF2 |
| (secret + clientNonce + serverNonce) |
| |
| All subsequent requests use encrypted bodies |
| Content-Type: application/x-zeq-e2e |
| Accept: application/x-zeq-e2e |
| X-E2E-Session: <sessionId> |
| |

Both sides derive the same AES-256-GCM key from the shared secret (ZEQ_E2E_SECRET) combined with both nonces. The nonces ensure each session gets a unique key.

Client Library

The client library at framework/lib/zeq-e2e-client.js is a zero-dependency ES module that works in any modern browser via the Web Crypto API.

Auto-Wrap Mode

The simplest integration — monkey-patches window.fetch for transparent encryption:

<script type="module">
import { zeqE2EHandshake, zeqE2EWrap } from '/static/zeq-e2e-client.js';
try {
await zeqE2EHandshake(window.location.origin);
zeqE2EWrap();
// All fetch() calls are now automatically encrypted
} catch (e) {
// Graceful fallback — plaintext continues to work
}
</script>

Explicit Fetch Mode

For finer control, use zeqE2EFetch as a drop-in replacement for fetch():

import { zeqE2EHandshake, zeqE2EFetch } from '/static/zeq-e2e-client.js';

await zeqE2EHandshake(window.location.origin);

// Encrypted GET
const operators = await zeqE2EFetch('/api/zeq/operators');
const data = await operators.json();

// Encrypted POST
const result = await zeqE2EFetch('/api/zeq/operators/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: 'KO42', params: { t: 1000 } })
});

WebSocket E2E

Encrypted WebSocket connections are supported via session query parameter:

import { zeqE2EHandshake, zeqE2EWebSocket } from '/static/zeq-e2e-client.js';

await zeqE2EHandshake(window.location.origin);
const ws = zeqE2EWebSocket('ws://localhost:8080/ws/pulse');

ws.onmessage = (event) => {
// Messages are automatically decrypted
const tick = JSON.parse(event.data);
console.log(`Phase: ${tick.phase}`);
};

Server Configuration

E2E encryption is enabled by setting the ZEQ_E2E_SECRET environment variable:

# .env — leave empty to disable E2E (graceful degradation)
ZEQ_E2E_SECRET=your-e2e-secret-here

If ZEQ_E2E_SECRET is not set, the server falls back to ZEQ_HITE_SECRET. If neither is set, the handshake endpoint returns an error and clients fall back to plaintext.

Wire Format

Encrypted payloads use the same binary format as HITE:

[IV: 12 bytes] [Auth Tag: 16 bytes] [Ciphertext: variable]
  • IV: Random 12-byte initialization vector (unique per message)
  • Auth Tag: 128-bit GCM authentication tag (integrity + authenticity)
  • Ciphertext: AES-256-GCM encrypted payload

Request/Response Flow

Encrypted Request

  1. Client serializes the request body to JSON
  2. Client encrypts with the session key → binary payload
  3. Client sends with Content-Type: application/x-zeq-e2e and X-E2E-Session: <id>
  4. Server middleware decrypts before route handlers see the request

Encrypted Response

  1. Route handler returns a normal JSON response
  2. Server middleware checks for Accept: application/x-zeq-e2e header + valid session
  3. Server encrypts the JSON response with the session key
  4. Client library automatically decrypts the response

Plaintext Fallback

Requests without E2E headers continue to work normally. E2E is opt-in per client — zero breaking changes for existing integrations.

App Integration

All Zeq OS browser applications include the E2E bootstrap snippet. On startup, each app attempts a handshake and wraps fetch(). If the handshake fails (server not configured, network error), the app continues with plaintext — no user-visible errors.

Security Properties

PropertyGuarantee
ConfidentialityAES-256-GCM encryption on all request/response bodies
IntegrityGCM authentication tags detect tampering
Forward secrecyEach session uses unique nonces → unique key
Session isolationSessions cannot decrypt each other's traffic
Graceful degradationPlaintext continues to work if E2E is unavailable