UNPKG

miio

Version:

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

169 lines (139 loc) 3.77 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; this.header.writeUInt32BE(this._serverStamp + secondsPassed, 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;