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) { 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); } }