Skip to content

Commit 544dd5a

Browse files
authored
Fix race condition when decrypting notifications during lock disconnection (#368)
* Fix race condition when decrypting notifications during lock disconnection * safer * tweak
1 parent 547befd commit 544dd5a

File tree

4 files changed

+60
-0
lines changed

4 files changed

+60
-0
lines changed

switchbot/devices/device.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -955,6 +955,12 @@ def _decrypt(self, data: bytearray) -> bytes:
955955
if len(data) == 0:
956956
return b""
957957
if self._iv is None:
958+
if self._expected_disconnect:
959+
_LOGGER.debug(
960+
"%s: Cannot decrypt, IV is None during expected disconnect",
961+
self.name,
962+
)
963+
return b""
958964
raise RuntimeError("Cannot decrypt: IV is None")
959965
decryptor = self._get_cipher().decryptor()
960966
return decryptor.update(data) + decryptor.finalize()

switchbot/devices/lock.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,12 @@ async def _disable_notifications(self) -> bool:
214214

215215
def _notification_handler(self, _sender: int, data: bytearray) -> None:
216216
if self._notifications_enabled and self._check_command_result(data, 0, {0xF}):
217+
if self._expected_disconnect:
218+
_LOGGER.debug(
219+
"%s: Ignoring lock notification during expected disconnect",
220+
self.name,
221+
)
222+
return
217223
self._update_lock_status(data)
218224
else:
219225
super()._notification_handler(_sender, data)

tests/test_encrypted_device.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,3 +365,22 @@ async def test_empty_data_encryption_decryption() -> None:
365365
# Test empty decryption
366366
decrypted = device._decrypt(bytearray())
367367
assert decrypted == b""
368+
369+
370+
@pytest.mark.asyncio
371+
async def test_decrypt_with_none_iv_during_disconnect() -> None:
372+
"""Test that decryption returns empty bytes when IV is None during expected disconnect."""
373+
device = create_encrypted_device()
374+
375+
# Simulate disconnection in progress
376+
device._expected_disconnect = True
377+
device._iv = None
378+
379+
# Should return empty bytes instead of raising
380+
result = device._decrypt(bytearray(b"encrypted_data"))
381+
assert result == b""
382+
383+
# Verify it still raises when not disconnecting
384+
device._expected_disconnect = False
385+
with pytest.raises(RuntimeError, match="Cannot decrypt: IV is None"):
386+
device._decrypt(bytearray(b"encrypted_data"))

tests/test_lock.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
from unittest.mock import AsyncMock, Mock, patch
23

34
import pytest
@@ -478,6 +479,34 @@ def test_notification_handler_not_enabled(model: str):
478479
mock_super.assert_called_once()
479480

480481

482+
@pytest.mark.parametrize(
483+
"model",
484+
[
485+
SwitchbotModel.LOCK,
486+
SwitchbotModel.LOCK_LITE,
487+
SwitchbotModel.LOCK_PRO,
488+
SwitchbotModel.LOCK_ULTRA,
489+
],
490+
)
491+
def test_notification_handler_during_disconnect(
492+
model: str, caplog: pytest.LogCaptureFixture
493+
) -> None:
494+
"""Test _notification_handler during expected disconnect."""
495+
device = create_device_for_command_testing(model)
496+
device._notifications_enabled = True
497+
device._expected_disconnect = True
498+
data = bytearray(b"\x0f\x00\x00\x00\x80\x00\x00\x00\x00\x00")
499+
with (
500+
patch.object(device, "_update_lock_status") as mock_update,
501+
caplog.at_level(logging.DEBUG),
502+
):
503+
device._notification_handler(0, data)
504+
# Should not update lock status during disconnect
505+
mock_update.assert_not_called()
506+
# Should log debug message
507+
assert "Ignoring lock notification during expected disconnect" in caplog.text
508+
509+
481510
@pytest.mark.parametrize(
482511
"model",
483512
[

0 commit comments

Comments
 (0)