1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-09 09:11:52 +00:00

Compare commits

..

2 Commits

Author SHA1 Message Date
J. Nick Koston
caff93d7b8 Add test coverage for secrets.choice in fallback PSK generation
Verifies that wizard_file uses secrets.choice (not random.choice)
to generate the 12-character fallback hotspot password.
2026-02-08 07:08:25 -06:00
J. Nick Koston
e039676422 [wizard] Use secrets module for fallback AP password generation
Replace random.choice() with secrets.choice() for generating
the fallback hotspot password. The random module uses Mersenne
Twister which is not cryptographically secure. The secrets
module is the correct choice for credential generation.

The file already imports secrets for other credential generation.
2026-02-08 06:44:45 -06:00
14 changed files with 34 additions and 69 deletions

View File

@@ -87,7 +87,6 @@ from esphome.cpp_types import ( # noqa: F401
size_t,
std_ns,
std_shared_ptr,
std_span,
std_string,
std_string_ref,
std_vector,

View File

@@ -68,7 +68,7 @@ void HOT Logger::write_msg_(const char *msg, uint16_t len) {
#ifdef CONFIG_PRINTK
// Requires the debug component and an active SWD connection.
// It is used for pyocd rtt -t nrf52840
printk("%.*s", static_cast<int>(len), msg);
k_str_out(const_cast<char *>(msg), len);
#endif
if (this->uart_dev_ == nullptr) {
return;

View File

@@ -436,7 +436,6 @@ def container_schema(widget_type: WidgetType, extras=None):
schema = schema.extend(widget_type.schema)
def validator(value):
value = value or {}
return append_layout_schema(schema, value)(value)
return validator

View File

@@ -396,9 +396,9 @@ static bool process_rolling_code(Provider &provider, PacketDecoder &decoder) {
/**
* Process a received packet
*/
void PacketTransport::process_(std::span<const uint8_t> data) {
void PacketTransport::process_(const std::vector<uint8_t> &data) {
auto ping_key_seen = !this->ping_pong_enable_;
PacketDecoder decoder(data.data(), data.size());
PacketDecoder decoder((data.data()), data.size());
char namebuf[256]{};
uint8_t byte;
FuData rdata{};

View File

@@ -9,9 +9,8 @@
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#include <map>
#include <span>
#include <vector>
#include <map>
/**
* Providing packet encoding functions for exchanging data with a remote host.
@@ -114,7 +113,7 @@ class PacketTransport : public PollingComponent {
virtual bool should_send() { return true; }
// to be called by child classes when a data packet is received.
void process_(std::span<const uint8_t> data);
void process_(const std::vector<uint8_t> &data);
void send_data_(bool all);
void flush_();
void add_data_(uint8_t key, const char *id, float data);

View File

@@ -13,7 +13,7 @@ from esphome.components.packet_transport import (
import esphome.config_validation as cv
from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID
from esphome.core import ID
from esphome.cpp_generator import MockObj
from esphome.cpp_generator import literal
CODEOWNERS = ["@clydebarrow"]
DEPENDENCIES = ["network"]
@@ -23,12 +23,8 @@ MULTI_CONF = True
udp_ns = cg.esphome_ns.namespace("udp")
UDPComponent = udp_ns.class_("UDPComponent", cg.Component)
UDPWriteAction = udp_ns.class_("UDPWriteAction", automation.Action)
trigger_argname = "data"
# Listener callback type (non-owning span from UDP component)
listener_args = cg.std_span.template(cg.uint8.operator("const"))
listener_argtype = [(listener_args, trigger_argname)]
# Automation/trigger type (owned vector, safe for deferred actions like delay)
trigger_args = cg.std_vector.template(cg.uint8)
trigger_argname = "data"
trigger_argtype = [(trigger_args, trigger_argname)]
CONF_ADDRESSES = "addresses"
@@ -122,13 +118,7 @@ async def to_code(config):
trigger_id, trigger_argtype, on_receive
)
trigger_lambda = await cg.process_lambda(
trigger.trigger(
cg.std_vector.template(cg.uint8)(
MockObj(trigger_argname).begin(),
MockObj(trigger_argname).end(),
)
),
listener_argtype,
trigger.trigger(literal(trigger_argname)), trigger_argtype
)
cg.add(var.add_listener(trigger_lambda))
cg.add(var.set_should_listen())

View File

@@ -12,7 +12,7 @@ bool UDPTransport::should_send() { return network::is_connected(); }
void UDPTransport::setup() {
PacketTransport::setup();
if (!this->providers_.empty() || this->is_encrypted_()) {
this->parent_->add_listener([this](std::span<const uint8_t> data) { this->process_(data); });
this->parent_->add_listener([this](std::vector<uint8_t> &buf) { this->process_(buf); });
}
}

View File

@@ -103,8 +103,8 @@ void UDPComponent::setup() {
}
void UDPComponent::loop() {
auto buf = std::vector<uint8_t>(MAX_PACKET_SIZE);
if (this->should_listen_) {
std::array<uint8_t, MAX_PACKET_SIZE> buf;
for (;;) {
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
auto len = this->listen_socket_->read(buf.data(), buf.size());
@@ -116,9 +116,9 @@ void UDPComponent::loop() {
#endif
if (len <= 0)
break;
size_t packet_len = static_cast<size_t>(len);
ESP_LOGV(TAG, "Received packet of length %zu", packet_len);
this->packet_listeners_.call(std::span<const uint8_t>(buf.data(), packet_len));
buf.resize(len);
ESP_LOGV(TAG, "Received packet of length %zu", len);
this->packet_listeners_.call(buf);
}
}
}

View File

@@ -10,9 +10,7 @@
#ifdef USE_SOCKET_IMPL_LWIP_TCP
#include <WiFiUdp.h>
#endif
#include <array>
#include <initializer_list>
#include <span>
#include <vector>
namespace esphome::udp {
@@ -28,7 +26,7 @@ class UDPComponent : public Component {
void set_broadcast_port(uint16_t port) { this->broadcast_port_ = port; }
void set_should_broadcast() { this->should_broadcast_ = true; }
void set_should_listen() { this->should_listen_ = true; }
void add_listener(std::function<void(std::span<const uint8_t>)> &&listener) {
void add_listener(std::function<void(std::vector<uint8_t> &)> &&listener) {
this->packet_listeners_.add(std::move(listener));
}
void setup() override;
@@ -43,7 +41,7 @@ class UDPComponent : public Component {
uint16_t broadcast_port_{};
bool should_broadcast_{};
bool should_listen_{};
CallbackManager<void(std::span<const uint8_t>)> packet_listeners_{};
CallbackManager<void(std::vector<uint8_t> &)> packet_listeners_{};
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
std::unique_ptr<socket::Socket> broadcast_socket_ = nullptr;

View File

@@ -12,7 +12,6 @@ std_shared_ptr = std_ns.class_("shared_ptr")
std_string = std_ns.class_("string")
std_string_ref = std_ns.namespace("string &")
std_vector = std_ns.class_("vector")
std_span = std_ns.class_("span")
uint8 = global_ns.namespace("uint8_t")
uint16 = global_ns.namespace("uint16_t")
uint32 = global_ns.namespace("uint32_t")

View File

@@ -1,6 +1,5 @@
import base64
from pathlib import Path
import random
import secrets
import string
from typing import Literal, NotRequired, TypedDict, Unpack
@@ -130,7 +129,7 @@ def wizard_file(**kwargs: Unpack[WizardFileKwargs]) -> str:
if len(ap_name) > 32:
ap_name = ap_name_base
kwargs["fallback_name"] = ap_name
kwargs["fallback_psk"] = "".join(random.choice(letters) for _ in range(12))
kwargs["fallback_psk"] = "".join(secrets.choice(letters) for _ in range(12))
base = BASE_CONFIG_FRIENDLY if kwargs.get("friendly_name") else BASE_CONFIG

View File

@@ -20,8 +20,6 @@ lvgl:
- id: lvgl_0
default_font: space16
displays: sdl0
top_layer:
- id: lvgl_1
displays: sdl1
on_idle:

View File

@@ -93,34 +93,23 @@ async def udp_listener(port: int = 0) -> AsyncGenerator[tuple[int, UDPReceiver]]
sock.close()
def _get_free_udp_port() -> int:
"""Get a free UDP port by binding to port 0 and releasing."""
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("127.0.0.1", 0))
port = sock.getsockname()[1]
sock.close()
return port
@pytest.mark.asyncio
async def test_udp_send_receive(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test UDP component can send and receive messages."""
"""Test UDP component can send messages with multiple addresses configured."""
# Track log lines to verify dump_config output
log_lines: list[str] = []
receive_event = asyncio.Event()
def on_log_line(line: str) -> None:
log_lines.append(line)
if "Received UDP:" in line:
receive_event.set()
async with udp_listener() as (broadcast_port, receiver):
listen_port = _get_free_udp_port()
config = yaml_config.replace("UDP_LISTEN_PORT_PLACEHOLDER", str(listen_port))
config = config.replace("UDP_BROADCAST_PORT_PLACEHOLDER", str(broadcast_port))
async with udp_listener() as (udp_port, receiver):
# Replace placeholders in the config
config = yaml_config.replace("UDP_LISTEN_PORT_PLACEHOLDER", str(udp_port + 1))
config = config.replace("UDP_BROADCAST_PORT_PLACEHOLDER", str(udp_port))
async with (
run_compiled(config, line_callback=on_log_line),
@@ -180,19 +169,3 @@ async def test_udp_send_receive(
assert "Address: 127.0.0.2" in log_text, (
f"Address 127.0.0.2 not found in dump_config. Log: {log_text[-2000:]}"
)
# Test receiving a UDP packet (exercises on_receive with std::span)
test_payload = b"TEST_RECEIVE_UDP"
send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
send_sock.sendto(test_payload, ("127.0.0.1", listen_port))
finally:
send_sock.close()
try:
await asyncio.wait_for(receive_event.wait(), timeout=5.0)
except TimeoutError:
pytest.fail(
f"on_receive did not fire. Expected 'Received UDP:' in logs. "
f"Last log lines: {log_lines[-20:]}"
)

View File

@@ -2,7 +2,7 @@
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch
import pytest
from pytest import MonkeyPatch
@@ -632,3 +632,14 @@ def test_wizard_accepts_rpipico_board(tmp_path: Path, monkeypatch: MonkeyPatch):
# rpipico doesn't support WiFi, so no api_encryption_key or ota_password
assert "api_encryption_key" not in call_kwargs
assert "ota_password" not in call_kwargs
def test_fallback_psk_uses_secrets_choice(
default_config: dict[str, Any],
) -> None:
"""Test that fallback PSK is generated using secrets.choice."""
with patch("esphome.wizard.secrets.choice", return_value="X") as mock_choice:
config = wz.wizard_file(**default_config)
assert 'password: "XXXXXXXXXXXX"' in config
assert mock_choice.call_count == 12