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/// Create a 6‑byte HMAC for authentication (IO‑Homecontrol proprietary scheme).
91/// Process: build IV from [data + challenge] → AES‑128‑ECB encrypt IV with system key → take first 6 bytes.
92/// This is NOT a standard HMAC; it is specific to IO‑Homecontrol and matches
93/// the protocol specification as implemented in compatible devices.
94/// @param data Frame data bytes (usually command + payload).
95/// @param len Length of data.
96/// @param challenge 6‑byte random challenge.
97/// @param key 16‑byte system key.
98/// @param hmac Output: 6‑byte HMAC.
99/// @note The IV construction appends two checksum bytes derived from the data stream
100/// (see compute_checksum()) followed by the 6‑byte challenge, padded to 16 bytes
101/// with 0x55. The AES result is truncated to 6 bytes for transmission.
102/// @return true on success.
103bool create_hmac(const uint8_t *data, uint8_t len, const uint8_t challenge[HMAC_SIZE], const uint8_t key[AES_KEY_SIZE],
104 uint8_t hmac[HMAC_SIZE]);
105
106/// Verify a received 6‑byte HMAC using constant‑time comparison.
107/// @param data Frame data bytes.
108/// @param len Length of data.
109/// @param hmac Received 6‑byte HMAC.
110/// @param challenge Challenge used in HMAC calculation.
111/// @param key 16‑byte system key.
112/// @return true if HMAC matches.
113bool verify_hmac(const uint8_t *data, uint8_t len, const uint8_t hmac[HMAC_SIZE], const uint8_t challenge[HMAC_SIZE],
114 const uint8_t key[AES_KEY_SIZE]);
115
116/// Create a 6‑byte HMAC for a 1W (one‑way) transmission — the sequence‑keyed sibling of
117/// create_hmac(). Process: build the IV from [span + sequence] (construct_iv_1w_sequence()) →
118/// AES‑128‑ECB encrypt with the controller's key → take the first 6 bytes.
119/// @warning There is NO default span. The authenticated bytes differ per command — e.g.
120/// CMD_ONEWAY_ADD_CONTROLLER (0x30) signs only `cmd + enc_key` (17 bytes), while
121/// CMD_EXECUTE (0x00) signs the full payload `cmd` through `fp2` (7 bytes) — see
122/// `tests/corpus/captures/oneway/reference_1w_oneway_execute_iv_vector.yaml` and
123/// `tests/corpus/captures/enrollment/reference_1w_enrollment_add_controller_kat.yaml`. The
124/// caller must pass the span the target command actually authenticates, confirmed by a
125/// known-answer vector — a wrong span silently produces a plausible-looking MAC that no
126/// device will accept, and only a KAT catches the mistake.
127/// @param data Command‑specific span bytes to authenticate (see @warning above).
128/// @param len Number of span bytes.
129/// @param sequence 2‑byte rolling sequence for this transmission.
130/// @param controller_key 16‑byte key held by the transmitting controller identity. This may be
131/// the hub's own system_key, or a foreign key adopted from another network (Phase 3A
132/// "key adoption"); the primitive is key‑agnostic and simply uses whichever key the
133/// caller supplies.
134/// @param hmac Output: 6‑byte HMAC.
135/// @return true on success.
136bool create_1w_hmac(const uint8_t *data, uint8_t len, uint16_t sequence, const uint8_t controller_key[AES_KEY_SIZE],
137 uint8_t hmac[HMAC_SIZE]);
138
139/// Encrypt or decrypt the system key during pairing (XOR with AES‑encrypted IV).
140/// The same operation works for both directions: encrypting for the device and
141/// decrypting the device's acknowledgement.
142/// @param data Frame data (typically the key‑init command byte).
143/// @param len Length of data (usually 1).
144/// @param challenge Device's 6‑byte challenge.
145/// @param in Input key (plaintext for encrypt, ciphertext for decrypt).
146/// @param out Output key.
147/// @warning This primitive is used ONLY during the key‑transfer phase (0x32). The
148/// resulting system key is then used for all normal authenticated exchanges.
149/// Never call this with arbitrary data outside the pairing sequence.
150/// @return true on success.
151bool crypt_key(const uint8_t *data, uint8_t len, const uint8_t challenge[HMAC_SIZE], const uint8_t in[AES_KEY_SIZE],
152 uint8_t out[AES_KEY_SIZE]);
153
154/// Encrypt or decrypt a 1W controller key during add-controller key adoption (CMD 0x30).
155/// Shares its core with crypt_key() — XOR with AES‑128‑ECB(IV, TRANSFER_KEY) — and differs only
156/// in IV construction: construct_iv_1w_node() derives the IV from the sender's node address
157/// alone, in place of crypt_key()'s frame-data-and-challenge IV. As with crypt_key(), the
158/// operation is its own inverse (XOR with the same keystream both ways), so there is no
159/// direction flag: the same call encrypts a plaintext key for transmission or decrypts an
160/// overheard ciphertext.
161/// @param node 3‑byte sender node address (from the frame header, plaintext).
162/// @param in Input key (plaintext to encrypt, or ciphertext to decrypt).
163/// @param out Output key.
164/// @warning `in`/`out` are real key material (a 1W installation's network key). Callers must
165/// never log, print, or otherwise let these bytes reach any path other than the one
166/// intentional, user-facing "adopted key" emission.
167/// @return true on success.
168bool crypt_1w_key(const uint8_t node[NODE_ID_SIZE], const uint8_t in[AES_KEY_SIZE], uint8_t out[AES_KEY_SIZE]);
169
170/// Generate 6 random bytes for a challenge using the ESP hardware RNG.
171/// @param out Output buffer (6 bytes).
172void generate_challenge(uint8_t out[HMAC_SIZE]);
173
174} // namespace crypto
175} // namespace home_io_control
176} // namespace esphome
void compute_checksum(uint8_t byte, uint8_t &c1, uint8_t &c2)
Proprietary checksum algorithm used in IV construction.
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 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.
static constexpr uint8_t NODE_ID_SIZE
Device/node addresses are 3 bytes (e.g., "123ABC").
Definition proto_sizes.h:20
static constexpr uint8_t AES_BLOCK_SIZE
AES block size.
Definition proto_sizes.h:24
static constexpr uint8_t HMAC_SIZE
Authentication HMAC is 6 bytes (truncated AES output).
Definition proto_sizes.h:22
static constexpr uint8_t AES_KEY_SIZE
AES-128 key size.
Definition proto_sizes.h:23
static constexpr uint8_t IV_SIZE
Initialization vector size for AES.
Definition proto_sizes.h:25
Fundamental IO-Homecontrol frame and crypto size constants.