mirror of
https://github.com/esphome/esphome.git
synced 2026-08-23 22:56:19 +00:00
Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d34efecf2 | ||
|
|
c4096d44d8 | ||
|
|
68f3a6b9a5 | ||
|
|
662bf7d7f0 | ||
|
|
0d71ab8efb | ||
|
|
4ce4768ebd | ||
|
|
fb13327922 | ||
|
|
4c62420f1b | ||
|
|
ca3f31643f | ||
|
|
d770004e0e | ||
|
|
b28efcd545 | ||
|
|
6b6d27f905 | ||
|
|
603c3539a3 | ||
|
|
f248a85b51 | ||
|
|
5a3d7e3292 | ||
|
|
c6d329db64 | ||
|
|
bdb203d742 | ||
|
|
f509516d60 | ||
|
|
4c856949c2 | ||
|
|
c90a5f4cff | ||
|
|
964fc1ef3f |
@@ -381,7 +381,6 @@ esphome/components/nextion/switch/* @senexcrenshaw
|
||||
esphome/components/nextion/text_sensor/* @senexcrenshaw
|
||||
esphome/components/nfc/* @jesserockz @kbx81
|
||||
esphome/components/noblex/* @AGalfra
|
||||
esphome/components/noise/* @esphome/core
|
||||
esphome/components/npi19/* @bakerkj
|
||||
esphome/components/nrf52/* @tomaszduda23
|
||||
esphome/components/number/* @esphome/core
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import base64
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -5,15 +6,6 @@ from esphome import automation
|
||||
from esphome.automation import Condition
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.logger import request_log_listener
|
||||
|
||||
# ENCRYPTION_SCHEMA and validate_encryption_key are re-exported for external
|
||||
# components and downstream consumers that import them from api
|
||||
from esphome.components.noise import ( # noqa: F401
|
||||
ENCRYPTION_SCHEMA,
|
||||
decode_encryption_key,
|
||||
encryption_schema,
|
||||
validate_encryption_key,
|
||||
)
|
||||
from esphome.config_helpers import get_logger_level
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -46,10 +38,6 @@ from esphome.core import CORE, ID, CoroPriority, EsphomeError, coroutine_with_pr
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigFragmentType, ConfigType
|
||||
|
||||
# Compat alias: downstream consumers (e.g. device-builder) referenced the
|
||||
# schema by its old private name before it moved to the noise component
|
||||
_encryption_schema = encryption_schema
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "api"
|
||||
@@ -58,15 +46,9 @@ CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
|
||||
def AUTO_LOAD(config: ConfigType) -> list[str]:
|
||||
"""Conditionally auto-load noise (encryption) and json (capture_response)."""
|
||||
"""Conditionally auto-load json only when capture_response is used."""
|
||||
base = ["socket"]
|
||||
|
||||
# A falsy config is a tooling probe for the maximal set (None from
|
||||
# dependency resolution, {} from the components-graph platform probe);
|
||||
# a validated config always carries defaults, never empty
|
||||
if not config or CONF_ENCRYPTION in config:
|
||||
base = base + ["noise"]
|
||||
|
||||
# Check if any homeassistant.action/homeassistant.service has capture_response: true
|
||||
# This flag is set during config validation in _validate_response_config
|
||||
if not config or CORE.data.get(DOMAIN, {}).get(CONF_CAPTURE_RESPONSE, False):
|
||||
@@ -148,6 +130,20 @@ def _register_provisioning_source(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
def validate_encryption_key(value: Any) -> str:
|
||||
value = cv.string_strict(value)
|
||||
try:
|
||||
decoded = base64.b64decode(value, validate=True)
|
||||
except ValueError as err:
|
||||
raise cv.Invalid("Invalid key format, please check it's using base64") from err
|
||||
|
||||
if len(decoded) != 32:
|
||||
raise cv.Invalid("Encryption key must be base64 and 32 bytes long")
|
||||
|
||||
# Return original data for roundtrip conversion
|
||||
return value
|
||||
|
||||
|
||||
CONF_SUPPORTS_RESPONSE = "supports_response"
|
||||
|
||||
# Enum values in api::enums namespace
|
||||
@@ -254,6 +250,18 @@ ACTIONS_SCHEMA = automation.validate_automation(
|
||||
),
|
||||
)
|
||||
|
||||
ENCRYPTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_KEY): cv.sensitive(validate_encryption_key),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _encryption_schema(config: ConfigType | None) -> ConfigType:
|
||||
if config is None:
|
||||
config = {}
|
||||
return ENCRYPTION_SCHEMA(config)
|
||||
|
||||
|
||||
def _consume_api_sockets(config: ConfigType) -> ConfigType:
|
||||
"""Register socket needs for API component."""
|
||||
@@ -289,7 +297,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
CONF_SERVICES, group_of_exclusion=CONF_ACTIONS
|
||||
): ACTIONS_SCHEMA,
|
||||
cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA,
|
||||
cv.Optional(CONF_ENCRYPTION): encryption_schema,
|
||||
cv.Optional(CONF_ENCRYPTION): _encryption_schema,
|
||||
cv.Optional(CONF_BATCH_DELAY, default="100ms"): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(max=cv.TimePeriod(milliseconds=65535)),
|
||||
@@ -476,7 +484,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
if (encryption_config := config.get(CONF_ENCRYPTION, None)) is not None:
|
||||
if key := encryption_config.get(CONF_KEY):
|
||||
decoded = decode_encryption_key(key)
|
||||
decoded = base64.b64decode(key)
|
||||
cg.add(var.set_noise_psk(list(decoded)))
|
||||
cg.add_define("USE_API_NOISE_PSK_FROM_YAML")
|
||||
else:
|
||||
@@ -490,6 +498,10 @@ async def to_code(config: ConfigType) -> None:
|
||||
# and plaintext disabled. Only a factory reset can remove it.
|
||||
cg.add_define("USE_API_PLAINTEXT")
|
||||
cg.add_define("USE_API_NOISE")
|
||||
cg.add_library("esphome/noise-c", "0.1.21")
|
||||
# Enable optimized memzero/memcmp in libsodium instead of volatile byte loops
|
||||
cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1")
|
||||
cg.add_build_flag("-DHAVE_INLINE_ASM=1")
|
||||
else:
|
||||
cg.add_define("USE_API_PLAINTEXT")
|
||||
|
||||
|
||||
@@ -2130,7 +2130,7 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio
|
||||
}
|
||||
#endif
|
||||
|
||||
noise::psk_t psk{};
|
||||
psk_t psk{};
|
||||
if (msg.key_len == 0) {
|
||||
if (this->parent_->clear_noise_psk(true)) {
|
||||
resp.success = true;
|
||||
@@ -2139,7 +2139,7 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio
|
||||
}
|
||||
} else if (base64_decode(msg.key, msg.key_len, psk.data(), psk.size()) != psk.size()) {
|
||||
ESP_LOGW(TAG, "Invalid encryption key length");
|
||||
} else if (noise::NoiseContext::is_all_zeros(psk)) {
|
||||
} else if (APINoiseContext::is_all_zeros(psk)) {
|
||||
// Accepting the reserved provisioning PSK would report success without
|
||||
// enabling encryption (or silently clear an existing key)
|
||||
ESP_LOGW(TAG, "Rejecting all-zero encryption key");
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
#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/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "proto.h"
|
||||
@@ -17,14 +17,6 @@
|
||||
|
||||
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";
|
||||
@@ -59,6 +51,45 @@ static constexpr size_t API_MAX_LOG_BYTES = 168;
|
||||
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
|
||||
#endif
|
||||
|
||||
/// Convert a noise error code to a readable error
|
||||
const LogString *noise_err_to_logstr(int err) {
|
||||
if (err == NOISE_ERROR_NO_MEMORY)
|
||||
return LOG_STR("NO_MEMORY");
|
||||
if (err == NOISE_ERROR_UNKNOWN_ID)
|
||||
return LOG_STR("UNKNOWN_ID");
|
||||
if (err == NOISE_ERROR_UNKNOWN_NAME)
|
||||
return LOG_STR("UNKNOWN_NAME");
|
||||
if (err == NOISE_ERROR_MAC_FAILURE)
|
||||
return LOG_STR("MAC_FAILURE");
|
||||
if (err == NOISE_ERROR_NOT_APPLICABLE)
|
||||
return LOG_STR("NOT_APPLICABLE");
|
||||
if (err == NOISE_ERROR_SYSTEM)
|
||||
return LOG_STR("SYSTEM");
|
||||
if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED)
|
||||
return LOG_STR("REMOTE_KEY_REQUIRED");
|
||||
if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED)
|
||||
return LOG_STR("LOCAL_KEY_REQUIRED");
|
||||
if (err == NOISE_ERROR_PSK_REQUIRED)
|
||||
return LOG_STR("PSK_REQUIRED");
|
||||
if (err == NOISE_ERROR_INVALID_LENGTH)
|
||||
return LOG_STR("INVALID_LENGTH");
|
||||
if (err == NOISE_ERROR_INVALID_PARAM)
|
||||
return LOG_STR("INVALID_PARAM");
|
||||
if (err == NOISE_ERROR_INVALID_STATE)
|
||||
return LOG_STR("INVALID_STATE");
|
||||
if (err == NOISE_ERROR_INVALID_NONCE)
|
||||
return LOG_STR("INVALID_NONCE");
|
||||
if (err == NOISE_ERROR_INVALID_PRIVATE_KEY)
|
||||
return LOG_STR("INVALID_PRIVATE_KEY");
|
||||
if (err == NOISE_ERROR_INVALID_PUBLIC_KEY)
|
||||
return LOG_STR("INVALID_PUBLIC_KEY");
|
||||
if (err == NOISE_ERROR_INVALID_FORMAT)
|
||||
return LOG_STR("INVALID_FORMAT");
|
||||
if (err == NOISE_ERROR_INVALID_SIGNATURE)
|
||||
return LOG_STR("INVALID_SIGNATURE");
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
|
||||
/// Initialize the frame helper, returns OK if successful.
|
||||
APIError APINoiseFrameHelper::init() {
|
||||
APIError err = init_common_();
|
||||
@@ -163,9 +194,9 @@ APIError APINoiseFrameHelper::loop() {
|
||||
*/
|
||||
APIError APINoiseFrameHelper::try_read_frame_() {
|
||||
// read header
|
||||
if (rx_header_buf_len_ < noise::FRAME_HEADER_SIZE) {
|
||||
if (rx_header_buf_len_ < 3) {
|
||||
// no header information yet
|
||||
uint8_t to_read = static_cast<uint8_t>(noise::FRAME_HEADER_SIZE) - rx_header_buf_len_;
|
||||
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) {
|
||||
@@ -177,7 +208,7 @@ APIError APINoiseFrameHelper::try_read_frame_() {
|
||||
return APIError::WOULD_BLOCK;
|
||||
}
|
||||
|
||||
if (rx_header_buf_[0] != noise::FRAME_INDICATOR) {
|
||||
if (rx_header_buf_[0] != 0x01) {
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
|
||||
return APIError::BAD_INDICATOR;
|
||||
@@ -317,15 +348,15 @@ APIError APINoiseFrameHelper::state_action_server_hello_() {
|
||||
return APIError::OK;
|
||||
}
|
||||
APIError APINoiseFrameHelper::state_action_handshake_() {
|
||||
noise::NoiseResponderHandshake::Action action = this->handshake_.action();
|
||||
if (action == noise::NoiseResponderHandshake::Action::ACTION_READ) {
|
||||
int action = noise_handshakestate_get_action(this->handshake_);
|
||||
if (action == NOISE_ACTION_READ_MESSAGE) {
|
||||
return this->state_action_handshake_read_();
|
||||
} else if (action == noise::NoiseResponderHandshake::Action::ACTION_WRITE) {
|
||||
} else if (action == NOISE_ACTION_WRITE_MESSAGE) {
|
||||
return this->state_action_handshake_write_();
|
||||
}
|
||||
// bad state for action
|
||||
this->state_ = State::FAILED;
|
||||
HELPER_LOG("Bad action for handshake: %d", (int) action);
|
||||
HELPER_LOG("Bad action for handshake: %d", action);
|
||||
return APIError::HANDSHAKESTATE_BAD_STATE;
|
||||
}
|
||||
APIError APINoiseFrameHelper::state_action_handshake_read_() {
|
||||
@@ -337,16 +368,20 @@ APIError APINoiseFrameHelper::state_action_handshake_read_() {
|
||||
if (this->rx_buf_.empty()) {
|
||||
this->send_explicit_handshake_reject_(LOG_STR("Empty handshake message"));
|
||||
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
|
||||
} else if (this->rx_buf_[0] != noise::HANDSHAKE_STATUS_OK) {
|
||||
} else if (this->rx_buf_[0] != 0x00) {
|
||||
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;
|
||||
}
|
||||
|
||||
int err = this->handshake_.read_message(this->rx_buf_.data() + 1, this->rx_buf_.size() - 1);
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_input(mbuf, this->rx_buf_.data() + 1, this->rx_buf_.size() - 1);
|
||||
int err = noise_handshakestate_read_message(this->handshake_, &mbuf, nullptr);
|
||||
if (err != 0) {
|
||||
// Special handling for MAC failure
|
||||
this->send_explicit_handshake_reject_(noise::reject_reason_for(err));
|
||||
this->send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? LOG_STR("Handshake MAC failure")
|
||||
: LOG_STR("Handshake error"));
|
||||
return this->handle_noise_error_(err, LOG_STR("noise_handshakestate_read_message"),
|
||||
APIError::HANDSHAKESTATE_READ_FAILED);
|
||||
}
|
||||
@@ -355,16 +390,18 @@ APIError APINoiseFrameHelper::state_action_handshake_read_() {
|
||||
}
|
||||
APIError APINoiseFrameHelper::state_action_handshake_write_() {
|
||||
uint8_t buffer[65];
|
||||
size_t msg_len = 0;
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1);
|
||||
|
||||
int err = this->handshake_.write_message(buffer + 1, sizeof(buffer) - 1, msg_len);
|
||||
int err = noise_handshakestate_write_message(this->handshake_, &mbuf, nullptr);
|
||||
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;
|
||||
buffer[0] = 0x00; // success
|
||||
|
||||
aerr = this->write_frame_(buffer, msg_len + 1);
|
||||
aerr = this->write_frame_(buffer, mbuf.size + 1);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
return this->check_handshake_finished_();
|
||||
@@ -372,22 +409,33 @@ APIError APINoiseFrameHelper::state_action_handshake_write_() {
|
||||
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);
|
||||
data[0] = 0x01; // failure
|
||||
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions
|
||||
size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason));
|
||||
reason_len = std::min(reason_len, sizeof(data) - 1);
|
||||
if (reason_len > 0) {
|
||||
memcpy_P(data + 1, reinterpret_cast<PGM_P>(reason), reason_len);
|
||||
}
|
||||
#else
|
||||
// Normal memory access
|
||||
const char *reason_str = LOG_STR_ARG(reason);
|
||||
size_t reason_len = strlen(reason_str);
|
||||
reason_len = std::min(reason_len, sizeof(data) - 1);
|
||||
if (reason_len > 0) {
|
||||
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - binary protocol, not a C string
|
||||
std::memcpy(data + 1, reason_str, reason_len);
|
||||
}
|
||||
#endif
|
||||
|
||||
size_t data_size = reason_len + 1;
|
||||
|
||||
// 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;
|
||||
}
|
||||
write_frame_(data, data_size);
|
||||
state_ = orig_state;
|
||||
}
|
||||
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
APIError aerr = this->check_data_state_();
|
||||
@@ -444,10 +492,12 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
// 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 noise header
|
||||
buf_start[0] = 0x01; // indicator
|
||||
// buf_start[1], buf_start[2] to be set after encryption
|
||||
|
||||
// Write message header (to be encrypted)
|
||||
constexpr uint8_t msg_offset = noise::FRAME_HEADER_SIZE;
|
||||
constexpr uint8_t msg_offset = 3;
|
||||
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
|
||||
@@ -465,10 +515,11 @@ APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, uint16_
|
||||
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));
|
||||
// Fill in the encrypted size
|
||||
buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8);
|
||||
buf_start[2] = static_cast<uint8_t>(mbuf.size);
|
||||
|
||||
encrypted_len_out = static_cast<uint16_t>(noise::FRAME_HEADER_SIZE + mbuf.size);
|
||||
encrypted_len_out = static_cast<uint16_t>(3 + mbuf.size); // indicator + size + encrypted data
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
@@ -517,19 +568,21 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s
|
||||
}
|
||||
|
||||
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
|
||||
uint8_t header[noise::FRAME_HEADER_SIZE];
|
||||
noise::write_frame_header(header, len);
|
||||
uint8_t header[3];
|
||||
header[0] = 0x01; // indicator
|
||||
header[1] = (uint8_t) (len >> 8);
|
||||
header[2] = (uint8_t) len;
|
||||
|
||||
if (len == 0) {
|
||||
return this->write_raw_buf_(header, noise::FRAME_HEADER_SIZE);
|
||||
return this->write_raw_buf_(header, 3);
|
||||
}
|
||||
struct iovec iov[2];
|
||||
iov[0].iov_base = header;
|
||||
iov[0].iov_len = noise::FRAME_HEADER_SIZE;
|
||||
iov[0].iov_len = 3;
|
||||
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);
|
||||
return this->write_raw_iov_(iov, 2, 3 + len);
|
||||
}
|
||||
|
||||
/** Initiate the data structures for the handshake.
|
||||
@@ -537,12 +590,45 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
|
||||
* @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);
|
||||
int err;
|
||||
// Noise_NNpsk0_25519_ChaChaPoly_SHA256, built on the stack:
|
||||
// noise_handshakestate_new_by_id copies it, so a member would waste
|
||||
// 104 bytes per connection, and a static const would sit in RAM on
|
||||
// ESP8266 (.rodata is DRAM there).
|
||||
const NoiseProtocolId nid = {
|
||||
.prefix_id = NOISE_PREFIX_STANDARD,
|
||||
.pattern_id = NOISE_PATTERN_NN,
|
||||
.modifier_ids = {NOISE_MODIFIER_PSK0},
|
||||
.dh_id = NOISE_DH_CURVE25519,
|
||||
.cipher_id = NOISE_CIPHER_CHACHAPOLY,
|
||||
.hash_id = NOISE_HASH_SHA256,
|
||||
.hybrid_id = NOISE_DH_NONE,
|
||||
};
|
||||
|
||||
err = noise_handshakestate_new_by_id(&handshake_, &nid, NOISE_ROLE_RESPONDER);
|
||||
APIError aerr =
|
||||
handle_noise_error_(err, LOG_STR("noise_handshakestate_new_by_id"), 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
|
||||
|
||||
const auto &psk = this->ctx_.get_psk();
|
||||
err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size());
|
||||
aerr = handle_noise_error_(err, LOG_STR("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, LOG_STR("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_.release();
|
||||
|
||||
err = noise_handshakestate_start(handshake_);
|
||||
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
@@ -551,17 +637,15 @@ APIError APINoiseFrameHelper::check_handshake_finished_() {
|
||||
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)
|
||||
int action = noise_handshakestate_get_action(handshake_);
|
||||
if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE)
|
||||
return APIError::OK;
|
||||
if (action != noise::NoiseResponderHandshake::Action::ACTION_SPLIT) {
|
||||
if (action != NOISE_ACTION_SPLIT) {
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Bad action for handshake: %d", (int) action);
|
||||
HELPER_LOG("Bad action for handshake: %d", action);
|
||||
return APIError::HANDSHAKESTATE_BAD_STATE;
|
||||
}
|
||||
// split() also frees the handshake state
|
||||
int err = this->handshake_.split(send_cipher_, recv_cipher_);
|
||||
int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_);
|
||||
APIError aerr =
|
||||
handle_noise_error_(err, LOG_STR("noise_handshakestate_split"), APIError::HANDSHAKESTATE_SPLIT_FAILED);
|
||||
if (aerr != APIError::OK)
|
||||
@@ -570,11 +654,17 @@ APIError APINoiseFrameHelper::check_handshake_finished_() {
|
||||
this->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;
|
||||
@@ -585,6 +675,16 @@ APINoiseFrameHelper::~APINoiseFrameHelper() {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#ifdef USE_API
|
||||
#ifdef USE_API_NOISE
|
||||
#include "noise/protocol.h"
|
||||
#include "esphome/components/noise/noise_handshake.h"
|
||||
#include "api_noise_context.h"
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
@@ -14,9 +14,9 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
// Pos 1-2: encrypted payload size (16-bit big-endian)
|
||||
// Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
|
||||
// Pos 7+: actual payload data
|
||||
static constexpr uint8_t HEADER_PADDING = noise::FRAME_HEADER_SIZE + 2 + 2; // frame header + type + data_len
|
||||
static constexpr uint8_t HEADER_PADDING = 1 + 2 + 2 + 2; // indicator + size + type + data_len
|
||||
|
||||
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, noise::NoiseContext &ctx)
|
||||
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, APINoiseContext &ctx)
|
||||
: APIFrameHelper(std::move(socket)), ctx_(ctx) {
|
||||
frame_header_padding_ = HEADER_PADDING;
|
||||
}
|
||||
@@ -52,13 +52,13 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
APIError handle_handshake_frame_error_(APIError aerr);
|
||||
APIError handle_noise_error_(int err, const LogString *func_name, APIError api_err);
|
||||
|
||||
// Pointers first (4 bytes each; the handshake wrapper holds one pointer)
|
||||
noise::NoiseResponderHandshake handshake_;
|
||||
// Pointers first (4 bytes each)
|
||||
NoiseHandshakeState *handshake_{nullptr};
|
||||
NoiseCipherState *send_cipher_{nullptr};
|
||||
NoiseCipherState *recv_cipher_{nullptr};
|
||||
|
||||
// Reference to noise context (4 bytes on 32-bit)
|
||||
noise::NoiseContext &ctx_;
|
||||
APINoiseContext &ctx_;
|
||||
|
||||
// Buffer for noise handshake prologue (released after handshake)
|
||||
APIBuffer prologue_;
|
||||
@@ -67,7 +67,7 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
// Fixed-size header buffer for noise protocol:
|
||||
// 1 byte for indicator + 2 bytes for message size (16-bit value, not varint)
|
||||
// Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase
|
||||
uint8_t rx_header_buf_[noise::FRAME_HEADER_SIZE];
|
||||
uint8_t rx_header_buf_[3];
|
||||
uint8_t rx_header_buf_len_ = 0;
|
||||
// 4 bytes total, no padding
|
||||
};
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
using psk_t = std::array<uint8_t, 32>;
|
||||
|
||||
class APINoiseContext {
|
||||
public:
|
||||
// The all-zeros PSK is reserved: it marks the device as unprovisioned and
|
||||
// doubles as the well-known provisioning PSK that unprovisioned devices
|
||||
// accept for Noise handshakes (passive-sniffing protection only, no
|
||||
// authentication). It is never a valid real key.
|
||||
static bool is_all_zeros(const psk_t &psk) {
|
||||
uint8_t acc = 0;
|
||||
for (uint8_t b : psk) {
|
||||
acc |= b;
|
||||
}
|
||||
return acc == 0;
|
||||
}
|
||||
void set_psk(psk_t psk) {
|
||||
this->psk_ = psk;
|
||||
this->has_psk_ = !is_all_zeros(psk);
|
||||
}
|
||||
const psk_t &get_psk() const { return this->psk_; }
|
||||
bool has_psk() const { return this->has_psk_; }
|
||||
|
||||
protected:
|
||||
psk_t psk_{};
|
||||
bool has_psk_{false};
|
||||
};
|
||||
#endif // USE_API_NOISE
|
||||
|
||||
} // namespace esphome::api
|
||||
@@ -588,7 +588,7 @@ bool APIServer::load_and_apply_noise_psk_() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool APIServer::save_noise_psk(noise::psk_t psk, bool make_active) {
|
||||
bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
|
||||
#ifdef USE_API_NOISE_PSK_FROM_YAML
|
||||
// When PSK is set from YAML, this function should never be called
|
||||
// but if it is, reject the change
|
||||
|
||||
@@ -5,10 +5,7 @@
|
||||
#include "api_buffer.h"
|
||||
// Must precede clients_ so APIConnection is complete for default_delete (libc++).
|
||||
#include "api_connection.h"
|
||||
#ifdef USE_API_NOISE
|
||||
// Only present in the build when the noise component is loaded
|
||||
#include "esphome/components/noise/noise.h"
|
||||
#endif
|
||||
#include "api_noise_context.h"
|
||||
#include "api_pb2.h"
|
||||
#include "api_pb2_service.h"
|
||||
#include "esphome/components/socket/socket.h"
|
||||
@@ -40,7 +37,7 @@ class UserServiceDescriptor;
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
struct SavedNoisePsk {
|
||||
noise::psk_t psk;
|
||||
psk_t psk;
|
||||
} PACKED; // NOLINT
|
||||
#endif
|
||||
|
||||
@@ -76,10 +73,10 @@ class APIServer final : public Component,
|
||||
APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; }
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
bool save_noise_psk(noise::psk_t psk, bool make_active = true);
|
||||
bool save_noise_psk(psk_t psk, bool make_active = true);
|
||||
bool clear_noise_psk(bool make_active = true);
|
||||
void set_noise_psk(noise::psk_t psk) { this->noise_ctx_.set_psk(psk); }
|
||||
noise::NoiseContext &get_noise_ctx() { return this->noise_ctx_; }
|
||||
void set_noise_psk(psk_t psk) { this->noise_ctx_.set_psk(psk); }
|
||||
APINoiseContext &get_noise_ctx() { return this->noise_ctx_; }
|
||||
#endif // USE_API_NOISE
|
||||
|
||||
void handle_disconnect(APIConnection *conn);
|
||||
@@ -357,7 +354,7 @@ class APIServer final : public Component,
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
noise::NoiseContext noise_ctx_;
|
||||
APINoiseContext noise_ctx_;
|
||||
ESPPreferenceObject noise_pref_;
|
||||
#endif // USE_API_NOISE
|
||||
};
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import base64
|
||||
import binascii
|
||||
from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_KEY
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
noise_ns = cg.esphome_ns.namespace("noise")
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema({})
|
||||
|
||||
|
||||
def validate_encryption_key(value: Any) -> str:
|
||||
value = cv.string_strict(value)
|
||||
try:
|
||||
decoded = base64.b64decode(value, validate=True)
|
||||
except ValueError as err:
|
||||
raise cv.Invalid("Invalid key format, please check it's using base64") from err
|
||||
|
||||
if len(decoded) != 32:
|
||||
raise cv.Invalid("Encryption key must be base64 and 32 bytes long")
|
||||
|
||||
# Return original data for roundtrip conversion
|
||||
return value
|
||||
|
||||
|
||||
def decode_encryption_key(value: str) -> bytes:
|
||||
"""Decode a base64 encryption key to its 32 raw bytes.
|
||||
|
||||
a2b_base64 matches the decode the clients use (aioesphomeapi
|
||||
decode_noise_psk), so both ends derive the same bytes. The length is
|
||||
re-checked so a caller cannot turn an unvalidated short decode into a
|
||||
zero-padded PSK.
|
||||
"""
|
||||
try:
|
||||
decoded = binascii.a2b_base64(value)
|
||||
except ValueError as err:
|
||||
raise cv.Invalid("Invalid key format, please check it's using base64") from err
|
||||
if len(decoded) != 32:
|
||||
raise cv.Invalid("Encryption key must be base64 and 32 bytes long")
|
||||
return decoded
|
||||
|
||||
|
||||
ENCRYPTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_KEY): cv.sensitive(validate_encryption_key),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def encryption_schema(config: ConfigType | None) -> ConfigType:
|
||||
# A bare `encryption:` block is valid; a missing key means the consumer
|
||||
# falls back to its keyless behavior (api provisioning, ota inheriting
|
||||
# the api key).
|
||||
if config is None:
|
||||
config = {}
|
||||
return ENCRYPTION_SCHEMA(config)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_NOISE")
|
||||
cg.add_library("esphome/noise-c", "0.1.21")
|
||||
# noise-c depends on libsodium, but declaring it here too lets the
|
||||
# library manager see the full set up front instead of discovering
|
||||
# libsodium only after noise-c has downloaded, so the two can download
|
||||
# in parallel. The version must match noise-c's library.json.
|
||||
cg.add_library("esphome/libsodium", "1.10021.4")
|
||||
# Enable optimized memzero/memcmp in libsodium instead of volatile byte loops
|
||||
cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1")
|
||||
cg.add_build_flag("-DHAVE_INLINE_ASM=1")
|
||||
@@ -1,88 +0,0 @@
|
||||
#include "noise.h"
|
||||
#ifdef USE_NOISE
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#include <noise/protocol.h>
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
#include <pgmspace.h>
|
||||
#endif
|
||||
|
||||
namespace esphome::noise {
|
||||
|
||||
static const char *const TAG = "noise";
|
||||
|
||||
const LogString *noise_err_to_logstr(int err) {
|
||||
if (err == NOISE_ERROR_NO_MEMORY)
|
||||
return LOG_STR("NO_MEMORY");
|
||||
if (err == NOISE_ERROR_UNKNOWN_ID)
|
||||
return LOG_STR("UNKNOWN_ID");
|
||||
if (err == NOISE_ERROR_UNKNOWN_NAME)
|
||||
return LOG_STR("UNKNOWN_NAME");
|
||||
if (err == NOISE_ERROR_MAC_FAILURE)
|
||||
return LOG_STR("MAC_FAILURE");
|
||||
if (err == NOISE_ERROR_NOT_APPLICABLE)
|
||||
return LOG_STR("NOT_APPLICABLE");
|
||||
if (err == NOISE_ERROR_SYSTEM)
|
||||
return LOG_STR("SYSTEM");
|
||||
if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED)
|
||||
return LOG_STR("REMOTE_KEY_REQUIRED");
|
||||
if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED)
|
||||
return LOG_STR("LOCAL_KEY_REQUIRED");
|
||||
if (err == NOISE_ERROR_PSK_REQUIRED)
|
||||
return LOG_STR("PSK_REQUIRED");
|
||||
if (err == NOISE_ERROR_INVALID_LENGTH)
|
||||
return LOG_STR("INVALID_LENGTH");
|
||||
if (err == NOISE_ERROR_INVALID_PARAM)
|
||||
return LOG_STR("INVALID_PARAM");
|
||||
if (err == NOISE_ERROR_INVALID_STATE)
|
||||
return LOG_STR("INVALID_STATE");
|
||||
if (err == NOISE_ERROR_INVALID_NONCE)
|
||||
return LOG_STR("INVALID_NONCE");
|
||||
if (err == NOISE_ERROR_INVALID_PRIVATE_KEY)
|
||||
return LOG_STR("INVALID_PRIVATE_KEY");
|
||||
if (err == NOISE_ERROR_INVALID_PUBLIC_KEY)
|
||||
return LOG_STR("INVALID_PUBLIC_KEY");
|
||||
if (err == NOISE_ERROR_INVALID_FORMAT)
|
||||
return LOG_STR("INVALID_FORMAT");
|
||||
if (err == NOISE_ERROR_INVALID_SIGNATURE)
|
||||
return LOG_STR("INVALID_SIGNATURE");
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
|
||||
const LogString *reject_reason_for(int err) {
|
||||
return err == NOISE_ERROR_MAC_FAILURE ? LOG_STR("Handshake MAC failure") : LOG_STR("Handshake error");
|
||||
}
|
||||
|
||||
size_t format_reject_payload(uint8_t *buf, size_t capacity, const LogString *reason) {
|
||||
if (capacity == 0) {
|
||||
// A caller bug; the MAC_FAILURE_PAYLOAD_SIZE static_asserts at the call
|
||||
// sites make this unreachable, kept as cheap memory safety
|
||||
ESP_LOGVV(TAG, "Reject buffer has no capacity");
|
||||
return 0;
|
||||
}
|
||||
buf[0] = HANDSHAKE_STATUS_REJECT;
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions
|
||||
size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason));
|
||||
reason_len = std::min(reason_len, capacity - 1);
|
||||
if (reason_len > 0) {
|
||||
memcpy_P(buf + 1, reinterpret_cast<PGM_P>(reason), reason_len);
|
||||
}
|
||||
#else
|
||||
const char *reason_str = LOG_STR_ARG(reason);
|
||||
size_t reason_len = strlen(reason_str);
|
||||
reason_len = std::min(reason_len, capacity - 1);
|
||||
if (reason_len > 0) {
|
||||
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - binary protocol, not a C string
|
||||
std::memcpy(buf + 1, reason_str, reason_len);
|
||||
}
|
||||
#endif
|
||||
return reason_len + 1;
|
||||
}
|
||||
|
||||
} // namespace esphome::noise
|
||||
#endif // USE_NOISE
|
||||
@@ -1,74 +0,0 @@
|
||||
#pragma once
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_NOISE
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::noise {
|
||||
|
||||
using psk_t = std::array<uint8_t, 32>;
|
||||
|
||||
class NoiseContext {
|
||||
public:
|
||||
// The all-zeros PSK is reserved: it marks the device as unprovisioned and
|
||||
// doubles as the well-known provisioning PSK that unprovisioned devices
|
||||
// accept for Noise handshakes (passive-sniffing protection only, no
|
||||
// authentication). It is never a valid real key.
|
||||
static bool is_all_zeros(const psk_t &psk) {
|
||||
uint8_t acc = 0;
|
||||
for (uint8_t b : psk) {
|
||||
acc |= b;
|
||||
}
|
||||
return acc == 0;
|
||||
}
|
||||
void set_psk(psk_t psk) {
|
||||
this->psk_ = psk;
|
||||
this->has_psk_ = !is_all_zeros(psk);
|
||||
}
|
||||
const psk_t &get_psk() const { return this->psk_; }
|
||||
bool has_psk() const { return this->has_psk_; }
|
||||
|
||||
protected:
|
||||
psk_t psk_{};
|
||||
bool has_psk_{false};
|
||||
};
|
||||
|
||||
/// Convert a noise error code to a readable error
|
||||
const LogString *noise_err_to_logstr(int err);
|
||||
|
||||
// Shared wire format for the noise transports (api and ota): every frame is
|
||||
// FRAME_INDICATOR, a 16-bit big-endian payload length, then the payload.
|
||||
// Handshake payloads start with a status byte; transport payloads end with
|
||||
// the ChaCha20-Poly1305 MAC.
|
||||
static constexpr uint8_t FRAME_INDICATOR = 0x01;
|
||||
static constexpr size_t FRAME_HEADER_SIZE = 3;
|
||||
static constexpr size_t MAC_SIZE = 16;
|
||||
static constexpr size_t MAX_HANDSHAKE_SIZE = 128;
|
||||
static constexpr uint8_t HANDSHAKE_STATUS_OK = 0x00;
|
||||
static constexpr uint8_t HANDSHAKE_STATUS_REJECT = 0x01;
|
||||
|
||||
inline void write_frame_header(uint8_t *buf, uint16_t payload_len) {
|
||||
buf[0] = FRAME_INDICATOR;
|
||||
buf[1] = (uint8_t) (payload_len >> 8);
|
||||
buf[2] = (uint8_t) payload_len;
|
||||
}
|
||||
|
||||
/// Fill buf with a handshake reject payload (status byte plus the reason
|
||||
/// text, PROGMEM aware); returns the payload length. buf needs capacity for
|
||||
/// the status byte plus the truncated reason.
|
||||
size_t format_reject_payload(uint8_t *buf, size_t capacity, const LogString *reason);
|
||||
|
||||
/// Reject reason for a failed handshake read. The MAC failure string is a
|
||||
/// wire contract: clients match it to report a wrong key.
|
||||
const LogString *reject_reason_for(int err);
|
||||
|
||||
/// Payload size of the MAC failure reject, the one reason string that is a
|
||||
/// wire contract (sizeof's NUL stands in for the status byte). static_assert
|
||||
/// reject buffers against this so a wrong key report can never truncate;
|
||||
/// longer caller-supplied reasons are informational and sized by the caller.
|
||||
static constexpr size_t MAC_FAILURE_PAYLOAD_SIZE = sizeof("Handshake MAC failure");
|
||||
|
||||
} // namespace esphome::noise
|
||||
#endif // USE_NOISE
|
||||
@@ -1,139 +0,0 @@
|
||||
#include "noise_handshake.h"
|
||||
#ifdef USE_NOISE
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::noise {
|
||||
|
||||
static const char *const TAG = "noise";
|
||||
|
||||
// Log the failing noise-c call at the same verbosity the api helper used
|
||||
// before this class existed; callers only see one collapsed error code.
|
||||
#define HANDSHAKE_STEP_LOG(func_name, err_code) \
|
||||
ESP_LOGVV(TAG, "%s failed: %s", LOG_STR_ARG(LOG_STR(func_name)), LOG_STR_ARG(noise_err_to_logstr(err_code)))
|
||||
|
||||
NoiseResponderHandshake::~NoiseResponderHandshake() {
|
||||
if (this->handshake_ != nullptr) {
|
||||
noise_handshakestate_free(this->handshake_);
|
||||
this->handshake_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
int NoiseResponderHandshake::init(const psk_t &psk, const uint8_t *prologue, size_t prologue_len) {
|
||||
if (this->handshake_ != nullptr) {
|
||||
noise_handshakestate_free(this->handshake_);
|
||||
this->handshake_ = nullptr;
|
||||
}
|
||||
// Noise_NNpsk0_25519_ChaChaPoly_SHA256, built on the stack:
|
||||
// noise_handshakestate_new_by_id copies it, so a member would waste
|
||||
// 104 bytes per connection, and a static const would sit in RAM on
|
||||
// ESP8266 (.rodata is DRAM there).
|
||||
const NoiseProtocolId nid = {
|
||||
.prefix_id = NOISE_PREFIX_STANDARD,
|
||||
.pattern_id = NOISE_PATTERN_NN,
|
||||
.modifier_ids = {NOISE_MODIFIER_PSK0},
|
||||
.dh_id = NOISE_DH_CURVE25519,
|
||||
.cipher_id = NOISE_CIPHER_CHACHAPOLY,
|
||||
.hash_id = NOISE_HASH_SHA256,
|
||||
.hybrid_id = NOISE_DH_NONE,
|
||||
};
|
||||
|
||||
int err = noise_handshakestate_new_by_id(&this->handshake_, &nid, NOISE_ROLE_RESPONDER);
|
||||
if (err != 0) {
|
||||
HANDSHAKE_STEP_LOG("noise_handshakestate_new_by_id", err);
|
||||
return err;
|
||||
}
|
||||
err = noise_handshakestate_set_pre_shared_key(this->handshake_, psk.data(), psk.size());
|
||||
if (err != 0) {
|
||||
HANDSHAKE_STEP_LOG("noise_handshakestate_set_pre_shared_key", err);
|
||||
return this->fail_init_(err);
|
||||
}
|
||||
err = noise_handshakestate_set_prologue(this->handshake_, prologue, prologue_len);
|
||||
if (err != 0) {
|
||||
HANDSHAKE_STEP_LOG("noise_handshakestate_set_prologue", err);
|
||||
return this->fail_init_(err);
|
||||
}
|
||||
err = noise_handshakestate_start(this->handshake_);
|
||||
if (err != 0) {
|
||||
HANDSHAKE_STEP_LOG("noise_handshakestate_start", err);
|
||||
return this->fail_init_(err);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Release a half-initialized state so a failed init() leaves the object as
|
||||
/// if init() was never called.
|
||||
int NoiseResponderHandshake::fail_init_(int err) {
|
||||
noise_handshakestate_free(this->handshake_);
|
||||
this->handshake_ = nullptr;
|
||||
return err;
|
||||
}
|
||||
|
||||
NoiseResponderHandshake::Action NoiseResponderHandshake::action() const {
|
||||
if (this->handshake_ == nullptr) {
|
||||
// A caller bug: init() was never called, or split() already released the state
|
||||
ESP_LOGVV(TAG, "action() on uninitialized or split handshake");
|
||||
return Action::ACTION_FAILED;
|
||||
}
|
||||
int raw = noise_handshakestate_get_action(this->handshake_);
|
||||
switch (raw) {
|
||||
case NOISE_ACTION_READ_MESSAGE:
|
||||
return Action::ACTION_READ;
|
||||
case NOISE_ACTION_WRITE_MESSAGE:
|
||||
return Action::ACTION_WRITE;
|
||||
case NOISE_ACTION_SPLIT:
|
||||
return Action::ACTION_SPLIT;
|
||||
default:
|
||||
// Preserve the raw code in debug logs; callers only see the collapsed enum
|
||||
ESP_LOGVV(TAG, "Unexpected noise action %d", raw);
|
||||
return Action::ACTION_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int NoiseResponderHandshake::read_message(uint8_t *data, size_t len) {
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_input(mbuf, data, len);
|
||||
return noise_handshakestate_read_message(this->handshake_, &mbuf, nullptr);
|
||||
}
|
||||
|
||||
int NoiseResponderHandshake::write_message(uint8_t *out, size_t capacity, size_t &out_len) {
|
||||
out_len = 0;
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_output(mbuf, out, capacity);
|
||||
int err = noise_handshakestate_write_message(this->handshake_, &mbuf, nullptr);
|
||||
if (err == 0)
|
||||
out_len = mbuf.size;
|
||||
return err;
|
||||
}
|
||||
|
||||
int NoiseResponderHandshake::split(NoiseCipherState *&send_cipher, NoiseCipherState *&recv_cipher) {
|
||||
// Defined error postcondition: noise-c leaves the out-params unwritten on
|
||||
// its early error returns, so a caller passing uninitialized locals must
|
||||
// never see garbage to free
|
||||
send_cipher = nullptr;
|
||||
recv_cipher = nullptr;
|
||||
int err = noise_handshakestate_split(this->handshake_, &send_cipher, &recv_cipher);
|
||||
if (err != 0)
|
||||
return err;
|
||||
noise_handshakestate_free(this->handshake_);
|
||||
this->handshake_ = nullptr;
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
// noise-c's only randomness source (the vendored library compiles no rand of
|
||||
// its own); HWRNG backed. Lives in this TU so every handshake consumer links
|
||||
// it and the definition can never be dropped from the archive.
|
||||
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::noise
|
||||
#endif // USE_NOISE
|
||||
@@ -1,63 +0,0 @@
|
||||
#pragma once
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_NOISE
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include <noise/protocol.h>
|
||||
|
||||
#include "noise.h"
|
||||
|
||||
namespace esphome::noise {
|
||||
|
||||
/** Sans-IO responder side of a Noise_NNpsk0_25519_ChaChaPoly_SHA256 handshake.
|
||||
*
|
||||
* Owns only the noise-c handshake state; the caller moves the raw handshake
|
||||
* messages (no framing) over its own transport, driven by action():
|
||||
* read_message() while READ, write_message() while WRITE, then split() to
|
||||
* take ownership of the transport ciphers. All methods return a noise-c
|
||||
* error code, 0 on success. Called outside their action() step (before
|
||||
* init(), after split()) the message methods return a noise-c error rather
|
||||
* than crashing; the library checks its state argument.
|
||||
*
|
||||
* Methods are deliberately small separate functions so callers on tight
|
||||
* stacks (RP2040 core0 scratch bank) never pay for more than one branch;
|
||||
* the curve25519 step alone needs ~2KB of stack.
|
||||
*/
|
||||
class NoiseResponderHandshake {
|
||||
public:
|
||||
// The ACTION_ prefix is macro-collision safety: SDK headers #define bare
|
||||
// names like READ/WRITE, and macros expand even inside an enum class.
|
||||
enum class Action : uint8_t { ACTION_READ, ACTION_WRITE, ACTION_SPLIT, ACTION_FAILED };
|
||||
|
||||
NoiseResponderHandshake() = default;
|
||||
~NoiseResponderHandshake();
|
||||
// Owns a raw noise-c handshake state; copying would double free it
|
||||
NoiseResponderHandshake(const NoiseResponderHandshake &) = delete;
|
||||
NoiseResponderHandshake &operator=(const NoiseResponderHandshake &) = delete;
|
||||
|
||||
/// Create and start the handshake with the given PSK and prologue. A
|
||||
/// repeated call frees the previous handshake state and starts over.
|
||||
[[nodiscard]] int init(const psk_t &psk, const uint8_t *prologue, size_t prologue_len);
|
||||
/// ACTION_FAILED is the catch-all: returned before init(), after split()
|
||||
/// has released the state, and when noise-c reports a failed handshake.
|
||||
[[nodiscard]] Action action() const;
|
||||
/// Process one received handshake message. The buffer is consumed in
|
||||
/// place: noise-c decrypts into it and zeroes it before returning.
|
||||
[[nodiscard]] int read_message(uint8_t *data, size_t len);
|
||||
/// Produce the next handshake message into out; out_len receives its size
|
||||
/// and is zero on error.
|
||||
[[nodiscard]] int write_message(uint8_t *out, size_t capacity, size_t &out_len);
|
||||
/// Hand out the transport ciphers and free the handshake state. The caller
|
||||
/// owns both cipher states and must free them with noise_cipherstate_free();
|
||||
/// both are set to nullptr on error.
|
||||
[[nodiscard]] int split(NoiseCipherState *&send_cipher, NoiseCipherState *&recv_cipher);
|
||||
|
||||
protected:
|
||||
int fail_init_(int err);
|
||||
|
||||
NoiseHandshakeState *handshake_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace esphome::noise
|
||||
#endif // USE_NOISE
|
||||
@@ -222,7 +222,6 @@
|
||||
#define API_MAX_SEND_QUEUE 8
|
||||
#define MAX_API_CONNECTIONS 6
|
||||
#define USE_MD5
|
||||
#define USE_NOISE
|
||||
#define USE_SHA256
|
||||
#ifndef USE_RP2 // no MQTT backend or esp_wireguard library on RP2
|
||||
#define USE_MQTT
|
||||
|
||||
+3
-3
@@ -45,7 +45,7 @@ lib_deps_base =
|
||||
lib_deps =
|
||||
${common.lib_deps_base}
|
||||
https://github.com/dudanov/MideaUART.git#eeea6c3e9b4474f067054592b435be1c4e466815 ; midea
|
||||
esphome/noise-c@0.1.21 ; noise (api, ota)
|
||||
esphome/noise-c@0.1.21 ; api
|
||||
improv/Improv@1.2.6 ; improv_serial / esp32_improv
|
||||
kikuchan98/pngle@1.1.0 ; online_image
|
||||
; Using the repository directly, otherwise ESP-IDF can't use the library
|
||||
@@ -244,7 +244,7 @@ lib_deps =
|
||||
${common:idf-component-libs.lib_deps}
|
||||
ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base
|
||||
droscy/esp_wireguard@0.4.5 ; wireguard
|
||||
esphome/noise-c@0.1.21 ; noise (api, ota)
|
||||
esphome/noise-c@0.1.21 ; api
|
||||
ESP32Async/AsyncTCP@3.4.5 ; async_tcp
|
||||
DNSServer ; captive_portal
|
||||
heman/AsyncMqttClient-esphome@2.0.0 ; mqtt
|
||||
@@ -641,7 +641,7 @@ build_unflags =
|
||||
extends = common
|
||||
platform = platformio/native
|
||||
lib_deps =
|
||||
esphome/noise-c@0.1.21 ; used by noise (api, ota)
|
||||
esphome/noise-c@0.1.21 ; used by api
|
||||
lvgl/lvgl@9.5.0 ; lvgl
|
||||
build_flags =
|
||||
${common.build_flags}
|
||||
|
||||
@@ -3,9 +3,8 @@ from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# api must run its to_code to define USE_API and USE_API_NOISE. The
|
||||
# AUTO_LOADed noise component runs its own to_code via the override in
|
||||
# tests/benchmarks/components/noise/__init__.py.
|
||||
# api must run its to_code to define USE_API, USE_API_PLAINTEXT,
|
||||
# and add the noise-c library dependency.
|
||||
manifest.enable_codegen()
|
||||
|
||||
original_to_code = manifest.to_code
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# to_code must run: it defines USE_NOISE and adds the noise-c library
|
||||
# the api benchmark sources need.
|
||||
manifest.enable_codegen()
|
||||
@@ -1,37 +0,0 @@
|
||||
"""Tests for the shared noise encryption key helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.noise import decode_encryption_key, validate_encryption_key
|
||||
|
||||
KEY = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="
|
||||
|
||||
|
||||
def test_validate_encryption_key_roundtrips() -> None:
|
||||
assert validate_encryption_key(KEY) == KEY
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["not-base64!!!", "AAECAw=="])
|
||||
def test_validate_encryption_key_rejects_bad_input(value: str) -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
validate_encryption_key(value)
|
||||
|
||||
|
||||
def test_decode_encryption_key_returns_32_bytes() -> None:
|
||||
assert decode_encryption_key(KEY) == bytes(range(32))
|
||||
|
||||
|
||||
def test_decode_encryption_key_rejects_invalid_base64() -> None:
|
||||
"""The shared helper raises cv.Invalid, not binascii.Error."""
|
||||
with pytest.raises(cv.Invalid, match="base64"):
|
||||
decode_encryption_key("A")
|
||||
|
||||
|
||||
def test_decode_encryption_key_rejects_short_decode() -> None:
|
||||
"""a2b_base64 stops at embedded padding; a short decode must not become
|
||||
a zero padded PSK on the device."""
|
||||
with pytest.raises(cv.Invalid, match="32 bytes"):
|
||||
decode_encryption_key("AAECAw==")
|
||||
@@ -1,7 +0,0 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# to_code must run: it defines USE_NOISE and adds the noise-c library
|
||||
# the component sources under test need.
|
||||
manifest.enable_codegen()
|
||||
@@ -1 +0,0 @@
|
||||
noise:
|
||||
@@ -1,2 +0,0 @@
|
||||
packages:
|
||||
noise: !include common.yaml
|
||||
@@ -1,2 +0,0 @@
|
||||
packages:
|
||||
noise: !include common.yaml
|
||||
@@ -1,2 +0,0 @@
|
||||
packages:
|
||||
noise: !include common.yaml
|
||||
@@ -1,2 +0,0 @@
|
||||
packages:
|
||||
noise: !include common.yaml
|
||||
@@ -1,199 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include <noise/protocol.h>
|
||||
|
||||
#include "esphome/components/noise/noise.h"
|
||||
#include "esphome/components/noise/noise_handshake.h"
|
||||
|
||||
namespace esphome::noise::testing {
|
||||
|
||||
using Action = NoiseResponderHandshake::Action;
|
||||
|
||||
// A raw noise-c initiator driving the same Noise_NNpsk0_25519_ChaChaPoly_SHA256
|
||||
// pattern the responder class implements, so the tests exercise a real
|
||||
// two-message handshake rather than mirrored calls into the class under test.
|
||||
class Initiator {
|
||||
public:
|
||||
Initiator(const psk_t &psk, const uint8_t *prologue, size_t prologue_len) {
|
||||
const NoiseProtocolId nid = {
|
||||
.prefix_id = NOISE_PREFIX_STANDARD,
|
||||
.pattern_id = NOISE_PATTERN_NN,
|
||||
.modifier_ids = {NOISE_MODIFIER_PSK0},
|
||||
.dh_id = NOISE_DH_CURVE25519,
|
||||
.cipher_id = NOISE_CIPHER_CHACHAPOLY,
|
||||
.hash_id = NOISE_HASH_SHA256,
|
||||
.hybrid_id = NOISE_DH_NONE,
|
||||
};
|
||||
EXPECT_EQ(noise_handshakestate_new_by_id(&this->state_, &nid, NOISE_ROLE_INITIATOR), 0);
|
||||
EXPECT_EQ(noise_handshakestate_set_pre_shared_key(this->state_, psk.data(), psk.size()), 0);
|
||||
EXPECT_EQ(noise_handshakestate_set_prologue(this->state_, prologue, prologue_len), 0);
|
||||
EXPECT_EQ(noise_handshakestate_start(this->state_), 0);
|
||||
}
|
||||
~Initiator() {
|
||||
if (this->state_ != nullptr)
|
||||
noise_handshakestate_free(this->state_);
|
||||
if (this->send_ != nullptr)
|
||||
noise_cipherstate_free(this->send_);
|
||||
if (this->recv_ != nullptr)
|
||||
noise_cipherstate_free(this->recv_);
|
||||
}
|
||||
Initiator(const Initiator &) = delete;
|
||||
Initiator &operator=(const Initiator &) = delete;
|
||||
|
||||
size_t write_message(uint8_t *out, size_t capacity) {
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_output(mbuf, out, capacity);
|
||||
EXPECT_EQ(noise_handshakestate_write_message(this->state_, &mbuf, nullptr), 0);
|
||||
return mbuf.size;
|
||||
}
|
||||
|
||||
int read_message(uint8_t *data, size_t len) {
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_input(mbuf, data, len);
|
||||
return noise_handshakestate_read_message(this->state_, &mbuf, nullptr);
|
||||
}
|
||||
|
||||
void split() { EXPECT_EQ(noise_handshakestate_split(this->state_, &this->send_, &this->recv_), 0); }
|
||||
|
||||
NoiseCipherState *send_{nullptr};
|
||||
NoiseCipherState *recv_{nullptr};
|
||||
|
||||
private:
|
||||
NoiseHandshakeState *state_{nullptr};
|
||||
};
|
||||
|
||||
static const uint8_t PROLOGUE[] = {'t', 'e', 's', 't', 'p', 'r', 'o', 'l', 'o', 'g', 'u', 'e'};
|
||||
|
||||
static psk_t make_psk(uint8_t seed) {
|
||||
psk_t psk;
|
||||
for (size_t i = 0; i < psk.size(); i++) {
|
||||
psk[i] = static_cast<uint8_t>(seed + i);
|
||||
}
|
||||
return psk;
|
||||
}
|
||||
|
||||
TEST(NoiseResponderHandshakeTest, ActionFailedBeforeInit) {
|
||||
NoiseResponderHandshake handshake;
|
||||
EXPECT_EQ(handshake.action(), Action::ACTION_FAILED);
|
||||
}
|
||||
|
||||
TEST(NoiseResponderHandshakeTest, MessageMethodsErrorBeforeInit) {
|
||||
// The class doc promises a noise-c error, not a crash, when the message
|
||||
// methods run outside their action() step; pin the library's null check
|
||||
NoiseResponderHandshake handshake;
|
||||
uint8_t buf[MAX_HANDSHAKE_SIZE] = {};
|
||||
size_t out_len = 0;
|
||||
EXPECT_NE(handshake.read_message(buf, sizeof(buf)), 0);
|
||||
EXPECT_NE(handshake.write_message(buf, sizeof(buf), out_len), 0);
|
||||
// Deliberately non-null: split() documents a nullptr postcondition on
|
||||
// error, so a caller's uninitialized locals never hold garbage to free
|
||||
auto *sentinel = reinterpret_cast<NoiseCipherState *>(0x1);
|
||||
NoiseCipherState *send_cipher = sentinel;
|
||||
NoiseCipherState *recv_cipher = sentinel;
|
||||
EXPECT_NE(handshake.split(send_cipher, recv_cipher), 0);
|
||||
EXPECT_EQ(send_cipher, nullptr);
|
||||
EXPECT_EQ(recv_cipher, nullptr);
|
||||
}
|
||||
|
||||
TEST(NoiseResponderHandshakeTest, FullHandshakeAndTransportRoundTrip) {
|
||||
const psk_t psk = make_psk(7);
|
||||
NoiseResponderHandshake responder;
|
||||
ASSERT_EQ(responder.init(psk, PROLOGUE, sizeof(PROLOGUE)), 0);
|
||||
EXPECT_EQ(responder.action(), Action::ACTION_READ);
|
||||
|
||||
Initiator initiator(psk, PROLOGUE, sizeof(PROLOGUE));
|
||||
uint8_t msg[MAX_HANDSHAKE_SIZE];
|
||||
size_t msg_len = initiator.write_message(msg, sizeof(msg));
|
||||
ASSERT_GT(msg_len, 0u);
|
||||
|
||||
ASSERT_EQ(responder.read_message(msg, msg_len), 0);
|
||||
ASSERT_EQ(responder.action(), Action::ACTION_WRITE);
|
||||
|
||||
size_t reply_len = 0;
|
||||
ASSERT_EQ(responder.write_message(msg, sizeof(msg), reply_len), 0);
|
||||
ASSERT_GT(reply_len, 0u);
|
||||
ASSERT_EQ(responder.action(), Action::ACTION_SPLIT);
|
||||
|
||||
ASSERT_EQ(initiator.read_message(msg, reply_len), 0);
|
||||
initiator.split();
|
||||
|
||||
NoiseCipherState *send_cipher = nullptr;
|
||||
NoiseCipherState *recv_cipher = nullptr;
|
||||
ASSERT_EQ(responder.split(send_cipher, recv_cipher), 0);
|
||||
ASSERT_NE(send_cipher, nullptr);
|
||||
ASSERT_NE(recv_cipher, nullptr);
|
||||
// The handshake state is released by split(); the class reports FAILED after
|
||||
EXPECT_EQ(responder.action(), Action::ACTION_FAILED);
|
||||
EXPECT_EQ(static_cast<size_t>(noise_cipherstate_get_mac_length(send_cipher)), MAC_SIZE);
|
||||
|
||||
// Responder encrypts, initiator decrypts
|
||||
uint8_t frame[64];
|
||||
static constexpr char PLAINTEXT[] = "encrypted ota";
|
||||
std::memcpy(frame, PLAINTEXT, sizeof(PLAINTEXT));
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_inout(mbuf, frame, sizeof(PLAINTEXT), sizeof(frame));
|
||||
ASSERT_EQ(noise_cipherstate_encrypt(send_cipher, &mbuf), 0);
|
||||
EXPECT_EQ(mbuf.size, sizeof(PLAINTEXT) + MAC_SIZE);
|
||||
|
||||
noise_buffer_set_inout(mbuf, frame, mbuf.size, sizeof(frame));
|
||||
ASSERT_EQ(noise_cipherstate_decrypt(initiator.recv_, &mbuf), 0);
|
||||
ASSERT_EQ(mbuf.size, sizeof(PLAINTEXT));
|
||||
EXPECT_EQ(std::memcmp(frame, PLAINTEXT, sizeof(PLAINTEXT)), 0);
|
||||
|
||||
noise_cipherstate_free(send_cipher);
|
||||
noise_cipherstate_free(recv_cipher);
|
||||
}
|
||||
|
||||
TEST(NoiseResponderHandshakeTest, ReInitRestartsHandshake) {
|
||||
// The documented retry shape: a repeated init() frees the previous state
|
||||
// and starts over. The first message under the new key authenticating
|
||||
// proves the restart took effect; the old state surviving would fail the
|
||||
// MAC here.
|
||||
NoiseResponderHandshake responder;
|
||||
ASSERT_EQ(responder.init(make_psk(7), PROLOGUE, sizeof(PROLOGUE)), 0);
|
||||
ASSERT_EQ(responder.init(make_psk(9), PROLOGUE, sizeof(PROLOGUE)), 0);
|
||||
EXPECT_EQ(responder.action(), Action::ACTION_READ);
|
||||
|
||||
Initiator initiator(make_psk(9), PROLOGUE, sizeof(PROLOGUE));
|
||||
uint8_t msg[MAX_HANDSHAKE_SIZE];
|
||||
size_t msg_len = initiator.write_message(msg, sizeof(msg));
|
||||
ASSERT_GT(msg_len, 0u);
|
||||
EXPECT_EQ(responder.read_message(msg, msg_len), 0);
|
||||
}
|
||||
|
||||
TEST(NoiseResponderHandshakeTest, WrongPskFailsWithMacFailure) {
|
||||
NoiseResponderHandshake responder;
|
||||
ASSERT_EQ(responder.init(make_psk(7), PROLOGUE, sizeof(PROLOGUE)), 0);
|
||||
|
||||
Initiator initiator(make_psk(200), PROLOGUE, sizeof(PROLOGUE));
|
||||
uint8_t msg[MAX_HANDSHAKE_SIZE];
|
||||
size_t msg_len = initiator.write_message(msg, sizeof(msg));
|
||||
ASSERT_GT(msg_len, 0u);
|
||||
|
||||
int err = responder.read_message(msg, msg_len);
|
||||
EXPECT_EQ(err, NOISE_ERROR_MAC_FAILURE);
|
||||
EXPECT_EQ(responder.action(), Action::ACTION_FAILED);
|
||||
}
|
||||
|
||||
TEST(NoiseResponderHandshakeTest, MismatchedPrologueFailsWithMacFailure) {
|
||||
// The prologue binds the plaintext preamble for downgrade resistance; a
|
||||
// tampered preamble must fail even with the right key.
|
||||
const psk_t psk = make_psk(7);
|
||||
NoiseResponderHandshake responder;
|
||||
ASSERT_EQ(responder.init(psk, PROLOGUE, sizeof(PROLOGUE)), 0);
|
||||
|
||||
static const uint8_t TAMPERED[] = {'x'};
|
||||
Initiator initiator(psk, TAMPERED, sizeof(TAMPERED));
|
||||
uint8_t msg[MAX_HANDSHAKE_SIZE];
|
||||
size_t msg_len = initiator.write_message(msg, sizeof(msg));
|
||||
ASSERT_GT(msg_len, 0u);
|
||||
|
||||
EXPECT_EQ(responder.read_message(msg, msg_len), NOISE_ERROR_MAC_FAILURE);
|
||||
}
|
||||
|
||||
} // namespace esphome::noise::testing
|
||||
@@ -1,74 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include <noise/protocol.h>
|
||||
|
||||
#include "esphome/components/noise/noise.h"
|
||||
|
||||
namespace esphome::noise::testing {
|
||||
|
||||
TEST(NoiseContextTest, AllZerosPskIsReserved) {
|
||||
psk_t zeros{};
|
||||
EXPECT_TRUE(NoiseContext::is_all_zeros(zeros));
|
||||
|
||||
psk_t psk{};
|
||||
psk[31] = 1;
|
||||
EXPECT_FALSE(NoiseContext::is_all_zeros(psk));
|
||||
|
||||
NoiseContext ctx;
|
||||
EXPECT_FALSE(ctx.has_psk());
|
||||
ctx.set_psk(zeros);
|
||||
EXPECT_FALSE(ctx.has_psk());
|
||||
ctx.set_psk(psk);
|
||||
EXPECT_TRUE(ctx.has_psk());
|
||||
EXPECT_EQ(ctx.get_psk(), psk);
|
||||
}
|
||||
|
||||
TEST(WireFormatTest, FrameHeaderIsIndicatorPlusBigEndianLength) {
|
||||
uint8_t header[FRAME_HEADER_SIZE];
|
||||
write_frame_header(header, 0x1234);
|
||||
EXPECT_EQ(header[0], FRAME_INDICATOR);
|
||||
EXPECT_EQ(header[1], 0x12);
|
||||
EXPECT_EQ(header[2], 0x34);
|
||||
}
|
||||
|
||||
TEST(WireFormatTest, RejectPayloadCarriesStatusByteAndMacFailureContract) {
|
||||
// The MAC failure string is a wire contract: clients match it to report a
|
||||
// wrong key. Format the payload exactly the way the handshake read path does.
|
||||
uint8_t buf[64];
|
||||
size_t len = format_reject_payload(buf, sizeof(buf), reject_reason_for(NOISE_ERROR_MAC_FAILURE));
|
||||
static constexpr char EXPECTED[] = "Handshake MAC failure";
|
||||
ASSERT_EQ(len, 1 + strlen(EXPECTED));
|
||||
EXPECT_EQ(buf[0], HANDSHAKE_STATUS_REJECT);
|
||||
EXPECT_EQ(memcmp(buf + 1, EXPECTED, strlen(EXPECTED)), 0);
|
||||
// The exported floor covers the full MAC failure payload exactly
|
||||
EXPECT_EQ(MAC_FAILURE_PAYLOAD_SIZE, 1 + strlen(EXPECTED));
|
||||
|
||||
// Any other error maps to the generic reason
|
||||
len = format_reject_payload(buf, sizeof(buf), reject_reason_for(NOISE_ERROR_INVALID_STATE));
|
||||
static constexpr char GENERIC[] = "Handshake error";
|
||||
ASSERT_EQ(len, 1 + strlen(GENERIC));
|
||||
EXPECT_EQ(memcmp(buf + 1, GENERIC, strlen(GENERIC)), 0);
|
||||
}
|
||||
|
||||
TEST(WireFormatTest, RejectPayloadTruncatesToCapacity) {
|
||||
uint8_t buf[8];
|
||||
size_t len = format_reject_payload(buf, sizeof(buf), reject_reason_for(NOISE_ERROR_MAC_FAILURE));
|
||||
ASSERT_EQ(len, sizeof(buf));
|
||||
EXPECT_EQ(buf[0], HANDSHAKE_STATUS_REJECT);
|
||||
EXPECT_EQ(memcmp(buf + 1, "Handsha", 7), 0);
|
||||
|
||||
// A one-byte buffer still carries the status byte
|
||||
uint8_t tiny[1];
|
||||
len = format_reject_payload(tiny, sizeof(tiny), reject_reason_for(NOISE_ERROR_MAC_FAILURE));
|
||||
ASSERT_EQ(len, 1u);
|
||||
EXPECT_EQ(tiny[0], HANDSHAKE_STATUS_REJECT);
|
||||
|
||||
// A zero-capacity buffer yields no payload and stays untouched
|
||||
uint8_t none[1] = {0xAA};
|
||||
EXPECT_EQ(format_reject_payload(none, 0, reject_reason_for(NOISE_ERROR_MAC_FAILURE)), 0u);
|
||||
EXPECT_EQ(none[0], 0xAA);
|
||||
}
|
||||
|
||||
} // namespace esphome::noise::testing
|
||||
Reference in New Issue
Block a user