1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-06 13:22:19 +01:00
Files
esphome/esphome/components/api/api_frame_helper_noise.cpp
2025-07-23 18:04:06 -10:00

584 lines
20 KiB
C++

#include "api_frame_helper_noise.h"
#ifdef USE_API
#ifdef USE_API_NOISE
#include "api_connection.h" // For ClientInfo struct
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "proto.h"
#include <cstring>
#include <cinttypes>
namespace esphome::api {
static const char *const TAG = "api.noise";
static const char *const PROLOGUE_INIT = "NoiseAPIInit";
static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__)
#ifdef HELPER_LOG_PACKETS
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
#define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str())
#else
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
#define LOG_PACKET_SENDING(data, len) ((void) 0)
#endif
/// Convert a noise error code to a readable error
std::string noise_err_to_str(int err) {
if (err == NOISE_ERROR_NO_MEMORY)
return "NO_MEMORY";
if (err == NOISE_ERROR_UNKNOWN_ID)
return "UNKNOWN_ID";
if (err == NOISE_ERROR_UNKNOWN_NAME)
return "UNKNOWN_NAME";
if (err == NOISE_ERROR_MAC_FAILURE)
return "MAC_FAILURE";
if (err == NOISE_ERROR_NOT_APPLICABLE)
return "NOT_APPLICABLE";
if (err == NOISE_ERROR_SYSTEM)
return "SYSTEM";
if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED)
return "REMOTE_KEY_REQUIRED";
if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED)
return "LOCAL_KEY_REQUIRED";
if (err == NOISE_ERROR_PSK_REQUIRED)
return "PSK_REQUIRED";
if (err == NOISE_ERROR_INVALID_LENGTH)
return "INVALID_LENGTH";
if (err == NOISE_ERROR_INVALID_PARAM)
return "INVALID_PARAM";
if (err == NOISE_ERROR_INVALID_STATE)
return "INVALID_STATE";
if (err == NOISE_ERROR_INVALID_NONCE)
return "INVALID_NONCE";
if (err == NOISE_ERROR_INVALID_PRIVATE_KEY)
return "INVALID_PRIVATE_KEY";
if (err == NOISE_ERROR_INVALID_PUBLIC_KEY)
return "INVALID_PUBLIC_KEY";
if (err == NOISE_ERROR_INVALID_FORMAT)
return "INVALID_FORMAT";
if (err == NOISE_ERROR_INVALID_SIGNATURE)
return "INVALID_SIGNATURE";
return to_string(err);
}
/// 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);
std::memcpy(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
state_ = State::CLIENT_HELLO;
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_("Bad indicator byte");
} else if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) {
send_explicit_handshake_reject_("Bad handshake packet len");
}
return aerr;
}
// Helper for handling noise library errors
APIError APINoiseFrameHelper::handle_noise_error_(int err, const char *func_name, APIError api_err) {
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("%s failed: %s", func_name, noise_err_to_str(err).c_str());
return api_err;
}
return APIError::OK;
}
/// Run through handshake messages (if in that phase)
APIError APINoiseFrameHelper::loop() {
// During handshake phase, process as many actions as possible until we can't progress
// socket_->ready() stays true until next main loop, but state_action() will return
// WOULD_BLOCK when no more data is available to read
while (state_ != State::DATA && this->socket_->ready()) {
APIError err = state_action_();
if (err == APIError::WOULD_BLOCK) {
break;
}
if (err != APIError::OK) {
return err;
}
}
// Use base class implementation for buffer sending
return APIFrameHelper::loop();
}
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
*
* @param frame: The struct to hold the frame information in.
* msg_start: points to the start of the payload - this pointer is only valid until the next
* try_receive_raw_ call
*
* @return 0 if a full packet is in rx_buf_
* @return -1 if error, check errno.
*
* 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_(std::vector<uint8_t> *frame) {
if (frame == nullptr) {
HELPER_LOG("Bad argument for try_read_frame_");
return APIError::BAD_ARG;
}
// read header
if (rx_header_buf_len_ < 3) {
// no header information yet
uint8_t to_read = 3 - 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] != 0x01) {
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];
if (state_ != State::DATA && msg_size > 128) {
// for handshake message only permit up to 128 bytes
state_ = State::FAILED;
HELPER_LOG("Bad packet len for handshake: %d", msg_size);
return APIError::BAD_HANDSHAKE_PACKET_LEN;
}
// reserve space for body
if (rx_buf_.size() != msg_size) {
rx_buf_.resize(msg_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(rx_buf_);
*frame = std::move(rx_buf_);
// consume msg
rx_buf_ = {};
rx_buf_len_ = 0;
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.
*/
APIError APINoiseFrameHelper::state_action_() {
int err;
APIError aerr;
if (state_ == State::INITIALIZE) {
HELPER_LOG("Bad state for method: %d", (int) state_);
return APIError::BAD_STATE;
}
if (state_ == State::CLIENT_HELLO) {
// waiting for client hello
std::vector<uint8_t> frame;
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK) {
return handle_handshake_frame_error_(aerr);
}
// ignore contents, may be used in future for flags
// Resize for: existing prologue + 2 size bytes + frame data
size_t old_size = prologue_.size();
prologue_.resize(old_size + 2 + frame.size());
prologue_[old_size] = (uint8_t) (frame.size() >> 8);
prologue_[old_size + 1] = (uint8_t) frame.size();
std::memcpy(prologue_.data() + old_size + 2, frame.data(), frame.size());
state_ = State::SERVER_HELLO;
}
if (state_ == State::SERVER_HELLO) {
// send server hello
const std::string &name = App.get_name();
const std::string &mac = get_mac_address();
std::vector<uint8_t> msg;
// Calculate positions and sizes
size_t name_len = name.size() + 1; // including null terminator
size_t mac_len = mac.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_len;
msg.resize(total_size);
// chosen proto
msg[0] = 0x01;
// node name, terminated by null byte
std::memcpy(msg.data() + name_offset, name.c_str(), name_len);
// node mac, terminated by null byte
std::memcpy(msg.data() + mac_offset, mac.c_str(), mac_len);
aerr = write_frame_(msg.data(), msg.size());
if (aerr != APIError::OK)
return aerr;
// start handshake
aerr = init_handshake_();
if (aerr != APIError::OK)
return aerr;
state_ = State::HANDSHAKE;
}
if (state_ == State::HANDSHAKE) {
int action = noise_handshakestate_get_action(handshake_);
if (action == NOISE_ACTION_READ_MESSAGE) {
// waiting for handshake msg
std::vector<uint8_t> frame;
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK) {
return handle_handshake_frame_error_(aerr);
}
if (frame.empty()) {
send_explicit_handshake_reject_("Empty handshake message");
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
} else if (frame[0] != 0x00) {
HELPER_LOG("Bad handshake error byte: %u", frame[0]);
send_explicit_handshake_reject_("Bad handshake error byte");
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
}
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_input(mbuf, frame.data() + 1, frame.size() - 1);
err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr);
if (err != 0) {
// Special handling for MAC failure
send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? "Handshake MAC failure" : "Handshake error");
return handle_noise_error_(err, "noise_handshakestate_read_message", APIError::HANDSHAKESTATE_READ_FAILED);
}
aerr = check_handshake_finished_();
if (aerr != APIError::OK)
return aerr;
} else if (action == NOISE_ACTION_WRITE_MESSAGE) {
uint8_t buffer[65];
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1);
err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr);
APIError aerr_write =
handle_noise_error_(err, "noise_handshakestate_write_message", APIError::HANDSHAKESTATE_WRITE_FAILED);
if (aerr_write != APIError::OK)
return aerr_write;
buffer[0] = 0x00; // success
aerr = write_frame_(buffer, mbuf.size + 1);
if (aerr != APIError::OK)
return aerr;
aerr = check_handshake_finished_();
if (aerr != APIError::OK)
return aerr;
} else {
// bad state for action
state_ = State::FAILED;
HELPER_LOG("Bad action for handshake: %d", action);
return APIError::HANDSHAKESTATE_BAD_STATE;
}
}
if (state_ == State::CLOSED || state_ == State::FAILED) {
return APIError::BAD_STATE;
}
return APIError::OK;
}
void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &reason) {
std::vector<uint8_t> data;
data.resize(reason.length() + 1);
data[0] = 0x01; // failure
// Copy error message in bulk
if (!reason.empty()) {
std::memcpy(data.data() + 1, reason.c_str(), reason.length());
}
// temporarily remove failed state
auto orig_state = state_;
state_ = State::EXPLICIT_REJECT;
write_frame_(data.data(), data.size());
state_ = orig_state;
}
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
int err;
APIError aerr;
aerr = state_action_();
if (aerr != APIError::OK) {
return aerr;
}
if (state_ != State::DATA) {
return APIError::WOULD_BLOCK;
}
std::vector<uint8_t> frame;
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK)
return aerr;
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, frame.data(), frame.size(), frame.size());
err = noise_cipherstate_decrypt(recv_cipher_, &mbuf);
APIError decrypt_err = handle_noise_error_(err, "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 = frame.data();
if (msg_size < 4) {
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) {
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->container = std::move(frame);
buffer->data_offset = 4;
buffer->data_len = data_len;
buffer->type = type;
return APIError::OK;
}
APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
// Resize to include MAC space (required for Noise encryption)
buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_);
PacketInfo packet{type, 0,
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
return write_protobuf_packets(buffer, std::span<const PacketInfo>(&packet, 1));
}
APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) {
APIError aerr = state_action_();
if (aerr != APIError::OK) {
return aerr;
}
if (state_ != State::DATA) {
return APIError::WOULD_BLOCK;
}
if (packets.empty()) {
return APIError::OK;
}
std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer
this->reusable_iovs_.clear();
this->reusable_iovs_.reserve(packets.size());
uint16_t total_write_len = 0;
// We need to encrypt each packet in place
for (const auto &packet : packets) {
// The buffer already has padding at offset
uint8_t *buf_start = buffer_data + packet.offset;
// Write noise header
buf_start[0] = 0x01; // indicator
// buf_start[1], buf_start[2] to be set after encryption
// Write message header (to be encrypted)
const uint8_t msg_offset = 3;
buf_start[msg_offset] = static_cast<uint8_t>(packet.message_type >> 8); // type high byte
buf_start[msg_offset + 1] = static_cast<uint8_t>(packet.message_type); // type low byte
buf_start[msg_offset + 2] = static_cast<uint8_t>(packet.payload_size >> 8); // data_len high byte
buf_start[msg_offset + 3] = static_cast<uint8_t>(packet.payload_size); // data_len low byte
// payload data is already in the buffer starting at offset + 7
// Make sure we have space for MAC
// The buffer should already have been sized appropriately
// Encrypt the message in place
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + packet.payload_size,
4 + packet.payload_size + frame_footer_size_);
int err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
APIError aerr = handle_noise_error_(err, "noise_cipherstate_encrypt", APIError::CIPHERSTATE_ENCRYPT_FAILED);
if (aerr != APIError::OK)
return aerr;
// Fill in the encrypted size
buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8);
buf_start[2] = static_cast<uint8_t>(mbuf.size);
// Add iovec for this encrypted packet
size_t packet_len = static_cast<size_t>(3 + mbuf.size); // indicator + size + encrypted data
this->reusable_iovs_.push_back({buf_start, packet_len});
total_write_len += packet_len;
}
// Send all encrypted packets in one writev call
return this->write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size(), total_write_len);
}
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
uint8_t header[3];
header[0] = 0x01; // indicator
header[1] = (uint8_t) (len >> 8);
header[2] = (uint8_t) len;
struct iovec iov[2];
iov[0].iov_base = header;
iov[0].iov_len = 3;
if (len == 0) {
return this->write_raw_(iov, 1, 3); // Just header
}
iov[1].iov_base = const_cast<uint8_t *>(data);
iov[1].iov_len = len;
return this->write_raw_(iov, 2, 3 + len); // Header + data
}
/** Initiate the data structures for the handshake.
*
* @return 0 on success, -1 on error (check errno)
*/
APIError APINoiseFrameHelper::init_handshake_() {
int err;
memset(&nid_, 0, sizeof(nid_));
// const char *proto = "Noise_NNpsk0_25519_ChaChaPoly_SHA256";
// err = noise_protocol_name_to_id(&nid_, proto, strlen(proto));
nid_.pattern_id = NOISE_PATTERN_NN;
nid_.cipher_id = NOISE_CIPHER_CHACHAPOLY;
nid_.dh_id = NOISE_DH_CURVE25519;
nid_.prefix_id = NOISE_PREFIX_STANDARD;
nid_.hybrid_id = NOISE_DH_NONE;
nid_.hash_id = NOISE_HASH_SHA256;
nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0;
err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER);
APIError aerr = handle_noise_error_(err, "noise_handshakestate_new_by_id", APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
const auto &psk = ctx_->get_psk();
err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size());
aerr = handle_noise_error_(err, "noise_handshakestate_set_pre_shared_key", APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size());
aerr = handle_noise_error_(err, "noise_handshakestate_set_prologue", APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
// set_prologue copies it into handshakestate, so we can get rid of it now
prologue_ = {};
err = noise_handshakestate_start(handshake_);
aerr = handle_noise_error_(err, "noise_handshakestate_start", APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
return APIError::OK;
}
APIError APINoiseFrameHelper::check_handshake_finished_() {
assert(state_ == State::HANDSHAKE);
int action = noise_handshakestate_get_action(handshake_);
if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE)
return APIError::OK;
if (action != NOISE_ACTION_SPLIT) {
state_ = State::FAILED;
HELPER_LOG("Bad action for handshake: %d", action);
return APIError::HANDSHAKESTATE_BAD_STATE;
}
int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_);
APIError aerr = handle_noise_error_(err, "noise_handshakestate_split", APIError::HANDSHAKESTATE_SPLIT_FAILED);
if (aerr != APIError::OK)
return aerr;
frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
HELPER_LOG("Handshake complete!");
noise_handshakestate_free(handshake_);
handshake_ = nullptr;
state_ = State::DATA;
return APIError::OK;
}
APINoiseFrameHelper::~APINoiseFrameHelper() {
if (handshake_ != nullptr) {
noise_handshakestate_free(handshake_);
handshake_ = nullptr;
}
if (send_cipher_ != nullptr) {
noise_cipherstate_free(send_cipher_);
send_cipher_ = nullptr;
}
if (recv_cipher_ != nullptr) {
noise_cipherstate_free(recv_cipher_);
recv_cipher_ = nullptr;
}
}
extern "C" {
// declare how noise generates random bytes (here with a good HWRNG based on the RF system)
void noise_rand_bytes(void *output, size_t len) {
if (!esphome::random_bytes(reinterpret_cast<uint8_t *>(output), len)) {
ESP_LOGE(TAG, "Acquiring random bytes failed; rebooting");
arch_restart();
}
}
}
} // namespace esphome::api
#endif // USE_API_NOISE
#endif // USE_API