Skip to content

security: make the fixed debug GCM IV an explicit opt-in (set_debug static_iv) - #745

Open
jasonacox wants to merge 3 commits into
masterfrom
debug-static-iv-optin
Open

jasonacox wants to merge 3 commits into
masterfrom
debug-static-iv-optin

Conversation

@jasonacox

Copy link
Copy Markdown
Owner

Why

When DEBUG logging is enabled, AESCipher.get_encryption_iv() makes every v3.5 (AES-GCM) message use the fixed IV b'0123456789ab'. GCM must never reuse an IV under the same key: with a reused IV, someone capturing LAN traffic can recover message contents and, worse, GCM's authentication subkey, letting them forge commands the device accepts for the rest of that session. Users are routinely asked to enable debug logging when troubleshooting (e.g. Home Assistant), and persistent sessions can last days, so "debug on" silently re-opened the hole #722 closed.

History: uzlonewolf added the fixed debug IV in 0caf951 (Jan 2023) during v3.5 bring-up (#256) to make captures reproducible. #722 replaced it with os.urandom(12), and cdc351c restored it on review feedback to preserve that troubleshooting workflow. This PR keeps the workflow but makes it an explicit choice instead of a side effect of the log level.

A side effect of the old behavior: tests.py enables DEBUG at import, so test_gcm_iv_is_random_and_12_bytes (the guard for #722) was skipped on every run.

Changes

  • AESCipher.STATIC_DEBUG_IV (default False) controls the fixed IV; the log level no longer does.
  • set_debug(toggle=True, color=True, static_iv=False): new parameter added at the end (existing calls unchanged). static_iv=True enables the fixed IV and logs a WARNING that encryption is not secure; set_debug(False) clears it.
  • Tests: the IV randomness test now runs (and asserts DEBUG is on, so it proves logging alone doesn't change the IV); a new test covers opt-in, opt-out, and the warning.
  • Docs: README and __init__.py function lists, PROTOCOL.md IV field, AGENTS.md safety rail (documents the single allowed exception), RELEASE.md.

No wire-format change: the IV is carried in each frame, and the device accepts any value.

Note: v3.5 session keys depend on the device's random nonce, so ciphertext still differs across sessions even with static_iv=True; the fixed IV only makes messages repeatable within a session. Debug logs already show each message's plaintext (final payload) and full encrypted frame including its IV.

Testing

  • python -m tests passes with 0 skips on cryptography (3.8, 3.10), pycryptodome, and with pyaes (GCM tests skip as expected); pylint -E clean.

🤖 Generated with Claude Code

Debug logging no longer switches v3.5 messages to the fixed IV
b'0123456789ab'. The fixed IV (added in 0caf951 for v3.5 bring-up and
restored in cdc351c) is now enabled only by set_debug(True, static_iv=True),
which logs a warning; set_debug(False) clears it.

Because tests.py runs with DEBUG on, test_gcm_iv_is_random_and_12_bytes was
always skipped; it now runs, and a new test covers the opt-in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 13, 2026 06:12
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The security behavior and regression coverage are sound; only a non-blocking documentation wording correction remains.

1 open finding
What changed in this PR

Decouples insecure fixed AES-GCM IVs from DEBUG logging while preserving explicit packet-analysis support.

Changes:

  • Adds static_iv opt-in with a security warning.
  • Restores random IV behavior under DEBUG logging and adds regression tests.
  • Updates security, protocol, API, and release documentation.
File Description
tinytuya/​core/​crypto_helper.py Gates the fixed IV behind explicit state.
tinytuya/​core/​core.py Extends set_debug() with the warned opt-in.
tinytuya/​__init__.py Documents the new parameter.
tests.py Tests random, fixed, and reset behavior.
RELEASE.md Records the security change.
README.md Updates the public API listing.
PROTOCOL.md Documents IV generation and override.
AGENTS.md Adds a crypto safety constraint.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tinytuya/core/core.py Outdated
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@jasonacox-sam jasonacox-sam left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This closes the loop #722 opened and cdc351c accidentally re-opened: "debug on" was silently downgrading every v3.5 session to a reused GCM IV, and test_gcm_iv_is_random_and_12_bytes was skipping itself on every CI run because tests.py enables DEBUG at import — so the guard for #722 was never actually guarding anything. That's a great catch on the test, not just the code.

The opt-in shape is right: STATIC_DEBUG_IV as a class flag set only by set_debug(..., static_iv=True), decoupled from the log level, with a log.warning at enable time, cleared when debug is toggled off, documented in PROTOCOL.md/README, and the previously-skipped test now asserts DEBUG is on while still requiring random IVs. The new test_static_iv_only_when_explicitly_requested covers on/off/reset paths. AGENTS.md exception note prevents future "convenience" reintroduction. No concerns; approving.

— Sam ⚙️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants