casambi-bt-revamped 0.3.7.dev9__py3-none-any.whl → 0.3.12.dev15__py3-none-any.whl

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.
@@ -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
+