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

tuyapi

Package Overview
Dependencies
Maintainers
1
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.4.0 to 7.5.0

461

index.js

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

* @class
* @param {Object} options
* @param {Object} options Options object
* @param {String} [options.ip] IP of device

@@ -61,2 +61,3 @@ * @param {Number} [options.port=6668] port of device

// Set device to user-passed options
version = version.toString();
this.device = {ip, port, id, gwID, key, productKey, version};

@@ -111,2 +112,5 @@ this.globalOptions = {

this._dpRefreshIds = [4, 5, 6, 18, 19, 20];
this._tmpLocalKey = null;
this._tmpRemoteKey = null;
this.sessionKey = null;
}

@@ -117,3 +121,3 @@

* Defaults to returning only the value of the first DPS index.
* @param {Object} [options]
* @param {Object} [options] Options object
* @param {Boolean} [options.schema]

@@ -137,3 +141,3 @@ * true to return entire list of properties from device

*/
get(options = {}) {
async get(options = {}) {
const payload = {

@@ -151,4 +155,3 @@ gwId: this.device.gwID,

debug('GET Payload:');
debug(payload);
const commandByte = this.device.version === '3.4' ? CommandType.DP_QUERY_NEW : CommandType.DP_QUERY;

@@ -158,35 +161,41 @@ // Create byte buffer

data: payload,
commandByte: CommandType.DP_QUERY,
commandByte,
sequenceN: ++this._currentSequenceN
});
// Send request and parse response
return new Promise((resolve, reject) => {
// Send request
this._send(buffer).then(async data => {
if (data === 'json obj data unvalid') {
// Some devices don't respond to DP_QUERY so, for DPS get commands, fall
// back to using SEND with null value. This appears to always work as
// long as the DPS key exist on the device.
// For schema there's currently no fallback options
const setOptions = {
dps: options.dps ? options.dps : 1,
set: null
};
data = await this.set(setOptions);
}
let data;
// Send request to read data - should work in most cases beside Protocol 3.2
if (this.device.version !== '3.2') {
debug('GET Payload:');
debug(payload);
if (typeof data !== 'object' || options.schema === true) {
// Return whole response
resolve(data);
} else if (options.dps) {
// Return specific property
resolve(data.dps[options.dps]);
} else {
// Return first property by default
resolve(data.dps['1']);
}
})
.catch(reject);
});
data = await this._send(buffer);
}
// If data read failed with defined error messages or device uses Protocol 3.2 we need to read differently
if (
this.device.version === '3.2' ||
data === 'json obj data unvalid' || data === 'data format error' /* || data === 'devid not found' */
) {
// Some devices don't respond to DP_QUERY so, for DPS get commands, fall
// back to using SEND with null value. This appears to always work as
// long as the DPS key exist on the device.
// For schema there's currently no fallback options
debug('GET needs to use SEND instead of DP_QUERY to get data');
const setOptions = {
dps: options.dps ? options.dps : 1,
set: null
};
data = await this.set(setOptions);
}
if (typeof data !== 'object' || options.schema === true) {
// Return whole response
return data;
} else if (options.dps) {
// Return specific property
return data.dps[options.dps];
} else {
// Return first property by default
return data.dps['1'];
}
}

@@ -197,3 +206,3 @@

* Defaults to returning all values.
* @param {Object} [options]
* @param {Object} [options] Options object
* @param {Boolean} [options.schema]

@@ -275,3 +284,3 @@ * true to return entire list of properties from device

* Sets a property on a device.
* @param {Object} options
* @param {Object} options Options object
* @param {Number} [options.dps=1] DPS index to set

@@ -317,3 +326,3 @@ * @param {*} [options.set] value to set

// Defaults
let dps = {};
let dps;

