From 271b0c24d3966a3d44e7cf69cd0365fcca71dc98 Mon Sep 17 00:00:00 2001 From: Igor Starovierov Date: Sun, 6 Oct 2024 12:58:59 +0000 Subject: [PATCH] feature: Fallback to GCM encryption if ECB fails --- src/client-options.js | 2 - src/client.js | 32 ++-------- src/encryption-service.js | 120 ++++++++++++++++++++++++++++++++++---- 3 files changed, 113 insertions(+), 41 deletions(-) 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..06cd439 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'); @@ -146,29 +146,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 @@ -475,14 +458,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') { diff --git a/src/encryption-service.js b/src/encryption-service.js index 70645d1..13208c4 100644 --- a/src/encryption-service.js +++ b/src/encryption-service.js @@ -6,6 +6,84 @@ const crypto = require('crypto'); * @private */ class EncryptionService { + constructor() { + /** + * @private + */ + this._aesCipher = new EcbCipher(); + + /** + * @private + */ + this._gcmCipher = new GcmCipher(); + + /** + * @type {EcbCipher|GcmCipher} + * @private + */ + this._activeCipher = this._aesCipher; + } + + /** + * @param {string} key + */ + setKey(key) { + this._aesCipher.setKey(key); + this._gcmCipher.setKey(key); + } + + /** + * @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) { + try { + return this._activeCipher.decrypt(input); + } catch (e) { + if (this._activeCipher === this._aesCipher) { + this._activeCipher = this._gcmCipher; + return this.decrypt(input); + } + + throw e; + } + } + + /** + * Encrypt UDP message + * + * @param {object} output Request object + * @returns {string} + */ + encrypt(output) { + const result = this._activeCipher.encrypt(output); + return result; + } + + /** + * Required for GCM + * + * @returns {string|undefined} + */ + getTag() { + return this._activeCipher.getTag(); + } +} + +/** + * @private + */ +class EcbCipher { /** * @param {string} [key] AES key */ @@ -57,9 +135,11 @@ class EncryptionService { const str = cipher.update(JSON.stringify(output), 'utf8', 'base64'); return str + cipher.final('base64'); } - + /** - * Required for GCM, return nothing here. + * Not applicable for ECB + * + * @returns {string|undefined} */ getTag() { return undefined; @@ -68,18 +148,23 @@ class EncryptionService { /** * 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 +187,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,19 +210,27 @@ 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 + this._encTag = cipher.getAuthTag().toString('base64').toString('utf-8'); + return outstr; } - + /** * Receive and clear the last generated tag + * + * @returns {string|undefined} */ getTag() { const tmpTag = this._encTag; @@ -142,5 +241,4 @@ class EncryptionServiceGCM { module.exports = { EncryptionService, - EncryptionServiceGCM, };