55constexpr uint32_t KEY_EXTRACTION_AUTO_OFF_MS = 10 * 60 * 1000;
61constexpr uint8_t KEY_EXTRACTION_ADVERTISED_SUBTYPE = 0;
62constexpr uint8_t KEY_EXTRACTION_ID_GEN_MAX_ATTEMPTS = 16;
63constexpr const char *KEY_EXTRACTION_TIMEOUT_NAME =
"key_extraction_auto_off";
64constexpr uint32_t RANDOM_LOW_BYTE_MASK = 0xFF;
68std::string format_key_hex(
const uint8_t key[
AES_KEY_SIZE]) {
73 snprintf(byte_buf,
sizeof(byte_buf),
"%02X", key[i]);
82 for (uint8_t attempt = 0; attempt < KEY_EXTRACTION_ID_GEN_MAX_ATTEMPTS; attempt++) {
84 out[i] =
static_cast<uint8_t
>(esp_random() & RANDOM_LOW_BYTE_MASK);
119 "Key extraction: ARMED for 10 minutes, throwaway ID %s. Put your existing hub into pairing/add-device "
123 this->set_timeout(KEY_EXTRACTION_TIMEOUT_NAME, KEY_EXTRACTION_AUTO_OFF_MS, [
this]() {
130 ESP_LOGW(
detail::TAG,
"Key extraction: window expired, no pairing attempt seen. Disarming.");
132 ESP_LOGW(
detail::TAG,
"Key extraction: window expired while in progress (reached stage=%s). Disarming.",
150 if (memcmp(frame.
dst, this->key_extraction_ctx_.throwaway_id,
NODE_ID_SIZE) != 0)
178 const uint16_t preamble = this->
radio_->response_preamble();
190 this->key_extraction_ctx_.advertised_type, this->key_extraction_ctx_.advertised_subtype,
191 KEY_EXTRACTION_MANUFACTURER_ID)) {
192 ESP_LOGW(
detail::TAG,
"Key extraction: failed to build discovery response");
196 ESP_LOGI(
detail::TAG,
"Key extraction: replied to discovery from hub %s with throwaway ID %s",
206 ESP_LOGW(
detail::TAG,
"Key extraction: failed to build discovery-confirm ack");
210 ESP_LOGI(
detail::TAG,
"Key extraction: acknowledged discovery confirm from hub %s",
222 this->key_extraction_ctx_.challenge)) {
223 ESP_LOGW(
detail::TAG,
"Key extraction: failed to build challenge request");
232 ESP_LOGW(
detail::TAG,
"Key extraction: key-transfer payload too short (%u bytes)", frame.
data_len);
242 ESP_LOGW(
detail::TAG,
"Key extraction: failed to build key confirm");
270 ESP_LOGW(
detail::TAG,
"========================================");
271 ESP_LOGW(
detail::TAG,
"SYSTEM KEY EXTRACTED -- DO NOT SHARE YOUR SYSTEM KEY");
272 ESP_LOGW(
detail::TAG,
"Anyone with this key and node_id can control every device on this installation.");
273 ESP_LOGW(
detail::TAG,
"This exchange has not been independently confirmed against your specific hub -- test");
274 ESP_LOGW(
detail::TAG,
"this key (e.g. by controlling a device with it) before relying on it.");
275 ESP_LOGW(
detail::TAG,
"Copy the block below into a new hub's YAML.");
276 ESP_LOGW(
detail::TAG,
"========================================");
278 ESP_LOGW(
detail::TAG,
" node_id: \"%s\"", node_id_str.c_str());
279 ESP_LOGW(
detail::TAG,
" system_key: \"%s\"", key_str.c_str());
280 ESP_LOGW(
detail::TAG,
"========================================");
void handle_key_extraction_discover_confirm_(const IoFrame &frame)
Handle an inbound CMD_DISCOVER_CONFIRM (0x2C) addressed to our throwaway node ID while armed.
void log_key_extraction_result_()
Emit the security-sensitive "system key extracted" log block (see redaction.h — this is the one delib...
virtual void set_key_extraction_armed(bool armed)
Arm or disarm the "Accept Foreign Pairing (Key Extraction)" responder.
void handle_key_extraction_key_init_(const IoFrame &frame)
Handle an inbound CMD_KEY_INIT (0x31) addressed to our throwaway node ID while armed.
std::function< void(bool)> key_extraction_armed_callback_
Invoked whenever the key-extraction armed state changes; see set_key_extraction_armed_callback().
void generate_key_extraction_throwaway_id_(uint8_t out[NODE_ID_SIZE])
Generate a random throwaway node ID for one key-extraction arm cycle, avoiding collisions with the br...
void handle_key_extraction_key_transfer_(const IoFrame &frame)
Handle an inbound CMD_KEY_TRANSFER (0x32) addressed to our throwaway node ID while armed.
bool transmit_frame_(const IoFrame &frame, uint32_t freq, uint16_t preamble)
Transmit a raw IoFrame on the current frequency with given preamble length.
void handle_key_extraction_discover_(const IoFrame &frame)
Handle an inbound CMD_DISCOVER_REQ (0x28) while the key-extraction responder is armed.
uint8_t node_id_[NODE_ID_SIZE]
pairing_responder::ResponderContext key_extraction_ctx_
State for the current "Accept Foreign Pairing" (key-extraction) arm cycle; DISARMED by default so a f...
bool try_handle_key_extraction_frame_(const IoFrame &frame)
Dispatch a frame to the key-extraction responder if it's one of its 0x28/0x2C/0x31/0x32 frames and th...
void broadcast_key_extraction_reply_(const IoFrame &frame)
Transmit a key-extraction reply frame on all 3 IO-homecontrol channels, using the radio driver's resp...
Internal helpers shared by the hub implementation .cpp files.
void generate_challenge(uint8_t out[HMAC_SIZE])
Generate 6 random bytes for a challenge using the ESP32 hardware RNG.
constexpr const char * TAG
Shared log tag for hub-level messages.
bool on_key_transfer(ResponderContext &ctx, const uint8_t transfer_payload[AES_KEY_SIZE])
Decide how to react to an inbound CMD_KEY_TRANSFER (0x32) while armed.
const char * responder_stage_name(ResponderState state)
Get a short, log/telemetry-friendly name for a responder state.
bool on_discover_confirm(ResponderContext &ctx)
Decide how to react to an inbound CMD_DISCOVER_CONFIRM (0x2C) addressed to our throwaway ID.
bool on_key_init(ResponderContext &ctx, const uint8_t challenge[HMAC_SIZE], const uint8_t hub_node_id[NODE_ID_SIZE])
Decide how to react to an inbound CMD_KEY_INIT (0x31) addressed to our throwaway ID.
@ ARMED_IDLE
Armed, listening for a discovery request (0x28).
@ DISARMED
Not armed; 0x28/0x2C/0x31/0x32 traffic is ignored.
bool on_discover_request(ResponderContext &ctx)
Decide how to react to an inbound CMD_DISCOVER_REQ (0x28) while armed.
static constexpr uint8_t CMD_DISCOVER_REQ
Broadcast discovery request.
static constexpr uint8_t NODE_ID_SIZE
Device/node addresses are 3 bytes (e.g., "123ABC").
static constexpr uint8_t CMD_KEY_TRANSFER
Send encrypted system key to device.
DeviceType
Device type identifiers reported by IO‑Homecontrol products.
@ ROLLER_SHUTTER
Roller shutter.
bool create_discover_resp(IoFrame &f, const uint8_t *own, const uint8_t *dst, DeviceType type, uint8_t subtype, uint8_t manufacturer_id)
Build a discovery response (0x29) — device side, used only by the key-extraction responder.
static constexpr uint32_t FREQ_CH1
The protocol uses 3 frequency channels in the 868 MHz ISM band.
static constexpr uint32_t FREQ_CH3
Channel 3: 869.85 MHz (2W only).
static constexpr uint8_t HMAC_SIZE
Authentication HMAC is 6 bytes (truncated AES output).
bool create_key_confirm(IoFrame &f, const uint8_t *own, const uint8_t *dst)
Build a key-confirm frame (0x33) — device side, used only by the key-extraction responder.
static constexpr uint8_t CMD_KEY_INIT
Initiate key transfer to device.
bool stored_node_id_is_valid(const uint8_t id[NODE_ID_SIZE])
Check if a stored node ID is valid (not all-zero, not all-0xFF).
static constexpr uint32_t FREQ_CH2
Channel 2: 868.95 MHz (1W and 2W, TX channel).
bool create_discover_confirm_ack(IoFrame &f, const uint8_t *own, const uint8_t *dst)
Build a discovery-confirm acknowledgement (0x2D) — device side, used only by the key-extraction respo...
std::string node_id_to_string(const uint8_t id[NODE_ID_SIZE])
Format a 3‑byte node ID as a 6‑character uppercase hex string.
bool create_challenge_req_device_role(IoFrame &f, const uint8_t *dst, const uint8_t *src, const uint8_t challenge[HMAC_SIZE])
Build a device-role challenge request (0x3C) — device side, used only by the key-extraction responder...
static constexpr uint8_t CMD_DISCOVER_CONFIRM
Confirm discovery to device.
static constexpr uint8_t BROADCAST_DISCOVER[NODE_ID_SIZE]
Broadcast address for device discovery (0x00003B).
static constexpr uint8_t BROADCAST_DISCOVER_ALT[NODE_ID_SIZE]
Alternate discovery / 1W broadcast address (0x00003F).
static constexpr uint8_t AES_KEY_SIZE
AES-128 key size.
static constexpr uint8_t MANUFACTURER_SOMFY
Somfy (shutters, awnings, blinds).
Pure decision logic for the device-role "Accept Foreign Pairing" (system-key extraction) responder.
Command builders for the IO‑Homecontrol protocol.
Cryptographic helpers for the IO‑Homecontrol protocol.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
uint8_t data[FRAME_MAX_DATA_SIZE]
Command parameters (0–23 bytes).
uint8_t src[NODE_ID_SIZE]
Source node ID (3 bytes).
uint8_t dst[NODE_ID_SIZE]
Destination node ID (3 bytes).
uint8_t data_len
Actual length of data.
Context for one key-extraction arm cycle.