schoolbox-ws/src/auth.ts
2025-11-07 23:34:40 +01:00

85 lines
2.3 KiB
TypeScript

import {
buildCommand,
buildError,
Command,
parseCommand,
} from "./commandbuilder.ts";
import { Device } from "./device.ts";
function toHex(data: Uint8Array): string {
return Array.from(data).map((b) => b.toString(16)).join("");
}
async function generateHMAC(key: string, message: string) {
const encoder = new TextEncoder();
const keyData = encoder.encode(key);
const messageData = encoder.encode(message);
const cryptoKey = await crypto.subtle.importKey(
"raw",
keyData,
{ name: "HMAC", hash: { name: "SHA-256" } },
false,
["sign"],
);
const signature = await crypto.subtle.sign(
"HMAC",
cryptoKey,
messageData,
);
return toHex(new Uint8Array(signature));
}
export class Authentication {
_socket: WebSocket;
_encoder: TextEncoder;
nonce: string;
device: Device | undefined;
signature: string | undefined;
constructor(socket: WebSocket) {
this._socket = socket;
this._encoder = new TextEncoder();
this.nonce = toHex(crypto.getRandomValues(new Uint8Array(32)));
}
async authenticate(message: Command) { // ugh, js doesn't have async constructors, hence the seeprate init function.
if (
message.d == undefined || !("id" in message.d) ||
typeof (message.d.id) !== "string" || !(message.d.id in Device.devices)
) {
this._socket.send(buildError(1, "Invalid packet. Missing ID"));
this._socket.close();
return;
}
this._socket.addEventListener("message", (msg) => this._validate(msg));
this.device = Device.devices[message.d.id];
this.signature = await generateHMAC(this.device.password, this.nonce);
this._socket.send(
buildCommand("auth_nonce", { nonce: this.nonce }),
);
}
_validate(msg: MessageEvent<any>) {
if (!this.device || !this.signature) return;
const parsed = parseCommand(msg.data);
if (
parsed === null || parsed.c !== "auth_validate" ||
parsed.d === undefined || !("signature" in parsed.d)
) return;
if (this.signature !== parsed.d.signature) {
this._socket.send(buildError(2, "Invalid signature."));
this._socket.close;
return;
}
this._socket.send(buildCommand("auth_ok"));
this.device?.connect(this._socket);
this._socket.removeEventListener("message", this._validate);
}
}