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
JavaScript
'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;