1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-26 12:43:48 +00:00

[api] Allow clearing noise psk if dynamically set (#11429)

This commit is contained in:
Jesse Hills
2025-10-22 14:24:56 +13:00
committed by GitHub
parent c6ae1a5909
commit 2c1927fd12
5 changed files with 101 additions and 22 deletions

View File

@@ -1572,7 +1572,13 @@ bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryption
resp.success = false; resp.success = false;
psk_t psk{}; psk_t psk{};
if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) { if (msg.key.empty()) {
if (this->parent_->clear_noise_psk(true)) {
resp.success = true;
} else {
ESP_LOGW(TAG, "Failed to clear encryption key");
}
} else if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) {
ESP_LOGW(TAG, "Invalid encryption key length"); ESP_LOGW(TAG, "Invalid encryption key length");
} else if (!this->parent_->save_noise_psk(psk, true)) { } else if (!this->parent_->save_noise_psk(psk, true)) {
ESP_LOGW(TAG, "Failed to save encryption key"); ESP_LOGW(TAG, "Failed to save encryption key");

View File

@@ -468,6 +468,31 @@ uint16_t APIServer::get_port() const { return this->port_; }
void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg,
const LogString *fail_log_msg, const psk_t &active_psk, bool make_active) {
if (!this->noise_pref_.save(&new_psk)) {
ESP_LOGW(TAG, "%s", LOG_STR_ARG(fail_log_msg));
return false;
}
// ensure it's written immediately
if (!global_preferences->sync()) {
ESP_LOGW(TAG, "Failed to sync preferences");
return false;
}
ESP_LOGD(TAG, "%s", LOG_STR_ARG(save_log_msg));
if (make_active) {
this->set_timeout(100, [this, active_psk]() {
ESP_LOGW(TAG, "Disconnecting all clients to reset PSK");
this->set_noise_psk(active_psk);
for (auto &c : this->clients_) {
DisconnectRequest req;
c->send_message(req, DisconnectRequest::MESSAGE_TYPE);
}
});
}
return true;
}
bool APIServer::save_noise_psk(psk_t psk, bool make_active) { bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
#ifdef USE_API_NOISE_PSK_FROM_YAML #ifdef USE_API_NOISE_PSK_FROM_YAML
// When PSK is set from YAML, this function should never be called // When PSK is set from YAML, this function should never be called
@@ -482,27 +507,21 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
} }
SavedNoisePsk new_saved_psk{psk}; SavedNoisePsk new_saved_psk{psk};
if (!this->noise_pref_.save(&new_saved_psk)) { return this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"), psk,
ESP_LOGW(TAG, "Failed to save Noise PSK"); make_active);
return false; #endif
} }
// ensure it's written immediately bool APIServer::clear_noise_psk(bool make_active) {
if (!global_preferences->sync()) { #ifdef USE_API_NOISE_PSK_FROM_YAML
ESP_LOGW(TAG, "Failed to sync preferences"); // When PSK is set from YAML, this function should never be called
return false; // but if it is, reject the change
} ESP_LOGW(TAG, "Key set in YAML");
ESP_LOGD(TAG, "Noise PSK saved"); return false;
if (make_active) { #else
this->set_timeout(100, [this, psk]() { SavedNoisePsk empty_psk{};
ESP_LOGW(TAG, "Disconnecting all clients to reset PSK"); psk_t empty{};
this->set_noise_psk(psk); return this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"), empty,
for (auto &c : this->clients_) { make_active);
DisconnectRequest req;
c->send_message(req, DisconnectRequest::MESSAGE_TYPE);
}
});
}
return true;
#endif #endif
} }
#endif #endif

View File

@@ -53,6 +53,7 @@ class APIServer : public Component, public Controller {
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
bool save_noise_psk(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(psk_t psk) { noise_ctx_->set_psk(psk); } void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(psk); }
std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; } std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; }
#endif // USE_API_NOISE #endif // USE_API_NOISE
@@ -174,6 +175,10 @@ class APIServer : public Component, public Controller {
protected: protected:
void schedule_reboot_timeout_(); void schedule_reboot_timeout_();
#ifdef USE_API_NOISE
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
const psk_t &active_psk, bool make_active);
#endif // USE_API_NOISE
// Pointers and pointer-like types first (4 bytes each) // Pointers and pointer-like types first (4 bytes each)
std::unique_ptr<socket::Socket> socket_ = nullptr; std::unique_ptr<socket::Socket> socket_ = nullptr;
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER #ifdef USE_API_CLIENT_CONNECTED_TRIGGER

View File

@@ -0,0 +1,10 @@
esphome:
name: noise-key-test
host:
api:
encryption:
key: "zX9/JHxMKwpP0jUGsF0iESCm1wRvNgR6NkKVOhn7kSs="
logger:

View File

@@ -49,3 +49,42 @@ async def test_noise_encryption_key_protection(
with pytest.raises(InvalidEncryptionKeyAPIError): with pytest.raises(InvalidEncryptionKeyAPIError):
async with api_client_connected(noise_psk=wrong_key) as client: async with api_client_connected(noise_psk=wrong_key) as client:
await client.device_info() await client.device_info()
@pytest.mark.asyncio
async def test_noise_encryption_key_clear_protection(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that noise encryption key set in YAML cannot be changed via API."""
# The key that's set in the YAML fixture
noise_psk = "zX9/JHxMKwpP0jUGsF0iESCm1wRvNgR6NkKVOhn7kSs="
# Keep ESPHome process running throughout all tests
async with run_compiled(yaml_config):
# First connection - test key change attempt
async with api_client_connected(noise_psk=noise_psk) as client:
# Verify connection is established
device_info = await client.device_info()
assert device_info is not None
# Try to set a new encryption key via API
new_key = b"" # Empty key to attempt to clear
# This should fail since key was set in YAML
success = await client.noise_encryption_set_key(new_key)
assert success is False
# Reconnect with the original key to verify it still works
async with api_client_connected(noise_psk=noise_psk) as client:
# Verify connection is still successful with original key
device_info = await client.device_info()
assert device_info is not None
assert device_info.name == "noise-key-test"
# Verify that connecting with a wrong key fails
wrong_key = base64.b64encode(b"y" * 32).decode() # Different key
with pytest.raises(InvalidEncryptionKeyAPIError):
async with api_client_connected(noise_psk=wrong_key) as client:
await client.device_info()