Home IO Control
ESPHome add-on for IO-Homecontrol devices
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
25namespace esphome {
26namespace home_io_control {
27
28// Forward declarations — full definitions included only in key_extraction_responder.cpp.
29struct TuningConfig;
30class RadioDriver;
31class 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
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 &registry, TransmitFrameFn transmit, NamedTimeoutFn schedule_auto_off);
51
52 /// Non-copyable — holds injected callbacks and references into hub member addresses.
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.
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.
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.
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.
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 &registry_;
175 TransmitFrameFn transmit_;
176 NamedTimeoutFn schedule_auto_off_;
177 std::function<void(bool)> armed_callback_;
178};
179
180namespace 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().
198std::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
Owns the per-hub device table, update callbacks, and linked-remote associations.
KeyExtractionResponder & operator=(const KeyExtractionResponder &)=delete
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...
KeyExtractionResponder(const KeyExtractionResponder &)=delete
Non-copyable — holds injected callbacks and references into hub member addresses.
void set_armed(bool armed)
Arm or disarm the "Recover System Key" (key extraction) responder.
void arm_post_extraction_grace()
(Re)arm the post-extraction grace window that replaces the old immediate disarm-on-extraction: called...
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_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...
void set_armed_callback(std::function< void(bool)> cb)
Register a callback invoked whenever the key-extraction armed state changes — manual toggle,...
bool awaiting_reply() const
True whenever the responder has replied at least once, is waiting on the hub's next step,...
KeyExtractionResponder(const uint8_t *node_id, RadioDriver **radio, const TuningConfig *tuning, DeviceRegistry &registry, TransmitFrameFn transmit, NamedTimeoutFn schedule_auto_off)
uint32_t key_extraction_hold_deadline_ms_
Deadline (millis()) until which loop() should hold CH2 for the key-extraction responder,...
Abstract radio driver for IO-Homecontrol.
Injected-capability callback aliases shared by the hub's collaborator objects.
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: ...
@ ARMED_IDLE
Armed, listening for a discovery request (0x28).
@ DISARMED
Not armed; 0x28/0x2C/0x31/0x32 traffic is ignored.
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
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
Pure decision logic for the device-role "Accept Foreign Pairing" (system-key extraction) responder.
IO-Homecontrol 2W frame container: control bytes, IoFrame and (de)serialization.
Fundamental IO-Homecontrol frame and crypto size constants.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
Definition proto_frame.h:93
All runtime tunable parameters for pairing and radio diagnostics.