diff --git a/src/client-options.js b/src/client-options.js index 30af666..1d9f22f 100644 --- a/src/client-options.js +++ b/src/client-options.js @@ -14,7 +14,6 @@ * @property {number} pollingInterval=3000 - Device properties polling interval * @property {number} pollingTimeout=1000 - Device properties polling timeout, emits `no_response` events in case of no response from HVAC device for a status request * @property {boolean} debug=false - Trace debug information - * @property {number} encryptionVersion=1 - The encryption method to use: 1 AES-ECB; 2: AES-GCM */ const CLIENT_OPTIONS = { host: '192.168.1.255', @@ -25,7 +24,6 @@ const CLIENT_OPTIONS = { pollingInterval: 3000, pollingTimeout: 1000, debug: false, - encryptionVersion: 1, }; module.exports = { diff --git a/src/client.js b/src/client.js index df42d79..ac0ebc4 100644 --- a/src/client.js +++ b/src/client.js @@ -5,7 +5,7 @@ const EventEmitter = require('events'); const diff = require('object-diff'); const clone = require('clone'); -const { EncryptionService, EncryptionServiceGCM } = require('./encryption-service'); +const { EncryptionService } = require('./encryption-service'); const { PROPERTY } = require('./property'); const { PROPERTY_VALUE } = require('./property-value'); const { CLIENT_OPTIONS } = require('./client-options'); @@ -110,6 +110,14 @@ class Client extends EventEmitter { */ this._socketTimeoutRef = null; + /** + * Bind response timeout reference + * + * @type {number|null} + * @private + */ + this._bindTimeoutRef = null; + /** * Status polling interval reference * @@ -146,29 +154,12 @@ class Client extends EventEmitter { * @private */ this._options = { ...CLIENT_OPTIONS, ...options }; - - /** - * Encryption service based on encryption version. - * @type {EncryptionService} - * @private - */ - switch (this._options.encryptionVersion) { - case 1: - this._encryptionService = new EncryptionService(); - break; - case 2: - this._encryptionService = new EncryptionServiceGCM(); - break; - default: - this._encryptionService = new EncryptionService(); - } - + /** - * Needed for scan request handling * @type {EncryptionService} * @private */ - this._encryptionServiceV1 = new EncryptionService(); + this._encryptionService = new EncryptionService(); /** * @private @@ -377,17 +368,18 @@ class Client extends EventEmitter { * @private */ async _sendBindRequest() { + const encrypted = this._encryptionService.encrypt({ + mac: this._cid, + t: 'bind', + uid: 0, + }); await this._socketSend({ cid: 'app', i: 1, t: 'pack', uid: 0, - pack: this._encryptionService.encrypt({ - mac: this._cid, - t: 'bind', - uid: 0, - }), - tag: this._encryptionService.getTag(), + pack: encrypted.payload, + tag: encrypted.tag, }); } @@ -434,13 +426,14 @@ class Client extends EventEmitter { */ async _sendRequest(message) { this._trace('OUT.MSG', message, this._encryptionService.getKey()); + const encrypted = this._encryptionService.encrypt(message); await this._socketSend({ cid: 'app', i: 0, t: 'pack', uid: 0, - pack: this._encryptionService.encrypt(message), - tag: this._encryptionService.getTag(), + pack: encrypted.payload, + tag: encrypted.tag, }); } @@ -475,14 +468,7 @@ class Client extends EventEmitter { this._trace('IN.MSG', message); // Extract encrypted package from message using device key (if available) - let pack; - if (!this._cid) { - //scan responses are always on v1 - pack = this._encryptionServiceV1.decrypt(message); - } else { - //use set encryption method - pack = this._encryptionService.decrypt(message); - } + const pack = this._unpack(message); // If package type is response to handshake if (pack.t === 'dev') { @@ -493,7 +479,7 @@ class Client extends EventEmitter { if (this._cid) { // If package type is binding confirmation if (pack.t === 'bindok') { - this._handleBindingConfirmationResponse(pack); + this._handleBindingConfirmationResponse(); return; } @@ -553,21 +539,23 @@ class Client extends EventEmitter { */ async _handleHandshakeResponse(message) { this._cid = message.cid || message.mac; + await this._sendBindRequest(); + this._bindTimeoutRef = setTimeout(async () => { + await this._sendBindRequest(); + }, 500); } /** * Handle device binding confirmation response * - * @param pack * @fires Client#connect * @private */ - async _handleBindingConfirmationResponse(pack) { + async _handleBindingConfirmationResponse() { this._trace('SOCKET', 'Connected to device', this._options.host); clearTimeout(this._socketTimeoutRef); - - this._encryptionService.setKey(pack.key); + clearTimeout(this._bindTimeoutRef); await this._requestStatus(); if (this._options.poll) { diff --git a/src/encryption-service.js b/src/encryption-service.js index 70645d1..e0cedd7 100644 --- a/src/encryption-service.js +++ b/src/encryption-service.js @@ -2,10 +2,90 @@ const crypto = require('crypto'); +/** + * @typedef EncryptedMessage + * @property {string} payload + * @property {string|undefined} tag + * @property {string} cipher + * @property {string} key + * @private + */ + /** * @private */ class EncryptionService { + constructor() { + /** + * @private + */ + this._aesCipher = new EcbCipher(); + + /** + * @private + */ + this._gcmCipher = new GcmCipher(); + + /** + * @type {EcbCipher|GcmCipher} + * @private + */ + this._activeCipher = this._aesCipher; + + /** + * @type {number} + * @private + */ + this._bindAttempt = 1; + } + + /** + * @returns {string} + */ + getKey() { + return this._activeCipher.getKey(); + } + + /** + * Decrypt UDP message + * + * @param {object} input Response object + * @param {string} input.pack Encrypted JSON string + * @returns {object} + */ + decrypt(input) { + const payload = this._activeCipher.decrypt(input); + + if (payload.t === 'bindok') { + this._activeCipher.setKey(payload.key); + } + + return payload; + } + + /** + * Encrypt UDP message + * + * @param {object} output Request object + * @returns {string} + */ + encrypt(output) { + if (output.t === 'bind') { + if (this._bindAttempt === 2) { + this._activeCipher = this._gcmCipher; + } + + this._bindAttempt++; + } + + return this._activeCipher.encrypt(output); + } +} + +/** + * @private + */ +class EcbCipher { /** * @param {string} [key] AES key */ @@ -55,31 +135,35 @@ class EncryptionService { encrypt(output) { const cipher = crypto.createCipheriv('aes-128-ecb', this._key, ''); const str = cipher.update(JSON.stringify(output), 'utf8', 'base64'); - return str + cipher.final('base64'); - } - - /** - * Required for GCM, return nothing here. - */ - getTag() { - return undefined; + const payload = str + cipher.final('base64'); + + return { + payload, + cipher: 'ecb', + key: this._key, + }; } } /** * Nonce and AAD values for GCM encryption + * + * @private */ -const GCM_NONCE = Buffer.from('5440784449675a516c5e6313',"hex"); //'\x54\x40\x78\x44\x49\x67\x5a\x51\x6c\x5e\x63\x13'; +const GCM_NONCE = Buffer.from('5440784449675a516c5e6313', 'hex'); //'\x54\x40\x78\x44\x49\x67\x5a\x51\x6c\x5e\x63\x13'; const GCM_AEAD = Buffer.from('qualcomm-test'); -class EncryptionServiceGCM { - +/** + * @private + */ +class GcmCipher { /** * @param {string} [key] AES key */ constructor(key = '{yxAHAY_Lm6pbC/<') { /** * Device crypto-key + * * @type {string} * @private */ @@ -102,12 +186,18 @@ class EncryptionServiceGCM { /** * Decrypt UDP message + * * @param {object} input Response object * @param {string} input.pack Encrypted JSON string * @param {string} input.tag Auth Tag for GCM decryption + * @returns {object} */ decrypt(input) { - const decipher = crypto.createDecipheriv('aes-128-gcm', this._key, GCM_NONCE); + const decipher = crypto.createDecipheriv( + 'aes-128-gcm', + this._key, + GCM_NONCE + ); decipher.setAAD(GCM_AEAD); if (input.tag) { const decTag = Buffer.from(input.tag, 'base64'); @@ -119,28 +209,32 @@ class EncryptionServiceGCM { /** * Encrypt UDP message. Sets _encTag to be received before sending with getTag() and added to message. + * * @param {object} output Request object + * @returns {string} */ encrypt(output) { - const cipher = crypto.createCipheriv('aes-128-gcm', this._key, GCM_NONCE); + const cipher = crypto.createCipheriv( + 'aes-128-gcm', + this._key, + GCM_NONCE + ); cipher.setAAD(GCM_AEAD); const str = cipher.update(JSON.stringify(output), 'utf8', 'base64'); - const outstr = str + cipher.final('base64'); - this._encTag = cipher.getAuthTag().toString('base64').toString("utf-8"); - return outstr - } - - /** - * Receive and clear the last generated tag - */ - getTag() { - const tmpTag = this._encTag; - this._encTag = undefined; - return tmpTag; + const payload = str + cipher.final('base64'); + const tag = cipher.getAuthTag().toString('base64').toString('utf-8'); + + return { + payload, + tag, + cipher: 'gcm', + key: this._key, + }; } } module.exports = { + EcbCipher, + GcmCipher, EncryptionService, - EncryptionServiceGCM, }; diff --git a/test/aes.spec.js b/test/aes.spec.js new file mode 100644 index 0000000..b29b1da --- /dev/null +++ b/test/aes.spec.js @@ -0,0 +1,442 @@ +const dgram = require('dgram'); + +const { Client } = require('../src/client'); + +const device = require('./support/device'); +const { + EncryptionService, + EcbCipher, + GcmCipher, +} = require('../src/encryption-service'); + +jest.mock('dgram'); +jest.useFakeTimers(); + +describe('AES encryption', () => { + let SUT; + let ecb; + let gcm; + let feedClient; + let clientEncrypt; + let clientSocketSend; + let done, connected; + + beforeEach(async () => { + ecb = new EcbCipher(); + gcm = new GcmCipher(); + + dgram.createSocket.mockReturnValue({ + bind: jest.fn(cb => cb()), + setBroadcast: jest.fn(), + on: (event, cb) => (feedClient = cb), + send: (buff, start, length, port, host, cb) => cb(), + close: cb => cb(), + }); + + SUT = new Client({ + autoConnect: false, + }); + + done = new Promise(resolve => SUT.once('update', resolve)); + SUT.once('error', console.error); + + clientSocketSend = jest.spyOn(SUT, '_socketSend'); + clientEncrypt = jest.spyOn(EncryptionService.prototype, 'encrypt'); + + connected = SUT.connect(); + }); + + afterEach(async () => { + await connected; + await done; + await SUT.disconnect(); + jest.restoreAllMocks(); + }); + + it('should get device status with ECB', async () => { + // 1) client sends SCAN + expect(clientSocketSend.mock.calls[0][0]).toMatchInlineSnapshot(` + { + "t": "scan", + } + `); + + // 2) device send DEV to the client as a response to SCAN + const message2 = device.scan(ecb); + expect(message2).toMatchInlineSnapshot(` + { + "cipher": "EcbCipher", + "key": "a3K8Bx%2r8Y7#xDh", + "payload": "{"t":"pack","i":0,"uid":0,"cid":"-CLIENT-ID-","tcid":"","pack":"LP24Ek0OaYogxs3iQLjL4GwpxwJULijnBHAZXZV9hr4AWT6MVTINHBKb52nmO2dIraMvU1lTEk6wfLHnMx7KWOtrGvS4JDfI3aJErejLGI1sKccCVC4o5wRwGV2VfYa+T+QdseyMn6rJqBDjrcCixkpqEMpcnu8xdjXvztU4yLwrD9Cbt47kA8HasyxJXbwOiieHPzc0Sc1my/xyXLHlUhVmSXbN2eMe8SoPC4s/dUamSgKomGMxz2kBOzbBnntE7S3m+bE+PAVAiTLUp1aVUg=="}", + "source": { + "cid": "-CLIENT-ID-", + "i": 0, + "pack": { + "bc": "gree", + "brand": "gree", + "catalog": "gree", + "cid": "-CLIENT-ID-", + "lock": 0, + "mac": "-CLIENT-ID-", + "mid": "10002", + "model": "gree", + "name": "2g8201b5", + "series": "gree", + "t": "dev", + "vender": "1", + "ver": "V1.1.13", + }, + "t": "pack", + "tcid": "", + "uid": 0, + }, + "tag": undefined, + } + `); + feedClient(message2.payload); + + // 3) client sends BIND request, attempt 1 ECB + expect(clientEncrypt.mock.calls[0][0]).toMatchInlineSnapshot(` + { + "mac": "-CLIENT-ID-", + "t": "bind", + "uid": 0, + } + `); + expect(clientEncrypt.mock.results[0].value).toMatchInlineSnapshot(` + { + "cipher": "ecb", + "key": "a3K8Bx%2r8Y7#xDh", + "payload": "ddMD+/erG3STAZvk6iV1oJxrMo6m/1rGE7RiuotePqdcAeWW/XDtzpfgvpySqWVy", + } + `); + + await jest.advanceTimersByTimeAsync(100); + + // 5) device sends BINDOK in response to BIND + const message5 = device.bind(ecb); + expect(message5).toMatchInlineSnapshot(` + { + "cipher": "EcbCipher", + "key": "---BINDED-KEY---", + "payload": "{"t":"pack","i":0,"uid":0,"cid":"-CLIENT-ID-","tcid":"","pack":"T2tGu9JTsZPLMhoPO/mBcqoSWBS63O9Gvp3U/IoUVhtQHOIPj97ISq4ABzS4fL7NmZ+VPPlQDQvy7hlZk+LTLoWC2xoMv7mooDYoeYl4Qnw="}", + "source": { + "cid": "-CLIENT-ID-", + "i": 0, + "pack": { + "key": "---BINDED-KEY---", + "mac": "-CLIENT-ID-", + "r": 200, + "t": "bindok", + }, + "t": "pack", + "tcid": "", + "uid": 0, + }, + "tag": undefined, + } + `); + feedClient(message5.payload); + + // await jest.advanceTimersByTimeAsync(500); + + // 6) client sends STATUS request + expect(clientEncrypt.mock.calls[1][0]).toMatchInlineSnapshot(` + { + "cols": [ + "Pow", + "Mod", + "TemUn", + "SetTem", + "TemSen", + "WdSpd", + "Air", + "Blo", + "Health", + "SwhSlp", + "Lig", + "SwingLfRig", + "SwUpDn", + "Quiet", + "Tur", + "SvSt", + "StHt", + ], + "mac": "-CLIENT-ID-", + "t": "status", + } + `); + expect(clientEncrypt.mock.results[1].value).toMatchInlineSnapshot(` + { + "cipher": "ecb", + "key": "---BINDED-KEY---", + "payload": "JavRSk7J1K823T9pA0lgKsE3GJbwrQLUMKiobwiFTEGxzx6Y67+U8TXRsLNtxKnfdJdeOnLymrBy3bWEGzPogUfVWHGFkj5prdUB5TC3LHVHubu1ILbqdLI9ecbR3+vb+YSBeoh8+G/wkxD8pnt5yoS1GRJuZLZzEGPFhEamq31cc9SS1oL3J5y4dQMniwH0vtm4P+QXaBmM/DBUs0CoGofZxbbqTmV20hhhqVVic/I=", + } + `); + + // 7) device sends DAT in response to STATUS + const message7 = device.status(ecb); + expect(message7).toMatchInlineSnapshot(` + { + "cipher": "EcbCipher", + "key": "---BINDED-KEY---", + "payload": "{"t":"pack","i":0,"uid":0,"cid":"-CLIENT-ID-","tcid":"","pack":"9/E19K3H5vyKLfsAsx0188oFr++Fxk1jrN8GJW8xMteRHX2MvYUrzB/PcZE1KzdgRX/I5jjvWOWlvAAarErFh8sN20illnIwlV4mt2lTZEml41UElq1N8TciM603+MNJvuMH3/9JYXURFN/BqoO28Z/KVOdf3CjMjwlPWBtkUmuilbRjliUc2HVcmk6Pidog6baW13wD0bszO4/QmRdOwDHpU1x9tI5mMi+UFZ44flaFMXwZZnRoxJ5MBTBQkKNRxt9e43Z2BmXNjx5UJylr50Et1bu5e5A4R9oevrUL3ts="}", + "source": { + "cid": "-CLIENT-ID-", + "i": 0, + "pack": { + "cols": [ + "Pow", + "Mod", + "TemUn", + "SetTem", + "TemSen", + "WdSpd", + "Air", + "Blo", + "Health", + "SwhSlp", + "Lig", + "SwingLfRig", + "SwUpDn", + "Quiet", + "Tur", + "SvSt", + "StHt", + ], + "dat": [ + 0, + 1, + 0, + 25, + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 2, + 2, + 0, + 0, + 0, + 0, + ], + "mac": "-CLIENT-ID-", + "r": 200, + "t": "dat", + }, + "t": "pack", + "tcid": "", + "uid": 0, + }, + "tag": undefined, + } + `); + + feedClient(message7.payload); + }); + + it('should get device status with GCM', async () => { + // 1) client sends SCAN + expect(clientSocketSend.mock.calls[0][0]).toMatchInlineSnapshot(` + { + "t": "scan", + } + `); + + // 2) device send DEV to the client as a response to SCAN + const message2 = device.scan(ecb); + expect(message2).toMatchInlineSnapshot(` + { + "cipher": "EcbCipher", + "key": "a3K8Bx%2r8Y7#xDh", + "payload": "{"t":"pack","i":0,"uid":0,"cid":"-CLIENT-ID-","tcid":"","pack":"LP24Ek0OaYogxs3iQLjL4GwpxwJULijnBHAZXZV9hr4AWT6MVTINHBKb52nmO2dIraMvU1lTEk6wfLHnMx7KWOtrGvS4JDfI3aJErejLGI1sKccCVC4o5wRwGV2VfYa+T+QdseyMn6rJqBDjrcCixkpqEMpcnu8xdjXvztU4yLwrD9Cbt47kA8HasyxJXbwOiieHPzc0Sc1my/xyXLHlUhVmSXbN2eMe8SoPC4s/dUamSgKomGMxz2kBOzbBnntE7S3m+bE+PAVAiTLUp1aVUg=="}", + "source": { + "cid": "-CLIENT-ID-", + "i": 0, + "pack": { + "bc": "gree", + "brand": "gree", + "catalog": "gree", + "cid": "-CLIENT-ID-", + "lock": 0, + "mac": "-CLIENT-ID-", + "mid": "10002", + "model": "gree", + "name": "2g8201b5", + "series": "gree", + "t": "dev", + "vender": "1", + "ver": "V1.1.13", + }, + "t": "pack", + "tcid": "", + "uid": 0, + }, + "tag": undefined, + } + `); + feedClient(message2.payload); + + // 3) client sends BIND request, attempt 1 ECB + expect(clientEncrypt.mock.calls[0][0]).toMatchInlineSnapshot(` + { + "mac": "-CLIENT-ID-", + "t": "bind", + "uid": 0, + } + `); + expect(clientEncrypt.mock.results[0].value).toMatchInlineSnapshot(` + { + "cipher": "ecb", + "key": "a3K8Bx%2r8Y7#xDh", + "payload": "ddMD+/erG3STAZvk6iV1oJxrMo6m/1rGE7RiuotePqdcAeWW/XDtzpfgvpySqWVy", + } + `); + + await jest.advanceTimersByTimeAsync(500); + + // 4) client sends BIND request, attempt 2 GCM + expect(clientEncrypt.mock.calls[1][0]).toMatchInlineSnapshot(` + { + "mac": "-CLIENT-ID-", + "t": "bind", + "uid": 0, + } + `); + expect(clientEncrypt.mock.results[1].value).toMatchInlineSnapshot(` + { + "cipher": "gcm", + "key": "{yxAHAY_Lm6pbC/<", + "payload": "JtoT1XUt89L+xbD+HwchGuYFpcEOwFPkOkY2VLSPhOTTY2QLz1tuNw==", + "tag": "nLD1n6lnA33dk/0u9V2siQ==", + } + `); + + // 5) device sends BINDOK in response to BIND + const message5 = device.bind(gcm); + expect(message5).toMatchInlineSnapshot(` + { + "cipher": "GcmCipher", + "key": "---BINDED-KEY---", + "payload": "{"t":"pack","i":0,"uid":0,"cid":"-CLIENT-ID-","tcid":"","pack":"JtoKliwtq5m94pPceGVXWs4iqtkAz2SKSSEaafei4uXTOi8EiBh8cMq4+qHmI6v6ptu9eePWIftb4JbIMuzOhLHP0A==","tag":"CWtsfNdNIuHTyB/Snl1bSw=="}", + "source": { + "cid": "-CLIENT-ID-", + "i": 0, + "pack": { + "key": "---BINDED-KEY---", + "mac": "-CLIENT-ID-", + "r": 200, + "t": "bindok", + }, + "t": "pack", + "tcid": "", + "uid": 0, + }, + "tag": "CWtsfNdNIuHTyB/Snl1bSw==", + } + `); + + feedClient(message5.payload); + + // 6) client sends STATUS request + expect(clientEncrypt.mock.calls[2][0]).toMatchInlineSnapshot(` + { + "cols": [ + "Pow", + "Mod", + "TemUn", + "SetTem", + "TemSen", + "WdSpd", + "Air", + "Blo", + "Health", + "SwhSlp", + "Lig", + "SwingLfRig", + "SwUpDn", + "Quiet", + "Tur", + "SvSt", + "StHt", + ], + "mac": "-CLIENT-ID-", + "t": "status", + } + `); + expect(clientEncrypt.mock.results[2].value).toMatchInlineSnapshot(` + { + "cipher": "gcm", + "key": "---BINDED-KEY---", + "payload": "UDrTdyaO9ZW39UVjLgF3uLeCNJWMC5lr4yJ01vL2QQqmxi0B9v9Y0pQ67Oe53rsi1014mp9NyLjjTK/Jz8yOaRMZQCQqe2AihWDzOLyl3OHk9S6wve7u6ahTlvr+Ot8z5T9ClNM0YO5LX9My/cVIoRqKh8bpKj0wp6EeaHNnzAnp9S6XY1PgOTkYmicP+fqX+3ceiv+U9VqSrFcSoyobQpVgOlF30Ujg8hkg9xJFyA==", + "tag": "kU+8zsV1q/tqV9mXlamlJg==", + } + `); + + // 7) device sends DAT in response to STATUS + const message7 = device.status(gcm); + expect(message7).toMatchInlineSnapshot(` + { + "cipher": "GcmCipher", + "key": "---BINDED-KEY---", + "payload": "{"t":"pack","i":0,"uid":0,"cid":"-CLIENT-ID-","tcid":"","pack":"UDrEOnDfs86Y9TkuNEI4uMDPffTsYIhA2lpTsPP2Pk2gsHJe5ONWpJI40/H1xswi0EZcyNdNqfXGB/HJt4uhUBIZQCQxe3UalGXzOLyizuTk/DCwve715JwB3vqBb/c08FEI5Pg/LeBFLuwC7O1S6xSE9OD3Jxp++6FmP00shwmWoS6UKBGlJisCh2Jw+fqp7UFM9LObmjSKnHAj+kFzTeM5agd5yTnl1Qx2rkM0wemYZsR6m+N0Rbsxa5QsHgd+WdEkZOizY73S/5SIWH0xkfNY1rBkJjnMYU6L9CalJA==","tag":"A5sgsk9Npgk9aUQ8fMmH2A=="}", + "source": { + "cid": "-CLIENT-ID-", + "i": 0, + "pack": { + "cols": [ + "Pow", + "Mod", + "TemUn", + "SetTem", + "TemSen", + "WdSpd", + "Air", + "Blo", + "Health", + "SwhSlp", + "Lig", + "SwingLfRig", + "SwUpDn", + "Quiet", + "Tur", + "SvSt", + "StHt", + ], + "dat": [ + 0, + 1, + 0, + 25, + 0, + 0, + 0, + 1, + 1, + 0, + 1, + 2, + 2, + 0, + 0, + 0, + 0, + ], + "mac": "-CLIENT-ID-", + "r": 200, + "t": "dat", + }, + "t": "pack", + "tcid": "", + "uid": 0, + }, + "tag": "A5sgsk9Npgk9aUQ8fMmH2A==", + } + `); + + feedClient(message7.payload); + }); +}); diff --git a/test/support/device.js b/test/support/device.js new file mode 100644 index 0000000..207f328 --- /dev/null +++ b/test/support/device.js @@ -0,0 +1,34 @@ +const { fixtures } = require('./fixtures'); + +const DEVICE_KEY = '---BINDED-KEY---'; + +const message = (fixture, cipher) => { + const payload = fixtures.pack(fixture); + const encrypted = cipher.encrypt(payload.pack); + + if (fixture === fixtures.bindOk) { + cipher.setKey(DEVICE_KEY); + } + + return { + key: cipher.getKey(), + cipher: cipher.constructor.name, + source: payload, + tag: encrypted.tag, + payload: JSON.stringify({ + ...payload, + pack: encrypted.payload, + tag: encrypted.tag, + }), + }; +}; + +const scan = cipher => message(fixtures.device, cipher); +const bind = cipher => message(fixtures.bindOk, cipher); +const status = cipher => message(fixtures.status, cipher); + +module.exports = { + scan, + bind, + status, +}; diff --git a/test/support/fixtures.js b/test/support/fixtures.js new file mode 100644 index 0000000..217eba0 --- /dev/null +++ b/test/support/fixtures.js @@ -0,0 +1,70 @@ +const DEVICE_KEY = '---BINDED-KEY---'; + +const device = { + t: 'dev', + cid: '-CLIENT-ID-', + bc: 'gree', + brand: 'gree', + catalog: 'gree', + mac: '-CLIENT-ID-', + mid: '10002', + model: 'gree', + name: '2g8201b5', + series: 'gree', + vender: '1', + ver: 'V1.1.13', + lock: 0, +}; + +const bindOk = { + t: 'bindok', + mac: '-CLIENT-ID-', + key: DEVICE_KEY, + r: 200, +}; + +const status = { + t: 'dat', + mac: '-CLIENT-ID-', + r: 200, + cols: [ + 'Pow', + 'Mod', + 'TemUn', + 'SetTem', + 'TemSen', + 'WdSpd', + 'Air', + 'Blo', + 'Health', + 'SwhSlp', + 'Lig', + 'SwingLfRig', + 'SwUpDn', + 'Quiet', + 'Tur', + 'SvSt', + 'StHt', + ], + dat: [0, 1, 0, 25, 0, 0, 0, 1, 1, 0, 1, 2, 2, 0, 0, 0, 0], +}; + +const pack = function (pack, i = 0) { + return { + t: 'pack', + i, + uid: 0, + cid: '-CLIENT-ID-', + tcid: '', + pack, + }; +}; + +module.exports = { + fixtures: { + device, + bindOk, + status, + pack, + }, +};