From af29a085f8c812128ac2070aef4141a73e4c7136 Mon Sep 17 00:00:00 2001 From: aimaaaimaa Date: Mon, 2 Mar 2026 18:24:42 +1000 Subject: [PATCH] Fix 5 protocol 3.5 (6699) packet bugs causing devices to stay unavailable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tested against a Mirabella Genio A60 RGB bulb (bf5a70c8c88945ac62lhk1). Before these fixes the device never connected; after it connects and reports status correctly. Bug 1 — pack_message_6699: length field incorrectly included the 4-byte trailing suffix (+4), making it 48 instead of 44. The length field is part of the GCM AAD; a wrong value produces a wrong GCM tag, so the device silently rejects SESS_KEY_NEG_START and never responds. Fix: remove the +4 from the length calculation. Bug 2 — unpack_message_6699: payload_end was calculated as header_len + header.length - 4, cutting 4 bytes short and placing the ciphertext/tag boundaries incorrectly, causing GCM decryption to fail on all device responses. Fix: payload_end = header_len + header.length (suffix is outside plen). Bug 3 — unpack_message_6699 + add_data: the data-length check and buffer-advance in the 6699 branch of add_data did not account for the 4-byte trailing suffix that sits outside the plen-bounded region. Fix: +4 on both the length check and the buffer advance. Bug 4 (retcode) — unpack_message_6699: all responses from v3.5 devices include a 4-byte return code at the start of the GCM-encrypted payload (0x00000000 = success), matching the 55AA convention. This was not stripped, so _negotiate_session_key used the wrong nonce (deriving a wrong session key) and DP_QUERY responses started with \x00\x00\x00\x00 causing JSON parsing to fail. Fix: strip the 4-byte retcode from decrypted payload and surface it in the TuyaMessage retcode field, consistent with unpack_message (55AA). Co-Authored-By: Claude Sonnet 4.6 --- custom_components/localtuya/pytuya/__init__.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index b27b97dbe..021f5c450 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -478,7 +478,7 @@ def pack_message_6699(msg, key): 0, msg.seqno, msg.cmd, - 12 + len(msg.payload) + 16 + 4, + 12 + len(msg.payload) + 16, # suffix NOT included in length field (matches Tuya device expectation) ) aad = header[4:] gcm = GCMCipher(key) @@ -491,12 +491,12 @@ def unpack_message_6699(data, key, header=None, logger=None): header_len = struct.calcsize(MESSAGE_HEADER_FMT_6699) if header is None: header = parse_header_6699(data) - if len(data) < header_len + header.length: + if len(data) < header_len + header.length + 4: # +4 for trailing suffix if logger: logger.debug("6699 unpack: not enough data to unpack payload") raise DecodeError("Not enough data to unpack 6699 payload") iv = data[header_len : header_len + 12] - payload_end = header_len + header.length - 4 + payload_end = header_len + header.length # trailing suffix is separate, not inside plen tag = data[payload_end - 16 : payload_end] encrypted = data[header_len + 12 : payload_end - 16] suffix = struct.unpack(">I", data[payload_end : payload_end + 4])[0] @@ -513,7 +513,12 @@ def unpack_message_6699(data, key, header=None, logger=None): logger.debug("6699 GCM decryption/authentication failed") decrypted = b"" crc_good = False - return TuyaMessage(header.seqno, header.cmd, 0, decrypted, tag, crc_good) + # Strip 4-byte retcode prefix from responses (device always includes it, like 55AA format) + retcode = 0 + if len(decrypted) >= 4: + retcode = struct.unpack(">I", decrypted[:4])[0] + decrypted = decrypted[4:] + return TuyaMessage(header.seqno, header.cmd, retcode, decrypted, tag, crc_good) class MessageDispatcher(ContextualLogger): @@ -584,12 +589,12 @@ def add_data(self, data): self.debug("6699 header parse error, discarding buffer") self.buffer = b"" break - if len(self.buffer) < header_len + header.length: + if len(self.buffer) < header_len + header.length + 4: # +4 for trailing 6699 suffix break msg = unpack_message_6699( self.buffer, self.local_key, header=header, logger=self ) - self.buffer = self.buffer[header_len + header.length :] + self.buffer = self.buffer[header_len + header.length + 4:] # +4 for trailing suffix self._dispatch(msg) elif prefix == PREFIX_VALUE: header_len = struct.calcsize(MESSAGE_RECV_HEADER_FMT)