Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updates to make testing happy #657

Merged
merged 6 commits into from
Sep 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading