Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
esphome::home_io_control::crypto Namespace Reference

Functions

void compute_checksum (uint8_t byte, uint8_t &c1, uint8_t &c2)
 Proprietary checksum algorithm used in IV construction.
static void construct_iv_prefix (const uint8_t *data, uint8_t len, uint8_t iv[IV_SIZE])
 Shared core of construct_iv() (2W) and construct_iv_1w_sequence() (1W): fills IV bytes 0-9 — up to 8 bytes of span data (0x55-padded if the span is shorter), then the two running checksum bytes (compute_checksum()) accumulated over the whole span.
void construct_iv (const uint8_t *data, uint8_t len, const uint8_t challenge[HMAC_SIZE], uint8_t iv[IV_SIZE])
 Construct the 16-byte initialization vector (IV) for AES encryption.
void construct_iv_1w_sequence (const uint8_t *data, uint8_t len, uint16_t sequence, uint8_t iv[IV_SIZE])
 Construct the 16-byte IV for the 1W sequence-keyed HMAC (create_1w_hmac()).
void construct_iv_1w_node (const uint8_t node[NODE_ID_SIZE], uint8_t iv[IV_SIZE])
 Construct the 16-byte IV for the 1W key-wrap primitive from a sender's node address alone.
bool aes128_encrypt (const uint8_t in[AES_BLOCK_SIZE], const uint8_t key[AES_KEY_SIZE], uint8_t out[AES_BLOCK_SIZE])
 AES-128 ECB encrypt a single 16-byte block.
bool aes128_decrypt (const uint8_t in[AES_BLOCK_SIZE], const uint8_t key[AES_KEY_SIZE], uint8_t out[AES_BLOCK_SIZE])
 AES‑128 ECB decrypt a single 16‑byte block.
bool create_hmac (const uint8_t *data, uint8_t len, const uint8_t challenge[HMAC_SIZE], const uint8_t key[AES_KEY_SIZE], uint8_t hmac[HMAC_SIZE])
 Create a 6-byte HMAC for authentication (proprietary IO-Homecontrol scheme).
bool create_1w_hmac (const uint8_t *data, uint8_t len, uint16_t sequence, const uint8_t controller_key[AES_KEY_SIZE], uint8_t hmac[HMAC_SIZE])
 Create the 6-byte authenticator for a 1W frame.
bool verify_hmac (const uint8_t *data, uint8_t len, const uint8_t hmac[HMAC_SIZE], const uint8_t challenge[HMAC_SIZE], const uint8_t key[AES_KEY_SIZE])
 Verify a received HMAC using constant-time comparison.
static bool xor_with_encrypted_iv (const uint8_t iv[IV_SIZE], const uint8_t in[AES_KEY_SIZE], uint8_t out[AES_KEY_SIZE])
 Shared core of crypt_key() (2W) and crypt_1w_key() (1W): AES-128-ECB-encrypt iv under the public TRANSFER_KEY, then XOR the result over in.
bool crypt_key (const uint8_t *data, uint8_t len, const uint8_t challenge[HMAC_SIZE], const uint8_t in[AES_KEY_SIZE], uint8_t out[AES_KEY_SIZE])
 Encrypt or decrypt a system key during pairing.
bool crypt_1w_key (const uint8_t node[NODE_ID_SIZE], const uint8_t in[AES_KEY_SIZE], uint8_t out[AES_KEY_SIZE])
 Encrypt or decrypt a 1W controller key during add-controller key adoption (CMD 0x30).
void generate_challenge (uint8_t out[HMAC_SIZE])
 Generate 6 random bytes for a challenge using the ESP32 hardware RNG.

Function Documentation

◆ aes128_decrypt()

bool esphome::home_io_control::crypto::aes128_decrypt ( const uint8_t in[AES_BLOCK_SIZE],
const uint8_t key[AES_KEY_SIZE],
uint8_t out[AES_BLOCK_SIZE] )

AES‑128 ECB decrypt a single 16‑byte block.

Parameters
in16‑byte ciphertext block.
key16‑byte AES key.
outOutput: 16‑byte plaintext block.
Returns
true on success.

Definition at line 355 of file proto_crypto.cpp.

◆ aes128_encrypt()

bool esphome::home_io_control::crypto::aes128_encrypt ( const uint8_t in[AES_BLOCK_SIZE],
const uint8_t key[AES_KEY_SIZE],
uint8_t out[AES_BLOCK_SIZE] )

AES-128 ECB encrypt a single 16-byte block.

AES‑128 ECB encrypt a single 16‑byte block.

