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 }
44 CommandType::DpQueryNew => {
45 payload.remove("gwId");
46 }
47 CommandType::LanExtStream => {
48 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 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 }
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 let encrypted = cipher.encrypt(payload, false, None, None, true)?;
79
80 let b64_payload = general_purpose::STANDARD.encode(&encrypted);
82 let b64_bytes = b64_payload.as_bytes();
83
84 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 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 let encrypted_b64 = &payload[19..];
111
112 let encrypted = general_purpose::STANDARD
114 .decode(encrypted_b64)
115 .map_err(|_| TuyaError::DecryptionFailed)?;
116
117 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}