Reassemble RSCP frames split across multiple socket reads - #146
Open
TommiG1 wants to merge 1 commit into
Open
Conversation
E3DC_RSCP_local._receive() performed a single socket.recv(BUFFER_SIZE)
call and assumed the entire encrypted RSCP frame arrived in that one
read. TCP gives no such guarantee: larger responses (e.g. an extensive
EH_REQ_GET_SAVED_ERRORS saved-error history) can be split across
multiple reads, which crashed rscpFrameDecode() with:
struct.error: unpack requires a buffer of N bytes
_receive() now loops over recv() calls until the frame length declared
in the header has been fully received. Each iteration re-decrypts the
accumulated ciphertext from scratch on a throwaway copy of the
encrypt/decrypt state (RSCPEncryptDecrypt.decrypt()'s default
incremental bookkeeping only tracks the immediately preceding chunk
and silently corrupts data across three or more reads), and only
commits the real, persistent encdec state once the full frame is
known to be available -- matching exactly what happens today when a
response arrives in a single read.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
E3DC_RSCP_local._receive()performs a singlesocket.recv(BUFFER_SIZE)call and assumes the entire encrypted RSCP frame arrives in that one read:TCP gives no such guarantee. For larger responses this can fail with:
This was reported while testing an integration that reads
EH_REQ_GET_SAVED_ERRORS(saved error history) — the response size scales with the number of stored history entries, so it's one of the few requests likely to exceed what a singlerecv()reliably returns, especially on systems with an extensive error history. See torbennehmer/hacs-e3dc#373 for context.Fix
_receive()now loops oversocket.recv()calls until the frame length declared in the RSCP header has actually been received, instead of trusting a single read.One subtlety:
RSCPEncryptDecrypt.decrypt()is stateful across calls (it tracks left-over partial AES blocks and IV chaining viaself.oldDecrypt), but its default (previouslyProcessedData=None) bookkeeping only accounts for how much of the immediately preceding chunk was consumed. That's only correct for exactly two calls; from a third call onward, any non-block-aligned leftover from before the previous chunk silently shifts the accounting and corrupts the decrypted output (verified locally with a reproduction using many small, non-block-aligned reads).To stay correct regardless of how many reads are needed, each iteration re-decrypts the whole accumulated ciphertext from scratch using a throwaway
copy.copy()of the encrypt/decrypt state, just to check whether a full frame is available yet. Once it is, the real, persistentself.encdecstate is advanced exactly once via a normaldecrypt()call on the full ciphertext — identical to what happens today when a response arrives in a single read, so per-session IV chaining for subsequent requests is unaffected.Testing
No formal test suite exists for this module, so I verified locally against a synthetic RSCP frame (a large
EH_REQ_GET_SAVED_ERRORS-style response, ~300 rows) fed through a real socket in small (500 byte), non-block-aligned chunks, comparing against the reference single-shot decryption:black,isort,flake8(incl.flake8-pyproject/max-line-length = 88) all pass on the changed file.pyrightreports no new issues (the one pre-existing hint aboutint | Nonesyntax on line 53 is unrelated, from running under Python 3.9 locally; CI targets 3.10+).Compatibility
No public API changes.
_receive()remains a private method with the same signature and return type; behavior for normal-sized responses that already fit in one read is unchanged (single loop iteration, same decrypt path).