mirror of
https://github.com/esphome/esphome.git
synced 2026-08-30 01:26:43 +00:00
633 lines
23 KiB
C++
633 lines
23 KiB
C++
#include "api_frame_helper_noise.h"
|
|
#ifdef USE_API
|
|
#ifdef USE_API_NOISE
|
|
#include "api_connection.h" // For ClientInfo struct
|
|
#include "esphome/components/noise/noise.h"
|
|
#include "esphome/core/application.h"
|
|
#include "esphome/core/entity_base.h"
|
|
#include "esphome/core/helpers.h"
|
|
#include "esphome/core/log.h"
|
|
#include "proto.h"
|
|
#include <cstring>
|
|
#include <cinttypes>
|
|
|
|
#ifdef USE_ESP8266
|
|
#include <pgmspace.h>
|
|
#endif
|
|
|
|
namespace esphome::api {
|
|
|
|
using noise::noise_err_to_logstr;
|
|
|
|
// api_frame_helper.h keeps its own MAX_HANDSHAKE_SIZE because that header is
|
|
// also compiled in plaintext-only builds without the noise component; keep
|
|
// the two definitions from drifting apart.
|
|
static_assert(MAX_HANDSHAKE_SIZE == noise::MAX_HANDSHAKE_SIZE,
|
|
"api and noise component handshake size limits must match");
|
|
|
|
static const char *const TAG = "api.noise";
|
|
#ifdef USE_ESP8266
|
|
static constexpr char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit";
|
|
#else
|
|
static const char *const PROLOGUE_INIT = "NoiseAPIInit";
|
|
#endif
|
|
static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
|
|
|
|
// Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512)
|
|
static constexpr size_t API_MAX_LOG_BYTES = 168;
|
|
|
|
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
|
#define HELPER_LOG(msg, ...) \
|
|
do { \
|
|
char peername_buf[socket::SOCKADDR_STR_LEN]; \
|
|
this->get_peername_to(peername_buf); \
|
|
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
|
|
} while (0)
|
|
#else
|
|
#define HELPER_LOG(msg, ...) ((void) 0)
|
|
#endif
|
|
|
|
#ifdef HELPER_LOG_PACKETS
|
|
#define LOG_PACKET_RECEIVED(buffer) \
|
|
do { \
|
|
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
|
|
ESP_LOGVV(TAG, "Received frame: %s", \
|
|
format_hex_pretty_to(hex_buf_, (buffer).data(), \
|
|
(buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \
|
|
} while (0)
|
|
#else
|
|
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
|
|
#endif
|
|
|
|
/// Initialize the frame helper, returns OK if successful.
|
|
APIError APINoiseFrameHelper::init() {
|
|
APIError err = init_common_();
|
|
if (err != APIError::OK) {
|
|
return err;
|
|
}
|
|
|
|
// init prologue
|
|
size_t old_size = prologue_.size();
|
|
prologue_.resize(old_size + PROLOGUE_INIT_LEN);
|
|
#ifdef USE_ESP8266
|
|
memcpy_P(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
|
|
#else
|
|
std::memcpy(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
|
|
#endif
|
|
|
|
state_ = State::CLIENT_HELLO;
|
|
return APIError::OK;
|
|
}
|
|
#ifdef USE_API_PLAINTEXT
|
|
APIError APINoiseFrameHelper::init_from_handoff(const uint8_t *header, uint8_t header_len) {
|
|
APIError err = this->init();
|
|
if (err != APIError::OK) {
|
|
return err;
|
|
}
|
|
// Seed the header bytes the plaintext helper consumed before detecting the
|
|
// Noise indicator; try_read_frame_ resumes from rx_header_buf_len_.
|
|
std::memcpy(this->rx_header_buf_, header, header_len);
|
|
this->rx_header_buf_len_ = header_len;
|
|
// Pump the handshake without gating on socket_->ready(): on LWIP the
|
|
// plaintext helper's partial read can drain rcvevent while the rest of the
|
|
// client hello sits in the lastdata cache, so ready() may report false even
|
|
// though data is available.
|
|
return this->pump_handshake_();
|
|
}
|
|
#endif // USE_API_PLAINTEXT
|
|
|
|
/// Drive the handshake state machine until DATA, WOULD_BLOCK, or a fatal
|
|
/// error. WOULD_BLOCK is not an error: reads stop naturally on EWOULDBLOCK
|
|
/// and resume on the next loop().
|
|
APIError APINoiseFrameHelper::pump_handshake_() {
|
|
while (this->state_ != State::DATA) {
|
|
APIError err = this->state_action_();
|
|
if (err == APIError::WOULD_BLOCK) {
|
|
break;
|
|
}
|
|
if (err != APIError::OK) {
|
|
return err;
|
|
}
|
|
}
|
|
return APIError::OK;
|
|
}
|
|
|
|
// Helper for handling handshake frame errors
|
|
APIError APINoiseFrameHelper::handle_handshake_frame_error_(APIError aerr) {
|
|
if (aerr == APIError::BAD_INDICATOR) {
|
|
send_explicit_handshake_reject_(LOG_STR("Bad indicator byte"));
|
|
} else if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) {
|
|
send_explicit_handshake_reject_(LOG_STR("Bad handshake packet len"));
|
|
}
|
|
return aerr;
|
|
}
|
|
|
|
// Helper for handling noise library errors
|
|
APIError APINoiseFrameHelper::handle_noise_error_(int err, const LogString *func_name, APIError api_err) {
|
|
if (err != 0) {
|
|
state_ = State::FAILED;
|
|
HELPER_LOG("%s failed: %s", LOG_STR_ARG(func_name), LOG_STR_ARG(noise_err_to_logstr(err)));
|
|
return api_err;
|
|
}
|
|
return APIError::OK;
|
|
}
|
|
|
|
/// Run through handshake messages (if in that phase)
|
|
APIError APINoiseFrameHelper::loop() {
|
|
// Check ready() once, not per state transition. On ESP8266 LWIP raw TCP,
|
|
// ready() returns false once the rx buffer is consumed. Re-checking each
|
|
// iteration would block handshake writes that must follow reads,
|
|
// deadlocking the handshake. pump_handshake_() stops on WOULD_BLOCK when
|
|
// no more data is available to read.
|
|
if (state_ != State::DATA && this->socket_->ready()) {
|
|
APIError err = this->pump_handshake_();
|
|
if (err != APIError::OK) {
|
|
return err;
|
|
}
|
|
}
|
|
|
|
if (!this->overflow_buf_.empty()) [[unlikely]] {
|
|
return this->drain_overflow_and_handle_errors_();
|
|
}
|
|
return APIError::OK;
|
|
}
|
|
|
|
/** Read a packet into the rx_buf_.
|
|
*
|
|
* @return APIError::OK if a full packet is in rx_buf_
|
|
*
|
|
* errno EWOULDBLOCK: Packet could not be read without blocking. Try again later.
|
|
* errno ENOMEM: Not enough memory for reading packet.
|
|
* errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
|
|
* errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase.
|
|
*/
|
|
APIError APINoiseFrameHelper::try_read_frame_() {
|
|
// read header
|
|
if (rx_header_buf_len_ < noise::FRAME_HEADER_SIZE) {
|
|
// no header information yet
|
|
uint8_t to_read = static_cast<uint8_t>(noise::FRAME_HEADER_SIZE) - rx_header_buf_len_;
|
|
ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read);
|
|
APIError err = handle_socket_read_result_(received);
|
|
if (err != APIError::OK) {
|
|
return err;
|
|
}
|
|
rx_header_buf_len_ += static_cast<uint8_t>(received);
|
|
if (static_cast<uint8_t>(received) != to_read) {
|
|
// not a full read
|
|
return APIError::WOULD_BLOCK;
|
|
}
|
|
|
|
if (rx_header_buf_[0] != noise::FRAME_INDICATOR) {
|
|
state_ = State::FAILED;
|
|
HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
|
|
return APIError::BAD_INDICATOR;
|
|
}
|
|
// header reading done
|
|
}
|
|
|
|
// read body
|
|
uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2];
|
|
|
|
// Check against size limits to prevent OOM: MAX_HANDSHAKE_SIZE for handshake, MAX_MESSAGE_SIZE for data
|
|
bool is_data = (state_ == State::DATA);
|
|
uint16_t limit = is_data ? MAX_MESSAGE_SIZE : MAX_HANDSHAKE_SIZE;
|
|
if (msg_size > limit) {
|
|
state_ = State::FAILED;
|
|
HELPER_LOG("Bad packet: message size %u exceeds maximum %u", msg_size, limit);
|
|
return is_data ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN;
|
|
}
|
|
|
|
// Reserve space for body (+ null terminator in DATA state so protobuf
|
|
// StringRef fields can be safely null-terminated in-place after decode.
|
|
// During handshake, rx_buf_.size() is used in prologue construction, so
|
|
// the buffer must be exactly msg_size to avoid prologue mismatch.)
|
|
uint16_t alloc_size = msg_size + (is_data ? RX_BUF_NULL_TERMINATOR : 0);
|
|
this->rx_buf_.resize(alloc_size);
|
|
|
|
if (rx_buf_len_ < msg_size) {
|
|
// more data to read
|
|
uint16_t to_read = msg_size - rx_buf_len_;
|
|
ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
|
|
APIError err = handle_socket_read_result_(received);
|
|
if (err != APIError::OK) {
|
|
return err;
|
|
}
|
|
rx_buf_len_ += static_cast<uint16_t>(received);
|
|
if (static_cast<uint16_t>(received) != to_read) {
|
|
// not all read
|
|
return APIError::WOULD_BLOCK;
|
|
}
|
|
}
|
|
|
|
LOG_PACKET_RECEIVED(this->rx_buf_);
|
|
|
|
// Clear state for next frame (rx_buf_ still contains data for caller)
|
|
this->rx_buf_len_ = 0;
|
|
this->rx_header_buf_len_ = 0;
|
|
|
|
return APIError::OK;
|
|
}
|
|
|
|
/** To be called from read/write methods.
|
|
*
|
|
* This method runs through the internal handshake methods, if in that state.
|
|
*
|
|
* If the handshake is still active when this method returns and a read/write can't take place at
|
|
* the moment, returns WOULD_BLOCK.
|
|
* If an error occurred, returns that error. Only returns OK if the transport is ready for data
|
|
* traffic.
|
|
*/
|
|
// Split into per-state methods so the compiler doesn't allocate stack space
|
|
// for all branches simultaneously. On RP2040 the core0 stack lives in a 4KB
|
|
// scratch RAM bank; the Noise crypto path (curve25519) needs ~2KB+ of stack,
|
|
// so every byte saved in the caller matters.
|
|
APIError APINoiseFrameHelper::state_action_() {
|
|
switch (this->state_) {
|
|
case State::INITIALIZE:
|
|
HELPER_LOG("Bad state for method: %d", (int) this->state_);
|
|
return APIError::BAD_STATE;
|
|
case State::CLIENT_HELLO:
|
|
return this->state_action_client_hello_();
|
|
case State::SERVER_HELLO:
|
|
return this->state_action_server_hello_();
|
|
case State::HANDSHAKE:
|
|
return this->state_action_handshake_();
|
|
case State::RESUME_DISCARD:
|
|
return this->state_action_resume_discard_();
|
|
case State::CLOSED:
|
|
case State::FAILED:
|
|
return APIError::BAD_STATE;
|
|
default:
|
|
return APIError::OK;
|
|
}
|
|
}
|
|
APIError APINoiseFrameHelper::state_action_client_hello_() {
|
|
// waiting for client hello
|
|
APIError aerr = this->try_read_frame_();
|
|
if (aerr != APIError::OK) {
|
|
return handle_handshake_frame_error_(aerr);
|
|
}
|
|
// Contents are extension flags (today: the resume offer); mixed into the
|
|
// prologue either way. Resize for: existing prologue + 2 size bytes + frame data
|
|
size_t old_size = this->prologue_.size();
|
|
size_t rx_size = this->rx_buf_.size();
|
|
this->prologue_.resize(old_size + 2 + rx_size);
|
|
this->prologue_[old_size] = (uint8_t) (rx_size >> 8);
|
|
this->prologue_[old_size + 1] = (uint8_t) rx_size;
|
|
if (rx_size > 0) {
|
|
std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), rx_size);
|
|
}
|
|
|
|
state_ = State::SERVER_HELLO;
|
|
return APIError::OK;
|
|
}
|
|
APIError APINoiseFrameHelper::state_action_server_hello_() {
|
|
// A verified resume offer (still in rx_buf_ from the client hello step)
|
|
// replaces the whole handshake; any failure falls back to the full one.
|
|
// send server hello
|
|
const auto &name = App.get_name();
|
|
char mac[MAC_ADDRESS_BUFFER_SIZE];
|
|
get_mac_address_into_buffer(mac);
|
|
|
|
// Calculate positions and sizes
|
|
size_t name_len = name.size() + 1; // including null terminator
|
|
size_t name_offset = 1;
|
|
size_t mac_offset = name_offset + name_len;
|
|
size_t total_size = 1 + name_len + MAC_ADDRESS_BUFFER_SIZE;
|
|
|
|
// 1 (proto) + name (max ESPHOME_DEVICE_NAME_MAX_LEN) + 1 (name null)
|
|
// + mac (MAC_ADDRESS_BUFFER_SIZE - 1) + 1 (mac null)
|
|
// + optional resume accept extension
|
|
constexpr size_t max_msg_size =
|
|
1 + ESPHOME_DEVICE_NAME_MAX_LEN + 1 + MAC_ADDRESS_BUFFER_SIZE + noise::RESUME_ACCEPT_SIZE;
|
|
uint8_t msg[max_msg_size];
|
|
|
|
// chosen proto
|
|
msg[0] = 0x01;
|
|
|
|
// node name, terminated by null byte
|
|
std::memcpy(msg + name_offset, name.c_str(), name_len);
|
|
// node mac, terminated by null byte
|
|
std::memcpy(msg + mac_offset, mac, MAC_ADDRESS_BUFFER_SIZE);
|
|
|
|
// The accept extension is written straight after the mac
|
|
bool resume =
|
|
this->ctx_.resume_cache().try_accept(this->rx_buf_.data(), this->rx_buf_.size(), this->prologue_.data(),
|
|
this->prologue_.size(), msg + total_size, send_cipher_, recv_cipher_);
|
|
if (resume) {
|
|
total_size += noise::RESUME_ACCEPT_SIZE;
|
|
}
|
|
|
|
APIError aerr = write_frame_(msg, total_size);
|
|
if (aerr != APIError::OK)
|
|
return aerr;
|
|
|
|
if (resume) {
|
|
this->prologue_.release();
|
|
this->frame_footer_size_ = noise_cipherstate_get_mac_length(this->send_cipher_);
|
|
state_ = State::RESUME_DISCARD;
|
|
return APIError::OK;
|
|
}
|
|
|
|
// start handshake
|
|
aerr = init_handshake_();
|
|
if (aerr != APIError::OK)
|
|
return aerr;
|
|
|
|
state_ = State::HANDSHAKE;
|
|
return APIError::OK;
|
|
}
|
|
APIError APINoiseFrameHelper::state_action_handshake_() {
|
|
noise::NoiseResponderHandshake::Action action = this->handshake_.action();
|
|
if (action == noise::NoiseResponderHandshake::Action::ACTION_READ) {
|
|
return this->state_action_handshake_read_();
|
|
} else if (action == noise::NoiseResponderHandshake::Action::ACTION_WRITE) {
|
|
return this->state_action_handshake_write_();
|
|
}
|
|
// bad state for action
|
|
this->state_ = State::FAILED;
|
|
HELPER_LOG("Bad action for handshake: %d", (int) action);
|
|
return APIError::HANDSHAKESTATE_BAD_STATE;
|
|
}
|
|
/// Read one handshake frame into rx_buf_ and validate its status byte.
|
|
APIError APINoiseFrameHelper::read_handshake_frame_() {
|
|
APIError aerr = this->try_read_frame_();
|
|
if (aerr != APIError::OK) {
|
|
return this->handle_handshake_frame_error_(aerr);
|
|
}
|
|
if (this->rx_buf_.empty()) {
|
|
this->send_explicit_handshake_reject_(LOG_STR("Empty handshake message"));
|
|
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
|
|
}
|
|
if (this->rx_buf_[0] != noise::HANDSHAKE_STATUS_OK) {
|
|
HELPER_LOG("Bad handshake error byte: %u", this->rx_buf_[0]);
|
|
this->send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte"));
|
|
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
|
|
}
|
|
return APIError::OK;
|
|
}
|
|
|
|
/// Resumed session: discard the client's already-in-flight handshake
|
|
/// message 1, then enter DATA.
|
|
APIError APINoiseFrameHelper::state_action_resume_discard_() {
|
|
APIError aerr = this->read_handshake_frame_();
|
|
if (aerr != APIError::OK) {
|
|
return aerr;
|
|
}
|
|
HELPER_LOG("Session resumed!");
|
|
state_ = State::DATA;
|
|
return APIError::OK;
|
|
}
|
|
|
|
APIError APINoiseFrameHelper::state_action_handshake_read_() {
|
|
APIError aerr = this->read_handshake_frame_();
|
|
if (aerr != APIError::OK) {
|
|
return aerr;
|
|
}
|
|
|
|
int err = this->handshake_.read_message(this->rx_buf_.data() + 1, this->rx_buf_.size() - 1);
|
|
if (err != 0) {
|
|
// Special handling for MAC failure
|
|
this->send_explicit_handshake_reject_(noise::reject_reason_for(err));
|
|
return this->handle_noise_error_(err, LOG_STR("noise_handshakestate_read_message"),
|
|
APIError::HANDSHAKESTATE_READ_FAILED);
|
|
}
|
|
|
|
return this->check_handshake_finished_();
|
|
}
|
|
APIError APINoiseFrameHelper::state_action_handshake_write_() {
|
|
uint8_t buffer[65];
|
|
size_t msg_len = 0;
|
|
|
|
int err = this->handshake_.write_message(buffer + 1, sizeof(buffer) - 1, msg_len);
|
|
APIError aerr = this->handle_noise_error_(err, LOG_STR("noise_handshakestate_write_message"),
|
|
APIError::HANDSHAKESTATE_WRITE_FAILED);
|
|
if (aerr != APIError::OK)
|
|
return aerr;
|
|
buffer[0] = noise::HANDSHAKE_STATUS_OK;
|
|
|
|
aerr = this->write_frame_(buffer, msg_len + 1);
|
|
if (aerr != APIError::OK)
|
|
return aerr;
|
|
return this->check_handshake_finished_();
|
|
}
|
|
void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reason) {
|
|
// Max reject message: "Bad handshake packet len" (24) + 1 (failure byte) = 25 bytes
|
|
uint8_t data[32];
|
|
static_assert(sizeof(data) >= noise::MAC_FAILURE_PAYLOAD_SIZE,
|
|
"reject buffer must fit the MAC failure wire contract");
|
|
size_t data_size = noise::format_reject_payload(data, sizeof(data), reason);
|
|
|
|
// temporarily remove failed state
|
|
auto orig_state = state_;
|
|
state_ = State::EXPLICIT_REJECT;
|
|
APIError aerr = write_frame_(data, data_size);
|
|
if (aerr != APIError::OK) {
|
|
// Best effort; the reject reason is a diagnosis aid, not a protocol step
|
|
ESP_LOGW(TAG, "Sending handshake reject failed: %d", (int) aerr);
|
|
}
|
|
if (state_ == State::EXPLICIT_REJECT) {
|
|
// write_frame_ may have moved the state to FAILED; keep that decision
|
|
state_ = orig_state;
|
|
}
|
|
}
|
|
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
|
APIError aerr = this->check_data_state_();
|
|
if (aerr != APIError::OK)
|
|
return aerr;
|
|
|
|
aerr = this->try_read_frame_();
|
|
if (aerr != APIError::OK)
|
|
return aerr;
|
|
|
|
NoiseBuffer mbuf;
|
|
noise_buffer_init(mbuf);
|
|
// read_packet() must only be called in DATA state; the extra
|
|
// RX_BUF_NULL_TERMINATOR byte is only allocated in DATA state
|
|
// (see try_read_frame_), so calling this during handshake would
|
|
// underflow the size calculation below.
|
|
#ifdef ESPHOME_DEBUG_API
|
|
assert(this->state_ == State::DATA);
|
|
#endif
|
|
// rx_buf_ has RX_BUF_NULL_TERMINATOR extra byte for null termination
|
|
// (only added in DATA state — see try_read_frame_), so subtract it
|
|
// to get the actual encrypted data size for decryption.
|
|
size_t encrypted_size = this->rx_buf_.size() - RX_BUF_NULL_TERMINATOR;
|
|
noise_buffer_set_inout(mbuf, this->rx_buf_.data(), encrypted_size, encrypted_size);
|
|
int err = noise_cipherstate_decrypt(this->recv_cipher_, &mbuf);
|
|
APIError decrypt_err =
|
|
handle_noise_error_(err, LOG_STR("noise_cipherstate_decrypt"), APIError::CIPHERSTATE_DECRYPT_FAILED);
|
|
if (decrypt_err != APIError::OK) {
|
|
return decrypt_err;
|
|
}
|
|
|
|
uint16_t msg_size = mbuf.size;
|
|
uint8_t *msg_data = this->rx_buf_.data();
|
|
if (msg_size < 4) {
|
|
this->state_ = State::FAILED;
|
|
HELPER_LOG("Bad data packet: size %d too short", msg_size);
|
|
return APIError::BAD_DATA_PACKET;
|
|
}
|
|
|
|
uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1];
|
|
uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3];
|
|
if (data_len > msg_size - 4) {
|
|
this->state_ = State::FAILED;
|
|
HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size);
|
|
return APIError::BAD_DATA_PACKET;
|
|
}
|
|
|
|
buffer->data = msg_data + 4; // Skip 4-byte header (type + length)
|
|
buffer->data_len = data_len;
|
|
buffer->type = type;
|
|
return APIError::OK;
|
|
}
|
|
// Encrypt a single noise message in place and return the encrypted frame length.
|
|
// Returns APIError::OK on success.
|
|
APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, uint16_t payload_size, uint8_t message_type,
|
|
uint16_t &encrypted_len_out) {
|
|
// The noise frame header is written after encryption, when the size is known
|
|
|
|
// Write message header (to be encrypted)
|
|
constexpr uint8_t msg_offset = noise::FRAME_HEADER_SIZE;
|
|
buf_start[msg_offset] = static_cast<uint8_t>(message_type >> 8); // type high byte
|
|
buf_start[msg_offset + 1] = static_cast<uint8_t>(message_type); // type low byte
|
|
buf_start[msg_offset + 2] = static_cast<uint8_t>(payload_size >> 8); // data_len high byte
|
|
buf_start[msg_offset + 3] = static_cast<uint8_t>(payload_size); // data_len low byte
|
|
// payload data is already in the buffer starting at offset + 7
|
|
|
|
// Encrypt the message in place
|
|
NoiseBuffer mbuf;
|
|
noise_buffer_init(mbuf);
|
|
noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + payload_size, 4 + payload_size + this->frame_footer_size_);
|
|
|
|
int err = noise_cipherstate_encrypt(this->send_cipher_, &mbuf);
|
|
APIError aerr =
|
|
this->handle_noise_error_(err, LOG_STR("noise_cipherstate_encrypt"), APIError::CIPHERSTATE_ENCRYPT_FAILED);
|
|
if (aerr != APIError::OK)
|
|
return aerr;
|
|
|
|
// Fill in the frame header now that the encrypted size is known
|
|
noise::write_frame_header(buf_start, static_cast<uint16_t>(mbuf.size));
|
|
|
|
encrypted_len_out = static_cast<uint16_t>(noise::FRAME_HEADER_SIZE + mbuf.size);
|
|
return APIError::OK;
|
|
}
|
|
|
|
APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
|
#ifdef ESPHOME_DEBUG_API
|
|
assert(this->state_ == State::DATA);
|
|
#endif
|
|
|
|
// Resize buffer to include footer space for Noise MAC
|
|
if (this->frame_footer_size_)
|
|
buffer.get_buffer()->resize(buffer.get_buffer()->size() + this->frame_footer_size_);
|
|
|
|
uint16_t payload_size =
|
|
static_cast<uint16_t>(buffer.get_buffer()->size() - HEADER_PADDING - this->frame_footer_size_);
|
|
uint8_t *buf_start = buffer.get_buffer()->data();
|
|
uint16_t encrypted_len;
|
|
APIError aerr = this->encrypt_noise_message_(buf_start, payload_size, type, encrypted_len);
|
|
if (aerr != APIError::OK)
|
|
return aerr;
|
|
return this->write_raw_fast_buf_(buf_start, encrypted_len);
|
|
}
|
|
|
|
APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) {
|
|
#ifdef ESPHOME_DEBUG_API
|
|
assert(this->state_ == State::DATA);
|
|
assert(!messages.empty());
|
|
#endif
|
|
|
|
// Noise messages are already contiguous in the buffer:
|
|
// HEADER_PADDING (7) exactly matches the fixed header size, and
|
|
// footer space (16) is consumed by the encryption MAC.
|
|
uint8_t *buffer_data = buffer.get_buffer()->data();
|
|
uint8_t *write_start = buffer_data + messages[0].offset;
|
|
uint16_t total_write_len = 0;
|
|
|
|
for (const auto &msg : messages) {
|
|
uint8_t *buf_start = buffer_data + msg.offset;
|
|
uint16_t encrypted_len;
|
|
APIError aerr = this->encrypt_noise_message_(buf_start, msg.payload_size, msg.message_type, encrypted_len);
|
|
if (aerr != APIError::OK)
|
|
return aerr;
|
|
total_write_len += encrypted_len;
|
|
}
|
|
|
|
return this->write_raw_fast_buf_(write_start, total_write_len);
|
|
}
|
|
|
|
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
|
|
uint8_t header[noise::FRAME_HEADER_SIZE];
|
|
noise::write_frame_header(header, len);
|
|
|
|
if (len == 0) {
|
|
return this->write_raw_buf_(header, noise::FRAME_HEADER_SIZE);
|
|
}
|
|
struct iovec iov[2];
|
|
iov[0].iov_base = header;
|
|
iov[0].iov_len = noise::FRAME_HEADER_SIZE;
|
|
iov[1].iov_base = const_cast<uint8_t *>(data);
|
|
iov[1].iov_len = len;
|
|
|
|
return this->write_raw_iov_(iov, 2, noise::FRAME_HEADER_SIZE + len);
|
|
}
|
|
|
|
/** Initiate the data structures for the handshake.
|
|
*
|
|
* @return 0 on success, -1 on error (check errno)
|
|
*/
|
|
APIError APINoiseFrameHelper::init_handshake_() {
|
|
int err = this->handshake_.init(this->ctx_.get_psk(), prologue_.data(), prologue_.size());
|
|
APIError aerr = handle_noise_error_(err, LOG_STR("noise_handshake_init"), APIError::HANDSHAKESTATE_SETUP_FAILED);
|
|
if (aerr != APIError::OK)
|
|
return aerr;
|
|
// init copies the prologue into the handshakestate, so we can get rid of it now
|
|
prologue_.release();
|
|
return APIError::OK;
|
|
}
|
|
|
|
APIError APINoiseFrameHelper::check_handshake_finished_() {
|
|
#ifdef ESPHOME_DEBUG_API
|
|
assert(state_ == State::HANDSHAKE);
|
|
#endif
|
|
|
|
noise::NoiseResponderHandshake::Action action = this->handshake_.action();
|
|
if (action == noise::NoiseResponderHandshake::Action::ACTION_READ ||
|
|
action == noise::NoiseResponderHandshake::Action::ACTION_WRITE)
|
|
return APIError::OK;
|
|
if (action != noise::NoiseResponderHandshake::Action::ACTION_SPLIT) {
|
|
state_ = State::FAILED;
|
|
HELPER_LOG("Bad action for handshake: %d", (int) action);
|
|
return APIError::HANDSHAKESTATE_BAD_STATE;
|
|
}
|
|
// split() also frees the handshake state
|
|
int err = this->handshake_.split(send_cipher_, recv_cipher_);
|
|
APIError aerr =
|
|
handle_noise_error_(err, LOG_STR("noise_handshakestate_split"), APIError::HANDSHAKESTATE_SPLIT_FAILED);
|
|
if (aerr != APIError::OK)
|
|
return aerr;
|
|
|
|
this->frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
|
|
|
|
HELPER_LOG("Handshake complete!");
|
|
state_ = State::DATA;
|
|
return APIError::OK;
|
|
}
|
|
|
|
APINoiseFrameHelper::~APINoiseFrameHelper() {
|
|
if (send_cipher_ != nullptr) {
|
|
noise_cipherstate_free(send_cipher_);
|
|
send_cipher_ = nullptr;
|
|
}
|
|
if (recv_cipher_ != nullptr) {
|
|
noise_cipherstate_free(recv_cipher_);
|
|
recv_cipher_ = nullptr;
|
|
}
|
|
}
|
|
|
|
} // namespace esphome::api
|
|
#endif // USE_API_NOISE
|
|
#endif // USE_API
|