96 lines
2.9 KiB
TypeScript
96 lines
2.9 KiB
TypeScript
import { base58 } from "@scure/base";
|
|
import * as ed from "@noble/ed25519";
|
|
import EnclaveWebSocket from "./ws";
|
|
import { sha512 } from "@noble/hashes/sha2.js";
|
|
import { getWSUrl } from "@/lib/serverList";
|
|
import { ServerMeta } from "./protocol";
|
|
|
|
ed.hashes.sha512 = sha512;
|
|
|
|
/**
|
|
* Represents a known Enclave server, whether connected or not.
|
|
*
|
|
* 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).
|
|
*
|
|
* 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 serverPublicKey?: Uint8Array;
|
|
public hostname: string;
|
|
public isSecure: boolean;
|
|
public websocket?: EnclaveWebSocket;
|
|
public meta?: ServerMeta;
|
|
|
|
public constructor(hostname: string, isSecure: boolean) {
|
|
this.hostname = hostname;
|
|
this.isSecure = isSecure;
|
|
}
|
|
|
|
public disconnect() {
|
|
this.websocket?.websocket.close();
|
|
this.websocket = undefined;
|
|
}
|
|
|
|
public async connect(
|
|
clientPublicKey: Uint8Array,
|
|
clientSecretKey: Uint8Array,
|
|
) {
|
|
this.websocket = new EnclaveWebSocket(
|
|
getWSUrl(this.hostname, this.isSecure),
|
|
);
|
|
|
|
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");
|
|
}
|
|
}
|
|
}
|