Skip to content

Commit

Permalink
feature: Add GCM encryption
Browse files Browse the repository at this point in the history
make it work!!!
  • Loading branch information
inwaar committed Oct 27, 2024
1 parent fcd8633 commit 5b133c3
Show file tree
Hide file tree
Showing 6 changed files with 695 additions and 69 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
70 changes: 29 additions & 41 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 @@ -110,6 +110,14 @@ class Client extends EventEmitter {
*/
this._socketTimeoutRef = null;

/**
* Bind response timeout reference
*
* @type {number|null}
* @private
*/
this._bindTimeoutRef = null;

/**
* Status polling interval reference
*
Expand Down Expand Up @@ -146,29 +154,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 @@ -377,17 +368,18 @@ class Client extends EventEmitter {
* @private
*/
async _sendBindRequest() {
const encrypted = this._encryptionService.encrypt({
mac: this._cid,
t: 'bind',
uid: 0,
});
await this._socketSend({
cid: 'app',
i: 1,
t: 'pack',
uid: 0,
pack: this._encryptionService.encrypt({
mac: this._cid,
t: 'bind',
uid: 0,
}),
tag: this._encryptionService.getTag(),
pack: encrypted.payload,
tag: encrypted.tag,
});
}

Expand Down Expand Up @@ -434,13 +426,14 @@ class Client extends EventEmitter {
*/
async _sendRequest(message) {
this._trace('OUT.MSG', message, this._encryptionService.getKey());
const encrypted = this._encryptionService.encrypt(message);
await this._socketSend({
cid: 'app',
i: 0,
t: 'pack',
uid: 0,
pack: this._encryptionService.encrypt(message),
tag: this._encryptionService.getTag(),
pack: encrypted.payload,
tag: encrypted.tag,
});
}

Expand Down Expand Up @@ -475,14 +468,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 All @@ -493,7 +479,7 @@ class Client extends EventEmitter {
if (this._cid) {
// If package type is binding confirmation
if (pack.t === 'bindok') {
this._handleBindingConfirmationResponse(pack);
this._handleBindingConfirmationResponse();
return;
}

Expand Down Expand Up @@ -553,21 +539,23 @@ class Client extends EventEmitter {
*/
async _handleHandshakeResponse(message) {
this._cid = message.cid || message.mac;

await this._sendBindRequest();
this._bindTimeoutRef = setTimeout(async () => {
await this._sendBindRequest();
}, 500);
}

/**
* Handle device binding confirmation response
*
* @param pack
* @fires Client#connect
* @private
*/
async _handleBindingConfirmationResponse(pack) {
async _handleBindingConfirmationResponse() {
this._trace('SOCKET', 'Connected to device', this._options.host);
clearTimeout(this._socketTimeoutRef);

this._encryptionService.setKey(pack.key);
clearTimeout(this._bindTimeoutRef);

await this._requestStatus();
if (this._options.poll) {
Expand Down
146 changes: 120 additions & 26 deletions src/encryption-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,90 @@

const crypto = require('crypto');

/**
* @typedef EncryptedMessage
* @property {string} payload
* @property {string|undefined} tag
* @property {string} cipher
* @property {string} key
* @private
*/

/**
* @private
*/
class EncryptionService {
constructor() {
/**
* @private
*/
this._aesCipher = new EcbCipher();

/**
* @private
*/
this._gcmCipher = new GcmCipher();

/**
* @type {EcbCipher|GcmCipher}
* @private
*/
this._activeCipher = this._aesCipher;

/**
* @type {number}
* @private
*/
this._bindAttempt = 1;
}

/**
* @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) {
const payload = this._activeCipher.decrypt(input);

if (payload.t === 'bindok') {
this._activeCipher.setKey(payload.key);
}

return payload;
}

/**
* Encrypt UDP message
*
* @param {object} output Request object
* @returns {string}
*/
encrypt(output) {
if (output.t === 'bind') {
if (this._bindAttempt === 2) {
this._activeCipher = this._gcmCipher;
}

this._bindAttempt++;
}

return this._activeCipher.encrypt(output);
}
}

/**
* @private
*/
class EcbCipher {
/**
* @param {string} [key] AES key
*/
Expand Down Expand Up @@ -55,31 +135,35 @@ class EncryptionService {
encrypt(output) {
const cipher = crypto.createCipheriv('aes-128-ecb', this._key, '');
const str = cipher.update(JSON.stringify(output), 'utf8', 'base64');
return str + cipher.final('base64');
}

/**
* Required for GCM, return nothing here.
*/
getTag() {
return undefined;
const payload = str + cipher.final('base64');

return {
payload,
cipher: 'ecb',
key: this._key,
};
}
}

/**
* 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 +186,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,28 +209,32 @@ 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
}

/**
* Receive and clear the last generated tag
*/
getTag() {
const tmpTag = this._encTag;
this._encTag = undefined;
return tmpTag;
const payload = str + cipher.final('base64');
const tag = cipher.getAuthTag().toString('base64').toString('utf-8');

return {
payload,
tag,
cipher: 'gcm',
key: this._key,
};
}
}

module.exports = {
EcbCipher,
GcmCipher,
EncryptionService,
EncryptionServiceGCM,
};
Loading

0 comments on commit 5b133c3

Please sign in to comment.