Wrap noise handshake decrypt errors with friendly API exceptions#1668
Conversation
If a peer responds during the Noise handshake with a preamble=0x00 frame
whose AEAD payload fails authentication, _handle_handshake previously let
cryptography.exceptions.InvalidTag (and any other noise-lib exception)
propagate up to asyncio. Callers awaiting ready_future would see the raw
crypto exception instead of the InvalidEncryptionKeyAPIError the
preamble=0x01 'Handshake MAC failure' path already produces.
Wrap _proto.read_message() so that:
- InvalidTag surfaces as InvalidEncryptionKeyAPIError carrying the
sanitized server_name/server_mac, matching _error_on_incorrect_preamble.
- Any other noise-lib exception surfaces as HandshakeAPIError so callers
get a clean APIConnectionError subclass rather than a third-party
exception type.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1668 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 25 25
Lines 4132 4143 +11
=========================================
+ Hits 4132 4143 +11 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR improves error surfacing during the Noise handshake by converting low-level cryptography/noiseprotocol exceptions into stable APIConnectionError subclasses, so callers awaiting ready_future don’t see third-party exception types.
Changes:
- Wrap
NoiseConnection.read_message(...)during handshake to mapInvalidTag→InvalidEncryptionKeyAPIErrorand all other exceptions →HandshakeAPIError. - Add regression tests covering both the AEAD-authentication-failure path and a non-
InvalidTagnoise-library failure path.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
aioesphomeapi/_frame_helper/noise.py |
Wrap handshake decryption failures into friendly API exceptions instead of leaking cryptography/noiseprotocol exceptions. |
tests/test__frame_helper.py |
Add tests to ensure handshake failures are surfaced as InvalidEncryptionKeyAPIError / HandshakeAPIError. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
PR Review — Wrap noise handshake decrypt errors with friendly API exceptionsSolid bugfix. The wrap correctly mirrors the existing 🟢 Suggestions1. Inconsistent return annotation on test functions (`tests/test__frame_helper.py`, L1024)
2. Test comment restates the PR rationale (`tests/test__frame_helper.py`, L1010-1013)The last sentence — 3. Consider preserving __cause__ for debugging (`aioesphomeapi/_frame_helper/noise.py`, L315-335)Copilot already flagged this, and it's a reasonable point: catching Checklist
SummarySolid bugfix. The wrap correctly mirrors the existing |
Rebase with requested adjustmentsBranch Changes applied
StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
|
@bluetoothbot gh_request fix the mypy error Run mypy aioesphomeapi |
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
WalkthroughThe PR adds exception handling to the noise protocol handshake in ChangesNoise Handshake Error Handling
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Possibly related PRs
Suggested reviewers
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
What does this implement/fix?
What
APINoiseFrameHelper._handle_handshakenow wrapsself._proto.read_message(...)so that AEAD failures becomeInvalidEncryptionKeyAPIErrorand any othernoise-library exception becomesHandshakeAPIError.Why
If a peer responds with a
preamble=0x00handshake frame whose ciphertext fails authentication, the old code letcryptography.exceptions.InvalidTag(and any othernoise-lib exception) propagate up unwrapped. The exception travelled throughdata_received(except *) into asyncio's transport, which then surfaced it viaconnection_lost→_handle_error→ready_future.set_exception(...). Callers awaitingready_futuresaw a third-party crypto exception type instead of anAPIConnectionErrorsubclass.ESPHome firmware always rejects a bad PSK via the dedicated
preamble=0x01"Handshake MAC failure" frame, so a buggy/hostile peer that ships a malformed handshake on the success preamble was the one path that bypassed the friendly error surface.How
InvalidTag→InvalidEncryptionKeyAPIError, with the already-sanitized_server_name/_server_macattached — matches_error_on_incorrect_preamble's "Handshake MAC failure" path.read_message→HandshakeAPIErrorwithHandshake failed: …text, so callers always get anAPIConnectionErrorsubclass.cdef void _handle_handshake(self, bytes msg) except *signature is unchanged, so the.pxdand the Cython build are untouched.Testing
SKIP_CYTHON=1 .venv/bin/python -m pytest tests/— 1044 passed, 2 skipped.tests/test__frame_helper.py:test_noise_frame_helper_handshake_invalid_tag— preamble=0x00 + 48 bytes of garbage →InvalidEncryptionKeyAPIError, with the sanitizedreceived_namepreserved.test_noise_frame_helper_handshake_other_noise_error— preamble=0x00 + too-short body →HandshakeAPIError("Handshake failed: …").REQUIRE_CYTHON=1 setup.py build_ext --inplacetranslates cleanly (local C compile fails on missingPython.honly; the.cfiles are produced).Types of changes
Related issue or feature (if applicable):
Pull request in esphome (if applicable):
api.protochange.Checklist:
tests/folder).Quality Report
Changes: 2 files changed, 84 insertions(+), 1 deletion(-)
Code scan: clean
Tests: failed (FAILED)
Branch hygiene: clean
Generated by Kōan post-mission quality pipeline