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 committed Aug 13, 2024
1 parent 5607ac8 commit 5905e90
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');
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 };

/**
* 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
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');
}

/**
* 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,
};

0 comments on commit 5905e90

Please sign in to comment.