Better structure and docs
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import EnclaveServer from "./server";
|
||||
import * as ed from "@noble/ed25519";
|
||||
|
||||
/**
|
||||
* Top-level application state and client-side logic.
|
||||
*
|
||||
* Owns all active connections — currently `EnclaveServer` instances, one
|
||||
* per connected server, keyed by server id — and will later also own
|
||||
* `DMServer` connections once direct messages are implemented.
|
||||
*
|
||||
* This is where UI-facing logic lives: handling user actions (button
|
||||
* clicks, switching the active server, sending a message from an input
|
||||
* box), and exposing state for the UI layer to render. The UI itself
|
||||
* should stay a pure display of what `Enclave` exposes — it should not
|
||||
* reach into `EnclaveServer` or `EnclaveWebSocket` directly.
|
||||
*/
|
||||
export default class Enclave {
|
||||
private clientSecretKey: Uint8Array;
|
||||
private clientPublicKey: Uint8Array;
|
||||
public server: EnclaveServer;
|
||||
|
||||
public constructor() {
|
||||
this.clientSecretKey = ed.utils.randomSecretKey();
|
||||
this.clientPublicKey = ed.getPublicKey(this.clientSecretKey);
|
||||
|
||||
this.server = new EnclaveServer(
|
||||
"localhost:3415",
|
||||
this.clientSecretKey,
|
||||
this.clientPublicKey,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
export interface ClientMeta {}
|
||||
|
||||
export interface ServerMeta {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export type ClientMethod =
|
||||
| {
|
||||
method: "Initialized";
|
||||
public_key: string;
|
||||
signature: string;
|
||||
timestamp: number;
|
||||
hostname: string;
|
||||
}
|
||||
| {
|
||||
method: "Error";
|
||||
error: string;
|
||||
};
|
||||
|
||||
export type ServerMethod =
|
||||
| {
|
||||
method: "Initialize";
|
||||
public_key: string;
|
||||
signature: string;
|
||||
timestamp: number;
|
||||
hostname: string;
|
||||
}
|
||||
| {
|
||||
method: "Meta";
|
||||
}
|
||||
| {
|
||||
method: "Error";
|
||||
error: string;
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import EnclaveWebSocket from "./ws";
|
||||
/**
|
||||
* Manages a single connection to an Enclave server: channels, server
|
||||
* metadata, and messages for that server.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
export default class EnclaveServer {
|
||||
public websocket: EnclaveWebSocket;
|
||||
|
||||
public constructor(
|
||||
hostname: string,
|
||||
clientSecretKey: Uint8Array,
|
||||
clientPublicKey: Uint8Array,
|
||||
) {
|
||||
this.websocket = new EnclaveWebSocket(hostname);
|
||||
this.websocket.clientSecretKey = clientSecretKey;
|
||||
this.websocket.clientPublicKey = clientPublicKey;
|
||||
this.websocket.init();
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
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<void>((ok) => {
|
||||
this.onOpenQueue.push(() => {
|
||||
this.websocket.send(JSON.stringify(method));
|
||||
ok();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.websocket.send(JSON.stringify(method));
|
||||
}
|
||||
|
||||
public async read(): Promise<ClientMethod> {
|
||||
return new Promise((ok) => {
|
||||
this.websocket.onmessage = (msg) => {
|
||||
const data = JSON.parse(msg.data);
|
||||
ok(data);
|
||||
this.websocket.onmessage = null;
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user