Skip to content

Commit

Permalink
Merge pull request #657 from Apollon77/35upd
Browse files Browse the repository at this point in the history
Updates to make testing happy
  • Loading branch information
Apollon77 authored Sep 15, 2024
2 parents 99d2582 + b0e7790 commit 8fc57a0
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 96 deletions.
8 changes: 6 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
name: Lint

on: push
on:
push:
branches:
- '*'
pull_request: {}

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v4

- name: Install and cache dependencies
uses: bahmutov/npm-install@v1
Expand Down
10 changes: 7 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
name: Test

on: push
on:
push:
branches:
- '*'
pull_request: {}

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x, 14.x, 16.x, 18.x]
node-version: [12.x, 14.x, 16.x, 18.x, 20.x, 22.x]

steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v4

- name: Install and cache dependencies
uses: bahmutov/npm-install@v1
Expand Down
98 changes: 56 additions & 42 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -411,17 +411,35 @@ class TuyaDevice extends EventEmitter {

// Queue this request and limit concurrent set requests to one
return this._setQueue.add(() => pTimeout(new Promise((resolve, reject) => {
// Make sure we only resolve or reject once
let resolvedOrRejected = false;

// Send request and wait for response
try {
if (this.device.version === '3.5') {
this._currentSequenceN++;
}

// Send request
this._send(buffer);
this._send(buffer).catch(error => {
if (options.shouldWaitForResponse && !resolvedOrRejected) {
reject(error);
}
});
if (options.shouldWaitForResponse) {
this._setResolver = resolve;
this._setResolver = () => {
if (!resolvedOrRejected) {
resolve();
}
};

this._setResolveAllowGet = options.isSetCallToGetData;
} else {
resolvedOrRejected = true;
resolve();
}
} catch (error) {
resolvedOrRejected = true;
reject(error);
}
}), this._responseTimeout * 2500, () => {
Expand Down Expand Up @@ -487,11 +505,16 @@ class TuyaDevice extends EventEmitter {
// Check for response
const now = new Date();

this._pingPongTimeout = setTimeout(() => {
if (this._lastPingAt < now) {
this.disconnect();
}
}, this._responseTimeout * 1000);
if (this._pingPongTimeout === null) {
// If we do not expect a pong from a former ping, we need to set a timeout
this._pingPongTimeout = setTimeout(() => {
if (this._lastPingAt < now) {
this.disconnect();
}
}, this._responseTimeout * 1000);
} else {
debug('There was no response to the last ping.');
}

// Send ping
this.client.write(buffer);
Expand Down Expand Up @@ -702,9 +725,6 @@ class TuyaDevice extends EventEmitter {
}

_packetHandler(packet) {
// Response was received, so stop waiting
clearTimeout(this._sendTimeout);

// Protocol 3.4, 3.5 - Response to Msg 0x03
if (packet.commandByte === CommandType.SESS_KEY_NEG_RES) {
if (!this.connectPromise) {
Expand All @@ -717,8 +737,9 @@ class TuyaDevice extends EventEmitter {
debug('Protocol 3.4, 3.5: Local Random Key: ' + this._tmpLocalKey.toString('hex'));
debug('Protocol 3.4, 3.5: Remote Random Key: ' + this._tmpRemoteKey.toString('hex'));

if(this.device.version === '3.4' || this.device.version === '3.5')
if (this.device.version === '3.4' || this.device.version === '3.5') {
this._currentSequenceN = packet.sequenceN - 1;
}

const calcLocalHmac = this.device.parser.cipher.hmac(this._tmpLocalKey).toString('hex');
const expLocalHmac = packet.payload.slice(16, 16 + 32).toString('hex');
Expand Down Expand Up @@ -749,10 +770,12 @@ class TuyaDevice extends EventEmitter {
this.sessionKey[i] = this._tmpLocalKey[i] ^ this._tmpRemoteKey[i];
}

if(this.device.version === '3.4')
if (this.device.version === '3.4') {
this.sessionKey = this.device.parser.cipher._encrypt34({data: this.sessionKey});
else if(this.device.version === '3.5')
} else if (this.device.version === '3.5') {
this.sessionKey = this.device.parser.cipher._encrypt35({data: this.sessionKey, iv: this._tmpLocalKey});
}

debug('Protocol 3.4, 3.5: Session Key: ' + this.sessionKey.toString('hex'));
debug('Protocol 3.4, 3.5: Initialization done');

Expand All @@ -770,6 +793,8 @@ class TuyaDevice extends EventEmitter {
*/
this.emit('heartbeat');

clearTimeout(this._pingPongTimeout);
this._pingPongTimeout = null;
this._lastPingAt = new Date();

return;
Expand All @@ -780,14 +805,6 @@ class TuyaDevice extends EventEmitter {
packet.commandByte === CommandType.CONTROL ||
packet.commandByte === CommandType.CONTROL_NEW
) && packet.payload === false) {

if(this.device.version === '3.5')
{
// Move resolver to next sequence for incoming response after ack
this._resolvers[(parseInt(packet.sequenceN) + 1).toString()] = this._resolvers[packet.sequenceN.toString()];
delete this._resolvers[packet.sequenceN.toString()];
}

debug('Got SET ack.');
return;
}
Expand All @@ -804,26 +821,26 @@ class TuyaDevice extends EventEmitter {
this._setResolveAllowGet = undefined;
delete this._resolvers[packet.sequenceN];
this._expectRefreshResponseForSequenceN = undefined;
} else {
} else if (packet.sequenceN in this._resolvers) {
// Call data resolver for sequence number
if (packet.sequenceN in this._resolvers) {
debug('Received DP_REFRESH response packet - resolve');
this._resolvers[packet.sequenceN](packet.payload);

// Remove resolver
delete this._resolvers[packet.sequenceN];
this._expectRefreshResponseForSequenceN = undefined;
} else if (this._expectRefreshResponseForSequenceN && this._expectRefreshResponseForSequenceN in this._resolvers) {
debug('Received DP_REFRESH response packet without data - resolve');
this._resolvers[this._expectRefreshResponseForSequenceN](packet.payload);

// Remove resolver
delete this._resolvers[this._expectRefreshResponseForSequenceN];
this._expectRefreshResponseForSequenceN = undefined;
} else {
debug('Received DP_REFRESH response packet - no resolver found for sequence number' + packet.sequenceN);
}

debug('Received DP_REFRESH response packet - resolve');
this._resolvers[packet.sequenceN](packet.payload);

// Remove resolver
delete this._resolvers[packet.sequenceN];
this._expectRefreshResponseForSequenceN = undefined;
} else if (this._expectRefreshResponseForSequenceN && this._expectRefreshResponseForSequenceN in this._resolvers) {
debug('Received DP_REFRESH response packet without data - resolve');
this._resolvers[this._expectRefreshResponseForSequenceN](packet.payload);

// Remove resolver
delete this._resolvers[this._expectRefreshResponseForSequenceN];
this._expectRefreshResponseForSequenceN = undefined;
} else {
debug('Received DP_REFRESH response packet - no resolver found for sequence number' + packet.sequenceN);
}

return;
}

Expand Down Expand Up @@ -910,9 +927,6 @@ class TuyaDevice extends EventEmitter {
this.device.parser.cipher.setSessionKey(null);

// Clear timeouts
clearTimeout(this._sendTimeout);
clearTimeout(this._connectTimeout);
clearTimeout(this._responseTimeout);
clearInterval(this._pingPongInterval);
clearTimeout(this._pingPongTimeout);

Expand Down
34 changes: 16 additions & 18 deletions lib/cipher.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class TuyaCipher {
return this._encrypt34(options);
}

else if (this.version === '3.5') {
if (this.version === '3.5') {
return this._encrypt35(options);
}

Expand Down Expand Up @@ -95,19 +95,18 @@ class TuyaCipher {
*/
_encrypt35(options) {
let encrypted;
let localIV = Buffer.from((Date.now() * 10).toString().substring(0, 12));
if(options.iv !== undefined) localIV = options.iv.slice(0, 12);
let localIV = Buffer.from((Date.now() * 10).toString().slice(0, 12));
if (options.iv !== undefined) {
localIV = options.iv.slice(0, 12);
}

const cipher = crypto.createCipheriv('aes-128-gcm', this.getKey(), localIV);
if(options.aad !== undefined)
{
if (options.aad === undefined) {
encrypted = Buffer.concat([cipher.update(options.data), cipher.final()]);
} else {
cipher.setAAD(options.aad);
encrypted = Buffer.concat([localIV, cipher.update(options.data), cipher.final(), cipher.getAuthTag(), Buffer.from([0x00, 0x00, 0x99, 0x66])]);
}
else
{
encrypted = Buffer.concat([cipher.update(options.data), cipher.final()]);
}

return encrypted;
}
Expand All @@ -123,7 +122,7 @@ class TuyaCipher {
return this._decrypt34(data);
}

else if (this.version === '3.5') {
if (this.version === '3.5') {
return this._decrypt35(data);
}

Expand Down Expand Up @@ -222,24 +221,23 @@ class TuyaCipher {
*/
_decrypt35(data) {
let result;
let header = data.slice(0, 14);
let iv = data.slice(14, 26);
let tag = data.slice(data.length - 16);
const header = data.slice(0, 14);
const iv = data.slice(14, 26);
const tag = data.slice(data.length - 16);
data = data.slice(26, data.length - 16);

try {
const decipher = crypto.createDecipheriv('aes-128-gcm', this.getKey(), iv);
decipher.setAuthTag(tag);
decipher.setAAD(header);

result = Buffer.concat([decipher.update(data), decipher.final()]);
result = result.slice(4); // remove 32bit return code
result = result.slice(4); // Remove 32bit return code
} catch (_) {
throw new Error('Decrypt failed');
}

// Try to parse data as JSON,
// otherwise return as string.

// Try to parse data as JSON, otherwise return as string.
// 3.5 protocol
// {"protocol":4,"t":1632405905,"data":{"dps":{"101":true},"cid":"00123456789abcde"}}
try {
Expand Down
Loading

0 comments on commit 8fc57a0

Please sign in to comment.