Parameters
in16‑byte plaintext block.
key16‑byte AES key.
outOutput: 16‑byte ciphertext block.
Returns
true on success.

Definition at line 351 of file proto_crypto.cpp.

◆ compute_checksum()

void esphome::home_io_control::crypto::compute_checksum ( uint8_t byte,
uint8_t & c1,
uint8_t & c2 )

Proprietary checksum algorithm used in IV construction.

Update running checksum bytes (c1, c2) with a new data byte (IO‑Homecontrol IV derivation).

This is NOT a standard CRC - it's a custom accumulator used by the IO-Homecontrol protocol to mix frame data into the initialization vector for AES encryption.

Parameters
byteInput data byte.
c1First checksum byte (inout).
c2Second checksum byte (inout).

Definition at line 272 of file proto_crypto.cpp.

◆ construct_iv()

void esphome::home_io_control::crypto::construct_iv ( const uint8_t * data,
uint8_t len,
const uint8_t challenge[HMAC_SIZE],
uint8_t iv[IV_SIZE] )

Construct the 16-byte initialization vector (IV) for AES encryption.

Construct the 16‑byte IV for AES encryption from frame data and challenge.

Layout: [bytes 0-7: frame data or 0x55 padding] [bytes 8-9: checksums] [bytes 10-15: challenge] The IV binds the HMAC to both the frame content and the random challenge, preventing replay attacks. Bytes 0-9 are construct_iv_prefix()'s shared core; see construct_iv_1w_sequence() for the 1W sibling that reuses it with a different tail.

Layout: bytes 0–7 = up to 8 frame bytes padded with 0x55, bytes 8–9 = checksums, bytes 10–15 = challenge.

Parameters
dataFrame data bytes.
lenNumber of data bytes.
challenge6‑byte challenge from the device.
ivOutput: 16‑byte IV.

Definition at line 318 of file proto_crypto.cpp.

Here is the call graph for this function:

◆ construct_iv_1w_node()

void esphome::home_io_control::crypto::construct_iv_1w_node ( const uint8_t node[NODE_ID_SIZE],
uint8_t iv[IV_SIZE] )

Construct the 16-byte IV for the 1W key-wrap primitive from a sender's node address alone.

Construct the 16‑byte IV for the 1W key‑wrap primitive (crypt_1w_key()) from a sender's node address alone.

A 1W broadcast (CMD_ONEWAY_ADD_CONTROLLER, 0x30) has no per-exchange challenge to bind to — the node address is the only public, per-sender input available, so it is repeated to fill the block. node[i % NODE_ID_SIZE] naturally lands iv[15] on node[0] (15 % 3 == 0), matching the published vector without a separate last-byte special case.

Layout: the 3‑byte node address repeated through byte 14 ({N0,N1,N2,N0,N1,N2,…}), with iv[15] = N0. Unlike construct_iv() (2W), this IV carries no challenge or frame data — a 1W broadcast has no per‑exchange challenge to bind to, so the sender's node address (already public: it is the frame header's plaintext source field) is the only per‑sender distinguishing input available.

Parameters
node3‑byte sender node address.
ivOutput: 16‑byte IV.

Definition at line 345 of file proto_crypto.cpp.

◆ construct_iv_1w_sequence()

void esphome::home_io_control::crypto::construct_iv_1w_sequence ( const uint8_t * data,
uint8_t len,
uint16_t sequence,
uint8_t iv[IV_SIZE] )

Construct the 16-byte IV for the 1W sequence-keyed HMAC (create_1w_hmac()).

Construct the 16‑byte IV for the 1W sequence‑keyed HMAC (create_1w_hmac()).

Layout: [bytes 0-7: span data or 0x55 padding] [bytes 8-9: checksums] [bytes 10-11: sequence, big-endian] [bytes 12-15: 0x55 padding]. Shares construct_iv_prefix() with the 2W construct_iv() — only the tail differs, matching a real 1W frame's shape: there is no per-exchange challenge, only the sequence number being transmitted. Verified against the published IV vector in tests/corpus/captures/oneway/reference_1w_oneway_execute_iv_vector.yaml (payload 00 01 43 D2 00 00 00, sequence 0x0599 -> IV 000143D2000000550500059955555555).

Layout: bytes 0–7 = up to 8 span bytes padded with 0x55, bytes 8–9 = checksums (the same shared core construct_iv() uses), bytes 10–11 = the 2‑byte sequence (big‑endian), bytes 12–15 = 0x55 padding. A 1W frame has no per‑exchange challenge to bind to — the transmitted sequence is the only per‑transmission value available, so it takes the challenge's place in the tail while the padded bytes fill the rest.

