Refactor the api noise handshake onto the shared responder

This commit is contained in:
J. Nick Koston
2026-08-20 01:16:40 -05:00
parent e14a3050ee
commit c6bb7e48d4
11 changed files with 151 additions and 141 deletions
+2 -2
View File
@@ -2130,7 +2130,7 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio
}
#endif
psk_t psk{};
noise::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 (APINoiseContext::is_all_zeros(psk)) {
} else if (noise::NoiseContext::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");
@@ -5,7 +5,6 @@
#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"
@@ -318,15 +317,15 @@ APIError APINoiseFrameHelper::state_action_server_hello_() {
return APIError::OK;
}
APIError APINoiseFrameHelper::state_action_handshake_() {
int action = noise_handshakestate_get_action(this->handshake_);
if (action == NOISE_ACTION_READ_MESSAGE) {
noise::NoiseResponderHandshake::Action action = this->handshake_.action();
if (action == noise::NoiseResponderHandshake::Action::ACTION_READ) {
return this->state_action_handshake_read_();
} else if (action == NOISE_ACTION_WRITE_MESSAGE) {
} else if (action == noise::NoiseResponderHandshake::Action::ACTION_WRITE) {
return this->state_action_handshake_write_();
}
// bad state for action
this->state_ = State::FAILED;
HELPER_LOG("Bad action for handshake: %d", action);
HELPER_LOG("Bad action for handshake: %d", (int) action);
return APIError::HANDSHAKESTATE_BAD_STATE;
}
APIError APINoiseFrameHelper::state_action_handshake_read_() {
@@ -344,14 +343,10 @@ APIError APINoiseFrameHelper::state_action_handshake_read_() {
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
}
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);
int err = this->handshake_.read_message(this->rx_buf_.data() + 1, this->rx_buf_.size() - 1);
if (err != 0) {
// Special handling for MAC failure
this->send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? LOG_STR("Handshake MAC failure")
: LOG_STR("Handshake error"));
this->send_explicit_handshake_reject_(noise::reject_reason_for(err));
return this->handle_noise_error_(err, LOG_STR("noise_handshakestate_read_message"),
APIError::HANDSHAKESTATE_READ_FAILED);
}
@@ -360,18 +355,16 @@ APIError APINoiseFrameHelper::state_action_handshake_read_() {
}
APIError APINoiseFrameHelper::state_action_handshake_write_() {
uint8_t buffer[65];
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1);
size_t msg_len = 0;
int err = noise_handshakestate_write_message(this->handshake_, &mbuf, nullptr);
int err = this->handshake_.write_message(buffer + 1, sizeof(buffer) - 1, msg_len);
APIError aerr = this->handle_noise_error_(err, LOG_STR("noise_handshakestate_write_message"),
APIError::HANDSHAKESTATE_WRITE_FAILED);
if (aerr != APIError::OK)
return aerr;
buffer[0] = 0x00; // success
buffer[0] = noise::HANDSHAKE_STATUS_OK;
aerr = this->write_frame_(buffer, mbuf.size + 1);
aerr = this->write_frame_(buffer, msg_len + 1);
if (aerr != APIError::OK)
return aerr;
return this->check_handshake_finished_();
@@ -379,6 +372,8 @@ 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);
// temporarily remove failed state
@@ -515,19 +510,19 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s
}
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
uint8_t header[3];
uint8_t header[noise::FRAME_HEADER_SIZE];
noise::write_frame_header(header, len);
if (len == 0) {
return this->write_raw_buf_(header, 3);
return this->write_raw_buf_(header, noise::FRAME_HEADER_SIZE);
}
struct iovec iov[2];
iov[0].iov_base = header;
iov[0].iov_len = 3;
iov[0].iov_len = noise::FRAME_HEADER_SIZE;
iov[1].iov_base = const_cast<uint8_t *>(data);
iov[1].iov_len = len;
return this->write_raw_iov_(iov, 2, 3 + len);
return this->write_raw_iov_(iov, 2, noise::FRAME_HEADER_SIZE + len);
}
/** Initiate the data structures for the handshake.
@@ -535,45 +530,12 @@ 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;
// 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);
int err = this->handshake_.init(this->ctx_.get_psk(), prologue_.data(), prologue_.size());
APIError aerr = handle_noise_error_(err, LOG_STR("noise_handshake_init"), APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
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
// init copies the prologue into the 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;
}
@@ -582,15 +544,17 @@ APIError APINoiseFrameHelper::check_handshake_finished_() {
assert(state_ == State::HANDSHAKE);
#endif
int action = noise_handshakestate_get_action(handshake_);
if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE)
noise::NoiseResponderHandshake::Action action = this->handshake_.action();
if (action == noise::NoiseResponderHandshake::Action::ACTION_READ ||
action == noise::NoiseResponderHandshake::Action::ACTION_WRITE)
return APIError::OK;
if (action != NOISE_ACTION_SPLIT) {
if (action != noise::NoiseResponderHandshake::Action::ACTION_SPLIT) {
state_ = State::FAILED;
HELPER_LOG("Bad action for handshake: %d", action);
HELPER_LOG("Bad action for handshake: %d", (int) action);
return APIError::HANDSHAKESTATE_BAD_STATE;
}
int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_);
// split() also frees the handshake state
int err = this->handshake_.split(send_cipher_, recv_cipher_);
APIError aerr =
handle_noise_error_(err, LOG_STR("noise_handshakestate_split"), APIError::HANDSHAKESTATE_SPLIT_FAILED);
if (aerr != APIError::OK)
@@ -599,17 +563,11 @@ 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;
@@ -3,7 +3,7 @@
#ifdef USE_API
#ifdef USE_API_NOISE
#include "noise/protocol.h"
#include "api_noise_context.h"
#include "esphome/components/noise/noise_handshake.h"
namespace esphome::api {
@@ -16,7 +16,7 @@ class APINoiseFrameHelper final : public APIFrameHelper {
// Pos 7+: actual payload data
static constexpr uint8_t HEADER_PADDING = 1 + 2 + 2 + 2; // indicator + size + type + data_len
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, APINoiseContext &ctx)
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, noise::NoiseContext &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)
NoiseHandshakeState *handshake_{nullptr};
// Pointers first (4 bytes each; the handshake wrapper holds one pointer)
noise::NoiseResponderHandshake handshake_;
NoiseCipherState *send_cipher_{nullptr};
NoiseCipherState *recv_cipher_{nullptr};
// Reference to noise context (4 bytes on 32-bit)
APINoiseContext &ctx_;
noise::NoiseContext &ctx_;
// Buffer for noise handshake prologue (released after handshake)
APIBuffer prologue_;
@@ -1,14 +0,0 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_API_NOISE
#include "esphome/components/noise/noise.h"
namespace esphome::api {
// Kept as aliases for external components that use the api names
using psk_t = noise::psk_t;
using APINoiseContext = noise::NoiseContext;
} // namespace esphome::api
#endif // USE_API_NOISE
+1 -1
View File
@@ -598,7 +598,7 @@ bool APIServer::load_and_apply_noise_psk_() {
return true;
}
bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
bool APIServer::save_noise_psk(noise::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
+9 -6
View File
@@ -5,7 +5,10 @@
#include "api_buffer.h"
// Must precede clients_ so APIConnection is complete for default_delete (libc++).
#include "api_connection.h"
#include "api_noise_context.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_pb2.h"
#include "api_pb2_service.h"
#include "esphome/components/socket/socket.h"
@@ -37,7 +40,7 @@ class UserServiceDescriptor;
#ifdef USE_API_NOISE
struct SavedNoisePsk {
psk_t psk;
noise::psk_t psk;
} PACKED; // NOLINT
#endif
@@ -73,10 +76,10 @@ class APIServer final : public Component,
APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; }
#ifdef USE_API_NOISE
bool save_noise_psk(psk_t psk, bool make_active = true);
bool save_noise_psk(noise::psk_t psk, bool make_active = true);
bool clear_noise_psk(bool make_active = true);
void set_noise_psk(psk_t psk) { this->noise_ctx_.set_psk(psk); }
APINoiseContext &get_noise_ctx() { return this->noise_ctx_; }
void set_noise_psk(noise::psk_t psk) { this->noise_ctx_.set_psk(psk); }
noise::NoiseContext &get_noise_ctx() { return this->noise_ctx_; }
#endif // USE_API_NOISE
void handle_disconnect(APIConnection *conn);
@@ -354,7 +357,7 @@ class APIServer final : public Component,
#endif
#ifdef USE_API_NOISE
APINoiseContext noise_ctx_;
noise::NoiseContext noise_ctx_;
ESPPreferenceObject noise_pref_;
#endif // USE_API_NOISE
};
-9
View File
@@ -4,7 +4,6 @@ import binascii
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_KEY
from esphome.core import CORE
from esphome.types import ConfigType
CODEOWNERS = ["@bdraco"]
@@ -67,11 +66,3 @@ async def to_code(config: ConfigType) -> None:
# 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")
# noise-c pulls noise_rand_bytes (our HWRNG binding, defined in noise.cpp)
# from the static archive. A consumer such as api may reference no other
# symbol from noise.cpp at normal log levels, so on the ESP-IDF link (which
# resolves archives in a group) the member is dropped and noise-c fails to
# link. Force the linker to keep it. The host toolchain links it without
# help, and its ld syntax differs (leading underscore), so skip it there.
if not CORE.is_host:
cg.add_build_flag("-Wl,-u,noise_rand_bytes")
+3 -12
View File
@@ -1,7 +1,5 @@
#include "noise.h"
#ifdef USE_NOISE
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <algorithm>
@@ -61,6 +59,9 @@ const LogString *reject_reason_for(int err) {
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;
@@ -83,15 +84,5 @@ size_t format_reject_payload(uint8_t *buf, size_t capacity, const LogString *rea
return reason_len + 1;
}
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::noise
#endif // USE_NOISE
+6
View File
@@ -63,5 +63,11 @@ size_t format_reject_payload(uint8_t *buf, size_t capacity, const LogString *rea
/// 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
+72 -13
View File
@@ -1,8 +1,18 @@
#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_);
@@ -11,6 +21,10 @@ NoiseResponderHandshake::~NoiseResponderHandshake() {
}
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
@@ -26,38 +40,66 @@ int NoiseResponderHandshake::init(const psk_t &psk, const uint8_t *prologue, siz
};
int err = noise_handshakestate_new_by_id(&this->handshake_, &nid, NOISE_ROLE_RESPONDER);
if (err != 0)
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)
return err;
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)
return err;
return noise_handshakestate_start(this->handshake_);
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 {
switch (noise_handshakestate_get_action(this->handshake_)) {
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::READ;
return Action::ACTION_READ;
case NOISE_ACTION_WRITE_MESSAGE:
return Action::WRITE;
return Action::ACTION_WRITE;
case NOISE_ACTION_SPLIT:
return Action::SPLIT;
return Action::ACTION_SPLIT;
default:
return Action::FAILED;
// 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(const uint8_t *data, size_t len) {
int NoiseResponderHandshake::read_message(uint8_t *data, size_t len) {
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_input(mbuf, const_cast<uint8_t *>(data), len);
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);
@@ -68,6 +110,11 @@ int NoiseResponderHandshake::write_message(uint8_t *out, size_t capacity, size_t
}
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;
@@ -76,5 +123,17 @@ int NoiseResponderHandshake::split(NoiseCipherState *&send_cipher, NoiseCipherSt
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
+27 -11
View File
@@ -16,7 +16,9 @@ namespace esphome::noise {
* 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.
* 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;
@@ -24,22 +26,36 @@ namespace esphome::noise {
*/
class NoiseResponderHandshake {
public:
enum class Action : uint8_t { READ, WRITE, SPLIT, FAILED };
// 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.
int init(const psk_t &psk, const uint8_t *prologue, size_t prologue_len);
Action action() const;
/// Process one received handshake message.
int read_message(const uint8_t *data, size_t len);
/// Produce the next handshake message into out (out_len receives its size).
int write_message(uint8_t *out, size_t capacity, size_t &out_len);
/// 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().
int split(NoiseCipherState *&send_cipher, NoiseCipherState *&recv_cipher);
/// 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};
};