Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
ADR 0007: Self-contained AES-128 implementation

Status: Accepted · Recorded: 2026-08

Context

Authentication needs AES-128-ECB in exactly one fixed mode and key size, for two things: the 6-byte truncated MAC in challenge-response, and the symmetric obfuscation that carries the system key during pairing.

ESP-IDF already contains mbedTLS, which can do this. But reaching it from an ESPHome external component means depending on how ESPHome's generated src component links mbedTLS — not a declared interface of the framework, and it varies with ESP-IDF version and configuration.

Options considered

  1. Depend on mbedTLS. Fewer lines to own, but ties the component's cryptography to whatever linkage the surrounding build happens to produce, for one fixed-mode operation. That breaking is a build failure in user configurations, found by users rather than CI.
  2. Ship a small self-contained AES-128 scoped to the one mode needed.

Decision

Option 2 — a compact FIPS-197 Rijndael implementation in the protocol layer's crypto module, specifically to avoid a build-time dependency on ESP-IDF/mbedTLS linkage from ESPHome's generated src component.

Consequences

  • Cryptography no longer depends on the surrounding build's mbedTLS configuration, and builds unchanged in the host test environment where no ESP-IDF exists at all.
  • This project owns correctness of that primitive. What actually guards it, strongest first:
    • Real captured exchanges (ADR 0010) replayed through the real code: recorded authenticated frames must verify, recorded key-transfers must decrypt to the expected corpus key. This is the load-bearing evidence for the protocol layer — those bytes came off the air, so agreeing with them means agreeing with reality.
    • Published NIST/FIPS-197 known-answer vectors for the AES-128 primitive itself — plaintext, key, and ciphertext copied from FIPS-197 Appendix B and NIST SP 800-38A, not generated by this codebase. This is the one check that is independent of everything else here: it cannot pass because two of our own ports agree with each other, only because the primitive matches the published standard.
    • Round-trip and negative tests covering encrypt/decrypt symmetry, and confirming a wrong challenge or IV fails rather than silently producing a plausible answer.
    • Cross-language vectors pinning C++ against the Python port used by the corpus tooling. These were generated from the same C++ implementation — they prove the two ports agree, not that either is independently correct.
  • No general-purpose crypto surface exists here; the module exposes only the protocol's own operations. A future need for another algorithm or mode should be re-evaluated against this decision, not quietly bolted on.