rust_async_tuyapi/
mesparse.rs

1//! # Message Parser
2//! The message parser is the low level API which takes care of encoding and decoding of Payloads.
3//! The normal user should not need to interact with this directly to communicate with Tuya
4//! devices, but rather create an instance of the TuyaDevice struct.
5use crate::cipher::TuyaCipher;
6use crate::crc::crc;
7use crate::error::ErrorKind;
8use crate::{Payload, Result};
9use hex::FromHex;
10use log::{debug, error};
11use nom::{
12    bytes::complete::tag,
13    combinator::{map, peek, recognize},
14    multi::{length_data, many1},
15    number::complete::be_u32,
16    sequence::tuple,
17    IResult,
18};
19
20use num_derive::{FromPrimitive, ToPrimitive};
21use num_traits::{FromPrimitive, ToPrimitive};
22use std::cmp::PartialEq;
23use std::convert::TryInto;
24use std::fmt;
25use std::mem::size_of;
26use std::str::FromStr;
27
28pub(crate) const UDP_KEY: &str = "yGAdlopoPVldABfn";
29
30lazy_static! {
31    static ref PREFIX_BYTES: [u8; 4] = <[u8; 4]>::from_hex("000055AA").unwrap();
32    static ref SUFFIX_BYTES: [u8; 4] = <[u8; 4]>::from_hex("0000AA55").unwrap();
33}
34
35/// Human readable definitions of command bytes.
36#[derive(Debug, FromPrimitive, ToPrimitive, Clone, PartialEq, Eq)]
37pub enum CommandType {
38    Udp = 0,
39    ApConfig = 1,
40    Active = 2,
41    SessKeyNegStart = 3,
42    SessKeyNegResp = 4,
43    SessKeyNegFinish = 5,
44    Unbind = 6,
45    Control = 7,
46    Status = 8,
47    HeartBeat = 9,
48    DpQuery = 10,
49    QueryWifi = 11,
50    UpdateDps = 12,
51    ControlNew = 13,
52    EnableWifi = 14,
53    DpQueryNew = 16,
54    SceneExecute = 17,
55    DpRefresh = 18,
56    UdpNew = 19,
57    ApConfigNew = 20,
58    LanGwActive = 240,
59    LanSubDevRequest = 241,
60    LanDeleteSubDev = 242,
61    LanReportSubDev = 243,
62    LanScene = 244,
63    LanPublishCloudConfig = 245,
64    LanPublishAppConfig = 246,
65    LanExportAppConfig = 247,
66    LanPublishScenePanel = 248,
67    LanRemoveGw = 249,
68    LanCheckGwUpdate = 250,
69    LanGwUpdate = 251,
70    LanSetGwChannel = 252,
71    Error = 255,
72}
73
74impl CommandType {
75    pub fn needs_protocol_header(&self) -> bool {
76        !matches!(
77            self,
78            CommandType::SessKeyNegStart
79                | CommandType::SessKeyNegResp
80                | CommandType::SessKeyNegFinish
81                | CommandType::HeartBeat
82                | CommandType::DpQuery
83                | CommandType::UpdateDps
84                | CommandType::DpQueryNew
85        )
86    }
87
88    pub fn has_raw_payload(&self) -> bool {
89        matches!(self, CommandType::SessKeyNegResp)
90    }
91}
92
93#[allow(clippy::enum_variant_names)]
94#[derive(Debug, PartialEq, Eq, Clone)]
95pub enum TuyaVersion {
96    ThreeOne,
97    ThreeThree,
98    ThreeFour,
99}
100
101impl TuyaVersion {
102    pub fn as_bytes(&self) -> &[u8] {
103        match &self {
104            TuyaVersion::ThreeOne => b"3.1",
105            TuyaVersion::ThreeThree => b"3.3",
106            TuyaVersion::ThreeFour => b"3.4",
107        }
108    }
109}
110
111impl FromStr for TuyaVersion {
112    type Err = ErrorKind;
113
114    fn from_str(s: &str) -> Result<Self> {
115        match s {
116            "3.1" => Ok(TuyaVersion::ThreeOne),
117            "3.3" => Ok(TuyaVersion::ThreeThree),
118            "3.4" => Ok(TuyaVersion::ThreeFour),
119            _ => Err(ErrorKind::VersionError(s.to_string())),
120        }
121    }
122}
123
124/// Representation of a message sent to and received from a Tuya device. The Payload is
125/// serialized to and deserialized from JSON. The sequence number, if sent in a command, will
126/// be included in the response to be able to connect command and response. The return code is
127/// only included if the Message is a response from a device.
128#[derive(Clone, Debug, PartialEq, Eq)]
129pub struct Message {
130    pub payload: Payload,
131    pub command: Option<CommandType>,
132    pub seq_nr: Option<u32>,
133    pub ret_code: Option<u8>,
134}
135
136impl fmt::Display for Message {
137    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
138        write!(
139            f,
140            "Payload: \"{}\", Command: {:?}, Seq Nr: {:?}, Return Code: {:?}",
141            self.payload,
142            self.command.clone().unwrap_or(CommandType::Error),
143            self.seq_nr,
144            self.ret_code,
145        )
146    }
147}
148
149impl Message {
150    pub fn new(payload: Payload, command: CommandType) -> Message {
151        Message {
152            payload,
153            command: Some(command),
154            seq_nr: None,
155            ret_code: None,
156        }
157    }
158}
159
160/// The message parser takes care of encoding and parsing messages before send and after
161/// receive. It uses a TuyaCipher to encrypt and decrypt messages sent with the Tuya
162/// protocol version 3.3.
163#[derive(Clone)]
164pub struct MessageParser {
165    version: TuyaVersion,
166    pub(crate) cipher: TuyaCipher,
167}
168
169/// MessageParser encodes and parses messages sent to and from Tuya devices. It may or may not
170/// encrypt the message, depending on message type and TuyaVersion. Likewise, the parsing may or may
171/// not need decrypting.
172impl MessageParser {
173    pub fn create(version: TuyaVersion, key: Option<String>) -> Result<MessageParser> {
174        let key = verify_key(key.as_deref())?;
175        let cipher = TuyaCipher::create(&key, version.clone());
176        Ok(MessageParser { version, cipher })
177    }
178
179    pub fn encode(&self, mes: &Message, encrypt: bool) -> Result<Vec<u8>> {
180        let mut encoded: Vec<u8> = vec![];
181        encoded.extend_from_slice(&*PREFIX_BYTES);
182        match mes.seq_nr {
183            Some(nr) => encoded.extend(&nr.to_be_bytes()),
184            None => encoded.extend(&0_u32.to_be_bytes()),
185        }
186        let command = mes.command.clone().ok_or(ErrorKind::CommandTypeMissing)?;
187        encoded.extend([0, 0, 0, command.to_u8().unwrap()].iter());
188        let payload = self.create_payload_header(mes, encrypt)?;
189        let ret_len = match mes.ret_code {
190            Some(_) => 4_u32,
191            None => 0_u32,
192        };
193        let msg_end_size = match self.version {
194            TuyaVersion::ThreeOne | TuyaVersion::ThreeThree => {
195                // u32:crc + u32:suffix
196                size_of::<u32>() + size_of::<u32>()
197            }
198            TuyaVersion::ThreeFour => {
199                // 32:hmac + uint32:suffix
200                32 + size_of::<u32>()
201            }
202        };
203        encoded.extend(
204            (payload.len() as u32 + msg_end_size as u32 + ret_len)
205                .to_be_bytes()
206                .iter(),
207        );
208        if let Some(ret_code) = mes.ret_code {
209            encoded.extend(&ret_code.to_be_bytes());
210        }
211        encoded.extend(payload);
212        match self.version {
213            TuyaVersion::ThreeOne | TuyaVersion::ThreeThree => {
214                encoded.extend(crc(&encoded).to_be_bytes().iter());
215            }
216            TuyaVersion::ThreeFour => {
217                encoded.extend(self.cipher.hmac(&encoded)?.iter());
218                // encoded.extend(self.cipher.hmac(&encoded)?.iter().flat_map(|b| b.to_be_bytes()));
219            }
220        }
221        encoded.extend_from_slice(&*SUFFIX_BYTES);
222        debug!(
223            "Encoded message ({}):\n{}",
224            mes.seq_nr.unwrap_or(0),
225            hex::encode(&encoded)
226        );
227
228        Ok(encoded)
229    }
230
231    fn create_payload_header(&self, mes: &Message, encrypt: bool) -> Result<Vec<u8>> {
232        match self.version {
233            TuyaVersion::ThreeOne => {
234                if encrypt {
235                    self.create_payload_with_header(mes.payload.clone().try_into()?)
236                } else {
237                    mes.payload.clone().try_into()
238                }
239            }
240            TuyaVersion::ThreeThree | TuyaVersion::ThreeFour => match mes.command {
241                Some(ref cmd) if cmd.needs_protocol_header() => {
242                    self.create_payload_with_header(mes.payload.clone().try_into()?)
243                }
244                _ => {
245                    let payload: Vec<u8> = mes.payload.clone().try_into()?;
246                    self.cipher.encrypt(&payload)
247                }
248            },
249        }
250    }
251
252    fn create_payload_with_header(&self, payload: Vec<u8>) -> Result<Vec<u8>> {
253        let mut payload_with_header = Vec::new();
254        match self.version {
255            TuyaVersion::ThreeOne => {
256                payload_with_header.extend(self.version.as_bytes());
257                payload_with_header.extend(vec![0; 12]);
258                payload_with_header.extend(self.cipher.encrypt(&payload)?);
259            }
260            TuyaVersion::ThreeThree => {
261                payload_with_header.extend(self.version.as_bytes());
262                payload_with_header.extend(self.cipher.md5(&payload));
263                payload_with_header.extend(self.cipher.encrypt(&payload)?);
264            }
265            TuyaVersion::ThreeFour => {
266                debug!("pre Final payload: {}", hex::encode(&payload));
267                let payload = {
268                    let mut v = self.version.as_bytes().to_vec();
269                    v.extend(&vec![0; 12]);
270                    v.extend(&payload);
271                    v
272                };
273
274                debug!("Final payload: {}", hex::encode(&payload));
275
276                payload_with_header.extend(self.cipher.encrypt(&payload)?);
277
278                debug!("Payload encrypted: {}", hex::encode(&payload_with_header));
279            }
280        }
281        Ok(payload_with_header)
282    }
283
284    pub fn parse(&self, buf: &[u8]) -> Result<Vec<Message>> {
285        let (buf, messages) = self.parse_messages(buf).map_err(|err| match err {
286            nom::Err::Error(e) => ErrorKind::ParseError(e.code),
287            nom::Err::Incomplete(_) => ErrorKind::ParsingIncomplete,
288            nom::Err::Failure(e) if e.code == nom::error::ErrorKind::ManyMN => ErrorKind::CRCError,
289            nom::Err::Failure(e) => ErrorKind::ParseError(e.code),
290        })?;
291        if !buf.is_empty() {
292            return Err(ErrorKind::BufferNotCompletelyParsedError);
293        }
294        Ok(messages)
295    }
296
297    fn parse_messages<'a>(&self, orig_buf: &'a [u8]) -> IResult<&'a [u8], Vec<Message>> {
298        let crc_size = match self.version {
299            TuyaVersion::ThreeOne | TuyaVersion::ThreeThree => size_of::<u32>(),
300            TuyaVersion::ThreeFour => 32,
301        };
302
303        // TODO: can this be statically initialized??
304        let be_u32_minus4 = map(be_u32, |n: u32| n - 4);
305        let (buf, vec) = many1(tuple((
306            tag(*PREFIX_BYTES),
307            be_u32,
308            be_u32,
309            length_data(be_u32_minus4),
310            tag(*SUFFIX_BYTES),
311        )))(orig_buf)?;
312        let mut messages = vec![];
313        for (_, seq_nr, command, recv_data_orig, _) in vec {
314            // check if the recv_data contains a return code
315            let (recv_data, maybe_retcode) = peek(be_u32)(recv_data_orig)?;
316            let (recv_data, ret_code, _ret_len) = if maybe_retcode & 0xFFFF_FF00 == 0 {
317                // Has a return code
318                let (recv_data, ret_code) = recognize(be_u32)(recv_data)?;
319                (recv_data, Some(ret_code[3]), 4_usize)
320            } else {
321                // Has no return code
322                (recv_data, None, 0_usize)
323            };
324            let (payload, rc) = recv_data.split_at(recv_data.len() - crc_size);
325
326            match self.version {
327                TuyaVersion::ThreeOne | TuyaVersion::ThreeThree => {
328                    let recv_crc = u32::from_be_bytes([rc[0], rc[1], rc[2], rc[3]]);
329                    // For v3.1/v3.3, use the shortened recv_data for CRC calculation
330                    if crc(&orig_buf[0..recv_data_orig.len() + 12]) != recv_crc {
331                        error!(
332                            "Found CRC: {:#x}, Expected CRC: {:#x}",
333                            recv_crc,
334                            crc(&orig_buf[0..recv_data_orig.len() + 12])
335                        );
336                        // I hijack the ErrorKind::ManyMN here to propagate a CRC error
337                        // TODO: should probably create and use a special CRC error here
338                        return Err(nom::Err::Failure(nom::error::Error::new(
339                            rc,
340                            nom::error::ErrorKind::ManyMN,
341                        )));
342                    }
343                }
344                TuyaVersion::ThreeFour => {
345                    // Verify HMAC-SHA256 for v3.4 protocol integrity
346                    // HMAC covers: prefix(4) + seq(4) + cmd(4) + length(4) + payload (includes retcode)
347                    // Use recv_data_orig which has the full data before retcode extraction
348                    let hmac_end = 16 + recv_data_orig.len() - crc_size;
349                    let data_for_hmac = &orig_buf[0..hmac_end];
350                    let expected_hmac = self.cipher.hmac(data_for_hmac).map_err(|_| {
351                        nom::Err::Failure(nom::error::Error::new(rc, nom::error::ErrorKind::Verify))
352                    })?;
353                    if rc != expected_hmac.as_slice() {
354                        error!(
355                            "HMAC verification failed - expected {}, got {}",
356                            hex::encode(&expected_hmac),
357                            hex::encode(rc)
358                        );
359                        return Err(nom::Err::Failure(nom::error::Error::new(
360                            rc,
361                            nom::error::ErrorKind::Verify,
362                        )));
363                    }
364                }
365            }
366
367            let command = FromPrimitive::from_u32(command).or(None);
368            let payload = self.try_decrypt(payload, &command);
369            let message = Message {
370                payload,
371                command,
372                seq_nr: Some(seq_nr),
373                ret_code,
374            };
375            messages.push(message);
376        }
377        Ok((buf, messages))
378    }
379
380    fn try_decrypt(&self, payload: &[u8], command: &Option<CommandType>) -> Payload {
381        let payload = match self.cipher.decrypt(payload) {
382            Ok(decrypted) => decrypted,
383            Err(_) => payload.to_vec(),
384        };
385
386        match command {
387            Some(command) if command.has_raw_payload() => Payload::Raw(payload),
388            _ => {
389                if let Ok(p) = serde_json::from_slice(payload.as_slice()) {
390                    Payload::Struct(p)
391                } else {
392                    Payload::String(
393                        std::str::from_utf8(payload.as_slice())
394                            .unwrap_or("Payload invalid")
395                            .to_string(),
396                    )
397                }
398            }
399        }
400    }
401}
402
403fn verify_key(key: Option<&str>) -> Result<Vec<u8>> {
404    match key {
405        Some(key) => {
406            if key.len() == 16 {
407                Ok(key.as_bytes().to_vec())
408            } else {
409                Err(ErrorKind::KeyLength(key.len()))
410            }
411        }
412        None => {
413            let default_key = md5::compute(UDP_KEY).0;
414            Ok(default_key.to_vec())
415        }
416    }
417}
418
419#[cfg(test)]
420mod tests {
421    use super::*;
422    use crate::PayloadStruct;
423    use serde_json::json;
424    use std::collections::HashMap;
425    #[test]
426    fn test_key_length_is_16() {
427        let key = Some("0123456789ABCDEF");
428        assert!(verify_key(key).is_ok());
429    }
430
431    #[test]
432    fn test_key_lenght_not_16_gives_error() {
433        let bad_key = Some("13579BDF");
434        assert!(verify_key(bad_key).is_err());
435    }
436
437    #[test]
438    fn test_parse_mqttversion() {
439        let version1 = TuyaVersion::from_str("3.1").unwrap();
440        assert_eq!(version1, TuyaVersion::ThreeOne);
441
442        let version3 = TuyaVersion::from_str("3.3").unwrap();
443        assert_eq!(version3, TuyaVersion::ThreeThree);
444
445        let version4 = TuyaVersion::from_str("3.4").unwrap();
446        assert_eq!(version4, TuyaVersion::ThreeFour);
447
448        assert!(TuyaVersion::from_str("3.5").is_err());
449    }
450
451    #[test]
452    fn test_parse_messages() {
453        let packet =
454            hex::decode("000055aa00000000000000090000000c00000000b051ab030000aa55").unwrap();
455        let expected = Message {
456            command: Some(CommandType::HeartBeat),
457            payload: Payload::String("".to_string()),
458            seq_nr: Some(0),
459            ret_code: Some(0),
460        };
461        let mp = MessageParser::create(TuyaVersion::ThreeOne, None).unwrap();
462        let (buf, messages) = mp.parse_messages(&packet).unwrap();
463        assert_eq!(messages[0], expected);
464        assert_eq!(buf, &[] as &[u8]);
465    }
466
467    #[test]
468    fn test_parse_messages_with_payload() {
469        let packet = hex::decode("000055aa00000000000000070000005b00000000332e33d8bab8946c604148a45c15326ed3b99d683695a73c624e75a5aaa31f4061f5b99033e6d01f0b0abf9dbc76b2a54eb4bf60976b1dc496169db9e5a3fd627f2c3d9c4744585e471b6a2fc479ca01f7e18e0000aa55").unwrap();
470        let mut dps = HashMap::new();
471        dps.insert("1".to_string(), json!(true));
472        let expected = Message {
473            command: Some(CommandType::Control),
474            payload: Payload::Struct(PayloadStruct {
475                dev_id: "46052834d8f15b92e53b".to_string(),
476                gw_id: None,
477                uid: None,
478                t: None,
479                dp_id: None,
480                dps: Some(serde_json::to_value(dps).unwrap()),
481            }),
482            seq_nr: Some(0),
483            ret_code: Some(0),
484        };
485        let mp = MessageParser::create(TuyaVersion::ThreeThree, None).unwrap();
486        let (buf, messages) = mp.parse_messages(&packet).unwrap();
487        assert_eq!(messages[0], expected);
488        assert_eq!(buf, &[] as &[u8]);
489    }
490
491    #[test]
492    fn test_parse_data_format_error() {
493        let packet =
494            hex::decode("000055aa00000000000000070000003b00000001332e33d504910232d355a59ed1f6ed1f4a816a1e8e30ed09987c020ae45d72c70592bb233c79c43a5b9ae49b6ead38725deb520000aa55").unwrap();
495        let expected = Message {
496            command: Some(CommandType::Control),
497            payload: Payload::String("data format error".to_string()),
498            seq_nr: Some(0),
499            ret_code: Some(1),
500        };
501        let mp = MessageParser::create(TuyaVersion::ThreeThree, None).unwrap();
502        let (buf, messages) = mp.parse_messages(&packet).unwrap();
503        assert_eq!(messages[0], expected);
504        assert_eq!(buf, &[] as &[u8]);
505    }
506
507    #[test]
508    fn test_parse_double_messages() {
509        let packet =
510            hex::decode("000055aa00000000000000090000000c00000000b051ab030000aa55000055aa000000000000000a0000000c00000000b051ab030000aa55").unwrap();
511        let expected = vec![
512            Message {
513                command: Some(CommandType::HeartBeat),
514                payload: Payload::String("".to_string()),
515                seq_nr: Some(0),
516                ret_code: Some(0),
517            },
518            Message {
519                command: Some(CommandType::DpQuery),
520                payload: Payload::String("".to_string()),
521                seq_nr: Some(0),
522                ret_code: Some(0),
523            },
524        ];
525        let mp = MessageParser::create(TuyaVersion::ThreeOne, None).unwrap();
526        let (buf, messages) = mp.parse_messages(&packet).unwrap();
527        assert_eq!(messages[0], expected[0]);
528        assert_eq!(messages[1], expected[1]);
529        assert_eq!(buf, &[] as &[u8]);
530    }
531
532    #[test]
533    fn test_encode_with_and_without_encryption_and_version_three_one() {
534        let mut dps = HashMap::new();
535        dps.insert("1".to_string(), json!(true));
536        dps.insert("2".to_string(), json!(0));
537        let payload = Payload::Struct(PayloadStruct {
538            dev_id: "002004265ccf7fb1b659".to_string(),
539            gw_id: None,
540            uid: None,
541            t: None,
542            dp_id: None,
543            dps: Some(serde_json::to_value(dps).unwrap()),
544        });
545        let mes = Message {
546            command: Some(CommandType::DpQuery),
547            payload,
548            seq_nr: Some(0),
549            ret_code: Some(0),
550        };
551        let parser = MessageParser::create(TuyaVersion::ThreeOne, None).unwrap();
552        let encrypted = parser.encode(&mes, true).unwrap();
553        let unencrypted = parser.encode(&mes, false).unwrap();
554        // Only encrypt 3.1 if the flag is set
555        assert_ne!(encrypted, unencrypted);
556    }
557
558    #[test]
559    fn test_encode_with_and_without_encryption_and_version_three_three() {
560        let mut dps = HashMap::new();
561        dps.insert("1".to_string(), json!(true));
562        let payload = Payload::Struct(PayloadStruct {
563            dev_id: "002004265ccf7fb1b659".to_string(),
564            gw_id: None,
565            uid: None,
566            t: None,
567            dp_id: None,
568            dps: Some(serde_json::to_value(dps).unwrap()),
569        });
570        let mes = Message {
571            command: Some(CommandType::DpQuery),
572            payload,
573            seq_nr: Some(0),
574            ret_code: Some(0),
575        };
576        let parser = MessageParser::create(TuyaVersion::ThreeThree, None).unwrap();
577
578        let encrypted = parser.encode(&mes, true).unwrap();
579        let unencrypted = parser.encode(&mes, false).unwrap();
580        // Always encrypt 3.3, no matter what the flag is
581        assert_eq!(encrypted, unencrypted);
582    }
583}