ESPHome 2026.11.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
11#ifdef USE_ESP8266
12#include <pgmspace.h>
13#endif
14
15namespace esphome {
16
17ESPHOME_LOG_TAG(TAG, "esphome.ota");
18
19#ifdef USE_ESP8266
20static constexpr char OTA_NOISE_PROLOGUE_INIT[] PROGMEM = "NoiseOTAInit";
21#else
22static constexpr char OTA_NOISE_PROLOGUE_INIT[] = "NoiseOTAInit";
23#endif
24static constexpr size_t OTA_NOISE_PROLOGUE_INIT_LEN = sizeof(OTA_NOISE_PROLOGUE_INIT) - 1;
25
27 if (this->send_cipher != nullptr) {
28 noise_cipherstate_free(this->send_cipher);
29 }
30 if (this->recv_cipher != nullptr) {
31 noise_cipherstate_free(this->recv_cipher);
32 }
33}
34
36 // Default placement, PSRAM first where present: the session lives for one upload
38}
39
47bool ESPHomeOTAComponent::noise_start_session_(uint8_t server_feature_flags) {
48 // A provisioned key cleared between the offer and here is not guarded: the
49 // session runs on the zero key load_psk fills in and fails the client's MAC
50 static constexpr size_t PROLOGUE_ACK_LEN = 2; // OTA_RESPONSE_OK + version
51 static constexpr size_t PROLOGUE_CLIENT_FEATURES_LEN = 1;
52 static constexpr size_t PROLOGUE_FEATURE_ACK_LEN = 2; // OTA_RESPONSE_FEATURE_FLAGS + server flags
53 uint8_t prologue[OTA_NOISE_PROLOGUE_INIT_LEN + sizeof(MAGIC_BYTES) + PROLOGUE_ACK_LEN + PROLOGUE_CLIENT_FEATURES_LEN +
54 PROLOGUE_FEATURE_ACK_LEN];
55 progmem_memcpy(prologue, OTA_NOISE_PROLOGUE_INIT, OTA_NOISE_PROLOGUE_INIT_LEN);
56 uint8_t *p = prologue + OTA_NOISE_PROLOGUE_INIT_LEN;
57 // Magic bytes, already validated in MAGIC_READ
58 std::memcpy(p, MAGIC_BYTES, sizeof(MAGIC_BYTES));
59 p += sizeof(MAGIC_BYTES);
60 // Our magic ack
62 *p++ = USE_OTA_VERSION;
63 // The feature byte the client sent
64 *p++ = this->ota_features_;
65 // The feature ack we sent (noise requires the extended protocol)
67 *p++ = server_feature_flags;
68
69 // The caller only starts a session when the context holds a key
70 int err = this->noise_ == nullptr ? NOISE_ERROR_NO_MEMORY
71 : this->noise_->handshake.init(this->noise_context_(), prologue, sizeof(prologue));
72 if (err != 0) {
73 // Raw noise codes throughout: the name table would cost flash in builds
74 // where only the OTA uses noise
75 ESP_LOGW(TAG, "Session init: %d", err);
76 this->cleanup_connection_();
77 return false;
78 }
79 return true;
80}
81
88 NoiseSession &s = *this->noise_;
89 while (true) {
90 if (s.writing) {
91 if (!this->noise_try_write_frame_()) {
92 return false; // would block, or errored and cleaned up
93 }
94 s.writing = false;
95 s.frame_pos = 0;
96 s.frame_len = 0;
97 }
98 switch (s.handshake.action()) {
100 if (!this->noise_try_read_frame_()) {
101 return false;
102 }
103 const uint16_t payload_len = s.frame_len - noise::FRAME_HEADER_SIZE;
104 s.frame_pos = 0;
105 s.frame_len = 0;
106 if (s.frame_buf[noise::FRAME_HEADER_SIZE] != noise::HANDSHAKE_STATUS_OK) {
107 ESP_LOGW(TAG, "Client rejected the handshake: %u", s.frame_buf[noise::FRAME_HEADER_SIZE]);
108 this->cleanup_connection_();
109 return false;
110 }
111 int err = s.handshake.read_message(s.frame_buf + noise::FRAME_HEADER_SIZE + 1, payload_len - 1);
112 if (err != 0) {
113 // A MAC failure here almost always means the uploader has a different key
114 const LogString *reason = noise::reject_reason_for(err);
115 ESP_LOGW(TAG, "Handshake read: %s (%d)", LOG_STR_ARG(reason), err);
116 this->noise_send_reject_(reason);
117 this->cleanup_connection_();
118 return false;
119 }
120 break;
121 }
123 size_t msg_len = 0;
124 int err =
125 s.handshake.write_message(s.frame_buf + noise::FRAME_HEADER_SIZE + 1, noise::MAX_HANDSHAKE_SIZE, msg_len);
126 if (err != 0) {
127 ESP_LOGW(TAG, "Handshake write: %d", err);
128 this->cleanup_connection_();
129 return false;
130 }
131 const uint16_t payload_len = msg_len + 1;
132 noise::write_frame_header(s.frame_buf, payload_len);
133 s.frame_buf[noise::FRAME_HEADER_SIZE] = noise::HANDSHAKE_STATUS_OK;
134 s.frame_len = noise::FRAME_HEADER_SIZE + payload_len;
135 s.frame_pos = 0;
136 s.writing = true;
137 break;
138 }
140 int err = s.handshake.split(s.send_cipher, s.recv_cipher);
141 if (err != 0) {
142 ESP_LOGW(TAG, "Handshake split: %d", err);
143 this->cleanup_connection_();
144 return false;
145 }
146 ESP_LOGD(TAG, "Noise handshake complete");
147 return true;
148 }
149 default: {
150 ESP_LOGW(TAG, "Bad handshake state");
151 this->cleanup_connection_();
152 return false;
153 }
154 }
155 }
156}
157
160size_t ESPHomeOTAComponent::noise_frame_payload_len_(const uint8_t *header, size_t min_len, size_t max_len) {
161 const size_t payload_len = encode_uint16(header[1], header[2]);
162 if (header[0] != noise::FRAME_INDICATOR || payload_len < min_len || payload_len > max_len) {
163 ESP_LOGW(TAG, "Bad frame: 0x%02X, %zu bytes", header[0], payload_len);
164 return 0;
165 }
166 return payload_len;
167}
168
171 NoiseSession &s = *this->noise_;
172 while (true) {
173 // The header first, then the body once the header says how long it is
174 const uint16_t want = s.frame_len == 0 ? noise::FRAME_HEADER_SIZE : s.frame_len;
175 if (s.frame_pos < want) {
176 ssize_t read = this->client_->read(s.frame_buf + s.frame_pos, want - s.frame_pos);
177 if (!this->handle_read_error_(read, LOG_STR("read noise"))) {
178 return false;
179 }
180 s.frame_pos += read;
181 continue;
182 }
183 if (s.frame_len != 0) {
184 return true;
185 }
186 const size_t payload_len = this->noise_frame_payload_len_(s.frame_buf, 1, 1 + noise::MAX_HANDSHAKE_SIZE);
187 if (payload_len == 0) {
188 this->cleanup_connection_();
189 return false;
190 }
191 s.frame_len = noise::FRAME_HEADER_SIZE + payload_len;
192 }
193}
194
197 NoiseSession &s = *this->noise_;
198 while (s.frame_pos < s.frame_len) {
199 ssize_t written = this->client_->write(s.frame_buf + s.frame_pos, s.frame_len - s.frame_pos);
200 if (!this->handle_write_error_(written, LOG_STR("write noise frame"))) {
201 return false;
202 }
203 s.frame_pos += written;
204 }
205 return true;
206}
207
209void ESPHomeOTAComponent::noise_send_reject_(const LogString *reason) {
210 // Every reason here comes from noise::reject_reason_for(), so the exported
211 // floor is the exact capacity needed
212 uint8_t data[noise::FRAME_HEADER_SIZE + noise::MAC_FAILURE_PAYLOAD_SIZE];
213 const size_t payload_len =
214 noise::format_reject_payload(data + noise::FRAME_HEADER_SIZE, sizeof(data) - noise::FRAME_HEADER_SIZE, reason);
215 noise::write_frame_header(data, payload_len);
216 this->client_->write(data, noise::FRAME_HEADER_SIZE + payload_len); // Best effort, non-blocking
217}
218
221 NoiseBuffer mbuf;
222 noise_buffer_init(mbuf);
223 noise_buffer_set_inout(mbuf, buf, len, len);
224 int err = noise_cipherstate_decrypt(this->noise_->recv_cipher, &mbuf);
225 if (err != 0) {
226 ESP_LOGW(TAG, "Decrypt: %d", err);
227 return -1;
228 }
229 return mbuf.size;
230}
231
236ssize_t ESPHomeOTAComponent::noise_read_frame_blocking_(uint8_t *buf, size_t min_ciphertext, size_t max_ciphertext) {
237 uint8_t header[noise::FRAME_HEADER_SIZE];
238 if (!this->readall_(header, sizeof(header))) {
239 return -1;
240 }
241 const size_t ciphertext_len = this->noise_frame_payload_len_(header, min_ciphertext, max_ciphertext);
242 if (ciphertext_len == 0) {
243 return -1;
244 }
245 if (!this->readall_(buf, ciphertext_len)) {
246 return -1;
247 }
248 return this->noise_decrypt_(buf, ciphertext_len);
249}
250
255bool ESPHomeOTAComponent::noise_readall_(uint8_t *buf, size_t len) {
256 return this->noise_read_frame_blocking_(buf, len + noise::MAC_SIZE, len + noise::MAC_SIZE) == (ssize_t) len;
257}
258
265 const size_t max_ciphertext = std::min(capacity + noise::MAC_SIZE, OTA_BUFFER_SIZE);
266 return this->noise_read_frame_blocking_(buf, noise::MAC_SIZE + 1, max_ciphertext);
267}
268
270bool ESPHomeOTAComponent::noise_write_(const uint8_t *data, size_t len) {
271 uint8_t frame[noise::FRAME_HEADER_SIZE + NOISE_MAX_RESPONSE + noise::MAC_SIZE];
273 return false;
274 memcpy(frame + noise::FRAME_HEADER_SIZE, data, len);
275 NoiseBuffer mbuf;
276 noise_buffer_init(mbuf);
277 noise_buffer_set_inout(mbuf, frame + noise::FRAME_HEADER_SIZE, len, len + noise::MAC_SIZE);
278 int err = noise_cipherstate_encrypt(this->noise_->send_cipher, &mbuf);
279 if (err != 0) {
280 ESP_LOGW(TAG, "Encrypt: %d", err);
281 return false;
282 }
283 noise::write_frame_header(frame, mbuf.size);
284 return this->writeall_(frame, noise::FRAME_HEADER_SIZE + mbuf.size);
285}
286
287} // namespace esphome
288#endif // USE_OTA_ENCRYPTION
289#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.
bool noise_write_(const uint8_t *data, size_t len)
Blocking write of a short response (at most NOISE_MAX_RESPONSE bytes) as one encrypted frame.
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)
Start the responder handshake, on the session reserved at offer time.
bool noise_try_read_frame_()
Non-blocking read of one handshake frame into the session buffer.
RAMUniquePtr< NoiseSession > noise_
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)
void noise_send_reject_(const LogString *reason)
Best-effort explicit reject frame so the client can log a readable reason.
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_
static constexpr size_t NOISE_MAX_RESPONSE
An STL allocator that uses SPI or internal RAM.
Definition helpers.h:2298
RAMUniquePtr< T > make_unique(Args &&...args)
Value initialize one T; empty on exhaustion.
Definition helpers.h:2361
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:27
ESPHOME_LOG_TAG(TAG, CRASH_LOG_TAG)
void write_frame_header(uint8_t *buf, uint16_t payload_len)
Definition noise.h:70
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:100
const LogString * reject_reason_for(int err)
Reject reason for a failed handshake read.
Definition noise.cpp:96
@ OTA_RESPONSE_FEATURE_FLAGS
Definition ota_backend.h:31
void progmem_memcpy(void *dst, const void *src, size_t len)
Definition hal.h:56
const void size_t len
Definition hal.h:68
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:998
int written
Definition helpers.h:1262
uint16_t uint16_t & capacity
Definition helpers.cpp:34
uint8_t frame_buf[noise::FRAME_HEADER_SIZE+1+noise::MAX_HANDSHAKE_SIZE]
noise::NoiseResponderHandshake handshake