@@ -335,3 +344,3 @@ if (options.multiple === true) {

// Get time
const timeStamp = parseInt(new Date() / 1000, 10);
const timeStamp = parseInt(Date.now() / 1000, 10);

@@ -355,5 +364,31 @@ // Construct payload

if (this.device.version === '3.4') {
/*
{
"data": {
"cid": "xxxxxxxxxxxxxxxx",
"ctype": 0,
"dps": {
"1": "manual"
}
},
"protocol": 5,
"t": 1633243332
}
*/
payload = {
data: {
ctype: 0,
...payload
},
protocol: 5,
t: timeStamp
};
delete payload.data.t;
}
debug('SET Payload:');
debug(payload);
const commandByte = this.device.version === '3.4' ? CommandType.CONTROL_NEW : CommandType.CONTROL;
// Encode into packet

@@ -363,3 +398,3 @@ const buffer = this.device.parser.encode({

encrypted: true, // Set commands must be encrypted
commandByte: CommandType.CONTROL,
commandByte,
sequenceN: ++this._currentSequenceN

@@ -456,2 +491,58 @@ });

/**
* Create a deferred promise that resolves as soon as the connection is established.
*/
createDeferredConnectPromise() {
let res;
let rej;
this.connectPromise = new Promise((resolve, reject) => {
res = resolve;
rej = reject;
});
this.connectPromise.resolve = res;
this.connectPromise.reject = rej;
}
/**
* Finish connecting and resolve
*/
_finishConnect() {
this._connected = true;
/**
* Emitted when socket is connected
* to device. This event may be emitted
* multiple times within the same script,
* so don't use this as a trigger for your
* initialization code.
* @event TuyaDevice#connected
*/
this.emit('connected');
// Periodically send heartbeat ping
this._pingPongInterval = setInterval(async () => {
await this._sendPing();
}, this._pingPongPeriod * 1000);
// Automatically ask for dp_refresh so we
// can emit a `dp_refresh` event as soon as possible
if (this.globalOptions.issueRefreshOnConnect) {
this.refresh();
}
// Automatically ask for current state so we
// can emit a `data` event as soon as possible
if (this.globalOptions.issueGetOnConnect) {
this.get();
}
// Resolve
if (this.connectPromise) {
this.connectPromise.resolve(true);
delete this.connectPromise;
}
}
/**
* Connects to the device. Can be called even

@@ -466,141 +557,134 @@ * if device is already connected.

connect() {
if (!this.isConnected()) {
return new Promise((resolve, reject) => {
let resolvedOrRejected = false;
this.client = new net.Socket();
if (this.isConnected()) {
// Return if already connected
return Promise.resolve(true);
}
// Attempt to connect
debug(`Connecting to ${this.device.ip}...`);
this.client.connect(this.device.port, this.device.ip);
if (this.connectPromise) {
// If a connect approach still in progress simply return same Promise
return this.connectPromise;
}
// Default connect timeout is ~1 minute,
// 5 seconds is a more reasonable default
// since `retry` is used.
this.client.setTimeout(this._connectTimeout * 1000, () => {
/**
* Emitted on socket error, usually a
* result of a connection timeout.
* Also emitted on parsing errors.
* @event TuyaDevice#error
* @property {Error} error error event
*/
// this.emit('error', new Error('connection timed out'));
this.client.destroy();
this.emit('error', new Error('connection timed out'));
if (!resolvedOrRejected) {
reject(new Error('connection timed out'));
resolvedOrRejected = true;
}
});
this.createDeferredConnectPromise();
// Add event listeners to socket
this.client = new net.Socket();
// Parse response data
this.client.on('data', data => {
debug(`Received data: ${data.toString('hex')}`);
// Default connect timeout is ~1 minute,
// 5 seconds is a more reasonable default
// since `retry` is used.
this.client.setTimeout(this._connectTimeout * 1000, () => {
/**
* Emitted on socket error, usually a
* result of a connection timeout.
* Also emitted on parsing errors.
* @event TuyaDevice#error
* @property {Error} error error event
*/
// this.emit('error', new Error('connection timed out'));
this.client.destroy();
this.emit('error', new Error('connection timed out'));
if (this.connectPromise) {
this.connectPromise.reject(new Error('connection timed out'));
delete this.connectPromise;
}
});
let packets;
// Add event listeners to socket
try {
packets = this.device.parser.parse(data);
// Parse response data
this.client.on('data', data => {
debug(`Received data: ${data.toString('hex')}`);
if (this.nullPayloadOnJSONError) {
for (const packet of packets) {
if (packet.payload && packet.payload === 'json obj data unvalid') {
this.emit('error', packet.payload);
let packets;
packet.payload = {
dps: {
1: null,
2: null,
3: null,
101: null,
102: null,
103: null
}
};
try {
packets = this.device.parser.parse(data);
if (this.nullPayloadOnJSONError) {
for (const packet of packets) {
if (packet.payload && packet.payload === 'json obj data unvalid') {
this.emit('error', packet.payload);
packet.payload = {
dps: {
1: null,
2: null,
3: null,
101: null,
102: null,
103: null
}
}
};
}
} catch (error) {
debug(error);
this.emit('error', error);
return;
}
}
} catch (error) {
debug(error);
this.emit('error', error);
return;
}
packets.forEach(packet => {
debug('Parsed:');
debug(packet);
packets.forEach(packet => {
debug('Parsed:');
debug(packet);
this._packetHandler.bind(this)(packet);
});
});
this._packetHandler.bind(this)(packet);
});
});
// Handle errors
this.client.on('error', err => {
debug('Error event from socket.', this.device.ip, err);
// Handle errors
this.client.on('error', err => {
debug('Error event from socket.', this.device.ip, err);
this.emit('error', new Error('Error from socket: ' + err.message));
this.emit('error', new Error('Error from socket: ' + err.message));
if (!this._connected && !resolvedOrRejected) {
reject(err);
resolvedOrRejected = true;
}
if (!this._connected && this.connectPromise) {
this.connectPromise.reject(err);
delete this.connectPromise;
}
this.client.destroy();
});
this.client.destroy();
});
// Handle socket closure
this.client.on('close', () => {
debug(`Socket closed: ${this.device.ip}`);
// Handle socket closure
this.client.on('close', () => {
debug(`Socket closed: ${this.device.ip}`);
this.disconnect();
});
this.disconnect();
});
this.client.on('connect', async () => {
debug('Socket connected.');
this.client.on('connect', async () => {
debug('Socket connected.');
this._connected = true;
// Remove connect timeout
this.client.setTimeout(0);
// Remove connect timeout
this.client.setTimeout(0);
if (this.device.version === '3.4') {
// Negotiate session key then emit 'connected'
// 16 bytes random + 32 bytes hmac
try {
this._tmpLocalKey = this.device.parser.cipher.random();
const buffer = this.device.parser.encode({
data: this._tmpLocalKey,
encrypted: true,
commandByte: CommandType.SESS_KEY_NEG_START,
sequenceN: ++this._currentSequenceN
});
/**
* Emitted when socket is connected
* to device. This event may be emitted
* multiple times within the same script,
* so don't use this as a trigger for your
* initialization code.
* @event TuyaDevice#connected
*/
this.emit('connected');
debug('Protocol 3.4: Negotiate Session Key - Send Msg 0x03');
this.client.write(buffer);
} catch (error) {
debug('Error binding key for protocol 3.4: ' + error);
}
// Periodically send heartbeat ping
this._pingPongInterval = setInterval(async () => {
await this._sendPing();
}, this._pingPongPeriod * 1000);
return;
}
// Automatically ask for dp_refresh so we
// can emit a `dp_refresh` event as soon as possible
if (this.globalOptions.issueRefreshOnConnect) {
this.refresh();
}
this._finishConnect();
});
// Automatically ask for current state so we
// can emit a `data` event as soon as possible
if (this.globalOptions.issueGetOnConnect) {
this.get();
}
debug(`Connecting to ${this.device.ip}...`);
this.client.connect(this.device.port, this.device.ip);
// Return
if (!resolvedOrRejected) {
resolve(true);
resolvedOrRejected = true;
}
});
});
}
// Return if already connected
return Promise.resolve(true);
return this.connectPromise;
}

