using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; namespace com.clusterrr.TuyaNet { /// /// Class to encode and decode data sent over local network. /// internal static class TuyaParser { private static byte[] PROTOCOL_VERSION_BYTES_31 = Encoding.ASCII.GetBytes("3.1"); private static byte[] PROTOCOL_VERSION_BYTES_33 = Encoding.ASCII.GetBytes("3.3"); private static byte[] PROTOCOL_33_HEADER = Enumerable.Concat(PROTOCOL_VERSION_BYTES_33, new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }).ToArray(); private static byte[] PREFIX = new byte[] { 0, 0, 0x55, 0xAA }; private static byte[] SUFFIX = { 0, 0, 0xAA, 0x55 }; private static uint SeqNo = 0; internal static IEnumerable BigEndian(IEnumerable seq) => BitConverter.IsLittleEndian ? seq.Reverse() : seq; internal static byte[] Encrypt(byte[] data, byte[] key) { var aes = new AesManaged() { Mode = CipherMode.ECB, Key = key }; using (var ms = new MemoryStream()) using (var cs = new CryptoStream(ms, aes.CreateEncryptor(), CryptoStreamMode.Write)) { cs.Write(data, 0, data.Length); cs.Close(); data = ms.ToArray(); // encrypt the data } return data; } internal static byte[] Decrypt(byte[] data, byte[] key) { if (data.Length == 0) return data; var aes = new AesManaged() { Mode = CipherMode.ECB, Key = key }; using (var ms = new MemoryStream()) using (var cs = new CryptoStream(ms, aes.CreateDecryptor(), CryptoStreamMode.Write)) { cs.Write(data, 0, data.Length); cs.Close(); data = ms.ToArray(); // dencrypt the data } return data; } internal static byte[] EncodeRequest(TuyaCommand command, string json, byte[] key, TuyaProtocolVersion protocolVersion = TuyaProtocolVersion.V33) { // Remove spaces and newlines var root = JObject.Parse(json); json = root.ToString(Newtonsoft.Json.Formatting.None); byte[] payload = Encoding.UTF8.GetBytes(json); if (protocolVersion == TuyaProtocolVersion.V33) { // Encrypt payload = Encrypt(payload, key); // Add protocol 3.3 header if ((command != TuyaCommand.DP_QUERY) && (command != TuyaCommand.UPDATE_DPS)) payload = Enumerable.Concat(PROTOCOL_33_HEADER, payload).ToArray(); } else if (command == TuyaCommand.CONTROL) { // Encrypt payload = Encrypt(payload, key); // Encode to base64 string data64 = Convert.ToBase64String(payload); // Make string payload = Encoding.UTF8.GetBytes($"data={data64}||lpv=3.1||"); using (var md5 = MD5.Create()) using (var ms = new MemoryStream()) { // Calculate MD5 of data ms.Write(payload, 0, payload.Length); // ...and encryption key ms.Write(key, 0, key.Length); string md5s = BitConverter.ToString( // Make string from MD5 md5.ComputeHash(ms.ToArray()) // Calculate MD5 ) .Replace("-", string.Empty) // Remove '-' .Substring(8, 16) // Get part of it .ToLower(); // Lowercase // Data with protocol header, MD5 hash and data payload = Encoding.UTF8.GetBytes($"3.1{md5s}{data64}"); } } using (var ms = new MemoryStream()) { byte[] seqNo = BitConverter.GetBytes(SeqNo++); if (BitConverter.IsLittleEndian) Array.Reverse(seqNo); // Make big-endian byte[] dataLength = BitConverter.GetBytes(payload.Length + 8); if (BitConverter.IsLittleEndian) Array.Reverse(dataLength); // Make big-endian ms.Write(PREFIX, 0, 4); // Prefix ms.Write(seqNo, 0, 4); // Packet number ms.Write(new byte[] { 0, 0, 0, (byte)command }, 0, 4); // Command number ms.Write(dataLength, 0, 4); // Length of data + length of suffix ms.Write(payload, 0, payload.Length); // Data var crc32 = new Crc32(); var crc = crc32.Get(ms.ToArray()); byte[] crcBin = BitConverter.GetBytes(crc); if (BitConverter.IsLittleEndian) Array.Reverse(crcBin); // Make big-endian ms.Write(crcBin, 0, 4); // CRC32 checksum ms.Write(SUFFIX, 0, 4); // Suffix payload = ms.ToArray(); } return payload; } internal static TuyaLocalResponse DecodeResponse(byte[] data, byte[] key, TuyaProtocolVersion protocolVersion = TuyaProtocolVersion.V33) { // Check length and prefix if (data.Length < 20 || !data.Take(PREFIX.Length).SequenceEqual(PREFIX)) { throw new InvalidDataException("Invalid header/prefix"); } // Check length int length = BitConverter.ToInt32(BigEndian(data.Skip(12).Take(4)).ToArray(), 0); if (data.Length != 16 + length) { throw new InvalidDataException("Invalid length"); } // Check suffix if (!data.Skip(16 + length - SUFFIX.Length).Take(SUFFIX.Length).SequenceEqual(SUFFIX)) { throw new InvalidDataException("Invalid suffix"); } // Packet number // uint seq = BitConverter.ToUInt32(BinEndian(data.Skip(4).Take(4)).ToArray(), 0); // Command var command = (TuyaCommand)BitConverter.ToUInt32(BigEndian(data.Skip(8).Take(4)).ToArray(), 0); // Return code int returnCode = BitConverter.ToInt32(BigEndian(data.Skip(16).Take(4)).ToArray(), 0); // Data data = data.Skip(20).Take(length - 12).ToArray(); var realVersion = protocolVersion; // Remove version 3.1 header if (data.Take(PROTOCOL_VERSION_BYTES_31.Length).SequenceEqual(PROTOCOL_VERSION_BYTES_31)) { data = data.Skip(PROTOCOL_VERSION_BYTES_31.Length).ToArray(); realVersion = TuyaProtocolVersion.V31; } // Remove version 3.3 header if (data.Take(PROTOCOL_VERSION_BYTES_33.Length).SequenceEqual(PROTOCOL_VERSION_BYTES_33)) { data = data.Skip(PROTOCOL_33_HEADER.Length).ToArray(); realVersion = TuyaProtocolVersion.V33; } if (realVersion == TuyaProtocolVersion.V33) { data = Decrypt(data, key); } if (data.Length == 0) return new TuyaLocalResponse(command, returnCode, null); var json = Encoding.UTF8.GetString(data); if (!json.StartsWith("{") || !json.EndsWith("}")) throw new InvalidDataException($"Response is not JSON: {json}"); return new TuyaLocalResponse(command, returnCode, json); } } }