Parameters
dataSpan bytes to authenticate. The span is command‑specific — see create_1w_hmac().
lenNumber of span bytes.
sequence2‑byte rolling sequence for this transmission.
ivOutput: 16‑byte IV.

Definition at line 330 of file proto_crypto.cpp.

Here is the call graph for this function:

◆ construct_iv_prefix()

void esphome::home_io_control::crypto::construct_iv_prefix ( const uint8_t * data,
uint8_t len,
uint8_t iv[IV_SIZE] )
static

Shared core of construct_iv() (2W) and construct_iv_1w_sequence() (1W): fills IV bytes 0-9 — up to 8 bytes of span data (0x55-padded if the span is shorter), then the two running checksum bytes (compute_checksum()) accumulated over the whole span.

The two callers differ only in how they fill the remaining tail (bytes 10-15): a 6-byte challenge for 2W, or a 2-byte sequence plus 0x55 padding for 1W. Factoring this out keeps the checksum/pad accumulation in exactly one place rather than forked per variant — the same reasoning that keeps xor_with_encrypted_iv() (below) shared between crypt_key() and crypt_1w_key().

Definition at line 297 of file proto_crypto.cpp.

Here is the call graph for this function:

◆ create_1w_hmac()

bool esphome::home_io_control::crypto::create_1w_hmac ( const uint8_t * data,
uint8_t len,
uint16_t sequence,
const uint8_t controller_key[AES_KEY_SIZE],
uint8_t hmac[HMAC_SIZE] )

Create the 6-byte authenticator for a 1W frame.

Create a 6‑byte HMAC for a 1W (one‑way) transmission — the sequence‑keyed sibling of create_hmac().

See proto_crypto.h for the full contract — in particular that data/len is a command-specific span the caller must get right, and that controller_key is whichever key the controller identity holds. Structurally identical to create_hmac() apart from the IV tail (sequence instead of challenge), so both truncate the same AES-128 output the same way.

Process: build the IV from [span + sequence] (construct_iv_1w_sequence()) → AES‑128‑ECB encrypt with the controller's key → take the first 6 bytes.

Warning
There is NO default span. The authenticated bytes differ per command — e.g. CMD_ONEWAY_ADD_CONTROLLER (0x30) signs only cmd + enc_key (17 bytes), while CMD_EXECUTE (0x00) signs the full payload cmd through fp2 (7 bytes) — see tests/corpus/captures/oneway/reference_1w_oneway_execute_iv_vector.yaml and tests/corpus/captures/enrollment/reference_1w_enrollment_add_controller_kat.yaml. The caller must pass the span the target command actually authenticates, confirmed by a known-answer vector — a wrong span silently produces a plausible-looking MAC that no device will accept, and only a KAT catches the mistake.
Parameters
dataCommand‑specific span bytes to authenticate (see
Warning
above).
Parameters
lenNumber of span bytes.
sequence2‑byte rolling sequence for this transmission.
controller_key16‑byte key held by the transmitting controller identity. This may be the hub's own system_key, or a foreign key adopted from another network (Phase 3A "key adoption"); the primitive is key‑agnostic and simply uses whichever key the caller supplies.
hmacOutput: 6‑byte HMAC.
Returns
true on success.

Definition at line 378 of file proto_crypto.cpp.

Here is the call graph for this function:

◆ create_hmac()

bool esphome::home_io_control::crypto::create_hmac ( const uint8_t * data,
uint8_t len,
const uint8_t challenge[HMAC_SIZE],
const uint8_t key[AES_KEY_SIZE],
uint8_t hmac[HMAC_SIZE] )

Create a 6-byte HMAC for authentication (proprietary IO-Homecontrol scheme).

Create a 6‑byte HMAC for authentication (IO‑Homecontrol proprietary scheme).

See proto_crypto.h for full parameter documentation and construction details.

Process: build IV from [data + challenge] → AES‑128‑ECB encrypt IV with system key → take first 6 bytes. This is NOT a standard HMAC; it is specific to IO‑Homecontrol and matches the protocol specification as implemented in compatible devices.

Parameters
dataFrame data bytes (usually command + payload).
lenLength of data.
challenge6‑byte random challenge.
key16‑byte system key.
hmacOutput: 6‑byte HMAC.
Note
The IV construction appends two checksum bytes derived from the data stream (see compute_checksum()) followed by the 6‑byte challenge, padded to 16 bytes with 0x55. The AES result is truncated to 6 bytes for transmission.
Returns
true on success.

