New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

tuyapi

Package Overview
Dependencies
Maintainers
0
Versions
69
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

tuyapi - npm Package Compare versions

Comparing version 7.5.2 to 7.6.0

3

index.d.ts

@@ -40,3 +40,4 @@ declare module 'tuyapi' {

dps: number;
set: string|number|boolean
set: string|number|boolean;
cid?: string;
multiple?: boolean;

@@ -43,0 +44,0 @@ shouldWaitForResponse?: boolean;

@@ -150,3 +150,3 @@ // Import packages

const commandByte = this.device.version === '3.4' ? CommandType.DP_QUERY_NEW : CommandType.DP_QUERY;
const commandByte = this.device.version === '3.4' || this.device.version === '3.5' ? CommandType.DP_QUERY_NEW : CommandType.DP_QUERY;

@@ -371,3 +371,3 @@ // Create byte buffer

if (this.device.version === '3.4') {
if (this.device.version === '3.4' || this.device.version === '3.5') {
/*

@@ -404,3 +404,3 @@ {

const commandByte = this.device.version === '3.4' ? CommandType.CONTROL_NEW : CommandType.CONTROL;
const commandByte = this.device.version === '3.4' || this.device.version === '3.5' ? CommandType.CONTROL_NEW : CommandType.CONTROL;
const sequenceN = ++this._currentSequenceN;

@@ -417,13 +417,31 @@ // Encode into packet

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 = data => {
if (!resolvedOrRejected) {
resolve(data);
}
};
this._setResolveAllowGet = options.isSetCallToGetData;
} else {
resolvedOrRejected = true;
resolve();
}
} catch (error) {
resolvedOrRejected = true;
reject(error);

@@ -494,7 +512,12 @@ }

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

@@ -679,3 +702,3 @@ // Send ping

if (this.device.version === '3.4') {
if (this.device.version === '3.4' || this.device.version === '3.5') {
// Negotiate session key then emit 'connected'

@@ -692,6 +715,6 @@ // 16 bytes random + 32 bytes hmac

debug('Protocol 3.4: Negotiate Session Key - Send Msg 0x03');
debug('Protocol 3.4, 3.5: Negotiate Session Key - Send Msg 0x03');
this.client.write(buffer);
} catch (error) {
debug('Error binding key for protocol 3.4: ' + error);
debug('Error binding key for protocol 3.4, 3.5: ' + error);
}

@@ -712,9 +735,6 @@

_packetHandler(packet) {
// Response was received, so stop waiting
clearTimeout(this._sendTimeout);
// Protocol 3.4 - Response to Msg 0x03
// Protocol 3.4, 3.5 - Response to Msg 0x03
if (packet.commandByte === CommandType.SESS_KEY_NEG_RES) {
if (!this.connectPromise) {
debug('Protocol 3.4: Ignore Key exchange message because no connection in progress.');
debug('Protocol 3.4, 3.5: Ignore Key exchange message because no connection in progress.');
return;

@@ -725,5 +745,9 @@ }

this._tmpRemoteKey = packet.payload.subarray(0, 16);
debug('Protocol 3.4: Local Random Key: ' + this._tmpLocalKey.toString('hex'));
debug('Protocol 3.4: Remote Random Key: ' + this._tmpRemoteKey.toString('hex'));
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') {
this._currentSequenceN = packet.sequenceN - 1;
}
const calcLocalHmac = this.device.parser.cipher.hmac(this._tmpLocalKey).toString('hex');

@@ -758,6 +782,11 @@ const expLocalHmac = packet.payload.slice(16, 16 + 32).toString('hex');

this.sessionKey = this.device.parser.cipher._encrypt34({data: this.sessionKey});
debug('Protocol 3.4: Session Key: ' + this.sessionKey.toString('hex'));
debug('Protocol 3.4: Initialization done');
if (this.device.version === '3.4') {
this.sessionKey = this.device.parser.cipher._encrypt34({data: this.sessionKey});
} 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');
this.device.parser.cipher.setSessionKey(this.sessionKey);

@@ -777,2 +806,4 @@ this.device.key = this.sessionKey;

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

@@ -804,22 +835,22 @@

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);
debug('Received DP_REFRESH response packet - resolve');
this._resolvers[packet.sequenceN](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);
}
// 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;

@@ -911,5 +942,2 @@ }

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

@@ -916,0 +944,0 @@ clearTimeout(this._pingPongTimeout);

@@ -19,3 +19,3 @@ const crypto = require('crypto');

/**
* Sets the session key used for Protocol 3.4
* Sets the session key used for Protocol 3.4, 3.5
* @param {Buffer} sessionKey Session key

@@ -41,2 +41,6 @@ */

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

@@ -88,2 +92,27 @@ }

/**
* Encrypt data for protocol 3.5
* @param {Object} options Options for encryption
* @param {String} options.data data to encrypt
* @param {Boolean} [options.base64=true] `true` to return result in Base64
* @returns {Buffer|String} returns Buffer unless options.base64 is true
*/
_encrypt35(options) {
let encrypted;
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) {
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])]);
}
return encrypted;
}
/**
* Decrypts data.

@@ -99,2 +128,6 @@ * @param {String|Buffer} data to decrypt

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

@@ -188,2 +221,47 @@ }

/**
* Decrypts data for protocol 3.5
* @param {String|Buffer} data to decrypt
* @returns {Object|String}
* returns object if data is JSON, else returns string
*/
_decrypt35(data) {
let result;
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
} catch (_) {
throw new Error('Decrypt failed');
}
// Try to parse data as JSON, otherwise return as string.
// 3.5 protocol
// {"protocol":4,"t":1632405905,"data":{"dps":{"101":true},"cid":"00123456789abcde"}}
try {
if (result.indexOf(this.version) === 0) {
result = result.slice(15);
}
const res = JSON.parse(result);
if ('data' in res) {
const resData = res.data;
resData.t = res.t;
return resData; // Or res.data // for compatibility with tuya-mqtt
}
return res;
} catch (_) {
return result;
}
}
/**
* Calculates a MD5 hash.

@@ -200,3 +278,3 @@ * @param {String} data to hash

* Gets the key used for encryption/decryption
* @returns {String} sessionKey (if set for protocol 3.4) or key
* @returns {String} sessionKey (if set for protocol 3.4, 3.5) or key
*/

