diff --git a/src/client-options.js b/src/client-options.js index 1d9f22f..30af666 100644 --- a/src/client-options.js +++ b/src/client-options.js @@ -14,6 +14,7 @@ * @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', @@ -24,6 +25,7 @@ const CLIENT_OPTIONS = { pollingInterval: 3000, pollingTimeout: 1000, debug: false, + encryptionVersion: 1, }; module.exports = { diff --git a/src/client.js b/src/client.js index 44d11b0..df42d79 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 } = require('./encryption-service'); +const { EncryptionService, EncryptionServiceGCM } = require('./encryption-service'); const { PROPERTY } = require('./property'); const { PROPERTY_VALUE } = require('./property-value'); const { CLIENT_OPTIONS } = require('./client-options'); @@ -139,12 +139,6 @@ class Client extends EventEmitter { */ this._transformer = new PropertyTransformer(); - /** - * @type {EncryptionService} - * @private - */ - this._encryptionService = new EncryptionService(); - /** * Client options * @@ -152,6 +146,29 @@ 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(); /** * @private @@ -370,6 +387,7 @@ class Client extends EventEmitter { t: 'bind', uid: 0, }), + tag: this._encryptionService.getTag(), }); } @@ -422,6 +440,7 @@ class Client extends EventEmitter { t: 'pack', uid: 0, pack: this._encryptionService.encrypt(message), + tag: this._encryptionService.getTag(), }); } @@ -456,7 +475,14 @@ class Client extends EventEmitter { this._trace('IN.MSG', message); // Extract encrypted package from message using device key (if available) - const pack = this._unpack(message); + 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); + } // If package type is response to handshake if (pack.t === 'dev') { diff --git a/src/encryption-service.js b/src/encryption-service.js index e1c7d3b..70645d1 100644 --- a/src/encryption-service.js +++ b/src/encryption-service.js @@ -57,8 +57,90 @@ class EncryptionService { const str = cipher.update(JSON.stringify(output), 'utf8', 'base64'); return str + cipher.final('base64'); } + + /** + * Required for GCM, return nothing here. + */ + getTag() { + return undefined; + } +} + +/** + * Nonce and AAD values for GCM encryption + */ +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 { + + /** + * @param {string} [key] AES key + */ + constructor(key = '{yxAHAY_Lm6pbC/<') { + /** + * Device crypto-key + * @type {string} + * @private + */ + this._key = key; + } + + /** + * @param {string} key + */ + setKey(key) { + this._key = key; + } + + /** + * @returns {string} + */ + getKey() { + return this._key; + } + + /** + * Decrypt UDP message + * @param {object} input Response object + * @param {string} input.pack Encrypted JSON string + * @param {string} input.tag Auth Tag for GCM decryption + */ + decrypt(input) { + 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'); + decipher.setAuthTag(decTag); + } + const str = decipher.update(input.pack, 'base64', 'utf8'); + return JSON.parse(str + decipher.final('utf8')); + } + + /** + * Encrypt UDP message. Sets _encTag to be received before sending with getTag() and added to message. + * @param {object} output Request object + */ + encrypt(output) { + 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; + } } module.exports = { EncryptionService, + EncryptionServiceGCM, };