Replies: 9 comments 34 replies
-
Yeah, that is decidedly non-trivial. v3.5 devices require a 3-way handshake to negotiate the session key before status can be queried or commands sent. To do this without using any of the existing functions is going to require rewriting the packet assembler, packet parser, and crypto routines and will most likely be at least a couple hundred lines of code. I think it would be more productive for both of us if I were to just write this example in javascript. |
Beta Was this translation helpful? Give feedback.
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
-
@uzlonewolf - Thank you!!! I really appreciate your efforts. I will go through your code and try to implement it for tuyapi!!! I'm very happy right now!!! |
Beta Was this translation helpful? Give feedback.
-
A few more updates and turn on / turn off implemented: const net = require('net');
const crypto = require('crypto');
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
class PacketBuilder
{
#seq = 1;
#buf;
key;
constructor(key)
{
this.key = Buffer.from(key);
this.#buf = Buffer.alloc(18);
this.#buf.writeUInt32BE(0x6699, 0); // Prefix
this.#buf.writeUInt16BE(0, 4); // Unknown
}
setKey(key)
{
this.key = Buffer.from(key);
}
build(cmd, data)
{
if(cmd == 0x0d)
{
// Prepend 'version header' to the data
data = '3.5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + data;
}
this.#buf.writeUInt32BE(this.#seq, 6); // Sequence
this.#buf.writeUInt32BE(cmd, 10); // Command
this.#buf.writeUInt32BE(12 + data.length + 16, 14); // IV length + Data length + Tag length
this.#seq++;
let localIV = Buffer.from((Date.now() * 10).toString().substring(0, 12));
console.log('localIV', localIV.toString('hex'));
// Encrypt payload
let cipher = crypto.createCipheriv('aes-128-gcm', this.key, localIV);
cipher.setAAD(this.#buf.slice(4, 18));
let encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
// Concat message
let enc = Buffer.concat([this.#buf, localIV, encrypted, cipher.getAuthTag(), Buffer.from([0x00, 0x00, 0x99, 0x66])]);
console.log('command:', cmd.toString(16), data);
console.log('encrypted packet:', enc.toString('hex'));
return enc;
}
}
class PacketParser
{
constructor(data, key)
{
// Create prefix var
this.prefix = data.slice(0, 4);
console.log('prefix:', this.prefix.toString('hex'));
// Create header var
this.header = data.slice(4, 18);
//console.log('header', this.header.toString('hex'));
// Create unknown var
this.unknown = data.slice(4, 6);
console.log('unknown:', this.unknown.toString('hex'));
// Create sequence var
this.sequence = data.slice(6, 10);
console.log('sequence:', this.sequence.toString('hex'));
// Create command var
this.command = data.slice(10, 14);
console.log('command:', this.command.toString('hex'));
// Create per-packet remoteIV var
this.remoteIV = data.slice(18, 30);
console.log('remoteIV:', this.remoteIV.toString('hex'));
// Create length var
this.len = parseInt(data.slice(14, 18).toString('hex'), 16);
console.log('len:', this.len);
// Create payload var
this.payload = data.slice(18 + 12, this.len + 2); // 18 - 16
console.log('payload:', this.payload.toString('hex'));
// Update tag from packet
this.tag = data.slice(this.len + 2, this.len + 18); // 18 - 16
console.log('tag:', this.tag.toString('hex'));
// Create footer
this.footer = data.slice(this.len + 18);
console.log('footer:', this.footer.toString('hex'));
// Decrypt payload
let decipher = crypto.createDecipheriv('aes-128-gcm', key, this.remoteIV);
decipher.setAAD(this.header);
decipher.setAuthTag(this.tag);
let decrypt = Buffer.concat([decipher.update(this.payload), decipher.final()]);
this.retcode = decrypt.slice(0, 4);
this.decrypt = decrypt.slice(4); // Remove return code
}
}
class SocketClient
{
constructor(ip, port)
{
this.ip = ip;
this.port = port;
this.client = new net.Socket();
this.connected = false;
this.client.on('data', (data) => this.handleData(data));
this.client.on('timeout', () =>
{
console.log('socket timeout');
this.client.destroy();
});
this.client.on('error', (err) =>
{
console.log('socket error:', err.message);
this.client.destroy();
});
this.client.on('close', () =>
{
console.log('connection closed');
this.connected = false;
});
}
connect()
{
return new Promise((resolve, reject) =>
{
this.client.connect(this.port, this.ip, () =>
{
console.log('connected!');
this.connected = true;
resolve();
});
});
}
write(message)
{
this.client.write(message);
}
send(message)
{
return new Promise((resolve, reject) =>
{
this.client.write(message, (err) =>
{
if (err) {
reject(err);
} else {
this.resolveResponse = resolve;
}
});
});
}
recv()
{
return new Promise((resolve) =>
{
this.resolveResponse = resolve;
});
}
handleData(data)
{
// Process the data as needed, e.g., decrypt, parse, etc.
if (this.resolveResponse)
{
this.resolveResponse(data);
this.resolveResponse = null;
}
}
close()
{
this.client.end();
}
}
(async () =>
{
/****** Create client, connect and send first sequence packet ******/
const client = new SocketClient(
'172.20.10.136',
6668
);
await client.connect();
let devKey = "<snip>";
// Session key neg start
let localSesKey = Buffer.from('0123456789abcdef', 'utf-8');
// Get a PacketBuilder
let pkt = new PacketBuilder(devKey);
// Build the message
let message = pkt.build(3, localSesKey);
// Send first sequence
console.log('Sending:', message.toString('hex') + '\n');
let data = await client.send(message);
/******* Receive sequence response packet *******/
console.log('received:', data.toString('hex'));
let decrypt = new PacketParser(data, pkt.key);
// Get deviceSesKey
let deviceSesKey = Buffer.from(decrypt.decrypt).slice(0, 16);
console.log('deviceSesKey', deviceSesKey.toString('hex'));
// Get hmac
let hmac = Buffer.from(decrypt.decrypt).slice(16);
console.log('hmac', hmac.toString('hex'));
//Create hmac for compare
let calcLocalHmac = crypto.createHmac('sha256', pkt.key).update(localSesKey, 'utf8').digest();
console.log('calcnonce', calcLocalHmac.toString('hex'));
// Check hmacs match
if(calcLocalHmac.toString('hex') === hmac.toString('hex')) console.log('nonces match'); else console.log('nonces FAILED!');
// Calculate the HMAC of the device session key, and send it
let deviceSesKeyHmac = crypto.createHmac('sha256', pkt.key).update(deviceSesKey, 'utf8').digest();
message = pkt.build(5, deviceSesKeyHmac);
client.write(message);
// Calculate session encryption key and store it in deviceSesKey
for (let i = 0; i < localSesKey.length; i++) {
deviceSesKey[i] ^= localSesKey[i];
}
let cipher = crypto.createCipheriv('aes-128-gcm', pkt.key, localSesKey.slice(0, 12));
deviceSesKey = Buffer.concat([cipher.update(deviceSesKey), cipher.final()]);
console.log('final session key:', deviceSesKey.toString('hex'));
// Update encryption key
pkt.key = deviceSesKey;
// Query device status
message = pkt.build(0x10, '{}');
data = await client.send(message);
// Print the result
console.log('received packet:', data.toString('hex') + '\n');
decrypt = new PacketParser(data, pkt.key);
console.log('received:', decrypt);
console.log('received payload:', decrypt.decrypt.toString('utf8'));
// Turn On (power is DP 20 for this bulb)
message = pkt.build(0x0d, '{"protocol":5,"t":' + Date.now().toString() + ',"data":{"dps":{"20":true}}}');
data = await client.send(message);
// Print the new DP status
console.log('received packet:', data.toString('hex') + '\n');
decrypt = new PacketParser(data, pkt.key);
console.log('received:', decrypt);
console.log('received payload:', decrypt.decrypt.toString('utf8'));
data = await client.recv();
// Print the DP Set result
console.log('received packet:', data.toString('hex') + '\n');
decrypt = new PacketParser(data, pkt.key);
console.log('received:', decrypt);
console.log('received payload:', decrypt.decrypt.toString('utf8'));
await sleep(3000);
// Turn Off (power is DP 20 for this bulb)
message = pkt.build(0x0d, '{"protocol":5,"t":' + Date.now().toString() + ',"data":{"dps":{"20":false}}}');
data = await client.send(message);
// Print the new DP status
console.log('received packet:', data.toString('hex') + '\n');
decrypt = new PacketParser(data, pkt.key);
console.log('received:', decrypt);
console.log('received payload:', decrypt.decrypt.toString('utf8'));
data = await client.recv();
// Print the DP Set result
console.log('received packet:', data.toString('hex') + '\n');
decrypt = new PacketParser(data, pkt.key);
console.log('received:', decrypt);
console.log('received payload:', decrypt.decrypt.toString('utf8'));
client.close();
})(); |
Beta Was this translation helpful? Give feedback.
-
@uzlonewolf Ok, so I've been able to implement 3.5 in tuyapi and put in PR's (with yours and Apollon77's help). I've got my head around 3.5. Now I'm trying to work out how to handle a 3.4 device (device is returning 55AA using tinytuya) that is a sub device on a 3.3 version gateway. If I could trouble you (if you have time) for a code example like you did for 3.5 above (with the turn on, turn off) I'd really appreciate it. |
Beta Was this translation helpful? Give feedback.
This comment has been hidden.
This comment has been hidden.
-
@uzlonewolf - I've had a thought about groups I thought I would bounce off of you. Groups have their own key and id's. I tried different combinations with no success but I think it's because I need to provide a parameter like id, cid, gwId... I have a feeling this may work. |
Beta Was this translation helpful? Give feedback.
-
I am trying to implement 3.5 protocol into tuyapi. I have created code to make a connection and decode the return (I believe). I will include the code just for visual. If you could cobble together a python quick test of connecting to a device, getting status, and turning it off or on, without using the full tinytuya library (like you did in your 3.5 notes for the first test) I would really appreciate it. I know this isn't technically an issue, but I would really appreciate the help. I hate to see tuyapi die when it only needs to update to 3.5. Here is what I have so far:
What I get back:
Beta Was this translation helpful? Give feedback.
All reactions