@@ -208,3 +286,3 @@ getKey() {

/**
* Returns the HMAC for the current key (sessionKey if set for protocol 3.4 or key)
* Returns the HMAC for the current key (sessionKey if set for protocol 3.4, 3.5 or key)
* @param {Buffer} data data to hash

@@ -211,0 +289,0 @@ * @returns {Buffer} HMAC

@@ -5,2 +5,3 @@ const Cipher = require('./cipher');

const HEADER_SIZE = 16;
const HEADER_SIZE_3_5 = 4;

@@ -113,3 +114,4 @@ /**

if (prefix !== 0x000055AA) {
// Only for 3.4 and 3.5 packets
if (prefix !== 0x000055AA && prefix !== 0x00006699) {
throw new TypeError(`Prefix does not match: ${buffer.toString('hex')}`);

@@ -121,3 +123,6 @@ }

const suffixLocation = buffer.indexOf('0000AA55', 0, 'hex');
let suffixLocation = buffer.indexOf('0000AA55', 0, 'hex');
if (suffixLocation === -1) {// Couldn't find 0000AA55 during parse
suffixLocation = buffer.indexOf('00009966', 0, 'hex');
}

@@ -132,18 +137,38 @@ if (suffixLocation !== buffer.length - 4) {

if (suffix !== 0x0000AA55) {
if (suffix !== 0x0000AA55 && suffix !== 0x00009966) {
throw new TypeError(`Suffix does not match: ${buffer.toString('hex')}`);
}
// Get sequence number
const sequenceN = buffer.readUInt32BE(4);
let sequenceN;
let commandByte;
let payloadSize;
// Get command byte
const commandByte = buffer.readUInt32BE(8);
if (suffix === 0x0000AA55) {
// Get sequence number
sequenceN = buffer.readUInt32BE(4);
// Get payload size
const payloadSize = buffer.readUInt32BE(12);
// Get command byte
commandByte = buffer.readUInt32BE(8);
// Check for payload
if (buffer.length - 8 < payloadSize) {
throw new TypeError(`Packet missing payload: payload has length ${payloadSize}.`);
// Get payload size
payloadSize = buffer.readUInt32BE(12);
// Check for payload
if (buffer.length - 8 < payloadSize) {
throw new TypeError(`Packet missing payload: payload has length ${payloadSize}.`);
}
} else if (suffix === 0x00009966) {
// Get sequence number
sequenceN = buffer.readUInt32BE(6);
// Get command byte
commandByte = buffer.readUInt32BE(10);
// Get payload size
payloadSize = buffer.readUInt32BE(14) + 14; // Add additional bytes for extras
// Check for payload
if (buffer.length - 8 < payloadSize) {
throw new TypeError(`Packet missing payload: payload has length ${payloadSize}.`);
}
}

@@ -165,28 +190,38 @@

let payload;
if (returnCode & 0xFFFFFF00) {
if (this.version === '3.4' && !packageFromDiscovery) {
payload = buffer.slice(HEADER_SIZE, HEADER_SIZE + payloadSize - 0x24);
if (this.version === '3.5') {
payload = buffer.slice(HEADER_SIZE_3_5, HEADER_SIZE_3_5 + payloadSize);
sequenceN = buffer.slice(6, 10).readUInt32BE();
commandByte = buffer.slice(10, 14).readUInt32BE();
} else {
if (returnCode & 0xFFFFFF00) {
if (this.version === '3.4' && !packageFromDiscovery) {
payload = buffer.slice(HEADER_SIZE, HEADER_SIZE + payloadSize - 0x24);
} else if (this.version === '3.5' && !packageFromDiscovery) {
payload = buffer.slice(HEADER_SIZE, HEADER_SIZE + payloadSize - 0x24);
} else {
payload = buffer.slice(HEADER_SIZE, HEADER_SIZE + payloadSize - 8);
}
} else if (this.version === '3.4' && !packageFromDiscovery) {
payload = buffer.slice(HEADER_SIZE + 4, HEADER_SIZE + payloadSize - 0x24);
} else if (this.version === '3.5' && !packageFromDiscovery) {
payload = buffer.slice(HEADER_SIZE + 4, HEADER_SIZE + payloadSize - 0x24);
} else {
payload = buffer.slice(HEADER_SIZE, HEADER_SIZE + payloadSize - 8);
payload = buffer.slice(HEADER_SIZE + 4, HEADER_SIZE + payloadSize - 8);
}
} else if (this.version === '3.4' && !packageFromDiscovery) {
payload = buffer.slice(HEADER_SIZE + 4, HEADER_SIZE + payloadSize - 0x24);
} else {
payload = buffer.slice(HEADER_SIZE + 4, HEADER_SIZE + payloadSize - 8);
}
// Check CRC
if (this.version === '3.4' && !packageFromDiscovery) {
const expectedCrc = buffer.slice(HEADER_SIZE + payloadSize - 0x24, buffer.length - 4).toString('hex');
const computedCrc = this.cipher.hmac(buffer.slice(0, HEADER_SIZE + payloadSize - 0x24)).toString('hex');
// Check CRC
if (this.version === '3.4' && !packageFromDiscovery) {
const expectedCrc = buffer.slice(HEADER_SIZE + payloadSize - 0x24, buffer.length - 4).toString('hex');
const computedCrc = this.cipher.hmac(buffer.slice(0, HEADER_SIZE + payloadSize - 0x24)).toString('hex');
if (expectedCrc !== computedCrc) {
throw new Error(`HMAC mismatch: expected ${expectedCrc}, was ${computedCrc}. ${buffer.toString('hex')}`);
}
} else {
const expectedCrc = buffer.readInt32BE(HEADER_SIZE + payloadSize - 8);
const computedCrc = crc(buffer.slice(0, payloadSize + 8));
if (expectedCrc !== computedCrc) {
throw new Error(`HMAC mismatch: expected ${expectedCrc}, was ${computedCrc}. ${buffer.toString('hex')}`);
}
} else if (this.version !== '3.5') {
const expectedCrc = buffer.readInt32BE(HEADER_SIZE + payloadSize - 8);
const computedCrc = crc(buffer.slice(0, payloadSize + 8));
if (expectedCrc !== computedCrc) {
throw new Error(`CRC mismatch: expected ${expectedCrc}, was ${computedCrc}. ${buffer.toString('hex')}`);
if (expectedCrc !== computedCrc) {
throw new Error(`CRC mismatch: expected ${expectedCrc}, was ${computedCrc}. ${buffer.toString('hex')}`);
}
}

@@ -221,2 +256,9 @@ }

// Incoming 3.5 data isn't 0 because of iv and tag so check size after
if (this.version === '3.5') {
if (data.length === 0) {
return false;
}
}
// Try to parse data as JSON.

@@ -297,2 +339,6 @@ // If error, return as string.

if (this.version === '3.5') {
return this._encode35(options);
}
return this._encodePre34(options);

@@ -431,4 +477,51 @@ }

}
/**
* Encodes a payload into a Tuya-protocol-complient packet for protocol version 3.5
* @param {Object} options Options for encoding
* @param {Buffer|String|Object} options.data data to encode
* @param {Boolean} options.encrypted whether or not to encrypt the data
* @param {Number} options.commandByte
* command byte of packet (use CommandType definitions)
* @param {Number} [options.sequenceN] optional, sequence number
* @returns {Buffer} Encoded Buffer
*/
_encode35(options) {
let payload = options.data;
if (options.commandByte !== CommandType.DP_QUERY &&
options.commandByte !== CommandType.HEART_BEAT &&
options.commandByte !== CommandType.DP_QUERY_NEW &&
options.commandByte !== CommandType.SESS_KEY_NEG_START &&
options.commandByte !== CommandType.SESS_KEY_NEG_FINISH &&
options.commandByte !== CommandType.DP_REFRESH) {
// Add 3.5 header
const buffer = Buffer.alloc(payload.length + 15);
Buffer.from('3.5').copy(buffer, 0);
payload.copy(buffer, 15);
payload = buffer;
// OO options.data = '3.5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + options.data;
}
// Allocate buffer for prefix, unknown, sequence, command, length
let buffer = Buffer.alloc(18);
// Add prefix, command, and length
buffer.writeUInt32BE(0x00006699, 0); // Prefix
buffer.writeUInt16BE(0x0, 4); // Unknown
buffer.writeUInt32BE(options.sequenceN, 6); // Sequence
buffer.writeUInt32BE(options.commandByte, 10); // Command
buffer.writeUInt32BE(payload.length + 28 /* 0x1c */, 14); // Length
const encrypted = this.cipher.encrypt({
data: payload,
aad: buffer.slice(4, 18)
});
buffer = Buffer.concat([buffer, encrypted]);
return buffer;
}
}
module.exports = {MessageParser, CommandType};
{
"name": "tuyapi",
"version": "7.5.2",
"version": "7.6.0",
"description": "An easy-to-use API for devices that use Tuya's cloud services",

@@ -41,3 +41,3 @@ "main": "index.js",

"dependencies": {
"debug": "^4.3.4",
"debug": "^4.3.7",
"p-queue": "6.6.2",

@@ -53,3 +53,3 @@ "p-retry": "4.6.2",

"delay": "4.4.1",
"documentation": "^12.3.0",
"documentation": "^14.0.0",
"nyc": "15.1.0",

@@ -56,0 +56,0 @@ "xo": "0.25.4"

@@ -199,3 +199,3 @@ # TuyAPI 🌧 🔌

### Ports
- [python-tuya](https://github.com/clach04/python-tuya) a Python port by [clach04](https://github.com/clach04)
- [TinyTuya](https://github.com/jasonacox/tinytuya) a Python port by [jasonacox](https://github.com/jasonacox) and [uzlonewolf](https://github.com/uzlonewolf)
- [aiotuya](https://github.com/frawau/aiotuya) a Python port by [frawau](https://github.com/frawau)

@@ -206,2 +206,3 @@ - [m4rcus.TuyaCore](https://github.com/Marcus-L/m4rcus.TuyaCore) a .NET port by [Marcus-L](https://github.com/Marcus-L)

- [rust-tuyapi](https://github.com/EmilSodergren/rust-tuyapi) a Rust port by [EmilSodergren](https://github.com/EmilSodergren)
- [GoTuya](https://github.com/Binozo/GoTuya) a Go port by [Binozo](https://github.com/Binozo)

@@ -223,2 +224,4 @@ ### Clients for Tuya's Cloud

- [smart-home-panel](https://github.com/MadeleineSmith/smart-home-panel-fe) A website for controlling a smart light bulb
- [GoTuya](https://github.com/Binozo/GoTuya) An easy-to-use api to control Tuya devices on the local network
- [luminea2mqtt](https://github.com/dennis9819/luminea2mqtt/tree/master) An expandable luminea2mqtt bridge with HA Autodiscover

@@ -225,0 +228,0 @@

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc