Home IO Control
ESPHome add-on for IO-Homecontrol devices
Toggle main menu visibility
Loading...
Searching...
No Matches
oneway_sequence_store.h
Go to the documentation of this file.
1
#pragma once
2
3
/// @file oneway_sequence_store.h
4
/// @brief Persistent rolling-sequence counters for one-way (1W) transmit.
5
/// @ingroup hioc_hub
6
///
7
/// 1W has no reply, no challenge and no acknowledgement. Its entire replay defence is a rolling
8
/// 16-bit sequence carried in every frame and mixed into the authenticator's IV: a device
9
/// remembers the highest sequence it has accepted from each transmitter and rejects anything at
10
/// or below it. That makes this counter the one piece of state the hub cannot afford to lose,
11
/// and the reason it is the only thing the component writes to persistent storage — see ADR 0025,
12
/// which records that exception to ADR 0018 and draws its boundary.
13
///
14
/// The failure is asymmetric, and every rule below follows from it:
15
///
16
/// - **Skipping sequences is safe.** A device accepts a forward jump within its acceptance
17
/// window, so a counter that runs ahead costs nothing.
18
/// - **Reusing one is not.** The device rejects it as a replay.
19
/// - **Falling behind is unrecoverable without intervention.** Every later command is rejected
20
/// too, permanently, and *silently* — 1W emits no error, so a hub with a stale counter keeps
21
/// transmitting well-formed, correctly-signed frames that nothing acts on.
22
///
23
/// This class is the only place in the component that increments or persists a sequence.
24
25
#include "
proto_sizes.h
"
26
27
#include "esphome/core/preferences.h"
28
29
#include <cstdint>
30
#include <vector>
31
32
namespace
esphome
{
33
namespace
home_io_control
{
34
35
// === Sequence-safety bounds ===
36
37
/// How far ahead of its stored high-water mark a device will still accept a jump. 1000 is the
38
/// window the iohomecontrol reference receiver applies, and it is the ceiling every
39
/// forward skip in this file has to stay under: a jump past it fails exactly like a stale
40
/// counter, and just as silently.
41
static
constexpr
uint16_t
ONEWAY_SEQUENCE_ACCEPTANCE_WINDOW
= 1000;
42
43
/// How many sequences one flash write reserves.
44
///
45
/// Persisting on every command would write flash on every button press. Reserving a block
46
/// instead trades that for a bounded forward skip: an unclean reboot forfeits the block's unused
47
/// remainder, which is the safe direction. Small on purpose — see the static_assert below for
48
/// the bound, and raise it only if that bound still holds.
49
static
constexpr
uint16_t
ONEWAY_SEQUENCE_STRIDE
= 8;
50
51
/// How many consecutive unclean reboots the stride must survive while staying inside a device's
52
/// acceptance window. Eight is far past plausible; the point is headroom, not precision.
53
static
constexpr
uint16_t
ONEWAY_SEQUENCE_REBOOT_HEADROOM
= 8;
54
55
static_assert
(
ONEWAY_SEQUENCE_STRIDE
*
ONEWAY_SEQUENCE_REBOOT_HEADROOM
<
ONEWAY_SEQUENCE_ACCEPTANCE_WINDOW
,
56
"the stride must let many unclean reboots in a row still land inside a device's "
57
"acceptance window — a larger stride buys fewer flash writes and pays in silent desync"
);
58
59
// === The store ===
60
61
/// @brief Per-controller-identity rolling sequence counters, persisted across reboots.
62
/// @ingroup hioc_hub
63
///
64
/// Keyed on the transmitting node address, not on the command: real capture logs show one remote
65
/// running a single counter across command types (0x01 → 0x00 → 0x20 at 2416 → 2417 → 2418), so
66
/// a per-command counter would not match what devices track.
67
///
68
/// One logical command consumes exactly one sequence. next() is called once, by whatever builds
69
/// the command — never inside a repeat loop. A 4-frame burst carrying four different sequences
70
/// is not one command to a device, and burns counter space four times as fast.
71
class
OneWaySequenceStore
{
72
public
:
73
/// @brief Register a controller identity and load its persisted counter.
74
///
75
/// Call once per identity at setup. With nothing persisted the counter starts at
76
/// `initial_sequence`; otherwise it resumes from whichever of the two is *higher*. That keeps
77
/// `initial_sequence` usable as the day-one remedy for a desynced device — raise it, reflash,
78
/// and the counter jumps ahead — while making it structurally unable to drag a live counter
79
/// backwards into replay territory. Use seed() when moving backwards is what you actually mean.
80
/// @param node_id 3-byte source address this identity transmits as.
81
/// @param initial_sequence Value to start from when nothing is persisted, or to jump forward to.
82
void
add_identity
(
const
uint8_t node_id[NODE_ID_SIZE], uint16_t initial_sequence);
83
84
/// @brief Reserve and return the next sequence for an identity.
85
///
86
/// Durably reserves before returning: when the in-RAM block is exhausted this persists the end
87
/// of the next block *and syncs it to flash* before handing anything back, so a caller can never
88
/// transmit a value the hub has not already committed to never reusing. A crash between the
89
/// write and the transmit costs one skipped sequence; the reverse ordering would risk a reuse.
90
/// @param node_id Identity's 3-byte source address.
91
/// @param out Output: the sequence to transmit.
92
/// @return false if the address is not a registered identity, or if the reservation could not
93
/// be persisted — in both cases nothing is handed out and nothing may be transmitted.
94
bool
next
(
const
uint8_t node_id[NODE_ID_SIZE], uint16_t &out);
95
96
/// @brief Force an identity's counter to a specific value and persist it immediately.
97
///
98
/// Unlike add_identity(), this *may* move the counter backwards, which is the whole point —
99
/// re-seed from a sequence observed on air, or from a user's estimate, when a counter has
100
/// desynced from the device. Today's documented remedy for a desynced counter is
101
/// `initial_sequence:` (docs/troubleshooting.md, "1W commands do nothing"), which goes through
102
/// add_identity() at boot instead.
103
/// @param node_id Identity's 3-byte source address.
104
/// @param value Next sequence to hand out.
105
/// @return false if the address is not registered or the write could not be persisted.
106
bool
seed
(
const
uint8_t node_id[NODE_ID_SIZE], uint16_t value);
107
108
/// @brief The next sequence this identity would hand out, without reserving it.
109
///
110
/// Unlike next(), this does not commit to transmitting the value it returns — it does not
111
/// reserve, persist, or advance the counter.
112
/// @param node_id Identity's 3-byte source address.
113
/// @param out Output: the sequence next() would return.
114
/// @return false if the address is not a registered identity.
115
bool
peek
(
const
uint8_t node_id[NODE_ID_SIZE], uint16_t &out)
const
;
116
117
private
:
118
/// One identity's counter: the value to hand out next, the end of the block already reserved
119
/// in flash, and the preference backing it.
120
struct
Counter {
121
uint8_t node_id[NODE_ID_SIZE]{};
122
uint16_t
next
{0};
///< Next value to hand out.
123
uint16_t reserved{0};
///< First value NOT covered by the persisted reservation.
124
ESPPreferenceObject pref;
125
};
126
127
Counter *find_(
const
uint8_t node_id[NODE_ID_SIZE]);
128
const
Counter *find_(
const
uint8_t node_id[NODE_ID_SIZE])
const
;
129
130
std::vector<Counter> counters_;
131
};
132
133
}
// namespace home_io_control
134
}
// namespace esphome
esphome::home_io_control::OneWaySequenceStore
Per-controller-identity rolling sequence counters, persisted across reboots.
Definition
oneway_sequence_store.h:71
esphome::home_io_control::OneWaySequenceStore::seed
bool seed(const uint8_t node_id[NODE_ID_SIZE], uint16_t value)
Force an identity's counter to a specific value and persist it immediately.
Definition
oneway_sequence_store.cpp:99
esphome::home_io_control::OneWaySequenceStore::add_identity
void add_identity(const uint8_t node_id[NODE_ID_SIZE], uint16_t initial_sequence)
Register a controller identity and load its persisted counter.
Definition
oneway_sequence_store.cpp:51
esphome::home_io_control::OneWaySequenceStore::next
bool next(const uint8_t node_id[NODE_ID_SIZE], uint16_t &out)
Reserve and return the next sequence for an identity.
Definition
oneway_sequence_store.cpp:76
esphome::home_io_control::OneWaySequenceStore::peek
bool peek(const uint8_t node_id[NODE_ID_SIZE], uint16_t &out) const
The next sequence this identity would hand out, without reserving it.
Definition
oneway_sequence_store.cpp:112
esphome::home_io_control
Definition
device_registry.cpp:13
esphome::home_io_control::ONEWAY_SEQUENCE_REBOOT_HEADROOM
static constexpr uint16_t ONEWAY_SEQUENCE_REBOOT_HEADROOM
How many consecutive unclean reboots the stride must survive while staying inside a device's acceptan...
Definition
oneway_sequence_store.h:53
esphome::home_io_control::ONEWAY_SEQUENCE_ACCEPTANCE_WINDOW
static constexpr uint16_t ONEWAY_SEQUENCE_ACCEPTANCE_WINDOW
How far ahead of its stored high-water mark a device will still accept a jump.
Definition
oneway_sequence_store.h:41
esphome::home_io_control::ONEWAY_SEQUENCE_STRIDE
static constexpr uint16_t ONEWAY_SEQUENCE_STRIDE
How many sequences one flash write reserves.
Definition
oneway_sequence_store.h:49
esphome
Definition
device_registry.cpp:12
proto_sizes.h
Fundamental IO-Homecontrol frame and crypto size constants.
components
home_io_control
oneway_sequence_store.h
Generated by
1.18.0