Repository navigation
Conversation
88aecf5 to
f3d2ea6
Compare
|
One follow-up commit on this PR (
#80 and #81 are rebased on it. Suite: 469 passed (468 before this commit). mypy |
A device holds a negotiated session per connection (SolixBLE.protocols): an outer protocol (PlainOuter: 0001 in clear, CBC session; EncryptedOuter: 4001 under the static GCM key, GCM session) and the ECDH path after the shared opening. The path sends a fresh P-256 key every negotiation, states its x005 method as the app does per outer, and sends x022 with the UTC offset as int32 seconds west and the POSIX time zone. The outer comes from the advertisement's capability byte when given, else the outer that last authorized on this instance, else the model's default. A device that drops the link before answering a plain 0001 refuses it: connect() reopens once with the encrypted outer and never steps down. Encrypted sessions authorize at 4827 00 or the first decryptable session push; 4827 on 030101 reaches the same handler, and a 09 status and its window are recorded on device.announcement. The opening re-send and deadline sit behind _negotiation_should_restart() / _negotiation_deadline(). PrimeDevice keeps its UUID, the encrypted default and _post_authorize. Prime commands use the base's typed fe 05 03 trailer, and 420a carries the region (set_region(), else the host locale's, else GB) typed 02 and this client's identifier as the owner, as the app sends it. The changed test frames are derived from the recorded sessions. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A device that refuses the plain 0001 can drop the link before the write returns, and the write then raises BleakError out of connect(). If the link is down by then, the error is the refusal: the negotiation sees a dropped link and connect() reopens with 4001, as when the drop lands after the write. Seen on an A1783. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Keys is a construct Struct over the shared secret (key, IV, 12-byte GCM nonce); the static key parses the key and nonce the same way. - The announcement is a construct Container with the same fields; device.announcement keeps its shape. - x803, x827 and the x005, x003 and x022 values parse and build through Int8ul, Int16ul and Int32sl; the client's a3 and MTU ceiling are named constants. - install_session_keys leaves the keys alone for an empty secret, as the no-packets case expects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
I think this is a good approach but the terminology is a bit confusing (though I don't know what else you could use to describe it), I think a diagram or two (even ASCII) would help a lot here, something which shows how those classes fit together and maybe a flow chart of how it works and what classes are called during a negotiation and how a packet is built / parsed. The way everything works has gotten a lot more complicated (though I don't have any objections to the complexity itself, its kind of needed) and I think it justifies some diagrams. I think its possible to embed diagrams in sphinx docs using mermaid if you want to try that but I have not used it much before. I had been thinking of restructuring it like this but I think your approach is probably better. I plan on creating a new pre-release with all of the current merged changes and seeing how that goes before taking another look at this either next week or the one after. |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the flip-dots#79 diagrams in their path-aware form, both paths' reply tables, and a note on how the A91B2 answers each outer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
ASCII diagrams in the RST, here's the mermaid as well, |
Third of four stacked PRs replacing #70; based on #78. It answers the protocol half of your review: each variant is its own class, chosen without a bool, and a device holds the one its firmware speaks.
A device now holds a negotiated session for each connection (
SolixBLE/protocols/,typing.Protocolinterfaces with@override):PlainOuter/EncryptedOuter): how the negotiation travels and which cipher the session uses. Plain opens with0001in the clear and runs CBC; encrypted opens with4001under the static GCM key and runs GCM. The session cipher follows the opening frame.EcdhPath): the ECDH key exchange that follows the shared opening, on either outer. It sends a fresh P-256 key every negotiation (as Add AS220 (SOLIX S2000) device support #65 and anker-solix-api#345 do), states its method inx005as the app does (a5 40on plain,a5 44with the device's MTU and auth mode on encrypted), and sendsx022with the UTC offset as int32 seconds west and the POSIX time zone.DeviceClass(ble_device, advertisement=...)), else the outer that last authorized on this instance, else the model's default. A device that drops the link before answering a plain0001refuses it, andconnect()reopens once with the encrypted outer in the same call; the encrypted outer is never stepped down to plain.DeviceClass(ble_device)with no advertisement keeps working for HaSolixBLE, andtype(device)stays the model class.0821; encrypted at4827status00, or at the first session push the client can decrypt. A4827on the device-initiated pattern030101(the grant after a button press) reaches the same handler. A09status and its confirmation window are recorded ondevice.announcement; waiting for the button is left to the pairing PR, which overrides_negotiation_should_restart()/_negotiation_deadline().PrimeDevicekeeps its UUID, the encrypted default and_post_authorize(4200,420a); its copy of the negotiation and its own_send_commandgo.Prime frames: every Prime command now ends with the typed timestamp trailer
fe 05 03 <ts>(the base_send_command), and420ais built as the app builds it:a2= region typed02(set_region(), else the host locale's, elseGB),a3= this client's identifier as the owner, noa5. These match what the Anker app sends to the Prime Charger 250W (A2345) and the Prime Charging Station (A91B2) and what their firmware reads. I have no 160W or 20K power bank to test on, so their new bytes are untested; could you, or a 160W/20K owner, try it?Derived, not recorded, test vectors: the C1000 plain
4022(new layout), the Prime4200/420a, and the 32 Prime command frames with the typed trailer. Each was derived from the recorded session (the pinned test key and the device's recorded public point) and the old frames decrypt to the old plaintexts first. The pinned keys reproduce the public points in the recorded0021/4021, so the recorded negotiations are unchanged up to those frames.Also:
typing_extensionsfor Python < 3.12 (override);PRIVATE_KEYandNEGOTIATION_COMMAND_*move fromconst.pytotests/const.py;tests/helpers.pygainsinstall_session_keys,RecordingLinkandMockDevice.refuse_after; docs:protocols.rst"Outer protocols".