rustuya/protocol/
v31.rs

1use crate::crypto::TuyaCipher;
2use crate::error::{Result, TuyaError};
3use crate::protocol::{CommandType, TuyaProtocol, Version, create_base_payload};
4use base64::{Engine as _, engine::general_purpose};
5use log::trace;
6use md5::{Digest, Md5};
7use serde_json::Value;
8
9pub struct ProtocolV31;
10
11impl TuyaProtocol for ProtocolV31 {
12    fn version(&self) -> Version {
13        Version::V3_1
14    }
15
16    fn get_effective_command(&self, command: CommandType) -> u32 {
17        command as u32
18    }
19
20    fn generate_payload(
21        &self,
22        device_id: &str,
23        command: CommandType,
24        data: Option<Value>,
25        cid: Option<&str>,
26        t: u64,
27    ) -> Result<(u32, Value)> {
28        let cmd_to_send = self.get_effective_command(command);
29        let mut payload =
30            create_base_payload(device_id, cid, data.clone(), Some(t.to_string().into()));
31
32        match command {
33            CommandType::UpdateDps => {
34                payload.retain(|k, _| k == "cid");
35                let d = data.unwrap_or_else(|| serde_json::json!([18, 19, 20]));
36                payload.insert("dpId".into(), d);
37            }
38            CommandType::Control | CommandType::ControlNew => {
39                payload.remove("gwId");
40            }
41            CommandType::DpQuery => {
42                // Keep all: gwId, devId, uid, cid, t, dps
43            }
44            CommandType::DpQueryNew => {
45                payload.remove("gwId");
46            }
47            CommandType::LanExtStream => {
48                // For LanExtStream in v3.1 and below, we keep everything at root
49                payload.clear();
50                if let Some(Value::Object(mut data_obj)) = data {
51                    if let Some(req_type) = data_obj.remove("reqType") {
52                        payload.insert("reqType".into(), req_type);
53                    }
54                    // Move remaining fields from data_obj to payload root
55                    for (k, v) in data_obj {
56                        payload.insert(k, v);
57                    }
58                }
59            }
60            CommandType::Status | CommandType::HeartBeat => {
61                payload.remove("uid");
62                payload.remove("t");
63            }
64            _ => {
65                // Default for others: gwId, devId, uid, cid, t, dps
66            }
67        }
68
69        let payload_obj = Value::Object(payload);
70        trace!("v3.1 generated payload (cmd {cmd_to_send}): {payload_obj}");
71
72        Ok((cmd_to_send, payload_obj))
73    }
74
75    fn pack_payload(&self, payload: &[u8], cmd: u32, cipher: &TuyaCipher) -> Result<Vec<u8>> {
76        if cmd == CommandType::Control as u32 || cmd == CommandType::ControlNew as u32 {
77            // 1. AES-128-ECB encrypt
78            let encrypted = cipher.encrypt(payload, false, None, None, true)?;
79
80            // 2. Base64 encode
81            let b64_payload = general_purpose::STANDARD.encode(&encrypted);
82            let b64_bytes = b64_payload.as_bytes();
83
84            // 3. Compute MD5 over: b"data=" + base64payload + b"||lpv=3.1||" + local_key
85            let mut hasher = Md5::new();
86            hasher.update(b"data=");
87            hasher.update(b64_bytes);
88            hasher.update(b"||lpv=3.1||");
89            hasher.update(cipher.key());
90
91            let hash = hasher.finalize();
92            let md5_hex = hex::encode(hash);
93            let md5_slice = &md5_hex[8..24];
94
95            // 4. Final payload: b"3.1" + md5slice + base64payload
96            let mut final_payload = Vec::with_capacity(3 + 16 + b64_bytes.len());
97            final_payload.extend_from_slice(b"3.1");
98            final_payload.extend_from_slice(md5_slice.as_bytes());
99            final_payload.extend_from_slice(b64_bytes);
100
101            Ok(final_payload)
102        } else {
103            Ok(payload.to_vec())
104        }
105    }
106
107    fn decrypt_payload(&self, payload: Vec<u8>, cipher: &TuyaCipher) -> Result<Vec<u8>> {
108        if payload.starts_with(b"3.1") && payload.len() > 19 {
109            // Strip "3.1" (3 bytes) and MD5 slice (16 bytes)
110            let encrypted_b64 = &payload[19..];
111
112            // Base64 decode
113            let encrypted = general_purpose::STANDARD
114                .decode(encrypted_b64)
115                .map_err(|_| TuyaError::DecryptionFailed)?;
116
117            // AES-ECB decrypt
118            cipher.decrypt(&encrypted, false, None, None, None)
119        } else {
120            Ok(payload)
121        }
122    }
123
124    fn has_version_header(&self, _payload: &[u8]) -> bool {
125        false
126    }
127
128    fn requires_session_key(&self) -> bool {
129        false
130    }
131
132    fn encrypt_session_key(
133        &self,
134        session_key: &[u8],
135        cipher: &TuyaCipher,
136        _nonce: &[u8],
137    ) -> Result<Vec<u8>> {
138        cipher.encrypt(session_key, false, None, None, false)
139    }
140
141    fn get_prefix(&self) -> u32 {
142        crate::protocol::PREFIX_55AA
143    }
144
145    fn get_hmac_key<'a>(&self, _cipher_key: &'a [u8]) -> Option<&'a [u8]> {
146        None
147    }
148
149    fn is_empty_payload_allowed(&self, _cmd: u32) -> bool {
150        false
151    }
152
153    fn should_check_dev22_fallback(&self) -> bool {
154        false
155    }
156}