Home IO Control
ESPHome add-on for IO-Homecontrol devices
Toggle main menu visibility
Loading...
Searching...
No Matches
key_extraction_responder.h
Go to the documentation of this file.
1
#pragma once
2
3
/// @file key_extraction_responder.h
4
/// @brief "Recover System Key" (key extraction) — device-role responder collaborator.
5
/// @ingroup hioc_hub
6
///
7
/// The impure side of the key-extraction feature: arming/disarming, throwaway node-ID generation,
8
/// the 10-minute auto-off timer, the post-extraction grace window, transmitting device-role
9
/// replies, and the security-sensitive result log block. The pure state-transition decisions live
10
/// in pairing_responder.h/.cpp (unchanged); this collaborator owns a
11
/// pairing_responder::ResponderContext and dispatches the six RX branches through
12
/// try_handle_frame(), called from process_received_packet_() (hub_status.cpp).
13
14
#include "
hub_hooks.h
"
15
#include "
pairing_responder.h
"
16
#include "
proto_frame.h
"
17
#include "
proto_sizes.h
"
18
19
#include "esphome/core/hal.h"
// millis() for the inline awaiting_reply()
20
21
#include <cstdint>
22
#include <functional>
23
#include <string>
24
25
namespace
esphome
{
26
namespace
home_io_control
{
27
28
// Forward declarations — full definitions included only in key_extraction_responder.cpp.
29
struct
TuningConfig
;
30
class
RadioDriver
;
31
class
DeviceRegistry
;
32
33
/// @brief Device-role responder for the "Recover System Key" feature.
34
///
35
/// Constructed once by IOHomeControlComponent; non-copyable because it is wired with injected
36
/// callbacks and references into hub member addresses (mirrors ManagementActions / ExchangeEngine).
37
/// @ingroup hioc_hub
38
class
KeyExtractionResponder
{
39
public
:
40
/// @param node_id Hub's real 3-byte node ID (throwaway-ID collision check).
41
/// @param radio Double pointer to the hub's active radio driver, so a test's
42
/// `comp.radio_ = &mock` propagates (mirrors ExchangeEngine).
43
/// @param tuning Runtime tuning config, owned by the hub
44
/// (`cold_broadcast_reply_preamble`).
45
/// @param registry Device registry, for the throwaway-ID collision check only.
46
/// @param transmit How to put a reply frame on air (see TransmitFrameFn).
47
/// @param schedule_auto_off Named-timeout scheduler for the 10-minute arm window and the
48
/// post-extraction grace window (see NamedTimeoutFn).
49
KeyExtractionResponder
(
const
uint8_t *node_id,
RadioDriver
**radio,
const
TuningConfig
*tuning,
50
DeviceRegistry
®istry,
TransmitFrameFn
transmit,
NamedTimeoutFn
schedule_auto_off);
51
52
/// Non-copyable — holds injected callbacks and references into hub member addresses.
53
KeyExtractionResponder
(
const
KeyExtractionResponder
&) =
delete
;
54
KeyExtractionResponder
&
operator=
(
const
KeyExtractionResponder
&) =
delete
;
55
56
/// @brief Arm or disarm the "Recover System Key" (key extraction) responder.
57
///
58
/// Arming picks a fresh throwaway node ID, resets the pairing_responder state machine to
59
/// ARMED_IDLE, and schedules a 10-minute auto-off. While armed, the 0x28/0x2C/0x31/0x32 branches
60
/// in process_received_packet_() emulate an unpaired device so a user's existing hub can pair to
61
/// it and hand over its node_id/system_key (see pairing_responder.h). Disarming — manual, via
62
/// the HA switch, on successful extraction, or on auto-off — immediately stops those branches
63
/// from responding; it never touches the real device registry or the hub's own node_id_/
64
/// system_key_. This is the body that was IOHomeControlComponent::set_key_extraction_armed().
65
/// @param armed Desired state.
66
void
set_armed
(
bool
armed);
67
68
/// Register a callback invoked whenever the key-extraction armed state changes — manual
69
/// toggle, successful extraction, or auto-off timeout — so the switch entity can keep its
70
/// displayed state in sync when the responder disarms itself rather than the user. Single-slot.
71
/// @param cb Callable receiving the new armed state.
72
void
set_armed_callback
(std::function<
void
(
bool
)> cb) { this->armed_callback_ = std::move(cb); }
73
74
/// Dispatch a frame to the responder if it's one of its 0x28/0x2C/0x31/0x32/0x36/0x3C frames and
75
/// the responder is armed. Kept a separate function (rather than inlined into
76
/// process_received_packet_()) purely to keep that function's cognitive complexity under the
77
/// clang-tidy threshold, mirroring PairingEngine::record_discovery_rx_telemetry_()'s reason for
78
/// existing.
79
/// @param frame Parsed inbound frame.
80
/// @return true if the frame was handled (caller should stop further dispatch for it).
81
[[nodiscard]]
bool
try_handle_frame
(
const
IoFrame
&frame);
82
83
/// Generate a random throwaway node ID for one key-extraction arm cycle, avoiding collisions
84
/// with the broadcast addresses, this hub's own real node ID, and any registered device.
85
/// @param out Output: 3-byte node ID.
86
void
generate_throwaway_id
(uint8_t out[NODE_ID_SIZE]);
87
88
/// (Re)arm the post-extraction grace window that replaces the old immediate disarm-on-extraction:
89
/// called once when the key is first recovered, and again on every sign of hub progress after
90
/// that (an inbound 0x36, an outbound 0x3D) so a slow multi-retry hub isn't cut off mid-round.
91
/// Uses the same named-timer replace-on-reschedule idiom as the 10-minute auto-off timer — see
92
/// key_extraction_responder.cpp for why a naive "only disarm if DISARMED" guard inside the
93
/// callback is not enough once a manual disarm-and-rearm can happen inside the window.
94
void
arm_post_extraction_grace
();
95
96
/// True whenever the responder has replied at least once, is waiting on the hub's next step, and
97
/// that wait is still within its bounded hold window — i.e. `key_extraction_ctx_.state` is
98
/// neither DISARMED (feature unused) nor ARMED_IDLE (armed, but no discovery request seen yet),
99
/// AND `key_extraction_hold_deadline_ms_` has not yet passed. loop() uses this to hold CH2
100
/// instead of running the generic idle-hop scan, and to defer background status polls, while an
101
/// attempt is in flight.
102
///
103
/// The deadline is a plain timestamp, not a named `set_timeout()` timer: releasing the CH2 hold
104
/// is purely a radio-scheduling optimization (see key_extraction_hold_deadline_ms_'s own doc
105
/// comment for why it is deliberately decoupled from `key_extraction_ctx_.state` itself), so
106
/// nothing needs to fire a callback when it lapses — the next loop() iteration simply stops
107
/// taking the CH2-hold branch on its own. Default-constructed, the deadline is 0, which is always
108
/// in the past relative to any real `millis()` reading once the device has been running — so a
109
/// mid-exchange state reached without the deadline having been (re)set (e.g. a reply-builder
110
/// failure between the state guard and the deadline update) safely never holds CH2, rather than
111
/// holding it unboundedly.
112
///
113
/// Defined inline: defer_background_poll_() calls this every loop() iteration.
114
[[nodiscard]]
bool
awaiting_reply
()
const
{
115
return
this->
key_extraction_ctx_
.
state
!=
pairing_responder::ResponderState::DISARMED
&&
116
this->
key_extraction_ctx_
.
state
!=
pairing_responder::ResponderState::ARMED_IDLE
&&
117
millis() < this->
key_extraction_hold_deadline_ms_
;
118
}
119
120
/// State for the current "Accept Foreign Pairing" (key-extraction) arm cycle; DISARMED by
121
/// default so a fresh boot never responds to foreign pairing traffic. See pairing_responder.h.
122
/// Public so the host tests that script individual RX branches can preset and inspect it.
123
pairing_responder::ResponderContext
key_extraction_ctx_
;
124
/// Deadline (millis()) until which loop() should hold CH2 for the key-extraction responder,
125
/// deliberately independent of `key_extraction_ctx_.state` itself. Set (not "armed" — this is a
126
/// plain timestamp, not a named `set_timeout()` timer) on every sign of hub progress: the three
127
/// pre-extraction reply handlers (handle_discover_(), handle_discover_confirm_(),
128
/// handle_key_init_(), key_extraction_responder.cpp) push it out by
129
/// KEY_EXTRACTION_MID_ATTEMPT_TIMEOUT_MS, and arm_post_extraction_grace() pushes it out by
130
/// KEY_EXTRACTION_POST_EXTRACT_GRACE_MS so the hold also covers the (much longer)
131
/// post-extraction node-verification phase it governs.
132
///
133
/// Deliberately independent of `key_extraction_ctx_.state`: the pure guards in
134
/// pairing_responder.cpp decide whether an inbound frame is accepted by checking `state` alone,
135
/// never this deadline, so a real (slower) hub's next frame still completes the exchange
136
/// correctly even if it arrives after the hold has expired. Coupling the two — letting the
137
/// deadline also force `state` back to ARMED_IDLE — would silently discard that live protocol
138
/// progress instead of just releasing the radio hold. See awaiting_reply()'s doc comment for how
139
/// this is consumed.
140
uint32_t
key_extraction_hold_deadline_ms_
{0};
141
142
private
:
143
/// Handle an inbound CMD_DISCOVER_REQ (0x28) while the responder is armed.
144
void
handle_discover_(
const
IoFrame
&frame);
145
/// Handle an inbound CMD_DISCOVER_CONFIRM (0x2C) addressed to our throwaway node ID while armed.
146
void
handle_discover_confirm_(
const
IoFrame
&frame);
147
/// Handle an inbound CMD_KEY_INIT (0x31) addressed to our throwaway node ID while armed.
148
void
handle_key_init_(
const
IoFrame
&frame);
149
/// Handle an inbound CMD_KEY_TRANSFER (0x32) addressed to our throwaway node ID while armed.
150
void
handle_key_transfer_(
const
IoFrame
&frame);
151
/// Handle an inbound CMD_NODE_VERIFY_REQ (0x36) addressed to our throwaway node ID while armed.
152
/// Some hubs (Velux KLR200) send this after completing the key exchange, to verify the backbone
153
/// address they were given — see pairing_responder::on_node_verify_req().
154
void
handle_node_verify_req_(
const
IoFrame
&frame);
155
/// Handle an inbound CMD_CHALLENGE_REQ (0x3C) addressed to our throwaway node ID while armed and
156
/// in SENT_NODE_VERIFY_RESP — the hub-issued challenge against our own CMD_NODE_VERIFY_RESP, closing the
157
/// node-verification round a CMD_NODE_VERIFY_REQ opened. See
158
/// pairing_responder::on_node_verify_challenge().
159
void
handle_node_verify_challenge_(
const
IoFrame
&frame);
160
/// Transmit a key-extraction reply frame on all 3 IO-homecontrol channels, using the radio
161
/// driver's response_preamble() rather than a fixed SHORT_PREAMBLE/LONG_PREAMBLE constant —
162
/// long enough that a channel-hopping receiver reliably lands on it, short enough that 3
163
/// sequential transmissions don't block the main loop for the better part of a second (see the
164
/// implementation comment in key_extraction_responder.cpp for the hardware-confirmed reasoning).
165
/// Shared by every RX handler so the preamble choice and channel list are defined once.
166
void
broadcast_reply_(
const
IoFrame
&frame);
167
/// Emit the security-sensitive "system key extracted" log block (see redaction.h — this is the
168
/// one deliberate, explicit exception to that file's masking, not a loosening of it).
169
void
log_result_();
170
171
const
uint8_t *node_id_;
172
RadioDriver
**radio_;
173
const
TuningConfig
*tuning_;
174
DeviceRegistry
®istry_;
175
TransmitFrameFn
transmit_;
176
NamedTimeoutFn
schedule_auto_off_;
177
std::function<void(
bool
)> armed_callback_;
178
};
179
180
namespace
detail {
181
182
/// @brief Build the ready-to-paste 2W system-key-extraction report: `node_id:`/`system_key:` as a
183
/// `home_io_control:` YAML block.
184
///
185
/// Pure — takes already-decoded values, performs no I/O — so it is directly unit-testable without
186
/// a live radio, mirroring build_oneway_adoption_report() (oneway_key_adoption.h); the two features
187
/// end up sharing report *structure* as well as format_key_hex(). The caller
188
/// (KeyExtractionResponder::log_result_()) logs the result through log_multiline_result() and
189
/// nowhere else — this is the single intentional place the recovered `system_key` is formatted for
190
/// display, a deliberate exception to redaction.h's masking.
191
///
192
/// The emitted keys must track the hub's own `CONFIG_SCHEMA` (`__init__.py`) by hand. `make
193
/// yaml-emitter-sync` (scripts/check-yaml-emitters.py) catches drift between the two by
194
/// cross-referencing this function's emitted key names against that schema statically.
195
/// @param node_id Recovered hub node_id, 3 bytes.
196
/// @param key Recovered system key, 16 bytes.
197
/// @return Multi-line report text, ready to pass to log_multiline_result().
198
std::string
build_key_extraction_report
(
const
uint8_t node_id[NODE_ID_SIZE],
const
uint8_t key[AES_KEY_SIZE]);
199
200
}
// namespace detail
201
202
}
// namespace home_io_control
203
}
// namespace esphome
esphome::home_io_control::DeviceRegistry
Owns the per-hub device table, update callbacks, and linked-remote associations.
Definition
device_registry.h:43
esphome::home_io_control::KeyExtractionResponder::operator=
KeyExtractionResponder & operator=(const KeyExtractionResponder &)=delete
esphome::home_io_control::KeyExtractionResponder::generate_throwaway_id
void generate_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...
Definition
key_extraction_responder.cpp:140
esphome::home_io_control::KeyExtractionResponder::KeyExtractionResponder
KeyExtractionResponder(const KeyExtractionResponder &)=delete
Non-copyable — holds injected callbacks and references into hub member addresses.
esphome::home_io_control::KeyExtractionResponder::set_armed
void set_armed(bool armed)
Arm or disarm the "Recover System Key" (key extraction) responder.
Definition
key_extraction_responder.cpp:160
esphome::home_io_control::KeyExtractionResponder::arm_post_extraction_grace
void arm_post_extraction_grace()
(Re)arm the post-extraction grace window that replaces the old immediate disarm-on-extraction: called...
Definition
key_extraction_responder.cpp:428
esphome::home_io_control::KeyExtractionResponder::key_extraction_ctx_
pairing_responder::ResponderContext key_extraction_ctx_
State for the current "Accept Foreign Pairing" (key-extraction) arm cycle; DISARMED by default so a f...
Definition
key_extraction_responder.h:123
esphome::home_io_control::KeyExtractionResponder::try_handle_frame
bool try_handle_frame(const IoFrame &frame)
Dispatch a frame to the responder if it's one of its 0x28/0x2C/0x31/0x32/0x36/0x3C frames and the res...
Definition
key_extraction_responder.cpp:206
esphome::home_io_control::KeyExtractionResponder::set_armed_callback
void set_armed_callback(std::function< void(bool)> cb)
Register a callback invoked whenever the key-extraction armed state changes — manual toggle,...
Definition
key_extraction_responder.h:72
esphome::home_io_control::KeyExtractionResponder::awaiting_reply
bool awaiting_reply() const
True whenever the responder has replied at least once, is waiting on the hub's next step,...
Definition
key_extraction_responder.h:114
esphome::home_io_control::KeyExtractionResponder::KeyExtractionResponder
KeyExtractionResponder(const uint8_t *node_id, RadioDriver **radio, const TuningConfig *tuning, DeviceRegistry ®istry, TransmitFrameFn transmit, NamedTimeoutFn schedule_auto_off)
Definition
key_extraction_responder.cpp:130
esphome::home_io_control::KeyExtractionResponder::key_extraction_hold_deadline_ms_
uint32_t key_extraction_hold_deadline_ms_
Deadline (millis()) until which loop() should hold CH2 for the key-extraction responder,...
Definition
key_extraction_responder.h:140
esphome::home_io_control::RadioDriver
Abstract radio driver for IO-Homecontrol.
Definition
radio_interface.h:156
hub_hooks.h
Injected-capability callback aliases shared by the hub's collaborator objects.
esphome::home_io_control::detail::build_key_extraction_report
std::string build_key_extraction_report(const uint8_t node_id[NODE_ID_SIZE], const uint8_t key[AES_KEY_SIZE])
Build the ready-to-paste 2W system-key-extraction report: node_id:/system_key: as a home_io_control: ...
Definition
key_extraction_responder.cpp:117
esphome::home_io_control::pairing_responder::ResponderState::ARMED_IDLE
@ ARMED_IDLE
Armed, listening for a discovery request (0x28).
Definition
pairing_responder.h:32
esphome::home_io_control::pairing_responder::ResponderState::DISARMED
@ DISARMED
Not armed; 0x28/0x2C/0x31/0x32 traffic is ignored.
Definition
pairing_responder.h:31
esphome::home_io_control
Definition
device_registry.cpp:13
esphome::home_io_control::TransmitFrameFn
std::function< bool(const IoFrame &frame, uint32_t freq_hz, uint16_t preamble)> TransmitFrameFn
Puts a frame on air on a given channel via the hub's protected transmit_frame_().
Definition
hub_hooks.h:34
esphome::home_io_control::NamedTimeoutFn
std::function< void(const char *name, uint32_t delay_ms, std::function< void()> callback)> NamedTimeoutFn
Schedules a named, replace-on-same-name timeout on the hub's ESPHome scheduler.
Definition
hub_hooks.h:27
esphome
Definition
device_registry.cpp:12
pairing_responder.h
Pure decision logic for the device-role "Accept Foreign Pairing" (system-key extraction) responder.
proto_frame.h
IO-Homecontrol 2W frame container: control bytes, IoFrame and (de)serialization.
proto_sizes.h
Fundamental IO-Homecontrol frame and crypto size constants.
esphome::home_io_control::IoFrame
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
Definition
proto_frame.h:93
esphome::home_io_control::TuningConfig
All runtime tunable parameters for pairing and radio diagnostics.
Definition
tuning_config.h:242
esphome::home_io_control::pairing_responder::ResponderContext
Context for one key-extraction arm cycle.
Definition
pairing_responder.h:56
esphome::home_io_control::pairing_responder::ResponderContext::state
ResponderState state
Current state.
Definition
pairing_responder.h:57
components
home_io_control
key_extraction_responder.h
Generated by
1.18.0