ESPHome 2026.9.0
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
17static const char *const 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
42bool ESPHomeOTAComponent::noise_start_session_(uint8_t server_feature_flags) {
43 // A provisioned key cleared between the offer and here is not guarded: the
44 // session runs on the zero key load_psk fills in and fails the client's MAC.
45 // Default placement, PSRAM first where present: the session only lives for one upload
47 static constexpr size_t PROLOGUE_ACK_LEN = 2; // OTA_RESPONSE_OK + version
48 static constexpr size_t PROLOGUE_CLIENT_FEATURES_LEN = 1;
49 static constexpr size_t PROLOGUE_FEATURE_ACK_LEN = 2; // OTA_RESPONSE_FEATURE_FLAGS + server flags
50 uint8_t prologue[OTA_NOISE_PROLOGUE_INIT_LEN + sizeof(MAGIC_BYTES) + PROLOGUE_ACK_LEN + PROLOGUE_CLIENT_FEATURES_LEN +
51 PROLOGUE_FEATURE_ACK_LEN];
52 progmem_memcpy(prologue, OTA_NOISE_PROLOGUE_INIT, OTA_NOISE_PROLOGUE_INIT_LEN);
53 uint8_t *p = prologue + OTA_NOISE_PROLOGUE_INIT_LEN;
54 // Magic bytes, already validated in MAGIC_READ
55 std::memcpy(p, MAGIC_BYTES, sizeof(MAGIC_BYTES));
56 p += sizeof(MAGIC_BYTES);
57 // Our magic ack
59 *p++ = USE_OTA_VERSION;
60 // The feature byte the client sent
61 *p++ = this->ota_features_;
62 // The feature ack we sent (noise requires the extended protocol)
64 *p++ = server_feature_flags;
65
66 // The caller only starts a session when the context holds a key
67 int err = this->noise_ == nullptr ? NOISE_ERROR_NO_MEMORY
68 : this->noise_->handshake.init(this->noise_context_(), prologue, sizeof(prologue));
69 if (err != 0) {
70 // Raw noise codes throughout: the name table would cost flash in builds
71 // where only the OTA uses noise
72 ESP_LOGW(TAG, "Session init: %d", err);
73 this->cleanup_connection_();
74 return false;
75 }
76 return true;
77}
78
85 NoiseSession &s = *this->noise_;
86 while (true) {
87 if (s.writing) {
88 if (!this->noise_try_write_frame_()) {
89 return false; // would block, or errored and cleaned up
90 }
91 s.writing = false;
92 s.frame_pos = 0;
93 s.frame_len = 0;
94 }
95 switch (s.handshake.action()) {
97 if (!this->noise_try_read_frame_()) {
98 return false;
99 }
100 const uint16_t payload_len = s.frame_len - noise::FRAME_HEADER_SIZE;
101 s.frame_pos = 0;
102 s.frame_len = 0;
103 if (s.frame_buf[noise::FRAME_HEADER_SIZE] != noise::HANDSHAKE_STATUS_OK) {
104 ESP_LOGW(TAG, "Client rejected the handshake: %u", s.frame_buf[noise::FRAME_HEADER_SIZE]);
105 this->cleanup_connection_();
106 return false;
107 }
108 int err = s.handshake.read_message(s.frame_buf + noise::FRAME_HEADER_SIZE + 1, payload_len - 1);
109 if (err != 0) {
110 // A MAC failure here almost always means the uploader has a different key
111 const LogString *reason = noise::reject_reason_for(err);
112 ESP_LOGW(TAG, "Handshake read: %s (%d)", LOG_STR_ARG(reason), err);
113 this->noise_send_reject_(reason);
114 this->cleanup_connection_();
115 return false;
116 }
117 break;
118 }
120 size_t msg_len = 0;
121 int err =
122 s.handshake.write_message(s.frame_buf + noise::FRAME_HEADER_SIZE + 1, noise::MAX_HANDSHAKE_SIZE, msg_len);
123 if (err != 0) {
124 ESP_LOGW(TAG, "Handshake write: %d", err);
125 this->cleanup_connection_();
126 return false;
127 }
128 const uint16_t payload_len = msg_len + 1;
129 noise::write_frame_header(s.frame_buf, payload_len);
130 s.frame_buf[noise::FRAME_HEADER_SIZE] = noise::HANDSHAKE_STATUS_OK;
131 s.frame_len = noise::FRAME_HEADER_SIZE + payload_len;
132 s.frame_pos = 0;
133 s.writing = true;
134 break;
135 }
137 int err = s.handshake.split(s.send_cipher, s.recv_cipher);
138 if (err != 0) {
139 ESP_LOGW(TAG, "Handshake split: %d", err);
140 this->cleanup_connection_();
141 return false;
142 }
143 ESP_LOGD(TAG, "Noise handshake complete");
144 return true;
145 }
146 default: {
147 ESP_LOGW(TAG, "Bad handshake state");
148 this->cleanup_connection_();
149 return false;
150 }
151 }
152 }
153}
154
157size_t ESPHomeOTAComponent::noise_frame_payload_len_(const uint8_t *header, size_t min_len, size_t max_len) {
158 const size_t payload_len = encode_uint16(header[1], header[2]);
159 if (header[0] != noise::FRAME_INDICATOR || payload_len < min_len || payload_len > max_len) {
160 ESP_LOGW(TAG, "Bad frame: 0x%02X, %zu bytes", header[0], payload_len);
161 return 0;
162 }
163 return payload_len;
164}
165
168 NoiseSession &s = *this->noise_;
169 while (true) {
170 // The header first, then the body once the header says how long it is
171 const uint16_t want = s.frame_len == 0 ? noise::FRAME_HEADER_SIZE : s.frame_len;
172 if (s.frame_pos < want) {
173 ssize_t read = this->client_->read(s.frame_buf + s.frame_pos, want - s.frame_pos);
174 if (!this->handle_read_error_(read, LOG_STR("read noise"))) {
175 return false;
176 }
177 s.frame_pos += read;
178 continue;
179 }
180 if (s.frame_len != 0) {
181 return true;
182 }
183 const size_t payload_len = this->noise_frame_payload_len_(s.frame_buf, 1, 1 + noise::MAX_HANDSHAKE_SIZE);
184 if (payload_len == 0) {
185 this->cleanup_connection_();
186 return false;
187 }
188 s.frame_len = noise::FRAME_HEADER_SIZE + payload_len;
189 }
190}
191
194 NoiseSession &s = *this->noise_;
195 while (s.frame_pos < s.frame_len) {
196 ssize_t written = this->client_->write(s.frame_buf + s.frame_pos, s.frame_len - s.frame_pos);
197 if (!this->handle_write_error_(written, LOG_STR("write noise frame"))) {
198 return false;
199 }
200 s.frame_pos += written;
201 }
202 return true;
203}
204
206void ESPHomeOTAComponent::noise_send_reject_(const LogString *reason) {
207 // Every reason here comes from noise::reject_reason_for(), so the exported
208 // floor is the exact capacity needed
209 uint8_t data[noise::FRAME_HEADER_SIZE + noise::MAC_FAILURE_PAYLOAD_SIZE];
210 const size_t payload_len =
211 noise::format_reject_payload(data + noise::FRAME_HEADER_SIZE, sizeof(data) - noise::FRAME_HEADER_SIZE, reason);
212 noise::write_frame_header(data, payload_len);
213 this->client_->write(data, noise::FRAME_HEADER_SIZE + payload_len); // Best effort, non-blocking
214}
215
218 NoiseBuffer mbuf;
219 noise_buffer_init(mbuf);
220 noise_buffer_set_inout(mbuf, buf, len, len);
221 int err = noise_cipherstate_decrypt(this->noise_->recv_cipher, &mbuf);
222 if (err != 0) {
223 ESP_LOGW(TAG, "Decrypt: %d", err);
224 return -1;
225 }
226 return mbuf.size;
227}
228
233ssize_t ESPHomeOTAComponent::noise_read_frame_blocking_(uint8_t *buf, size_t min_ciphertext, size_t max_ciphertext) {
234 uint8_t header[noise::FRAME_HEADER_SIZE];
235 if (!this->readall_(header, sizeof(header))) {
236 return -1;
237 }
238 const size_t ciphertext_len = this->noise_frame_payload_len_(header, min_ciphertext, max_ciphertext);
239 if (ciphertext_len == 0) {
240 return -1;
241 }
242 if (!this->readall_(buf, ciphertext_len)) {
243 return -1;
244 }
245 return this->noise_decrypt_(buf, ciphertext_len);
246}
247
252bool ESPHomeOTAComponent::noise_readall_(uint8_t *buf, size_t len) {
253 return this->noise_read_frame_blocking_(buf, len + noise::MAC_SIZE, len + noise::MAC_SIZE) == (ssize_t) len;
254}
255
262 const size_t max_ciphertext = std::min(capacity + noise::MAC_SIZE, OTA_BUFFER_SIZE);
263 return this->noise_read_frame_blocking_(buf, noise::MAC_SIZE + 1, max_ciphertext);
264}
265
268 uint8_t frame[noise::FRAME_HEADER_SIZE + 1 + noise::MAC_SIZE];
269 frame[noise::FRAME_HEADER_SIZE] = byte;
270 NoiseBuffer mbuf;
271 noise_buffer_init(mbuf);
272 noise_buffer_set_inout(mbuf, frame + noise::FRAME_HEADER_SIZE, 1, 1 + noise::MAC_SIZE);
273 int err = noise_cipherstate_encrypt(this->noise_->send_cipher, &mbuf);
274 if (err != 0) {
275 ESP_LOGW(TAG, "Encrypt: %d", err);
276 return false;
277 }
278 noise::write_frame_header(frame, mbuf.size);
279 return this->writeall_(frame, noise::FRAME_HEADER_SIZE + mbuf.size);
280}
281
282} // namespace esphome
283#endif // USE_OTA_ENCRYPTION
284#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.
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.
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_
An STL allocator that uses SPI or internal RAM.
Definition helpers.h:2142
RAMUniquePtr< T > make_unique(Args &&...args)
Value initialize one T; empty on exhaustion.
Definition helpers.h:2205
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:915
int written
Definition helpers.h:1130
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