Skip to content

Commit

Permalink
Adding GCM encryption support.
Browse files Browse the repository at this point in the history
This adds GCM encryption which can be toogled on via new client option `encryptionVersion` (1 = ECB, 2 = GCM). It is needed for newer firmware versions and replaces existing ECB for all pack encryptions BUT the scan command, which still runs on ECB for compatibility.

Tested on firmware 362001065279+U-WB05RT13V1.23
  • Loading branch information
cont1nuity authored and inwaar committed Oct 6, 2024
1 parent 063a8ea commit fc39b37
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 8 deletions.
2 changes: 2 additions & 0 deletions src/client-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -24,6 +25,7 @@ const CLIENT_OPTIONS = {
pollingInterval: 3000,
pollingTimeout: 1000,
debug: false,
encryptionVersion: 1,
};

module.exports = {
Expand Down
42 changes: 34 additions & 8 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 } = require('./encryption-service');
const { EncryptionService, EncryptionServiceGCM } = require('./encryption-service');

Check failure on line 8 in src/client.js

View workflow job for this annotation

GitHub Actions / ci (20.x)

Replace `·EncryptionService,·EncryptionServiceGCM·` with `⏎····EncryptionService,⏎····EncryptionServiceGCM,⏎`
const { PROPERTY } = require('./property');
const { PROPERTY_VALUE } = require('./property-value');
const { CLIENT_OPTIONS } = require('./client-options');
Expand Down Expand Up @@ -139,19 +139,36 @@ class Client extends EventEmitter {
*/
this._transformer = new PropertyTransformer();

/**
* @type {EncryptionService}
* @private
*/
this._encryptionService = new EncryptionService();

/**
* Client options
*
* @type {CLIENT_OPTIONS}
* @private
*/
this._options = { ...CLIENT_OPTIONS, ...options };

Check failure on line 149 in src/client.js

View workflow job for this annotation

GitHub Actions / ci (20.x)

Delete `········`
/**
* Encryption service based on encryption version.

Check failure on line 151 in src/client.js

View workflow job for this annotation

GitHub Actions / ci (20.x)

Expected 1 lines after block description
* @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();
}

Check failure on line 165 in src/client.js

View workflow job for this annotation

GitHub Actions / ci (20.x)

Delete `········`
/**
* Needed for scan request handling

Check failure on line 167 in src/client.js

View workflow job for this annotation

GitHub Actions / ci (20.x)

Expected 1 lines after block description
* @type {EncryptionService}
* @private
*/
this._encryptionServiceV1 = new EncryptionService();

/**
* @private
Expand Down Expand Up @@ -370,6 +387,7 @@ class Client extends EventEmitter {
t: 'bind',
uid: 0,
}),
tag: this._encryptionService.getTag(),
});
}

Expand Down Expand Up @@ -422,6 +440,7 @@ class Client extends EventEmitter {
t: 'pack',
uid: 0,
pack: this._encryptionService.encrypt(message),
tag: this._encryptionService.getTag(),
});
}

Expand Down Expand Up @@ -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') {
Expand Down
82 changes: 82 additions & 0 deletions src/encryption-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,90 @@ class EncryptionService {
const str = cipher.update(JSON.stringify(output), 'utf8', 'base64');
return str + cipher.final('base64');
}

Check failure on line 60 in src/encryption-service.js

View workflow job for this annotation

GitHub Actions / ci (20.x)

Delete `····`
/**

Check warning on line 61 in src/encryption-service.js

View workflow job for this annotation

GitHub Actions / ci (20.x)

Missing JSDoc @returns declaration
* 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';

Check failure on line 72 in src/encryption-service.js

View workflow job for this annotation

GitHub Actions / ci (20.x)

Replace `"hex"` with `·'hex'`
const GCM_AEAD = Buffer.from('qualcomm-test');

class EncryptionServiceGCM {

Check failure on line 75 in src/encryption-service.js

View workflow job for this annotation

GitHub Actions / ci (20.x)

Delete `⏎····`

/**
* @param {string} [key] AES key
*/
constructor(key = '{yxAHAY_Lm6pbC/<') {
/**
* Device crypto-key

Check failure on line 82 in src/encryption-service.js

View workflow job for this annotation

GitHub Actions / ci (20.x)

Expected 1 lines after block description
* @type {string}
* @private
*/
this._key = key;
}

/**
* @param {string} key
*/
setKey(key) {
this._key = key;
}

/**
* @returns {string}
*/
getKey() {
return this._key;
}

/**

Check warning on line 103 in src/encryption-service.js

View workflow job for this annotation

GitHub Actions / ci (20.x)

Missing JSDoc @returns declaration
* Decrypt UDP message

Check failure on line 104 in src/encryption-service.js

View workflow job for this annotation

GitHub Actions / ci (20.x)

Expected 1 lines after block description
* @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'));
}

/**

Check warning on line 120 in src/encryption-service.js

View workflow job for this annotation

GitHub Actions / ci (20.x)

Missing JSDoc @returns declaration
* 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
}

/**

Check warning on line 133 in src/encryption-service.js

View workflow job for this annotation

GitHub Actions / ci (20.x)

Missing JSDoc @returns declaration
* Receive and clear the last generated tag
*/
getTag() {
const tmpTag = this._encTag;
this._encTag = undefined;
return tmpTag;
}
}

module.exports = {
EncryptionService,
EncryptionServiceGCM,
};

0 comments on commit fc39b37

Please sign in to comment.