Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
proto_crypto.h
Go to the documentation of this file.
1#pragma once
2
3/// @file proto_crypto.h
4/// @brief Cryptographic helpers for the IO‑Homecontrol protocol.
5/// @ingroup hioc_protocol
6///
7/// IO‑Homecontrol uses AES‑128 encryption and a proprietary 6‑byte HMAC construction
8/// derived from the original Somfy implementation. The "HMAC" here is not a standard
9/// HMAC-SHA; it is a custom construction: the IV is built from frame bytes and checksums,
10/// then encrypted with the system key via AES‑128‑ECB, and the first 6 bytes are taken.
11///
12/// During pairing, the system key itself is transferred to the device using an XOR‑AES
13/// obfuscation with a globally shared transfer key. crypt_key() handles both encryption
14/// and decryption; the operation is symmetric.
15///
16/// crypt_1w_key() is a sibling of crypt_key() for the one-way (1W) protocol: it wraps a
17/// network key inside a CMD_ONEWAY_ADD_CONTROLLER (0x30) frame with the exact same
18/// encrypt-under-TRANSFER_KEY-then-XOR core, differing only in how the IV is built
19/// (construct_iv_1w_node() vs. construct_iv()). The two share that core rather than
20/// duplicating it — see xor_with_encrypted_iv() in proto_crypto.cpp.
21///
22/// create_1w_hmac() is create_hmac()'s 1W sibling: it authenticates an explicit,
23/// command-specific span with a rolling sequence number in place of a random challenge,
24/// sharing construct_iv()'s checksum/pad core (construct_iv_prefix() in proto_crypto.cpp)
25/// rather than duplicating it — see construct_iv_1w_sequence(). Unlike create_hmac(), it has
26/// no default span: the caller must supply exactly the bytes the target command authenticates,
27/// which differ per command (see create_1w_hmac()'s warning below).
28///
29/// @warning The transfer key (TRANSFER_KEY) is hardcoded and public—its only purpose
30/// is to obfuscate the system key during over‑the‑air transfer. The security
31/// of the installation relies entirely on keeping the system key secret.
32
33#include "proto_sizes.h"
34
35namespace esphome {
36namespace home_io_control {
37namespace crypto {
38
39/// Update running checksum bytes (c1, c2) with a new data byte (IO‑Homecontrol IV derivation).
40/// @param byte Input data byte.
41/// @param c1 First checksum byte (inout).
42/// @param c2 Second checksum byte (inout).
43void compute_checksum(uint8_t byte, uint8_t &c1, uint8_t &c2);
44
45/// Construct the 16‑byte IV for AES encryption from frame data and challenge.
46/// Layout: bytes 0–7 = up to 8 frame bytes padded with 0x55, bytes 8–9 = checksums,
47/// bytes 10–15 = challenge.
48/// @param data Frame data bytes.
49/// @param len Number of data bytes.
50/// @param challenge 6‑byte challenge from the device.
51/// @param iv Output: 16‑byte IV.
52void construct_iv(const uint8_t *data, uint8_t len, const uint8_t challenge[HMAC_SIZE], uint8_t iv[IV_SIZE]);
53
54/// Construct the 16‑byte IV for the 1W key‑wrap primitive (crypt_1w_key()) from a sender's
55/// node address alone. Layout: the 3‑byte node address repeated through byte 14
56/// ({N0,N1,N2,N0,N1,N2,…}), with iv[15] = N0. Unlike construct_iv() (2W), this IV carries no
57/// challenge or frame data — a 1W broadcast has no per‑exchange challenge to bind to, so the
58/// sender's node address (already public: it is the frame header's plaintext source field) is
59/// the only per‑sender distinguishing input available.
60/// @param node 3‑byte sender node address.
61/// @param iv Output: 16‑byte IV.
62void construct_iv_1w_node(const uint8_t node[NODE_ID_SIZE], uint8_t iv[IV_SIZE]);
63
64/// Construct the 16‑byte IV for the 1W sequence‑keyed HMAC (create_1w_hmac()).
65/// Layout: bytes 0–7 = up to 8 span bytes padded with 0x55, bytes 8–9 = checksums (the same
66/// shared core construct_iv() uses), bytes 10–11 = the 2‑byte sequence (big‑endian), bytes
67/// 12–15 = 0x55 padding. A 1W frame has no per‑exchange challenge to bind to — the transmitted
68/// sequence is the only per‑transmission value available, so it takes the challenge's place in
69/// the tail while the padded bytes fill the rest.
70/// @param data Span bytes to authenticate. The span is command‑specific — see create_1w_hmac().
71/// @param len Number of span bytes.
72/// @param sequence 2‑byte rolling sequence for this transmission.
73/// @param iv Output: 16‑byte IV.
74void construct_iv_1w_sequence(const uint8_t *data, uint8_t len, uint16_t sequence, uint8_t iv[IV_SIZE]);
75
76/// AES‑128 ECB encrypt a single 16‑byte block.
77/// @param in 16‑byte plaintext block.
78/// @param key 16‑byte AES key.
79/// @param out Output: 16‑byte ciphertext block.
80/// @return true on success.
81bool aes128_encrypt(const uint8_t in[AES_BLOCK_SIZE], const uint8_t key[AES_KEY_SIZE], uint8_t out[AES_BLOCK_SIZE]);
82
83/// AES‑128 ECB decrypt a single 16‑byte block.
84/// @param in 16‑byte ciphertext block.
85/// @param key 16‑byte AES key.
86/// @param out Output: 16‑byte plaintext block.
87/// @return true on success.
88bool aes128_decrypt(const uint8_t in[AES_BLOCK_SIZE], const uint8_t key[AES_KEY_SIZE], uint8_t out[AES_BLOCK_SIZE]);
89
90/// Length of a key check value (key_check_value()).
91static constexpr uint8_t KEY_CHECK_VALUE_SIZE = 3;
92
93/// Compute a key check value: the first 3 bytes of AES‑128 encrypting a block of zeros with `key`.
94///
95/// The usual way to name a key without revealing it: equal keys give equal values, and the value
96/// cannot be turned back into the key. Logged where a key must be compared across attempts, but must
97/// not be printed.
98/// @param key 16‑byte key.
99/// @param out Output: 3‑byte check value.
100/// @return true on success.
101bool key_check_value(const uint8_t key[AES_KEY_SIZE], uint8_t out[KEY_CHECK_VALUE_SIZE]);
102
103/// Create a 6‑byte HMAC for authentication (IO‑Homecontrol proprietary scheme).
104/// Process: build IV from [data + challenge] → AES‑128‑ECB encrypt IV with system key → take first 6 bytes.
105/// This is NOT a standard HMAC; it is specific to IO‑Homecontrol and matches
106/// the protocol specification as implemented in compatible devices.
107/// @param data Frame data bytes (usually command + payload).
108/// @param len Length of data.
109/// @param challenge 6‑byte random challenge.
110/// @param key 16‑byte system key.
111/// @param hmac Output: 6‑byte HMAC.
112/// @note The IV construction appends two checksum bytes derived from the data stream
113/// (see compute_checksum()) followed by the 6‑byte challenge, padded to 16 bytes
114/// with 0x55. The AES result is truncated to 6 bytes for transmission.
115/// @return true on success.
116bool create_hmac(const uint8_t *data, uint8_t len, const uint8_t challenge[HMAC_SIZE], const uint8_t key[AES_KEY_SIZE],
117 uint8_t hmac[HMAC_SIZE]);
118
119/// Verify a received 6‑byte HMAC using constant‑time comparison.
120/// @param data Frame data bytes.
121/// @param len Length of data.
122/// @param hmac Received 6‑byte HMAC.
123/// @param challenge Challenge used in HMAC calculation.
124/// @param key 16‑byte system key.
125/// @return true if HMAC matches.
126bool verify_hmac(const uint8_t *data, uint8_t len, const uint8_t hmac[HMAC_SIZE], const uint8_t challenge[HMAC_SIZE],
127 const uint8_t key[AES_KEY_SIZE]);
128
129/// Create a 6‑byte HMAC for a 1W (one‑way) transmission — the sequence‑keyed sibling of
130/// create_hmac(). Process: build the IV from [span + sequence] (construct_iv_1w_sequence()) →
131/// AES‑128‑ECB encrypt with the controller's key → take the first 6 bytes.
132/// @warning There is NO default span. The authenticated bytes differ per command — e.g.
133/// CMD_ONEWAY_ADD_CONTROLLER (0x30) signs only `cmd + enc_key` (17 bytes), while
134/// CMD_EXECUTE (0x00) signs the full payload `cmd` through `fp2` (7 bytes) — see
135/// `tests/corpus/captures/oneway/reference_1w_oneway_execute_iv_vector.yaml` and
136/// `tests/corpus/captures/enrollment/reference_1w_enrollment_add_controller_kat.yaml`. The
137/// caller must pass the span the target command actually authenticates, confirmed by a
138/// known-answer vector — a wrong span silently produces a plausible-looking MAC that no
139/// device will accept, and only a KAT catches the mistake.
140/// @param data Command‑specific span bytes to authenticate (see @warning above).
141/// @param len Number of span bytes.
142/// @param sequence 2‑byte rolling sequence for this transmission.
143/// @param controller_key 16‑byte key held by the transmitting controller identity. This may be
144/// the hub's own system_key, or a foreign key adopted from another network (Phase 3A
145/// "key adoption"); the primitive is key‑agnostic and simply uses whichever key the
146/// caller supplies.
147/// @param hmac Output: 6‑byte HMAC.
148/// @return true on success.
149bool create_1w_hmac(const uint8_t *data, uint8_t len, uint16_t sequence, const uint8_t controller_key[AES_KEY_SIZE],
150 uint8_t hmac[HMAC_SIZE]);
151
152/// Encrypt or decrypt the system key during pairing (XOR with AES‑encrypted IV).
153/// The same operation works for both directions: encrypting for the device and
154/// decrypting the device's acknowledgement.
155/// @param data Frame data (typically the key‑init command byte).
156/// @param len Length of data (usually 1).
157/// @param challenge Device's 6‑byte challenge.
158/// @param in Input key (plaintext for encrypt, ciphertext for decrypt).
159/// @param out Output key.
160/// @warning This primitive is used ONLY during the key‑transfer phase (0x32). The
161/// resulting system key is then used for all normal authenticated exchanges.
162/// Never call this with arbitrary data outside the pairing sequence.
163/// @return true on success.
164bool crypt_key(const uint8_t *data, uint8_t len, const uint8_t challenge[HMAC_SIZE], const uint8_t in[AES_KEY_SIZE],
165 uint8_t out[AES_KEY_SIZE]);
166
167/// Encrypt or decrypt a 1W controller key during add-controller key adoption (CMD 0x30).
168/// Shares its core with crypt_key() — XOR with AES‑128‑ECB(IV, TRANSFER_KEY) — and differs only
169/// in IV construction: construct_iv_1w_node() derives the IV from the sender's node address
170/// alone, in place of crypt_key()'s frame-data-and-challenge IV. As with crypt_key(), the
171/// operation is its own inverse (XOR with the same keystream both ways), so there is no
172/// direction flag: the same call encrypts a plaintext key for transmission or decrypts an
173/// overheard ciphertext.
174/// @param node 3‑byte sender node address (from the frame header, plaintext).
175/// @param in Input key (plaintext to encrypt, or ciphertext to decrypt).
176/// @param out Output key.
177/// @warning `in`/`out` are real key material (a 1W installation's network key). Callers must
178/// never log, print, or otherwise let these bytes reach any path other than the one
179/// intentional, user-facing "adopted key" emission.
180/// @return true on success.
181bool crypt_1w_key(const uint8_t node[NODE_ID_SIZE], const uint8_t in[AES_KEY_SIZE], uint8_t out[AES_KEY_SIZE]);
182
183/// Generate 6 random bytes for a challenge using the ESP hardware RNG.
184/// @param out Output buffer (6 bytes).
185void generate_challenge(uint8_t out[HMAC_SIZE]);
186
187} // namespace crypto
188} // namespace home_io_control
189} // namespace esphome
void compute_checksum(uint8_t byte, uint8_t &c1, uint8_t &c2)
Proprietary checksum algorithm used in IV construction.
static constexpr uint8_t KEY_CHECK_VALUE_SIZE
Length of a key check value (key_check_value()).
void generate_challenge(uint8_t out[HMAC_SIZE])
Generate 6 random bytes for a challenge using the ESP32 hardware RNG.
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 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 key_check_value(const uint8_t key[AES_KEY_SIZE], uint8_t out[KEY_CHECK_VALUE_SIZE])
Compute a key check value. See proto_crypto.h for the full contract.
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).
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.
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.
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 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 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.
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.
Fundamental IO-Homecontrol frame and crypto size constants.