@@ -612,2 +696,53 @@

// Protocol 3.4 - 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.');
return;
}
// 16 bytes _tmpRemoteKey and hmac on _tmpLocalKey
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'));
const calcLocalHmac = this.device.parser.cipher.hmac(this._tmpLocalKey).toString('hex');
const expLocalHmac = packet.payload.slice(16, 16 + 32).toString('hex');
if (expLocalHmac !== calcLocalHmac) {
const err = new Error(`HMAC mismatch(keys): expected ${expLocalHmac}, was ${calcLocalHmac}. ${packet.payload.toString('hex')}`);
if (this.connectPromise) {
this.connectPromise.reject(err);
delete this.connectPromise;
}
this.emit('error', err);
return;
}
// Send response 0x05
const buffer = this.device.parser.encode({
data: this.device.parser.cipher.hmac(this._tmpRemoteKey),
encrypted: true,
commandByte: CommandType.SESS_KEY_NEG_FINISH,
sequenceN: ++this._currentSequenceN
});
this.client.write(buffer);
// Calculate session key
this.sessionKey = Buffer.from(this._tmpLocalKey);
for (let i = 0; i < this._tmpLocalKey.length; i++) {
this.sessionKey[i] = this._tmpLocalKey[i] ^ this._tmpRemoteKey[i];
}
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');
this.device.parser.cipher.setSessionKey(this.sessionKey);
this.device.key = this.sessionKey;
return this._finishConnect();
}
if (packet.commandByte === CommandType.HEART_BEAT) {

@@ -626,3 +761,7 @@ debug(`Pong from ${this.device.ip}`);

if (packet.commandByte === CommandType.CONTROL && packet.payload === false) {
if (
(
packet.commandByte === CommandType.CONTROL ||
packet.commandByte === CommandType.CONTROL_NEW
) && packet.payload === false) {
debug('Got SET ack.');

@@ -652,2 +791,3 @@ return;

debug('Received DATA packet');
debug('data: ' + packet.commandByte + ' : ' + (Buffer.isBuffer(packet.payload) ? packet.payload.toString('hex') : JSON.stringify(packet.payload)));
/**

@@ -666,5 +806,8 @@ * Emitted when data is returned from device.

// Status response to SET command
if (packet.sequenceN === 0 &&
packet.commandByte === CommandType.STATUS &&
typeof this._setResolver === 'function') {
// 3.4 response sequenceN is not '0' just next TODO verify
if (/* Former code: packet.sequenceN === 0 && */
packet.commandByte === CommandType.STATUS &&
typeof this._setResolver === 'function'
) {
this._setResolver(packet.payload);

@@ -698,2 +841,3 @@

this._connected = false;
this.device.parser.cipher.setSessionKey(null);

@@ -733,2 +877,4 @@ // Clear timeouts

* @deprecated since v3.0.0. Will be removed in v4.0.0. Use find() instead.
* @param {Object} options Options object
* @returns {Promise<Boolean|Array>} Promise that resolves to `true` if device is found, `false` otherwise.
*/

@@ -744,3 +890,3 @@ resolveId(options) {

* you must call this before anything else.
* @param {Object} [options]
* @param {Object} [options] Options object
* @param {Boolean} [options.all]

@@ -823,3 +969,4 @@ * true to return array of all found devices

key: this.device.key,
version: this.device.version});
version: this.device.version
});
}

