casambi-bt-revamped 0.3.12.dev8__tar.gz → 0.3.12.dev10__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {casambi_bt_revamped-0.3.12.dev8/src/casambi_bt_revamped.egg-info → casambi_bt_revamped-0.3.12.dev10}/PKG-INFO +1 -1
- {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/setup.cfg +1 -1
- {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/_casambi.py +26 -9
- casambi_bt_revamped-0.3.12.dev10/src/CasambiBt/_classic_crypto.py +146 -0
- {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/_client.py +339 -17
- {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/_constants.py +9 -0
- {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/_network.py +26 -19
- {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/_version.py +1 -1
- {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10/src/casambi_bt_revamped.egg-info}/PKG-INFO +1 -1
- casambi_bt_revamped-0.3.12.dev10/tests/test_classic_protocol.py +385 -0
- casambi_bt_revamped-0.3.12.dev8/src/CasambiBt/_classic_crypto.py +0 -31
- casambi_bt_revamped-0.3.12.dev8/tests/test_classic_protocol.py +0 -157
- {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/LICENSE +0 -0
- {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/README.md +0 -0
- {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/pyproject.toml +0 -0
- {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/__init__.py +0 -0
- {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/_cache.py +0 -0
- {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/_discover.py +0 -0
- {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/_encryption.py +0 -0
- {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/_invocation.py +0 -0
- {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/_keystore.py +0 -0
- {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/_operation.py +0 -0
- {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/_switch_events.py +0 -0
- {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/_unit.py +0 -0
- {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/errors.py +0 -0
- {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/py.typed +0 -0
- {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/casambi_bt_revamped.egg-info/SOURCES.txt +0 -0
- {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/casambi_bt_revamped.egg-info/dependency_links.txt +0 -0
- {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/casambi_bt_revamped.egg-info/requires.txt +0 -0
- {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/casambi_bt_revamped.egg-info/top_level.txt +0 -0
- {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/tests/test_legacy_protocol_handling.py +0 -0
- {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/tests/test_switch_event_logs.py +0 -0
- {casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/tests/test_unit_state_logs.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: casambi-bt-revamped
|
|
3
|
-
Version: 0.3.12.
|
|
3
|
+
Version: 0.3.12.dev10
|
|
4
4
|
Summary: Forked Casambi Bluetooth client library with switch event support, use original if no special need. https://github.com/lkempf/casambi-bt
|
|
5
5
|
Home-page: https://github.com/rankjie/casambi-bt
|
|
6
6
|
Author: rankjie
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[metadata]
|
|
2
2
|
name = casambi-bt-revamped
|
|
3
|
-
version = 0.3.12.
|
|
3
|
+
version = 0.3.12.dev10
|
|
4
4
|
author = rankjie
|
|
5
5
|
author_email = rankjie@gmail.com
|
|
6
6
|
description = Forked Casambi Bluetooth client library with switch event support, use original if no special need. https://github.com/lkempf/casambi-bt
|
{casambi_bt_revamped-0.3.12.dev8 → casambi_bt_revamped-0.3.12.dev10}/src/CasambiBt/_casambi.py
RENAMED
|
@@ -206,16 +206,33 @@ class Casambi:
|
|
|
206
206
|
|
|
207
207
|
# Classic protocol uses signed command frames (u1.C1753e / u1.EnumC1754f).
|
|
208
208
|
if self._casaClient is not None and self._casaClient.protocolMode == ProtocolMode.CLASSIC:
|
|
209
|
-
#
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
209
|
+
# Check if we should use the alternative simple format from BLE captures
|
|
210
|
+
import os
|
|
211
|
+
use_simple = os.environ.get("CASAMBI_BT_CLASSIC_FORMAT", "").lower() == "simple"
|
|
212
|
+
|
|
213
|
+
if use_simple:
|
|
214
|
+
# Simple format: [counter][unit_id][param_len][dimmer]
|
|
215
|
+
# For "all units", use unit_id=0xFF
|
|
216
|
+
if isinstance(target, Unit):
|
|
217
|
+
cmd = self._casaClient.buildClassicCommandSimple(target.deviceId, level)
|
|
218
|
+
elif isinstance(target, Group):
|
|
219
|
+
# Groups in simple format: not fully confirmed, try group ID
|
|
220
|
+
cmd = self._casaClient.buildClassicCommandSimple(target.groudId, level)
|
|
221
|
+
elif target is None:
|
|
222
|
+
cmd = self._casaClient.buildClassicCommandSimple(0xFF, level)
|
|
223
|
+
else:
|
|
224
|
+
raise TypeError(f"Unkown target type {type(target)}")
|
|
217
225
|
else:
|
|
218
|
-
|
|
226
|
+
# EnumC1754f ordinals (ground truth: casambi-android u1.EnumC1754f):
|
|
227
|
+
# - AllUnitsLevel=4, UnitLevel=7, GroupLevel=26
|
|
228
|
+
if isinstance(target, Unit):
|
|
229
|
+
cmd = self._casaClient.buildClassicCommand(7, payload, target_id=target.deviceId)
|
|
230
|
+
elif isinstance(target, Group):
|
|
231
|
+
cmd = self._casaClient.buildClassicCommand(26, payload, target_id=target.groudId)
|
|
232
|
+
elif target is None:
|
|
233
|
+
cmd = self._casaClient.buildClassicCommand(4, payload)
|
|
234
|
+
else:
|
|
235
|
+
raise TypeError(f"Unkown target type {type(target)}")
|
|
219
236
|
|
|
220
237
|
await self._casaClient.send(cmd)
|
|
221
238
|
return
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Classic Casambi protocol helpers (CMAC signing/verification).
|
|
2
|
+
|
|
3
|
+
Ground truth:
|
|
4
|
+
- casambi-android `t1.P.o(...)` calculates a CMAC over:
|
|
5
|
+
connection_hash[0:8] + payload
|
|
6
|
+
and stores the CMAC (prefix) into the packet header.
|
|
7
|
+
|
|
8
|
+
The CMAC input structure from t1.P.o():
|
|
9
|
+
kVar.i(rVar.f16699L, 0, 8); // connection_hash[0:8]
|
|
10
|
+
kVar.i(b(), this.f16551t + i9, (c() - i9) - this.f16551t); // payload starting at seq offset
|
|
11
|
+
|
|
12
|
+
For "conformant" classic (rVar.Z=true):
|
|
13
|
+
CMAC input = conn_hash[0:8] || seq(2 bytes BE) || command_bytes
|
|
14
|
+
|
|
15
|
+
For "legacy" classic (rVar.Z=false):
|
|
16
|
+
CMAC input = conn_hash[0:8] || command_bytes
|
|
17
|
+
|
|
18
|
+
Test vectors based on community BLE captures (GitHub lkempf/casambi-bt#17):
|
|
19
|
+
|
|
20
|
+
Capture from sMauldaeschle:
|
|
21
|
+
Raw packet: 0215db43c40004040200b3
|
|
22
|
+
- Byte 0: 02 (auth level = visitor)
|
|
23
|
+
- Bytes 1-4: 15db43c4 (4-byte CMAC signature)
|
|
24
|
+
- Byte 5: 00 (padding/marker)
|
|
25
|
+
- Byte 6: 04 (counter/sequence)
|
|
26
|
+
- Byte 7: 04 (unit id)
|
|
27
|
+
- Byte 8: 02 (length of parameters)
|
|
28
|
+
- Byte 9: 00 (dimmer value)
|
|
29
|
+
- Byte 10: b3 (temperature/vertical value)
|
|
30
|
+
|
|
31
|
+
Capture from FliegenKLATSCH:
|
|
32
|
+
Raw packet: 0200 43A2 9600 0203 0254 FF
|
|
33
|
+
- Bytes 0-1: 0200 (auth 02, then...)
|
|
34
|
+
- Bytes 2-5: 43A2 9600 (first 4 bytes of CMAC)
|
|
35
|
+
- The CMAC input was: <8 bytes connection hash> 00 0203 0254 FF
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import logging
|
|
41
|
+
from typing import TYPE_CHECKING
|
|
42
|
+
|
|
43
|
+
from cryptography.hazmat.primitives.cmac import CMAC
|
|
44
|
+
from cryptography.hazmat.primitives.ciphers.algorithms import AES
|
|
45
|
+
|
|
46
|
+
if TYPE_CHECKING:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
_logger = logging.getLogger(__name__)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def classic_cmac(key: bytes, conn_hash8: bytes, payload: bytes, *, debug: bool = False) -> bytes:
|
|
53
|
+
"""Compute the Classic CMAC (16 bytes) over connection hash + payload.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
key: 16-byte AES key (visitor or manager key from cloud)
|
|
57
|
+
conn_hash8: First 8 bytes of the connection hash read from CA51/auth char
|
|
58
|
+
payload: The data to sign (for conformant: seq + command; for legacy: command only)
|
|
59
|
+
debug: If True, log CMAC input/output for debugging
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
16-byte CMAC
|
|
63
|
+
"""
|
|
64
|
+
if len(conn_hash8) != 8:
|
|
65
|
+
raise ValueError("conn_hash8 must be 8 bytes")
|
|
66
|
+
|
|
67
|
+
cmac_input = conn_hash8 + payload
|
|
68
|
+
cmac = CMAC(AES(key))
|
|
69
|
+
cmac.update(cmac_input)
|
|
70
|
+
result = cmac.finalize()
|
|
71
|
+
|
|
72
|
+
if debug:
|
|
73
|
+
_logger.warning(
|
|
74
|
+
"[CLASSIC_CMAC_DEBUG] key_len=%d conn_hash8=%s payload_len=%d payload_prefix=%s cmac=%s",
|
|
75
|
+
len(key),
|
|
76
|
+
conn_hash8.hex(),
|
|
77
|
+
len(payload),
|
|
78
|
+
payload[:16].hex() if len(payload) > 16 else payload.hex(),
|
|
79
|
+
result.hex(),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def classic_cmac_prefix(
|
|
86
|
+
key: bytes, conn_hash8: bytes, payload: bytes, prefix_len: int, *, debug: bool = False
|
|
87
|
+
) -> bytes:
|
|
88
|
+
"""Return the prefix bytes that are embedded into the Classic packet header.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
key: 16-byte AES key
|
|
92
|
+
conn_hash8: First 8 bytes of connection hash
|
|
93
|
+
payload: Data to sign
|
|
94
|
+
prefix_len: Number of CMAC bytes to use (4 for visitor, 16 for manager)
|
|
95
|
+
debug: If True, log CMAC computation
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
First prefix_len bytes of the CMAC
|
|
99
|
+
"""
|
|
100
|
+
mac = classic_cmac(key, conn_hash8, payload, debug=debug)
|
|
101
|
+
return mac[:prefix_len]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def verify_classic_cmac(
|
|
105
|
+
key: bytes, conn_hash8: bytes, payload: bytes, expected_prefix: bytes
|
|
106
|
+
) -> bool:
|
|
107
|
+
"""Verify a Classic CMAC signature prefix.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
key: 16-byte AES key
|
|
111
|
+
conn_hash8: First 8 bytes of connection hash
|
|
112
|
+
payload: Data that was signed
|
|
113
|
+
expected_prefix: The CMAC prefix from the packet header
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
True if the computed CMAC prefix matches expected_prefix
|
|
117
|
+
"""
|
|
118
|
+
computed = classic_cmac_prefix(key, conn_hash8, payload, len(expected_prefix))
|
|
119
|
+
return computed == expected_prefix
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# Test vectors from RFC 4493 (AES-CMAC) - used in test_classic_protocol.py
|
|
123
|
+
# These confirm our CMAC implementation is correct.
|
|
124
|
+
RFC4493_TEST_KEY = bytes.fromhex("2b7e151628aed2a6abf7158809cf4f3c")
|
|
125
|
+
RFC4493_VECTORS = [
|
|
126
|
+
# (message, expected_cmac)
|
|
127
|
+
(bytes.fromhex("6bc1bee22e409f96e93d7e117393172a"),
|
|
128
|
+
bytes.fromhex("070a16b46b4d4144f79bdd9dd04a287c")),
|
|
129
|
+
(bytes.fromhex("6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e51"),
|
|
130
|
+
bytes.fromhex("ce0cbf1738f4df6428b1d93bf12081c9")),
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
# Placeholder for real Casambi test vectors once we have them from captures
|
|
134
|
+
# Format: (visitor_key, conn_hash8, payload_after_header, expected_cmac_prefix_4, description)
|
|
135
|
+
CASAMBI_CLASSIC_TEST_VECTORS: list[tuple[bytes, bytes, bytes, bytes, str]] = [
|
|
136
|
+
# These will be populated from real BLE captures
|
|
137
|
+
# Example structure (not verified yet):
|
|
138
|
+
# (
|
|
139
|
+
# bytes.fromhex("...16 byte key..."),
|
|
140
|
+
# bytes.fromhex("...8 byte conn hash..."),
|
|
141
|
+
# bytes.fromhex("...payload after auth byte..."),
|
|
142
|
+
# bytes.fromhex("...4 byte expected cmac..."),
|
|
143
|
+
# "Description of what this packet does"
|
|
144
|
+
# ),
|
|
145
|
+
]
|
|
146
|
+
|