ESPHome 2026.10.0-dev
Loading...
Searching...
No Matches
ota_esphome_noise.cpp
Go to the documentation of this file.
1#include "ota_esphome.h"
2#ifdef USE_OTA
3#ifdef USE_OTA_ENCRYPTION
6#include "esphome/core/hal.h"
7#include "esphome/core/log.h"
8
9#include <cstring>
10#include <new>
11
12#ifdef USE_ESP8266
13#include <pgmspace.h>
14#endif
15
16namespace esphome {
17
18static const char *const TAG = "esphome.ota";
19
20#ifdef USE_ESP8266
21static constexpr char OTA_NOISE_PROLOGUE_INIT[] PROGMEM = "NoiseOTAInit";
22#else
23static constexpr char OTA_NOISE_PROLOGUE_INIT[] = "NoiseOTAInit";
24#endif
25static constexpr size_t OTA_NOISE_PROLOGUE_INIT_LEN = sizeof(OTA_NOISE_PROLOGUE_INIT) - 1;
26
28 if (this->send_cipher != nullptr) {
29 noise_cipherstate_free(this->send_cipher);
30 }
31 if (this->recv_cipher != nullptr) {
32 noise_cipherstate_free(this->recv_cipher);
33 }
34}
35
43bool ESPHomeOTAComponent::noise_start_session_(uint8_t server_feature_flags) {
44 // A provisioned key cleared between the offer and here is not guarded: the
45 // session runs on the zero key load_psk fills in and fails the client's MAC.
46 // Default-init: the frame buffer is written before it is read
47 // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
48 this->noise_ = std::unique_ptr<NoiseSession>(new (std::nothrow) NoiseSession);
49 static constexpr size_t PROLOGUE_ACK_LEN = 2; // OTA_RESPONSE_OK + version
50 static constexpr size_t PROLOGUE_CLIENT_FEATURES_LEN = 1;
51 static constexpr size_t PROLOGUE_FEATURE_ACK_LEN = 2; // OTA_RESPONSE_FEATURE_FLAGS + server flags
52 uint8_t prologue[OTA_NOISE_PROLOGUE_INIT_LEN + sizeof(MAGIC_BYTES) + PROLOGUE_ACK_LEN + PROLOGUE_CLIENT_FEATURES_LEN +
53 PROLOGUE_FEATURE_ACK_LEN];
54 progmem_memcpy(prologue, OTA_NOISE_PROLOGUE_INIT, OTA_NOISE_PROLOGUE_INIT_LEN);
55 uint8_t *p = prologue + OTA_NOISE_PROLOGUE_INIT_LEN;
56 // Magic bytes, already validated in MAGIC_READ
57 std::memcpy(p, MAGIC_BYTES, sizeof(MAGIC_BYTES));
58 p += sizeof(MAGIC_BYTES);
59 // Our magic ack
61 *p++ = USE_OTA_VERSION;
62 // The feature byte the client sent
63 *p++ = this->ota_features_;
64 // The feature ack we sent (noise requires the extended protocol)
66 *p++ = server_feature_flags;
67
68 // The caller only starts a session when the context holds a key
69 int err = this->noise_ == nullptr ? NOISE_ERROR_NO_MEMORY
70 : this->noise_->handshake.init(this->noise_context_(), prologue, sizeof(prologue));
71 if (err != 0) {
72 // Raw noise codes throughout: the name table would cost flash in builds
73 // where only the OTA uses noise
74 ESP_LOGW(TAG, "Session init: %d", err);
75 this->cleanup_connection_();
76 return false;
77 }
78 return true;
79}
80
87 NoiseSession &s = *this->noise_;
88 while (true) {
89 if (s.writing) {
90 if (!this->noise_try_write_frame_()) {
91 return false; // would block, or errored and cleaned up
92 }
93 s.writing = false;
94 s.frame_pos = 0;
95 s.frame_len = 0;
96 }
97 switch (s.handshake.action()) {
99 if (!this->noise_try_read_frame_()) {
100 return false;
101 }
102 const uint16_t payload_len = s.frame_len - noise::FRAME_HEADER_SIZE;
103 s.frame_pos = 0;
104 s.frame_len = 0;
105 if (s.frame_buf[noise::FRAME_HEADER_SIZE] != noise::HANDSHAKE_STATUS_OK) {
106 ESP_LOGW(TAG, "Client rejected the handshake: %u", s.frame_buf[noise::FRAME_HEADER_SIZE]);
107 this->cleanup_connection_();
108 return false;
109 }
110 int err = s.handshake.read_message(s.frame_buf + noise::FRAME_HEADER_SIZE + 1, payload_len - 1);
111 if (err != 0) {
112 // A MAC failure here almost always means the uploader has a different key
113 const LogString *reason = noise::reject_reason_for(err);
114 ESP_LOGW(TAG, "Handshake read: %s (%d)", LOG_STR_ARG(reason), err);
115 this->noise_send_reject_(reason);
116 this->cleanup_connection_();
117 return false;
118 }
119 break;
120 }
122 size_t msg_len = 0;
123 int err =
124 s.handshake.write_message(s.frame_buf + noise::FRAME_HEADER_SIZE + 1, noise::MAX_HANDSHAKE_SIZE, msg_len);
125 if (err != 0) {
126 ESP_LOGW(TAG, "Handshake write: %d", err);
127 this->cleanup_connection_();
128 return false;
129 }
130 const uint16_t payload_len = msg_len + 1;
131 noise::write_frame_header(s.frame_buf, payload_len);
132 s.frame_buf[noise::FRAME_HEADER_SIZE] = noise::HANDSHAKE_STATUS_OK;
133 s.frame_len = noise::FRAME_HEADER_SIZE + payload_len;
134 s.frame_pos = 0;
135 s.writing = true;
136 break;
137 }
139 int err = s.handshake.split(s.send_cipher, s.recv_cipher);
140 if (err != 0) {
141 ESP_LOGW(TAG, "Handshake split: %d", err);
142 this->cleanup_connection_();
143 return false;
144 }
145 ESP_LOGD(TAG, "Noise handshake complete");
146 return true;
147 }
148 default: {
149 ESP_LOGW(TAG, "Bad handshake state");
150 this->cleanup_connection_();
151 return false;
152 }
153 }
154 }
155}
156
159size_t ESPHomeOTAComponent::noise_frame_payload_len_(const uint8_t *header, size_t min_len, size_t max_len) {
160 const size_t payload_len = encode_uint16(header[1], header[2]);
161 if (header[0] != noise::FRAME_INDICATOR || payload_len < min_len || payload_len > max_len) {
162 ESP_LOGW(TAG, "Bad frame: 0x%02X, %zu bytes", header[0], payload_len);
163 return 0;
164 }
165 return payload_len;
166}
167
170 NoiseSession &s = *this->noise_;
171 while (true) {
172 // The header first, then the body once the header says how long it is
173 const uint16_t want = s.frame_len == 0 ? noise::FRAME_HEADER_SIZE : s.frame_len;
174 if (s.frame_pos < want) {
175 ssize_t read = this->client_->read(s.frame_buf + s.frame_pos, want - s.frame_pos);
176 if (!this->handle_read_error_(read, LOG_STR("read noise"))) {
177 return false;
178 }
179 s.frame_pos += read;
180 continue;
181 }
182 if (s.frame_len != 0) {
183 return true;
184 }
185 const size_t payload_len = this->noise_frame_payload_len_(s.frame_buf, 1, 1 + noise::MAX_HANDSHAKE_SIZE);
186 if (payload_len == 0) {
187 this->cleanup_connection_();
188 return false;
189 }
190 s.frame_len = noise::FRAME_HEADER_SIZE + payload_len;
191 }
192}
193
196 NoiseSession &s = *this->noise_;
197 while (s.frame_pos < s.frame_len) {
198 ssize_t written = this->client_->write(s.frame_buf + s.frame_pos, s.frame_len - s.frame_pos);
199 if (!this->handle_write_error_(written, LOG_STR("write noise frame"))) {
200 return false;
201 }
202 s.frame_pos += written;
203 }
204 return true;
205}
206
208void ESPHomeOTAComponent::noise_send_reject_(const LogString *reason) {
209 // Every reason here comes from noise::reject_reason_for(), so the exported
210 // floor is the exact capacity needed
211 uint8_t data[noise::FRAME_HEADER_SIZE + noise::MAC_FAILURE_PAYLOAD_SIZE];
212 const size_t payload_len =
213 noise::format_reject_payload(data + noise::FRAME_HEADER_SIZE, sizeof(data) - noise::FRAME_HEADER_SIZE, reason);
214 noise::write_frame_header(data, payload_len);
215 this->client_->write(data, noise::FRAME_HEADER_SIZE + payload_len); // Best effort, non-blocking
216}
217
220 NoiseBuffer mbuf;
221 noise_buffer_init(mbuf);
222 noise_buffer_set_inout(mbuf, buf, len, len);
223 int err = noise_cipherstate_decrypt(this->noise_->recv_cipher, &mbuf);
224 if (err != 0) {
225 ESP_LOGW(TAG, "Decrypt: %d", err);
226 return -1;
227 }
228 return mbuf.size;
229}
230
235ssize_t ESPHomeOTAComponent::noise_read_frame_blocking_(uint8_t *buf, size_t min_ciphertext, size_t max_ciphertext) {
236 uint8_t header[noise::FRAME_HEADER_SIZE];
237 if (!this->readall_(header, sizeof(header))) {
238 return -1;
239 }
240 const size_t ciphertext_len = this->noise_frame_payload_len_(header, min_ciphertext, max_ciphertext);
241 if (ciphertext_len == 0) {
242 return -1;
243 }
244 if (!this->readall_(buf, ciphertext_len)) {
245 return -1;
246 }
247 return this->noise_decrypt_(buf, ciphertext_len);
248}
249
254bool ESPHomeOTAComponent::noise_readall_(uint8_t *buf, size_t len) {
255 return this->noise_read_frame_blocking_(buf, len + noise::MAC_SIZE, len + noise::MAC_SIZE) == (ssize_t) len;
256}
257
264 const size_t max_ciphertext = std::min(capacity + noise::MAC_SIZE, OTA_BUFFER_SIZE);
265 return this->noise_read_frame_blocking_(buf, noise::MAC_SIZE + 1, max_ciphertext);
266}
267
270 uint8_t frame[noise::FRAME_HEADER_SIZE + 1 + noise::MAC_SIZE];
271 frame[noise::FRAME_HEADER_SIZE] = byte;
272 NoiseBuffer mbuf;
273 noise_buffer_init(mbuf);
274 noise_buffer_set_inout(mbuf, frame + noise::FRAME_HEADER_SIZE, 1, 1 + noise::MAC_SIZE);
275 int err = noise_cipherstate_encrypt(this->noise_->send_cipher, &mbuf);
276 if (err != 0) {
277 ESP_LOGW(TAG, "Encrypt: %d", err);
278 return false;
279 }
280 noise::write_frame_header(frame, mbuf.size);
281 return this->writeall_(frame, noise::FRAME_HEADER_SIZE + mbuf.size);
282}
283
284} // namespace esphome
285#endif // USE_OTA_ENCRYPTION
286#endif // USE_OTA
static constexpr size_t OTA_BUFFER_SIZE
bool handle_noise_handshake_()
Drive the non-blocking handshake from loop(); returns true once the transport ciphers are ready.
static constexpr uint8_t MAGIC_BYTES[5]
bool writeall_(const uint8_t *buf, size_t len)
ssize_t noise_decrypt_(uint8_t *buf, size_t len)
Decrypt a ciphertext in place; returns the plaintext size or -1.
bool noise_readall_(uint8_t *buf, size_t len)
Blocking read of one frame whose plaintext must be exactly len bytes (control units are one unit per ...
bool noise_start_session_(uint8_t server_feature_flags)
Allocate the session and start the responder handshake.
bool noise_try_read_frame_()
Non-blocking read of one handshake frame into the session buffer.
bool handle_write_error_(ssize_t written, const LogString *desc)
ssize_t noise_read_frame_blocking_(uint8_t *buf, size_t min_ciphertext, size_t max_ciphertext)
Blocking read of one frame whose ciphertext size must be within the given bounds, decrypted in place;...
size_t noise_frame_payload_len_(const uint8_t *header, size_t min_len, size_t max_len)
Payload length from a frame header, or 0 (logged) when the indicator or the length is out of range.
bool handle_read_error_(ssize_t read, const LogString *desc)
ssize_t noise_read_data_(uint8_t *buf, size_t capacity)
Blocking read of one data-phase frame, decrypted in place; returns the plaintext size,...
bool readall_(uint8_t *buf, size_t len)
std::unique_ptr< NoiseSession > noise_
void noise_send_reject_(const LogString *reason)
Best-effort explicit reject frame so the client can log a readable reason.
bool noise_write_byte_(uint8_t byte)
Blocking write of one response byte as an encrypted frame.
const noise::NoiseContext & noise_context_() const
bool noise_try_write_frame_()
Non-blocking write of the pending session-buffer frame.
std::unique_ptr< socket::Socket > client_
int write_message(uint8_t *out, size_t capacity, size_t &out_len)
Produce the next handshake message into out; out_len receives its size and is zero on error.
Action action() const
ACTION_FAILED is the catch-all: returned before init(), after split() has released the state,...
int read_message(uint8_t *data, size_t len)
Process one received handshake message.
int split(NoiseCipherState *&send_cipher, NoiseCipherState *&recv_cipher)
Hand out the transport ciphers and free the handshake state.
__int64 ssize_t
Definition httplib.h:178
constexpr float BME680_GAS_LOOKUP_TABLE_1[16] PROGMEM
Definition bme680.cpp:24
void write_frame_header(uint8_t *buf, uint16_t payload_len)
Definition noise.h:52
size_t format_reject_payload(uint8_t *buf, size_t capacity, const LogString *reason)
Fill buf with a handshake reject payload (status byte plus the reason text, PROGMEM aware); returns t...
Definition noise.cpp:69
const LogString * reject_reason_for(int err)
Reject reason for a failed handshake read.
Definition noise.cpp:65
@ OTA_RESPONSE_FEATURE_FLAGS
Definition ota_backend.h:30
void progmem_memcpy(void *dst, const void *src, size_t len)
Definition hal.h:48
const void size_t len
Definition hal.h:64
constexpr uint16_t encode_uint16(uint8_t msb, uint8_t lsb)
Encode a 16-bit value given the most and least significant byte.
Definition helpers.h:884
int written
Definition helpers.h:1099
uint16_t uint16_t & capacity
Definition helpers.cpp:25
uint8_t frame_buf[noise::FRAME_HEADER_SIZE+1+noise::MAX_HANDSHAKE_SIZE]
Definition ota_esphome.h:87
noise::NoiseResponderHandshake handshake
Definition ota_esphome.h:81