@@ -826,0 +973,0 @@

const crypto = require('crypto');
/**
* Low-level class for encrypting and decrypting payloads.
* @class
* @param {Object} options
* @param {Object} options - Options for the cipher.
* @param {String} options.key localKey of cipher

@@ -14,2 +13,3 @@ * @param {Number} options.version protocol version

constructor(options) {
this.sessionKey = null;
this.key = options.key;

@@ -20,4 +20,12 @@ this.version = options.version.toString();

/**
* Sets the session key used for Protocol 3.4
* @param {Buffer} sessionKey Session key
*/
setSessionKey(sessionKey) {
this.sessionKey = sessionKey;
}
/**
* Encrypts data.
* @param {Object} options
* @param {Object} options Options for encryption
* @param {String} options.data data to encrypt

@@ -30,4 +38,19 @@ * @param {Boolean} [options.base64=true] `true` to return result in Base64

encrypt(options) {
const cipher = crypto.createCipheriv('aes-128-ecb', this.key, '');
if (this.version === '3.4') {
return this._encrypt34(options);
}
return this._encryptPre34(options);
}
/**
* Encrypt data for protocol 3.3 and before
* @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
*/
_encryptPre34(options) {
const cipher = crypto.createCipheriv('aes-128-ecb', this.getKey(), '');
let encrypted = cipher.update(options.data, 'utf8', 'base64');

@@ -45,2 +68,23 @@ encrypted += cipher.final('base64');

/**
* Encrypt data for protocol 3.4
* @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
*/
_encrypt34(options) {
const cipher = crypto.createCipheriv('aes-128-ecb', this.getKey(), null);
cipher.setAutoPadding(false);
const encrypted = cipher.update(options.data);
cipher.final();
// Default base64 enable TODO: check if this is needed?
// if (options.base64 === false) {
// return Buffer.from(encrypted, 'base64');
// }
return encrypted;
}
/**
* Decrypts data.

@@ -52,2 +96,16 @@ * @param {String|Buffer} data to decrypt

decrypt(data) {
if (this.version === '3.4') {
return this._decrypt34(data);
}
return this._decryptPre34(data);
}
/**
* Decrypts data for protocol 3.3 and before
* @param {String|Buffer} data to decrypt
* @returns {Object|String}
* returns object if data is JSON, else returns string
*/
_decryptPre34(data) {
// Incoming data format

@@ -57,4 +115,4 @@ let format = 'buffer';

if (data.indexOf(this.version) === 0) {
if (this.version === '3.3') {
// Remove 3.3 header
if (this.version === '3.3' || this.version === '3.2') {
// Remove 3.3/3.2 header
data = data.slice(15);

@@ -74,3 +132,3 @@ } else {

try {
const decipher = crypto.createDecipheriv('aes-128-ecb', this.key, '');
const decipher = crypto.createDecipheriv('aes-128-ecb', this.getKey(), '');
result = decipher.update(data, format, 'utf8');

@@ -92,2 +150,43 @@ result += decipher.final('utf8');

/**
* Decrypts data for protocol 3.4
* @param {String|Buffer} data to decrypt
* @returns {Object|String}
* returns object if data is JSON, else returns string
*/
_decrypt34(data) {
let result;
try {
const decipher = crypto.createDecipheriv('aes-128-ecb', this.getKey(), null);
decipher.setAutoPadding(false);
result = decipher.update(data);
decipher.final();
// Remove padding
result = result.slice(0, (result.length - result[result.length - 1]));
} catch (_) {
throw new Error('Decrypt failed');
}
// Try to parse data as JSON,
// otherwise return as string.
// 3.4 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.

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

}
/**
* Gets the key used for encryption/decryption
* @returns {String} sessionKey (if set for protocol 3.4) or key
*/
getKey() {
return this.sessionKey === null ? this.key : this.sessionKey;
}
/**
* Returns the HMAC for the current key (sessionKey if set for protocol 3.4 or key)
* @param {Buffer} data data to hash
* @returns {Buffer} HMAC
*/
hmac(data) {
return crypto.createHmac('sha256', this.getKey()).update(data, 'utf8').digest(); // .digest('hex');
}
/**
* Returns 16 random bytes
* @returns {Buffer} Random bytes
*/
random() {
return crypto.randomBytes(16);
}
}
module.exports = TuyaCipher;

@@ -73,3 +73,3 @@ /* Reverse engineered by kueblc */

* Computes a Tuya flavored CRC32
* @param {Iterable} bytes
* @param {Iterable} bytes The bytes to compute the CRC32 for
* @returns {Number} Tuya CRC32

@@ -76,0 +76,0 @@ */

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

* of command bytes.
* See also https://github.com/tuya/tuya-iotos-embeded-sdk-wifi-ble-bk7231n/blob/master/sdk/include/lan_protocol.h
* @readonly

@@ -17,5 +18,8 @@ * @private

ACTIVE: 2,
BIND: 3,
RENAME_GW: 4,
RENAME_DEVICE: 5,
BIND: 3, // ?? leave in for backward compatibility
SESS_KEY_NEG_START: 3, // negotiate session key
RENAME_GW: 4, // ?? leave in for backward compatibility
SESS_KEY_NEG_RES: 4, // negotiate session key response
RENAME_DEVICE: 5, // ?? leave in for backward compatibility
SESS_KEY_NEG_FINISH: 5,// finalize session key negotiation
UNBIND: 6,

@@ -32,5 +36,7 @@ CONTROL: 7,

SCENE_EXECUTE: 17,
DP_REFRESH: 18,
DP_REFRESH: 18, // Request refresh of DPS UPDATEDPS / LAN_QUERY_DP
UDP_NEW: 19,
AP_CONFIG_NEW: 20,
BOARDCAST_LPV34: 35,
LAN_EXT_STREAM: 40,
LAN_GW_ACTIVE: 240,

@@ -66,3 +72,3 @@ LAN_SUB_DEV_REQUEST: 241,

* @class
* @param {Object} options
* @param {Object} options Options
* @param {String} options.key localKey of cipher

@@ -93,3 +99,3 @@ * @param {Number} [options.version=3.1] protocol version

* Parses a Buffer of data containing at least
* one complete packet at the begining of the buffer.
* one complete packet at the beginning of the buffer.
* Will return multiple packets if necessary.

@@ -155,3 +161,9 @@ * @param {Buffer} buffer of data to parse

if (returnCode & 0xFFFFFF00) {
payload = buffer.slice(HEADER_SIZE, HEADER_SIZE + payloadSize - 8);
if (this.version === '3.4') {
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') {
payload = buffer.slice(HEADER_SIZE + 4, HEADER_SIZE + payloadSize - 0x24);
} else {

@@ -162,7 +174,16 @@ payload = buffer.slice(HEADER_SIZE + 4, HEADER_SIZE + payloadSize - 8);

// Check CRC
const expectedCrc = buffer.readInt32BE(HEADER_SIZE + payloadSize - 8);
const computedCrc = crc(buffer.slice(0, payloadSize + 8));
if (this.version === '3.4' && (commandByte !== CommandType.UDP || commandByte !== CommandType.UDP_NEW)) {
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(`CRC mismatch: expected ${expectedCrc}, was ${computedCrc}. ${buffer.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(`CRC mismatch: expected ${expectedCrc}, was ${computedCrc}. ${buffer.toString('hex')}`);
}
}

@@ -239,11 +260,8 @@

parse(buffer) {
const packets = this.parseRecursive(buffer, []);
return packets;
return this.parseRecursive(buffer, []);
}
/**
* Encodes a payload into a
* Tuya-protocol-complient packet.
* @param {Object} options
* Encodes a payload into a Tuya-protocol-compliant packet.
* @param {Object} options Options for encoding
* @param {Buffer|String|Object} options.data data to encode

@@ -254,3 +272,3 @@ * @param {Boolean} options.encrypted whether or not to encrypt the data

* @param {Number} [options.sequenceN] optional, sequence number
* @returns {Buffer}
* @returns {Buffer} Encoded Buffer
*/

@@ -272,7 +290,25 @@ encode(options) {

if (this.version === '3.4') {
return this._encode34(options);
}
return this._encodePre34(options);
}
/**
* Encodes a payload into a Tuya-protocol-compliant packet for protocol version 3.3 and below.
* @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
*/
_encodePre34(options) {
// Construct payload
let payload = options.data;
// Protocol 3.3 is always encrypted
if (this.version === '3.3') {
// Protocol 3.3 and 3.2 is always encrypted
if (this.version === '3.3' || this.version === '3.2') {
// Encrypt data

@@ -330,4 +366,66 @@ payload = this.cipher.encrypt({

}
/**
* Encodes a payload into a Tuya-protocol-complient packet for protocol version 3.4
* @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
*/
_encode34(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.4 header
// check this: mqc_very_pcmcd_mcd(int a1, unsigned int a2)
const buffer = Buffer.alloc(payload.length + 15);
Buffer.from('3.4').copy(buffer, 0);
payload.copy(buffer, 15);
payload = buffer;
}
// ? if (payload.length > 0) { // is null messages need padding - PING work without
const padding = 0x10 - (payload.length & 0xF);
const buf34 = Buffer.alloc((payload.length + padding), padding);
payload.copy(buf34);
payload = buf34;
// }
payload = this.cipher.encrypt({
data: payload
});
payload = Buffer.from(payload);
// Allocate buffer with room for payload + 24 bytes for
// prefix, sequence, command, length, crc, and suffix
const buffer = Buffer.alloc(payload.length + 52);
// Add prefix, command, and length
buffer.writeUInt32BE(0x000055AA, 0);
buffer.writeUInt32BE(options.commandByte, 8);
buffer.writeUInt32BE(payload.length + 0x24, 12);
if (options.sequenceN) {
buffer.writeUInt32BE(options.sequenceN, 4);
}
// Add payload, crc, and suffix
payload.copy(buffer, 16);
const calculatedCrc = this.cipher.hmac(buffer.slice(0, payload.length + 16));// & 0xFFFFFFFF;
calculatedCrc.copy(buffer, payload.length + 16);
buffer.writeUInt32BE(0x0000AA55, payload.length + 48);
return buffer;
}
}
module.exports = {MessageParser, CommandType};
{
"name": "tuyapi",
"version": "7.4.0",
"version": "7.5.0",
"description": "An easy-to-use API for devices that use Tuya's cloud services",

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

"dependencies": {
"debug": "4.1.1",
"p-queue": "6.6.1",
"p-retry": "4.2.0",
"debug": "^4.3.4",
"p-queue": "6.6.2",
"p-retry": "4.6.2",
"p-timeout": "3.2.0"

@@ -51,6 +51,6 @@ },

"clone": "2.1.2",
"coveralls": "3.0.9",
"delay": "4.3.0",
"documentation": "^12.1.4",
"nyc": "15.0.0",
"coveralls": "3.1.1",
"delay": "4.4.1",
"documentation": "^12.3.0",
"nyc": "15.1.0",
"xo": "0.25.4"

@@ -57,0 +57,0 @@ },

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