From 15abde1c54ffea97f727e655e59aa1fb5232c061 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Mon, 17 Aug 2026 08:59:04 +0200 Subject: [PATCH] Enclave server architecture --- src/app/app.ts | 8 ++-- src/app/server.ts | 94 ++++++++++++++++++++++++++++++++++++++--------- src/app/ws.ts | 60 ------------------------------ 3 files changed, 80 insertions(+), 82 deletions(-) diff --git a/src/app/app.ts b/src/app/app.ts index 5ec9152..26ccd24 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -23,10 +23,8 @@ export default class Enclave { this.clientSecretKey = ed.utils.randomSecretKey(); this.clientPublicKey = ed.getPublicKey(this.clientSecretKey); - this.server = new EnclaveServer( - "localhost:3415", - this.clientSecretKey, - this.clientPublicKey, - ); + this.server = new EnclaveServer("localhost:3415"); + + this.server.connect(this.clientPublicKey, this.clientSecretKey); } } diff --git a/src/app/server.ts b/src/app/server.ts index 7ff68fd..8b17950 100644 --- a/src/app/server.ts +++ b/src/app/server.ts @@ -1,28 +1,88 @@ +import { base58 } from "@scure/base"; +import * as ed from "@noble/ed25519"; import EnclaveWebSocket from "./ws"; +import { sha512 } from "@noble/hashes/sha2.js"; + +ed.hashes.sha512 = sha512; + /** - * Manages a single connection to an Enclave server: channels, server - * metadata, and messages for that server. + * Represents a known Enclave server, whether connected or not. * - * Wraps one `EnclaveWebSocket` instance and layers server-level concepts - * on top of the raw protocol (channels, messages, server meta) — it does - * not know about the handshake/key exchange itself, only the connection - * it's given. + * Most `EnclaveServer` instances exist purely as metadata — name, + * description, icon, and the server's public key — fetched from the + * server's HTTP endpoints, with no live connection. A server only + * connects when the user opens it (e.g. clicking its icon in the UI). * - * One `EnclaveServer` instance exists per connected server. It has no - * knowledge of other servers, DMs, or app-level UI state — that lives in - * `Enclave`, which owns and manages multiple `EnclaveServer` instances. + * On connect, `EnclaveServer` performs the key exchange and identity + * handshake itself, storing the resulting `serverPublicKey`, and creates + * the underlying `EnclaveWebSocket` for the live protocol connection. + * Once connected, it also manages channels and messages for that server. + * + * `Enclave` owns and manages multiple `EnclaveServer` instances — one + * per known server, connected or not. */ export default class EnclaveServer { - public websocket: EnclaveWebSocket; + public serverPublicKey?: Uint8Array; + public hostname: string; + public websocket?: EnclaveWebSocket; - public constructor( - hostname: string, - clientSecretKey: Uint8Array, + public constructor(hostname: string) { + this.hostname = hostname; + } + + public async disconnect() { + this.websocket?.websocket.close(); + this.websocket = undefined; + } + + public async connect( clientPublicKey: Uint8Array, + clientSecretKey: Uint8Array, ) { - this.websocket = new EnclaveWebSocket(hostname); - this.websocket.clientSecretKey = clientSecretKey; - this.websocket.clientPublicKey = clientPublicKey; - this.websocket.init(); + this.websocket = new EnclaveWebSocket(this.hostname); + + const publicKeyString = base58.encode(clientPublicKey); + + const timestamp = Date.now(); + + const msg = new TextEncoder().encode(`${timestamp}@${this.hostname}`); + + this.websocket.send({ + method: "Initialize", + public_key: publicKeyString, + signature: base58.encode(ed.sign(msg, clientSecretKey)), + + timestamp, + hostname: this.hostname, + }); + + const initialized = await this.websocket.read(); + + if (initialized.method !== "Initialized") { + this.disconnect(); + throw Error("Invalid method from server, closing. "); + } + + if (initialized.hostname !== this.hostname) { + this.disconnect(); + throw Error("Server is trying to be a middle man"); + } + + if (Math.abs(initialized.timestamp - timestamp) > 2000) { + this.disconnect(); + throw Error("Server timestamp is desynced"); + } + + const sigMsg = new TextEncoder().encode( + `${initialized.timestamp}@${this.hostname}@${publicKeyString}`, + ); + + this.serverPublicKey = base58.decode(initialized.public_key); + const signature = base58.decode(initialized.signature); + + if (!ed.verify(signature, sigMsg, this.serverPublicKey)) { + this.websocket.websocket.close(); + throw Error("Invalid signature"); + } } } diff --git a/src/app/ws.ts b/src/app/ws.ts index e419f0f..a34a7c5 100644 --- a/src/app/ws.ts +++ b/src/app/ws.ts @@ -1,85 +1,25 @@ import { ClientMethod, ServerMethod } from "./protocol"; -import { base58 } from "@scure/base"; -import * as ed from "@noble/ed25519"; -import { sha512 } from "@noble/hashes/sha2.js"; - -ed.hashes.sha512 = sha512; /** * A protocol-aware wrapper around the browser `WebSocket`. * - * Owns exactly one connection's lifecycle: the initial key exchange and - * identity handshake, and storage of the resulting keys (this client's - * keypair, the server's verified public key) for the lifetime of the - * connection. - * * Exposes typed protocol methods (`ServerMethod` / `ClientMethod`) rather * than raw WebSocket messages. Has no concept of channels, messages, or * app state — `EnclaveServer` builds on top of this to add those. */ export default class EnclaveWebSocket { public websocket: WebSocket; - public hostname: string; onOpenQueue: Array<() => void>; - public clientPublicKey: Uint8Array; - public clientSecretKey: Uint8Array; - - public serverPublicKey?: Uint8Array; - public constructor(hostname: string) { - this.clientPublicKey = new Uint8Array(); - this.clientSecretKey = new Uint8Array(); this.onOpenQueue = new Array(); - this.hostname = hostname; this.websocket = new WebSocket("ws://" + hostname); this.websocket.onopen = () => { this.onOpenQueue.forEach((fun) => fun()); }; } - public async init() { - const publicKeyString = base58.encode(this.clientPublicKey); - - const timestamp = Date.now(); - - const msg = new TextEncoder().encode(`${timestamp}@${this.hostname}`); - - this.send({ - method: "Initialize", - public_key: publicKeyString, - signature: base58.encode(ed.sign(msg, this.clientSecretKey)), - - timestamp, - hostname: this.hostname, - }); - - const initialized = await this.read(); - - if (initialized.method !== "Initialized") { - this.websocket.close(); - throw Error("Invalid method from server, closing. "); - } - - if (initialized.hostname !== this.hostname) { - this.websocket.close(); - throw Error("Server is trying to be a middle man"); - } - - const sigMsg = new TextEncoder().encode( - `${initialized.timestamp}@${this.hostname}@${publicKeyString}`, - ); - - this.serverPublicKey = base58.decode(initialized.public_key); - const signature = base58.decode(initialized.signature); - - if (!ed.verify(signature, sigMsg, this.serverPublicKey)) { - this.websocket.close(); - throw Error("Invalid signature"); - } - } - public async send(method: ServerMethod) { if (this.websocket.readyState !== WebSocket.OPEN) { return new Promise((ok) => {