UNPKG

node-miio

Version:

Control Mi Home devices, such as Mi Robot Vacuums, Mi Air Purifiers, Mi Smart Home Gateway (Aqara) and more

192 lines (162 loc) 4.08 kB
'use strict'; const crypto = require('crypto'); const debug = require('debug')('miio:packet'); class Packet { constructor(discovery = false) { this.discovery = discovery; this.header = Buffer.alloc(2 + 2 + 4 + 4 + 4 + 16); this.header[0] = 0x21; this.header[1] = 0x31; for (let i = 4; i < 32; i++) { this.header[i] = 0xff; } this._serverStampTime = 0; this._token = null; } handshake() { this.data = null; } handleHandshakeReply() { if (this._token === null) { const token = this.checksum; if (token.toString('hex').match(/^[fF0]+$/)) { // Device did not return its token so we set our token to null this._token = null; } else { this.token = this.checksum; } } } get needsHandshake() { /* * Handshake if we: * 1) do not have a token * 2) it has been longer then 120 seconds since last received message */ return !this._token || Date.now() - this._serverStampTime > 120000; } get raw() { if (this.data) { // Send a command to the device if (!this._token) { throw new Error('Token is required to send commands'); } for (let i = 4; i < 8; i++) { this.header[i] = 0x00; } // Update the stamp to match server if (this._serverStampTime) { const secondsPassed = Math.floor( (Date.now() - this._serverStampTime) / 1000 ); try { this.header.writeUInt32BE(this._serverStamp + secondsPassed, 12); } catch (err) { // If it fails to post the difference in seconds, just use the same stamp it previously had this.header.writeUInt32BE(this._serverStamp, 12); } } // Encrypt the data let cipher = crypto.createCipheriv( 'aes-128-cbc', this._tokenKey, this._tokenIV ); let encrypted = Buffer.concat([cipher.update(this.data), cipher.final()]); // Set the length this.header.writeUInt16BE(32 + encrypted.length, 2); // Calculate the checksum let digest = crypto .createHash('md5') .update(this.header.slice(0, 16)) .update(this._token) .update(encrypted) .digest(); digest.copy(this.header, 16); debug('->', this.header); return Buffer.concat([this.header, encrypted]); } else { // Handshake this.header.writeUInt16BE(32, 2); for (let i = 4; i < 32; i++) { this.header[i] = 0xff; } debug('->', this.header); return this.header; } } set raw(msg) { msg.copy(this.header, 0, 0, 32); debug('<-', this.header); const stamp = this.stamp; if (stamp > 0) { // If the device returned a stamp, store it this._serverStamp = this.stamp; this._serverStampTime = Date.now(); } const encrypted = msg.slice(32); if (this.discovery) { // This packet is only intended to be used for discovery this.data = encrypted.length > 0; } else { // Normal packet, decrypt data if (encrypted.length > 0) { if (!this._token) { debug('<- No token set, unable to handle packet'); this.data = null; return; } const digest = crypto .createHash('md5') .update(this.header.slice(0, 16)) .update(this._token) .update(encrypted) .digest(); const checksum = this.checksum; if (!checksum.equals(digest)) { debug( '<- Invalid packet, checksum was', checksum, 'should be', digest ); this.data = null; } else { let decipher = crypto.createDecipheriv( 'aes-128-cbc', this._tokenKey, this._tokenIV ); this.data = Buffer.concat([ decipher.update(encrypted), decipher.final(), ]); } } else { this.data = null; } } } get token() { return this._token; } set token(t) { this._token = Buffer.from(t); this._tokenKey = crypto.createHash('md5').update(t).digest(); this._tokenIV = crypto .createHash('md5') .update(this._tokenKey) .update(t) .digest(); } get checksum() { return this.header.slice(16); } get deviceId() { return this.header.readUInt32BE(8); } get stamp() { return this.header.readUInt32BE(12); } } module.exports = Packet;