Skip to content

Commit

Permalink
feat: allow one failed decryption before reauth (#76)
Browse files Browse the repository at this point in the history
  • Loading branch information
Ernst79 authored Mar 24, 2024
1 parent 4ba5da7 commit 4682109
Show file tree
Hide file tree
Showing 2 changed files with 45 additions and 4 deletions.
26 changes: 23 additions & 3 deletions src/xiaomi_ble/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -1486,6 +1486,10 @@ def __init__(self, bindkey: bytes | None = None) -> None:
# or encryption is not in use
self.bindkey_verified = False

# If True then the decryption has failed or has not been verified yet.
# If False then the decryption has succeeded.
self.decryption_failed = True

# If this is True, then we have not seen an advertisement with a payload
# Until we see a payload, we can't tell if this device is encrypted or not
self.pending = True
Expand Down Expand Up @@ -1739,10 +1743,15 @@ def _parse_xiaomi(
if payload_length < next_start:
# The payload segments are corrupted - if this is legacy encryption
# then the key is probably just wrong
# V4 encryption has an authentication tag, so we don't apply the
# V4/V5 encryption has an authentication tag, so we don't apply the
# same restriction there.
if self.encryption_scheme == EncryptionScheme.MIBEACON_LEGACY:
self.bindkey_verified = False
if self.decryption_failed is True:
# we only ask for reautentification
# till the decryption has failed twice.
self.bindkey_verified = False
else:
self.decryption_failed = True
_LOGGER.debug(
"Invalid payload data length, payload: %s", payload.hex()
)
Expand Down Expand Up @@ -1893,7 +1902,12 @@ def _decrypt_mibeacon_v4_v5(
nonce, encrypted_payload + mic, associated_data
)
except InvalidTag as error:
self.bindkey_verified = False
if self.decryption_failed is True:
# we only ask for reautentification till
# the decryption has failed twice.
self.bindkey_verified = False
else:
self.decryption_failed = True
_LOGGER.warning("Decryption failed: %s", error)
_LOGGER.debug("mic: %s", mic.hex())
_LOGGER.debug("nonce: %s", nonce.hex())
Expand All @@ -1906,6 +1920,7 @@ def _decrypt_mibeacon_v4_v5(
to_mac(xiaomi_mac),
)
return None
self.decryption_failed = False
self.bindkey_verified = True
return decrypted_payload

Expand Down Expand Up @@ -1938,6 +1953,11 @@ def _decrypt_mibeacon_legacy(

assert cipher is not None # nosec
# decrypt the data
# note that V2/V3 encryption will often pass the decryption process with a
# wrong encryption key, resulting in useless data, and we won't be able
# to verify this, as V2/V3 encryption does not use a tag to verify
# the decrypted data. This will be filtered as wrong data length
# during the conversion of the payload to sensor data.
try:
decrypted_payload = cipher.decrypt(encrypted_payload)
except ValueError as error:
Expand Down
23 changes: 22 additions & 1 deletion tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ def test_bindkey_wrong():
device = XiaomiBluetoothDeviceData(bindkey=bytes.fromhex(bindkey))
assert device.supported(advertisement)
assert not device.bindkey_verified
assert device.decryption_failed
assert device.update(advertisement) == SensorUpdate(
title="Motion Sensor C40F (RTCGQ02LM)",
devices={
Expand Down Expand Up @@ -250,8 +251,18 @@ def test_bindkey_verified_can_be_unset_v4():

device = XiaomiBluetoothDeviceData(bindkey=bytes.fromhex(bindkey))
device.bindkey_verified = True
device.decryption_failed = False

assert device.supported(advertisement)
# the first advertisement will fail decryption, but we don't ask to reauth yet
assert device.bindkey_verified
assert device.decryption_failed

data_string = b"XY\x8d\n\x18\x0f\xc4\xe0D\xefT|\xc2z\\\x03\xa1\x00\x00\x00y"
advertisement = bytes_to_service_info(data_string, address="54:EF:44:E0:C4:0F")
assert device.supported(advertisement)
# the second advertisement will fail decryption again, but now we ask to reauth
assert device.decryption_failed
assert not device.bindkey_verified


Expand All @@ -264,6 +275,7 @@ def test_bindkey_wrong_legacy():
device = XiaomiBluetoothDeviceData(bindkey=bytes.fromhex(bindkey))
assert device.supported(advertisement)
assert not device.bindkey_verified
assert device.decryption_failed
assert device.update(advertisement) == SensorUpdate(
title="Dimmer Switch 988B (YLKG07YL/YLKG08YL)",
devices={
Expand All @@ -288,7 +300,6 @@ def test_bindkey_wrong_legacy():
),
},
)

assert device.unhandled == {}


Expand All @@ -300,8 +311,18 @@ def test_bindkey_verified_can_be_unset_legacy():

device = XiaomiBluetoothDeviceData(bindkey=bytes.fromhex(bindkey))
device.bindkey_verified = True
device.decryption_failed = False

assert device.supported(advertisement)
# the first advertisement will fail decryption, but we don't ask to reauth yet
assert device.bindkey_verified
assert device.decryption_failed

data_string = b"X0\xb6\x03\xd3\x8b\x98\xc5A$\xf8\xc3I\x14vu~\x00\x00\x00\x99"
advertisement = bytes_to_service_info(data_string, address="F8:24:41:C5:98:8B")
assert device.supported(advertisement)
# the second advertisement will fail decryption again, but now we ask to reauth
assert device.decryption_failed
assert not device.bindkey_verified


Expand Down

0 comments on commit 4682109

Please sign in to comment.