Enclave server architecture
This commit is contained in:
+3
-5
@@ -23,10 +23,8 @@ export default class Enclave {
|
|||||||
this.clientSecretKey = ed.utils.randomSecretKey();
|
this.clientSecretKey = ed.utils.randomSecretKey();
|
||||||
this.clientPublicKey = ed.getPublicKey(this.clientSecretKey);
|
this.clientPublicKey = ed.getPublicKey(this.clientSecretKey);
|
||||||
|
|
||||||
this.server = new EnclaveServer(
|
this.server = new EnclaveServer("localhost:3415");
|
||||||
"localhost:3415",
|
|
||||||
this.clientSecretKey,
|
this.server.connect(this.clientPublicKey, this.clientSecretKey);
|
||||||
this.clientPublicKey,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+77
-17
@@ -1,28 +1,88 @@
|
|||||||
|
import { base58 } from "@scure/base";
|
||||||
|
import * as ed from "@noble/ed25519";
|
||||||
import EnclaveWebSocket from "./ws";
|
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
|
* Represents a known Enclave server, whether connected or not.
|
||||||
* metadata, and messages for that server.
|
|
||||||
*
|
*
|
||||||
* Wraps one `EnclaveWebSocket` instance and layers server-level concepts
|
* Most `EnclaveServer` instances exist purely as metadata — name,
|
||||||
* on top of the raw protocol (channels, messages, server meta) — it does
|
* description, icon, and the server's public key — fetched from the
|
||||||
* not know about the handshake/key exchange itself, only the connection
|
* server's HTTP endpoints, with no live connection. A server only
|
||||||
* it's given.
|
* connects when the user opens it (e.g. clicking its icon in the UI).
|
||||||
*
|
*
|
||||||
* One `EnclaveServer` instance exists per connected server. It has no
|
* On connect, `EnclaveServer` performs the key exchange and identity
|
||||||
* knowledge of other servers, DMs, or app-level UI state — that lives in
|
* handshake itself, storing the resulting `serverPublicKey`, and creates
|
||||||
* `Enclave`, which owns and manages multiple `EnclaveServer` instances.
|
* 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 {
|
export default class EnclaveServer {
|
||||||
public websocket: EnclaveWebSocket;
|
public serverPublicKey?: Uint8Array;
|
||||||
|
public hostname: string;
|
||||||
|
public websocket?: EnclaveWebSocket;
|
||||||
|
|
||||||
public constructor(
|
public constructor(hostname: string) {
|
||||||
hostname: string,
|
this.hostname = hostname;
|
||||||
clientSecretKey: Uint8Array,
|
}
|
||||||
|
|
||||||
|
public async disconnect() {
|
||||||
|
this.websocket?.websocket.close();
|
||||||
|
this.websocket = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async connect(
|
||||||
clientPublicKey: Uint8Array,
|
clientPublicKey: Uint8Array,
|
||||||
|
clientSecretKey: Uint8Array,
|
||||||
) {
|
) {
|
||||||
this.websocket = new EnclaveWebSocket(hostname);
|
this.websocket = new EnclaveWebSocket(this.hostname);
|
||||||
this.websocket.clientSecretKey = clientSecretKey;
|
|
||||||
this.websocket.clientPublicKey = clientPublicKey;
|
const publicKeyString = base58.encode(clientPublicKey);
|
||||||
this.websocket.init();
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,85 +1,25 @@
|
|||||||
import { ClientMethod, ServerMethod } from "./protocol";
|
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`.
|
* 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
|
* Exposes typed protocol methods (`ServerMethod` / `ClientMethod`) rather
|
||||||
* than raw WebSocket messages. Has no concept of channels, messages, or
|
* than raw WebSocket messages. Has no concept of channels, messages, or
|
||||||
* app state — `EnclaveServer` builds on top of this to add those.
|
* app state — `EnclaveServer` builds on top of this to add those.
|
||||||
*/
|
*/
|
||||||
export default class EnclaveWebSocket {
|
export default class EnclaveWebSocket {
|
||||||
public websocket: WebSocket;
|
public websocket: WebSocket;
|
||||||
public hostname: string;
|
|
||||||
onOpenQueue: Array<() => void>;
|
onOpenQueue: Array<() => void>;
|
||||||
|
|
||||||
public clientPublicKey: Uint8Array;
|
|
||||||
public clientSecretKey: Uint8Array;
|
|
||||||
|
|
||||||
public serverPublicKey?: Uint8Array;
|
|
||||||
|
|
||||||
public constructor(hostname: string) {
|
public constructor(hostname: string) {
|
||||||
this.clientPublicKey = new Uint8Array();
|
|
||||||
this.clientSecretKey = new Uint8Array();
|
|
||||||
this.onOpenQueue = new Array();
|
this.onOpenQueue = new Array();
|
||||||
|
|
||||||
this.hostname = hostname;
|
|
||||||
this.websocket = new WebSocket("ws://" + hostname);
|
this.websocket = new WebSocket("ws://" + hostname);
|
||||||
this.websocket.onopen = () => {
|
this.websocket.onopen = () => {
|
||||||
this.onOpenQueue.forEach((fun) => fun());
|
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) {
|
public async send(method: ServerMethod) {
|
||||||
if (this.websocket.readyState !== WebSocket.OPEN) {
|
if (this.websocket.readyState !== WebSocket.OPEN) {
|
||||||
return new Promise<void>((ok) => {
|
return new Promise<void>((ok) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user