Skip to content

Commit

Permalink
feature: Fallback to GCM encryption if ECB fails
Browse files Browse the repository at this point in the history
  • Loading branch information
inwaar committed Oct 6, 2024
1 parent fc39b37 commit 271b0c2
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 41 deletions.
2 changes: 0 additions & 2 deletions src/client-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -25,7 +24,6 @@ const CLIENT_OPTIONS = {
pollingInterval: 3000,
pollingTimeout: 1000,
debug: false,
encryptionVersion: 1,
};

module.exports = {
Expand Down
32 changes: 4 additions & 28 deletions src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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') {
Expand Down
120 changes: 109 additions & 11 deletions src/encryption-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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;
Expand All @@ -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
*/
Expand All @@ -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');
Expand All @@ -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;
Expand All @@ -142,5 +241,4 @@ class EncryptionServiceGCM {

module.exports = {
EncryptionService,
EncryptionServiceGCM,
};

0 comments on commit 271b0c2

Please sign in to comment.