Security: random nonces/IVs and GCM auth enforcement for v3.4/v3.5 sessions#722
Conversation
Three protocol-crypto hardening fixes, all wire-compatible (no change to what devices accept, no public API change): 1. AES-GCM message nonce (v3.5): was derived from time.time() at ~0.1s granularity, and a fixed constant b'0123456789ab' whenever DEBUG logging was enabled -- both reuse the nonce across messages under one session key, breaking GCM confidentiality and enabling tag forgery. Now os.urandom(12). The IV is transmitted in the frame, so this changes nothing on the wire. 2. Session-key client nonce (v3.4/v3.5): was hardcoded to '0123456789abcdef', making the session key a deterministic function of the device nonce alone and reusing the same GCM IV every 3.5 session. Now os.urandom(16) per negotiation. Devices accept any client nonce. 3. Enforce GCM authentication on receive: 6699/GCM frames whose auth tag fails were passed on as raw ciphertext. XenonDevice._receive now raises DecodeError (handled by existing retry logic) and Monitor drops the frame. unpack_message is left pure so offline packet-analysis tooling still works. Adds regression tests for nonce/IV randomness and a RELEASE.md entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR hardens session cryptography for v3.4/v3.5 by eliminating deterministic/non-unique nonces and ensuring AES-GCM authentication failures don’t propagate unauthenticated ciphertext into payload decoding. The changes are intended to be wire-compatible by relying on protocol fields that already carry the IV/nonce.
Changes:
- Switch v3.5 AES-GCM message IVs to
os.urandom(12)(removing time-derived / debug-constant IVs). - Switch v3.4/v3.5 session negotiation client nonce to
os.urandom(16)per negotiation. - Enforce dropping/rejecting 6699 frames when GCM authentication fails (raise
DecodeErrorinXenonDevice._receive(), drop inMonitor).
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tinytuya/core/XenonDevice.py | Rejects unauthenticated 6699/GCM frames and randomizes session negotiation client nonce. |
| tinytuya/core/Monitor.py | Drops 6699/GCM frames that fail authentication instead of decoding ciphertext. |
| tinytuya/core/crypto_helper.py | Generates random AES-GCM IVs via os.urandom(12) when iv is True. |
| tests.py | Adds regression tests for nonce/IV randomness. |
| RELEASE.md | Documents the session crypto hardening changes in Unreleased notes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
jasonacox-sam
left a comment
There was a problem hiding this comment.
This is a critical security fix and the implementation is clean.
Verified each change:
crypto_helper.py:os.urandom(12)replaces both thetime.time()-derived nonce and theb'0123456789ab'debug constant. GCM nonce reuse under a single key is catastrophic — this was a real vulnerability, not theoretical.XenonDevice._negotiate_session_key_generate_step_1(): hardcodedb'0123456789abcdef'→os.urandom(16). The session key was deterministic given the device nonce alone — good catch._receive()GCM auth enforcement: raisingDecodeErroron failedcrc_goodis the right call. Feeding unauthenticated ciphertext to the decoder was silently processing garbage as if it were real data.Monitor._decode_and_dispatch(): dropping the frame instead of decoding is consistent.
Agree with Copilot that a regression test exercising the GCM auth-failure path (forged/corrupt tag → DecodeError) would round out the test coverage, but the existing randomness tests cover the core change. Not a blocker.
The deliberate exclusion of strict 55AA CRC32/HMAC rejection is the right call — that path needs real hardware validation before enabling by default.
Ship it. — Sam ⚙️
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
|
@jasonacox-sam can you run hardware test of this PR? Perhaps a scan and exercise a status query of some of the various device versions? |
| raise NotImplementedError( 'Crypto library does not support GCM' ) | ||
| if iv is True: | ||
| if log.isEnabledFor( logging.DEBUG ): | ||
| iv = b'0123456789ab' |
There was a problem hiding this comment.
I believe the debug mode hardcoded iv was by design to help with troubleshooting. We should revert this (debug conditional). @jasonacox-sam can you do that?
There was a problem hiding this comment.
Done ✅ — Restored the debug-conditional IV in get_encryption_iv():
if iv is True:
if log.isEnabledFor( logging.DEBUG ):
# Debug mode: fixed IV for troubleshooting and packet analysis
iv = b'0123456789ab'
else:
iv = os.urandom(12)Production traffic now uses os.urandom(12) (fixing the nonce-reuse issue), while debug mode keeps the fixed IV so packet captures remain easy to correlate.
— Sam ⚙️
Keep os.urandom(12) for production use but revert to the hardcoded IV b'0123456789ab' when DEBUG logging is enabled, preserving the troubleshooting/packet-analysis workflow. Per review feedback from jasonacox on PR #722.
Hardware Test Results — PR #722Ran a full hardware sweep using the PR branch ( 🔍 Network Scan
No regressions observed in broadcast parsing across versions. ✅ Status Query ResultsTested
All 6 device/version combinations returned valid
|
|
@jasonacox-sam -Thanks! Yes, please update to skip this test about iv when DEBUG is active. |
|
@jasonacox — Done ✅ Added a — Sam ⚙️ |
|
@jasonacox-sam Please conduct hardware regression testing of this repo before we merge. Report findings. |
Hardware Regression Test Results — PR #722@jasonacox — Regression testing complete. All protocols pass, no issues found. Branch: Unit TestsThe 1 skip is the GCM IV randomness test — intentionally skipped when Network Scan
Device Status Queries — All Protocol Versions
Coverage Summary
What This ValidatesThe crypto changes in this PR (random nonces/IVs, GCM auth enforcement for v3.4/v3.5) don't break any existing device communication across all four protocol versions. The v3.4 Zone Controls and v3.5 Hutch bulb — the devices most affected by the GCM changes — connect and return full DPS without errors. Verdict: Safe to merge. 🟢 — Sam ⚙️ |
Session crypto hardening (v3.4/v3.5)
Three protocol-crypto fixes, all wire-compatible — no change to what devices accept, no public API change. Verified against
tools/fake-v35-device.pybehavior (it reads the client nonce and IV off the wire).Random AES-GCM nonces (v3.5) — message nonces were derived from
time.time()at ~0.1 s granularity (back-to-back messages under one session key reuse the nonce), and became the fixed constantb'0123456789ab'whenever DEBUG logging was enabled. Nonce reuse under GCM leaks plaintext XOR and enables tag forgery. Nowos.urandom(12); the IV is transmitted in the frame, so nothing changes on the wire.Random session-key client nonce (v3.4/v3.5) — the client nonce was hardcoded
b'0123456789abcdef', making the session key a deterministic function of the device nonce alone (and reusing the same GCM IV every 3.5 session). Nowos.urandom(16)per negotiation.Enforce GCM authentication on receive — 6699 frames whose auth tag fails were previously passed downstream as raw undecrypted ciphertext.
XenonDevice._receive()now raisesDecodeError(handled by existing retry logic) andMonitordrops the frame.unpack_message()itself is left pure so offline packet-analysis tooling (tools/pcap_parse.py) still works.Deliberately not included: strict rejection of 55AA CRC32/HMAC mismatches. That path has never been enforced and enabling it by default needs validation against real hardware first — flagged for a follow-up discussion.
Adds regression tests for nonce/IV randomness (23 → 25 tests).
pylint -Eclean.🤖 Generated with Claude Code