UNPKG

matterbridge-roborock-vacuum-plugin

Version:
99 lines (98 loc) 4.47 kB
import crypto from 'node:crypto'; import CRC32 from 'crc-32'; import { ResponseMessage } from '../broadcast/model/responseMessage.js'; import * as CryptoUtils from './cryptoHelper.js'; import { Protocol } from '../broadcast/model/protocol.js'; import { Parser } from 'binary-parser/dist/binary_parser.js'; export class MessageDeserializer { context; headerMessageParser; contentMessageParser; logger; supportedVersions = ['1.0', 'A01', 'B01']; constructor(context, logger) { this.context = context; this.logger = logger; this.headerMessageParser = new Parser() .endianness('big') .string('version', { length: 3, }) .uint32('seq') .uint32('nonce') .uint32('timestamp') .uint16('protocol'); this.contentMessageParser = new Parser() .endianness('big') .uint16('payloadLen') .buffer('payload', { length: 'payloadLen', }) .uint32('crc32'); } deserialize(duid, message) { const header = this.headerMessageParser.parse(message); if (!this.supportedVersions.includes(header.version)) { throw new Error('unknown protocol version ' + header.version); } if (header.protocol === Protocol.hello_response || header.protocol === Protocol.ping_response) { const dpsValue = { id: header.seq, result: { version: header.version, nonce: header.nonce, }, }; return new ResponseMessage(duid, { [header.protocol.toString()]: dpsValue }); } const data = this.contentMessageParser.parse(message.subarray(this.headerMessageParser.sizeOf())); const crc32 = CRC32.buf(message.subarray(0, message.length - 4)) >>> 0; const expectedCrc32 = message.readUInt32BE(message.length - 4); if (crc32 != expectedCrc32) { throw new Error(`Wrong CRC32 ${crc32}, expected ${expectedCrc32}`); } const localKey = this.context.getLocalKey(duid); if (!localKey) { this.logger.notice(`Unable to retrieve local key for ${duid}, it should be from other vacuums`); return new ResponseMessage(duid, { dps: { id: 0, result: null } }); } if (header.version == '1.0') { const aesKey = CryptoUtils.md5bin(CryptoUtils.encodeTimestamp(header.timestamp) + localKey + CryptoUtils.SALT); const decipher = crypto.createDecipheriv('aes-128-ecb', aesKey, null); data.payload = Buffer.concat([decipher.update(data.payload), decipher.final()]); } else if (header.version == 'A01') { const iv = CryptoUtils.md5hex(header.nonce.toString(16).padStart(8, '0') + '726f626f726f636b2d67a6d6da').substring(8, 24); const decipher = crypto.createDecipheriv('aes-128-cbc', localKey, iv); data.payload = Buffer.concat([decipher.update(data.payload), decipher.final()]); } else if (header.version == 'B01') { const iv = CryptoUtils.md5hex(header.nonce.toString(16).padStart(8, '0') + '5wwh9ikChRjASpMU8cxg7o1d2E').substring(9, 25); const decipher = crypto.createDecipheriv('aes-128-cbc', localKey, iv); data.payload = Buffer.concat([decipher.update(data.payload), decipher.final()]); } if (header.protocol == Protocol.map_response) { return new ResponseMessage(duid, { dps: { id: 0, result: null } }); } if (header.protocol == Protocol.rpc_response || header.protocol == Protocol.general_request) { return this.deserializeRpcResponse(duid, data); } else { this.logger.error('unknown protocol: ' + header.protocol); return new ResponseMessage(duid, { dps: { id: 0, result: null } }); } } deserializeRpcResponse(duid, data) { const payload = JSON.parse(data.payload.toString()); const dps = payload.dps; this.parseJsonInDps(dps, Protocol.general_request); this.parseJsonInDps(dps, Protocol.rpc_response); return new ResponseMessage(duid, dps); } parseJsonInDps(dps, index) { const indexString = index.toString(); if (dps[indexString] !== undefined) { dps[indexString] = JSON.parse(dps[indexString]); } } }