UNPKG

matterbridge-roborock-vacuum-plugin

Version:
130 lines (129 loc) 5.23 kB
import * as dgram from 'node:dgram'; import { Parser } from 'binary-parser/dist/binary_parser.js'; import crypto from 'node:crypto'; import CRC32 from 'crc-32'; import { AbstractClient } from '../abstractClient.js'; export class LocalNetworkUDPClient extends AbstractClient { clientName = 'LocalNetworkUDPClient'; shouldReconnect = false; PORT = 58866; server = undefined; V10Parser; L01Parser; constructor(logger, context) { super(logger, context); this.V10Parser = new Parser() .endianness('big') .string('version', { length: 3 }) .uint32('seq') .uint16('protocol') .uint16('payloadLen') .buffer('payload', { length: 'payloadLen' }) .uint32('crc32'); this.L01Parser = new Parser() .endianness('big') .string('version', { length: 3 }) .string('field1', { length: 4 }) .string('field2', { length: 2 }) .uint16('payloadLen') .buffer('payload', { length: 'payloadLen' }) .uint32('crc32'); this.logger = logger; } connect() { try { this.server = dgram.createSocket('udp4'); this.server.bind(this.PORT); this.server.on('message', this.onMessage.bind(this)); this.server.on('error', this.onError.bind(this)); } catch (err) { this.logger.error(`Failed to create UDP socket: ${err}`); this.server = undefined; } } disconnect() { if (this.server) { return new Promise((resolve) => { this.server?.close(() => { this.server = undefined; resolve(); }); }); } return Promise.resolve(); } send(duid, request) { this.logger.debug(`Sending request to ${duid}: ${JSON.stringify(request)}`); return Promise.resolve(); } async onError(result) { this.logger.error(`UDP socket error: ${result}`); if (this.server) { this.server.close(); this.server = undefined; } } async onMessage(buffer) { const message = await this.deserializeMessage(buffer); this.logger.debug('Received message: ' + JSON.stringify(message)); } async deserializeMessage(buffer) { const version = buffer.toString('latin1', 0, 3); if (version !== '1.0' && version !== 'L01' && version !== 'A01') { throw new Error('unknown protocol version ' + version); } let data; switch (version) { case '1.0': data = await this.deserializeV10Message(buffer); return JSON.parse(data); case 'L01': data = await this.deserializeL01Message(buffer); return JSON.parse(data); case 'A01': return undefined; default: throw new Error('unknown protocol version ' + version); } } async deserializeV10Message(message) { const data = this.V10Parser.parse(message); const crc32 = CRC32.buf(message.subarray(0, message.length - 4)) >>> 0; const expectedCrc32 = data.crc32; if (crc32 != expectedCrc32) { throw new Error('wrong CRC32 ' + crc32 + ', expected ' + expectedCrc32); } const decipher = crypto.createDecipheriv('aes-128-ecb', Buffer.from('qWKYcdQWrbm9hPqe', 'utf8'), null); decipher.setAutoPadding(false); let decrypted = decipher.update(data.payload, 'binary', 'utf8'); decrypted += decipher.final('utf8'); const paddingLength = decrypted.charCodeAt(decrypted.length - 1); return decrypted.slice(0, -paddingLength); } async deserializeL01Message(message) { const data = this.L01Parser.parse(message); const crc32 = CRC32.buf(message.subarray(0, message.length - 4)) >>> 0; const expectedCrc32 = data.crc32; if (crc32 != expectedCrc32) { throw new Error('wrong CRC32 ' + crc32 + ', expected ' + expectedCrc32); } const payload = data.payload; const key = crypto.createHash('sha256').update(Buffer.from('qWKYcdQWrbm9hPqe', 'utf8')).digest(); const digestInput = message.subarray(0, 9); const digest = crypto.createHash('sha256').update(digestInput).digest(); const iv = digest.subarray(0, 12); const tag = payload.subarray(payload.length - 16); const ciphertext = payload.subarray(0, payload.length - 16); const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv); decipher.setAuthTag(tag); try { const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]); return decrypted.toString('utf8'); } catch (e) { const message = e && typeof e === 'object' && 'message' in e ? e.message : String(e); throw new Error('failed to decrypt: ' + message + ' / iv: ' + iv.toString('hex') + ' / tag: ' + tag.toString('hex') + ' / encrypted: ' + ciphertext.toString('hex')); } } }