ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
xxtea_encryption.cpp
Go to the documentation of this file.
1#include "xxtea_encryption.h"
2#ifdef USE_PACKET_INTERFACE_XXTEA
3#include <algorithm>
4#include <cstring>
7#include "esphome/core/log.h"
8
10
11ESPHOME_LOG_TAG(TAG, "packet_interface.xxtea");
12
13static constexpr uint32_t PREF_HASH = fnv1a_hash("packet_interface.xxtea");
14// Different for each mode, so devices set up differently drop each other's packets
15static constexpr uint8_t CHECK_BYTES[3] = {0x58, 0x54, 0x45};
16static constexpr uint8_t ROLLING_CHECK_BYTES[3] = {0x58, 0x54, 0x52};
17static constexpr size_t CHECK_SIZE = 4;
18// Boot number, counter, and the echo of the receiver's boot number
19static constexpr size_t ROLLING_SIZE = 12;
20// XXTEA needs at least two words
21static constexpr size_t MIN_LENGTH = 8;
22static constexpr size_t MAX_PADDING = 4;
23
24static void put_uint32(uint8_t *buf, uint32_t value) {
25 buf[0] = value;
26 buf[1] = value >> 8;
27 buf[2] = value >> 16;
28 buf[3] = value >> 24;
29}
30
31static uint32_t get_uint32(const uint8_t *buf) { return encode_uint32(buf[3], buf[2], buf[1], buf[0]); }
32
33static constexpr size_t trailer_size(bool rolling_code) {
34 return rolling_code ? ROLLING_SIZE + CHECK_SIZE : CHECK_SIZE;
35}
36
38 // XXTEA works on whole 32-bit words
39 : PacketInterface(remaining_packet_size(lower->get_max_packet_size() & ~size_t{3}, trailer_size(rolling_code))),
40 lower_(lower),
41 trailer_size_(trailer_size(rolling_code)),
42 rolling_code_(rolling_code) {
43 this->stack_on_(lower, CAPABILITY_ENCRYPTION);
44}
45
46void XxteaEncryption::set_key(std::initializer_list<uint32_t> key) {
47 std::copy_n(key.begin(), std::min(key.size(), this->key_.size()), this->key_.begin());
48}
49
51 if (this->max_packet_size_ == 0) {
52 this->mark_failed(LOG_STR("max_packet_size too small for the configured layers"));
53 return;
54 }
55 size_t words = this->lower_->get_max_packet_size() / 4;
56 RAMAllocator<uint32_t> allocator;
57 this->tx_buffer_ = allocator.allocate(words * 2);
58 if (this->tx_buffer_ == nullptr) {
59 this->mark_failed(LOG_STR("Not enough memory for the packet buffers"));
60 return;
61 }
62 this->rx_buffer_ = this->tx_buffer_ + words;
63 if (this->rolling_code_) {
65 this->pref_.load(&this->boot_);
66 this->next_boot_();
67 }
69 [this](const PacketBuffer &data, const PacketMetaData &meta_data) { this->receive_(data, meta_data); });
70}
71
73 ESP_LOGCONFIG(TAG,
74 "XXTEA Encryption on '%s':\n"
75 " Max Packet Size: %zu\n"
76 " Rolling Code: %s",
77 this->name_, this->max_packet_size_, YESNO(this->rolling_code_));
78 if (this->rolling_code_) {
79 ESP_LOGCONFIG(TAG, " Boot Number: %" PRIu32, this->boot_);
80 }
81}
82
83// A new boot number must be saved before it is used, or a crash could make the peer see it again
85 this->boot_++;
86 this->counter_ = 0;
87 if (!this->pref_.save(&this->boot_) || !global_preferences->sync()) {
88 ESP_LOGW(TAG, "%s: Could not save the boot number, so a restart could reuse it", this->name_);
89 }
90}
91
93 if (this->tx_buffer_ == nullptr)
94 return false;
95 size_t size = data.size();
96 if (size > this->max_packet_size_) {
97 ESP_LOGW(TAG, "%s: Packet of %zu bytes is larger than %zu", this->name_, size, this->max_packet_size_);
98 return false;
99 }
100 size_t padding = (4 - (size + this->trailer_size_) % 4) % 4;
101 if (size + padding + this->trailer_size_ < MIN_LENGTH)
102 padding += 4;
103 size_t length = size + padding + this->trailer_size_;
104 auto *buf = reinterpret_cast<uint8_t *>(this->tx_buffer_);
105 data.copy_to(buf, size);
106 memset(buf + size, 0, padding);
107 uint8_t *check = buf + length - CHECK_SIZE;
108 if (this->rolling_code_) {
109 put_uint32(check - ROLLING_SIZE, this->boot_);
110 put_uint32(check - ROLLING_SIZE + 4, this->counter_);
111 put_uint32(check - ROLLING_SIZE + 8, this->echo_boot_);
112 memcpy(check, ROLLING_CHECK_BYTES, sizeof(ROLLING_CHECK_BYTES));
113 if (++this->counter_ == 0)
114 this->next_boot_();
115 } else {
116 memcpy(check, CHECK_BYTES, sizeof(CHECK_BYTES));
117 }
118 check[CHECK_SIZE - 1] = padding;
119 xxtea::encrypt(this->tx_buffer_, length / 4, this->key_.data());
120 return this->lower_->send_to_interface(PacketBuffer(buf, length), meta_data);
121}
122
124 if (this->peer_seen_ && (boot < this->peer_boot_ || (boot == this->peer_boot_ && counter <= this->peer_counter_)))
125 return false;
126 this->peer_seen_ = true;
127 this->peer_boot_ = boot;
128 this->peer_counter_ = counter;
129 return true;
130}
131
132void XxteaEncryption::receive_(const PacketBuffer &data, const PacketMetaData &meta_data) {
133 size_t length = data.size();
134 if (length % 4 != 0 || length < std::max(MIN_LENGTH, this->trailer_size_) ||
135 length > this->lower_->get_max_packet_size()) {
136 ESP_LOGV(TAG, "%s: Packet of %zu bytes dropped", this->name_, length);
137 this->note_received_(false);
138 return;
139 }
140 auto *buf = reinterpret_cast<uint8_t *>(this->rx_buffer_);
141 data.copy_to(buf, length);
142 xxtea::decrypt(this->rx_buffer_, length / 4, this->key_.data());
143 const uint8_t *check = buf + length - CHECK_SIZE;
144 const uint8_t *expected = this->rolling_code_ ? ROLLING_CHECK_BYTES : CHECK_BYTES;
145 size_t padding = check[CHECK_SIZE - 1];
146 if (memcmp(check, expected, sizeof(CHECK_BYTES)) != 0 || padding > MAX_PADDING ||
147 padding > length - this->trailer_size_) {
148 ESP_LOGV(TAG, "%s: Packet failed authentication", this->name_);
149 this->note_received_(false);
150 return;
151 }
152 this->note_received_(true);
153 if (this->rolling_code_) {
154 uint32_t boot = get_uint32(check - ROLLING_SIZE);
155 uint32_t counter = get_uint32(check - ROLLING_SIZE + 4);
156 uint32_t echo = get_uint32(check - ROLLING_SIZE + 8);
157 // Learn the peer's boot number even from a packet that is then dropped, so both sides can catch up
158 if (boot > this->echo_boot_)
159 this->echo_boot_ = boot;
160 // A packet recorded before this boot cannot carry this boot's number
161 if (echo != this->boot_) {
162 ESP_LOGV(TAG, "%s: Packet for boot %" PRIu32 " dropped", this->name_, echo);
163 return;
164 }
165 if (!this->accept_counter_(boot, counter)) {
166 // A boot number lower than one already heard means the peer lost its saved boot number, or a replay
167 if (boot < this->peer_boot_ && !this->old_boot_warned_) {
168 this->old_boot_warned_ = true;
169 ESP_LOGW(TAG,
170 "%s: Packets from an older boot %" PRIu32 " of the peer are dropped; if its flash was erased, "
171 "restart this device",
172 this->name_, boot);
173 }
174 ESP_LOGV(TAG, "%s: Old packet %" PRIu32 ":%" PRIu32 " dropped", this->name_, boot, counter);
175 return;
176 }
177 }
178 this->on_receive_from_interface_(PacketBuffer(buf, length - this->trailer_size_ - padding), meta_data);
179}
180
181} // namespace esphome::packet_interface
182#endif
void mark_failed()
Mark this component as failed.
An STL allocator that uses SPI or internal RAM.
Definition helpers.h:2298
T * allocate(size_t n)
Definition helpers.h:2325
A buffer class that efficiently represents a composite buffer with three sections:
size_t copy_to(uint8_t *dest, size_t count, size_t offset=0) const
Copy data from the buffer to a destination pointer.
size_t size() const
Get the total size of the buffer.
A class that will transport data to and from an abstract channel.
size_t get_max_packet_size() const
The largest packet this layer accepts; fixed when the layer is constructed.
void note_received_(bool understood)
Called by a layer for each received packet it understood, or could not make sense of.
void stack_on_(PacketInterface *lower, Capability capability)
Called by a layer's constructor to record itself as the layer above lower, adding its own capability.
virtual bool send_to_interface(const PacketBuffer &data, const PacketMetaData &meta_data)=0
Method to be implemented by child classes to send data to the interface.
void add_packet_interface_listener(F &&callback)
Add a listener that will be called when data is received.
void on_receive_from_interface_(const PacketBuffer &data, const PacketMetaData &meta_data)
This function should be called by implementing classes with received data to be passed to any listene...
A struct to hold metadata about a packet.
XxteaEncryption(PacketInterface *lower, bool rolling_code)
bool accept_counter_(uint32_t boot, uint32_t counter)
bool send_to_interface(const PacketBuffer &data, const PacketMetaData &meta_data) override
void receive_(const PacketBuffer &data, const PacketMetaData &meta_data)
void set_key(std::initializer_list< uint32_t > key)
const char * key
constexpr size_t remaining_packet_size(size_t size, size_t used)
The room left for a packet of size bytes after used bytes, or 0 if it is too small.
ESPHOME_LOG_TAG(TAG, "packet_interface.session")
void encrypt(uint32_t *v, size_t n, const uint32_t *k)
Encrypt a block of data in-place using XXTEA algorithm with 256-bit key.
Definition xxtea.cpp:8
void decrypt(uint32_t *v, size_t n, const uint32_t *k)
Decrypt a block of data in-place using XXTEA algorithm with 256-bit key.
Definition xxtea.cpp:28
constexpr uint32_t fnv1a_hash_extend(uint32_t hash, const char *str)
Extend a FNV-1a hash with additional string data.
Definition helpers.h:950
uint16_t size
Definition helpers.cpp:34
ESPPreferences * global_preferences
constexpr uint32_t encode_uint32(uint8_t byte1, uint8_t byte2, uint8_t byte3, uint8_t byte4)
Encode a 32-bit value given four bytes in most to least significant byte order.
Definition helpers.h:1006
constexpr uint32_t fnv1a_hash(const char *str)
Calculate a FNV-1a hash of str.
Definition helpers.h:973
static void uint32_t
ESPPreferenceObject make_preference(size_t, uint32_t, bool)
Definition preferences.h:24
bool sync()
Commit pending writes to flash.
Definition preferences.h:33