Definition at line 361 of file proto_crypto.cpp.

Here is the call graph for this function:

◆ crypt_1w_key()

bool esphome::home_io_control::crypto::crypt_1w_key ( const uint8_t node[NODE_ID_SIZE],
const uint8_t in[AES_KEY_SIZE],
uint8_t out[AES_KEY_SIZE] )

Encrypt or decrypt a 1W controller key during add-controller key adoption (CMD 0x30).

See proto_crypto.h for the full contract; this is crypt_key()'s 1W sibling, sharing the same xor_with_encrypted_iv() core with a node-derived IV in place of a challenge-derived one.

Shares its core with crypt_key() — XOR with AES‑128‑ECB(IV, TRANSFER_KEY) — and differs only in IV construction: construct_iv_1w_node() derives the IV from the sender's node address alone, in place of crypt_key()'s frame-data-and-challenge IV. As with crypt_key(), the operation is its own inverse (XOR with the same keystream both ways), so there is no direction flag: the same call encrypts a plaintext key for transmission or decrypts an overheard ciphertext.

Parameters
node3‑byte sender node address (from the frame header, plaintext).
inInput key (plaintext to encrypt, or ciphertext to decrypt).
outOutput key.
Warning
in/out are real key material (a 1W installation's network key). Callers must never log, print, or otherwise let these bytes reach any path other than the one intentional, user-facing "adopted key" emission.
Returns
true on success.

Definition at line 435 of file proto_crypto.cpp.

Here is the call graph for this function:

◆ crypt_key()

bool esphome::home_io_control::crypto::crypt_key ( const uint8_t * data,
uint8_t len,
const uint8_t challenge[HMAC_SIZE],
const uint8_t in[AES_KEY_SIZE],
uint8_t out[AES_KEY_SIZE] )

Encrypt or decrypt a system key during pairing.

Encrypt or decrypt the system key during pairing (XOR with AES‑encrypted IV).

The system key is XORed with AES(IV, TRANSFER_KEY) - the same operation encrypts and decrypts. The TRANSFER_KEY is a hardcoded key known to all IO-Homecontrol devices.

The same operation works for both directions: encrypting for the device and decrypting the device's acknowledgement.

Parameters
dataFrame data (typically the key‑init command byte).
lenLength of data (usually 1).
challengeDevice's 6‑byte challenge.
inInput key (plaintext for encrypt, ciphertext for decrypt).
outOutput key.
Warning
This primitive is used ONLY during the key‑transfer phase (0x32). The resulting system key is then used for all normal authenticated exchanges. Never call this with arbitrary data outside the pairing sequence.
Returns
true on success.

Definition at line 425 of file proto_crypto.cpp.

Here is the call graph for this function:

◆ generate_challenge()

void esphome::home_io_control::crypto::generate_challenge ( uint8_t out[HMAC_SIZE])

Generate 6 random bytes for a challenge using the ESP32 hardware RNG.

Generate 6 random bytes for a challenge using the ESP hardware RNG.

Parameters
outOutput buffer (6 bytes).

Definition at line 442 of file proto_crypto.cpp.

◆ verify_hmac()

bool esphome::home_io_control::crypto::verify_hmac ( const uint8_t * data,
uint8_t len,
const uint8_t hmac[HMAC_SIZE],
const uint8_t challenge[HMAC_SIZE],
const uint8_t key[AES_KEY_SIZE] )

Verify a received HMAC using constant-time comparison.

Verify a received 6‑byte HMAC using constant‑time comparison.

Full documentation in proto_crypto.h.

Parameters
dataFrame data bytes.
lenLength of data.
hmacReceived 6‑byte HMAC.
challengeChallenge used in HMAC calculation.
key16‑byte system key.
Returns
true if HMAC matches.

Definition at line 392 of file proto_crypto.cpp.

Here is the call graph for this function:

◆ xor_with_encrypted_iv()

bool esphome::home_io_control::crypto::xor_with_encrypted_iv ( const uint8_t iv[IV_SIZE],
const uint8_t in[AES_KEY_SIZE],
uint8_t out[AES_KEY_SIZE] )
static

Shared core of crypt_key() (2W) and crypt_1w_key() (1W): AES-128-ECB-encrypt iv under the public TRANSFER_KEY, then XOR the result over in.

The two callers differ only in how they build iv (construct_iv() vs. construct_iv_1w_node()); factoring this tail out keeps the encrypt-under-TRANSFER_KEY-then-XOR logic in exactly one place instead of two copies that could drift apart.

Definition at line 412 of file proto_crypto.cpp.

Here is the call graph for this function: