use crate::crypto::TuyaCipher;
use crate::error::{Result, TuyaError};
use crate::protocol::{CommandType, TuyaProtocol, Version, create_base_payload};
use base64::{Engine as _, engine::general_purpose};
use log::trace;
use md5::{Digest, Md5};
use serde_json::Value;
pub struct ProtocolV31;
impl TuyaProtocol for ProtocolV31 {
fn version(&self) -> Version {
Version::V3_1
}
fn get_effective_command(&self, command: CommandType) -> u32 {
command as u32
}
fn generate_payload(
&self,
device_id: &str,
command: CommandType,
data: Option<Value>,
cid: Option<&str>,
t: u64,
) -> Result<(u32, Value)> {
let cmd_to_send = self.get_effective_command(command);
let mut payload =
create_base_payload(device_id, cid, data.clone(), Some(t.to_string().into()));
match command {
CommandType::UpdateDps => {
payload.retain(|k, _| k == "cid");
let d = data.unwrap_or_else(|| serde_json::json!([18, 19, 20]));
payload.insert("dpId".into(), d);
}
CommandType::Control | CommandType::ControlNew => {
payload.remove("gwId");
}
CommandType::DpQuery => {
}
CommandType::DpQueryNew => {
payload.remove("gwId");
}
CommandType::LanExtStream => {
payload.clear();
if let Some(Value::Object(mut data_obj)) = data {
if let Some(req_type) = data_obj.remove("reqType") {
payload.insert("reqType".into(), req_type);
}
for (k, v) in data_obj {
payload.insert(k, v);
}
}
}
CommandType::Status | CommandType::HeartBeat => {
payload.remove("uid");
payload.remove("t");
}
_ => {
}
}
let payload_obj = Value::Object(payload);
trace!("v3.1 generated payload (cmd {cmd_to_send}): {payload_obj}");
Ok((cmd_to_send, payload_obj))
}
fn pack_payload(&self, payload: &[u8], cmd: u32, cipher: &TuyaCipher) -> Result<Vec<u8>> {
if cmd == CommandType::Control as u32 || cmd == CommandType::ControlNew as u32 {
let encrypted = cipher.encrypt(payload, false, None, None, true)?;
let b64_payload = general_purpose::STANDARD.encode(&encrypted);
let b64_bytes = b64_payload.as_bytes();
let mut hasher = Md5::new();
hasher.update(b"data=");
hasher.update(b64_bytes);
hasher.update(b"||lpv=3.1||");
hasher.update(cipher.key());
let hash = hasher.finalize();
let md5_hex = hex::encode(hash);
let md5_slice = &md5_hex[8..24];
let mut final_payload = Vec::with_capacity(3 + 16 + b64_bytes.len());
final_payload.extend_from_slice(b"3.1");
final_payload.extend_from_slice(md5_slice.as_bytes());
final_payload.extend_from_slice(b64_bytes);
Ok(final_payload)
} else {
Ok(payload.to_vec())
}
}
fn decrypt_payload(&self, payload: Vec<u8>, cipher: &TuyaCipher) -> Result<Vec<u8>> {
if payload.starts_with(b"3.1") && payload.len() > 19 {
let encrypted_b64 = &payload[19..];
let encrypted = general_purpose::STANDARD
.decode(encrypted_b64)
.map_err(|_| TuyaError::DecryptionFailed)?;
cipher.decrypt(&encrypted, false, None, None, None)
} else {
Ok(payload)
}
}
fn has_version_header(&self, _payload: &[u8]) -> bool {
false
}
fn requires_session_key(&self) -> bool {
false
}
fn encrypt_session_key(
&self,
session_key: &[u8],
cipher: &TuyaCipher,
_nonce: &[u8],
) -> Result<Vec<u8>> {
cipher.encrypt(session_key, false, None, None, false)
}
fn get_prefix(&self) -> u32 {
crate::protocol::PREFIX_55AA
}
fn get_hmac_key<'a>(&self, _cipher_key: &'a [u8]) -> Option<&'a [u8]> {
None
}
fn is_empty_payload_allowed(&self, _cmd: u32) -> bool {
false
}
fn should_check_dev22_fallback(&self) -> bool {
false
}
}