Add secure transport and OAM router runtime implementations

- Implement secure transport mechanisms in `gateway_knx_secure_transport.cpp` for handling secure sessions, including AES encryption, session key generation, and secure packet wrapping and unwrapping.
- Introduce `OamRouterRuntime` in `oam_router_runtime.cpp` to manage OAM router identity, individual addresses, and tunnel frame handling.
- Enhance secure session management with functions for session allocation, authentication, and secure packet processing.
- Ensure compatibility with existing KNXnet/IP protocols while adding support for secure communications.

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-25 08:18:01 +08:00
parent 0467179f70
commit 2b779d5532
22 changed files with 2665 additions and 77 deletions
+23 -2
View File
@@ -4,6 +4,9 @@
#include "gateway_controller.hpp"
#include "gateway_runtime.hpp"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "host/ble_gap.h"
@@ -29,6 +32,8 @@ constexpr uint16_t kChannel2Uuid = 0xFFF2;
constexpr uint16_t kGatewayUuid = 0xFFF3;
constexpr int64_t kGenericDedupeWindowUs = 120000;
constexpr size_t kGatewayCharacteristicIndex = 2;
constexpr int kGatewayNotifyAllocationAttempts = 6;
constexpr TickType_t kGatewayNotifyRetryDelayTicks = pdMS_TO_TICKS(20);
gateway::GatewayBleBridge* s_active_bridge = nullptr;
uint16_t s_value_handles[3] = {0, 0, 0};
@@ -348,9 +353,25 @@ void GatewayBleBridge::notifyCharacteristic(size_t index, const std::vector<uint
}
characteristic_values_[index] = payload;
struct os_mbuf* buffer = ble_hs_mbuf_from_flat(payload.data(), payload.size());
const int allocation_attempts =
index == kGatewayCharacteristicIndex ? kGatewayNotifyAllocationAttempts : 1;
struct os_mbuf* buffer = nullptr;
for (int attempt = 0; attempt < allocation_attempts; ++attempt) {
if (conn_handle_ == kInvalidConnectionHandle || !notify_enabled_[index]) {
return;
}
buffer = ble_hs_mbuf_from_flat(payload.data(), payload.size());
if (buffer != nullptr) {
break;
}
if (attempt + 1 < allocation_attempts) {
vTaskDelay(kGatewayNotifyRetryDelayTicks);
}
}
if (buffer == nullptr) {
ESP_LOGW(kTag, "failed to allocate notify mbuf idx=%u", static_cast<unsigned>(index));
ESP_LOGW(kTag, "failed to allocate notify mbuf idx=%u attempts=%d len=%u",
static_cast<unsigned>(index), allocation_attempts,
static_cast<unsigned>(payload.size()));
return;
}
const int rc = ble_gatts_notify_custom(conn_handle_, s_value_handles[index], buffer);
@@ -3,6 +3,9 @@
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
#include "gateway_knx.hpp"
namespace gateway::openknx {
@@ -27,6 +30,14 @@ struct FactoryCertificatePayload {
std::string checksum;
};
struct IpSecureCredentialStatus {
bool activated{false};
bool backboneKeyAvailable{false};
bool deviceAuthenticationKeyAvailable{false};
uint8_t tunnelUserCount{0};
uint64_t routingSequence{0};
};
bool LoadFactoryFdsk(uint8_t* data, size_t len);
FactoryFdskInfo LoadFactoryFdskInfo();
bool GenerateFactoryFdsk(FactoryFdskInfo* info = nullptr);
@@ -34,4 +45,21 @@ bool WriteFactoryFdskHex(const std::string& hex_key, FactoryFdskInfo* info = nul
bool ResetFactoryFdskCache(FactoryFdskInfo* info = nullptr);
FactoryCertificatePayload BuildFactoryCertificatePayload();
bool LoadOamFactoryFdsk(uint8_t* data, size_t len);
FactoryFdskInfo LoadOamFactoryFdskInfo();
bool GenerateOamFactoryFdsk(FactoryFdskInfo* info = nullptr);
bool WriteOamFactoryFdskHex(const std::string& hex_key,
FactoryFdskInfo* info = nullptr);
bool ResetOamFactoryFdskCache(FactoryFdskInfo* info = nullptr);
FactoryCertificatePayload BuildOamFactoryCertificatePayload();
IpSecureCredentialStatus LoadOamIpSecureCredentialStatus();
::gateway::GatewayKnxIpSecureCredentialMaterial LoadOamIpSecureCredentialMaterial();
bool WriteOamIpSecureKeyringHex(const std::string& backbone_key_hex,
const std::vector<std::string>& tunnel_user_key_hex,
const std::string& device_auth_key_hex,
bool activated);
bool StoreOamIpSecureRoutingSequence(uint64_t sequence);
bool ClearOamIpSecureKeyring();
} // namespace gateway::openknx
@@ -199,6 +199,22 @@ cJSON* FactoryCertificateToCjson(const openknx::FactoryCertificatePayload& certi
return root;
}
cJSON* IpSecureCredentialStatusToCjson(
const openknx::IpSecureCredentialStatus& status) {
cJSON* root = cJSON_CreateObject();
if (root == nullptr) {
return nullptr;
}
cJSON_AddBoolToObject(root, "activated", status.activated);
cJSON_AddBoolToObject(root, "backboneKeyAvailable", status.backboneKeyAvailable);
cJSON_AddBoolToObject(root, "deviceAuthenticationKeyAvailable",
status.deviceAuthenticationKeyAvailable);
cJSON_AddNumberToObject(root, "tunnelUserCount", status.tunnelUserCount);
cJSON_AddNumberToObject(root, "routingSequence",
static_cast<double>(status.routingSequence));
return root;
}
cJSON* SecurityFailuresToCjson() {
cJSON* root = cJSON_CreateObject();
if (root == nullptr) {
@@ -1522,6 +1538,10 @@ struct GatewayBridgeService::ChannelRuntime {
[this](uint16_t group_object_number, const uint8_t* data, size_t len) {
return service.routeKnxGroupObjectWrite(group_object_number, data, len);
});
knx_router->setOamIpSecureRoutingSequenceStoreHandler([](uint64_t sequence) {
openknx::StoreOamIpSecureRoutingSequence(sequence);
});
knx_router->setOamIpSecureCredentials(openknx::LoadOamIpSecureCredentialMaterial());
if (const auto active_knx = activeKnxConfigLocked(); active_knx.has_value()) {
knx->setConfig(active_knx.value());
knx_router->setConfig(active_knx.value());
@@ -2154,6 +2174,7 @@ struct GatewayBridgeService::ChannelRuntime {
runtime_config.individual_address);
knx->setConfig(runtime_config);
knx_router->setConfig(runtime_config);
knx_router->setOamIpSecureCredentials(openknx::LoadOamIpSecureCredentialMaterial());
knx_router->setCommissioningOnly(commissioning_only);
if (!runtime_config.ip_router_enabled) {
knx_started = false;
@@ -2218,6 +2239,7 @@ struct GatewayBridgeService::ChannelRuntime {
endpoint_runtime = const_cast<GatewayBridgeService&>(service).selectKnxEndpointRuntime();
}
bool programming_mode = false;
bool oam_programming_mode = false;
bool programming_control_available = false;
int endpoint_owner_gateway_id = -1;
if (endpoint_runtime != nullptr) {
@@ -2227,6 +2249,7 @@ struct GatewayBridgeService::ChannelRuntime {
endpoint_runtime->knx_router->started();
if (programming_control_available) {
programming_mode = endpoint_runtime->knx_router->programmingMode();
oam_programming_mode = endpoint_runtime->knx_router->oamProgrammingMode();
}
}
const auto effective_knx =
@@ -2236,6 +2259,7 @@ struct GatewayBridgeService::ChannelRuntime {
cJSON_AddBoolToObject(knx_json, "started", knx_started);
cJSON_AddBoolToObject(knx_json, "routerReady", knx_router != nullptr && knx_router->started());
cJSON_AddBoolToObject(knx_json, "programmingMode", programming_mode);
cJSON_AddBoolToObject(knx_json, "oamProgrammingMode", oam_programming_mode);
cJSON_AddBoolToObject(knx_json, "programmingControlAvailable",
programming_control_available);
cJSON_AddBoolToObject(knx_json, "endpointOwner",
@@ -2265,7 +2289,11 @@ struct GatewayBridgeService::ChannelRuntime {
#else
cJSON_AddBoolToObject(security_json, "knxnetIpSecureServicesRecognized", false);
#endif
#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
cJSON_AddBoolToObject(security_json, "knxnetIpSecureImplemented", true);
#else
cJSON_AddBoolToObject(security_json, "knxnetIpSecureImplemented", false);
#endif
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS)
cJSON_AddBoolToObject(security_json, "developmentEndpointsEnabled", true);
#else
@@ -2294,6 +2322,27 @@ struct GatewayBridgeService::ChannelRuntime {
cJSON_AddItemToObject(security_json, "failures", failures_json);
}
#endif
cJSON* oam_security_json = cJSON_CreateObject();
if (oam_security_json != nullptr) {
cJSON* oam_fdsk_json = FactoryFdskInfoToCjson(
openknx::LoadOamFactoryFdskInfo(), false);
if (oam_fdsk_json != nullptr) {
cJSON_AddItemToObject(oam_security_json, "factorySetupKey", oam_fdsk_json);
}
cJSON* oam_certificate_json = FactoryCertificateToCjson(
openknx::BuildOamFactoryCertificatePayload(), false);
if (oam_certificate_json != nullptr) {
cJSON_AddItemToObject(oam_security_json, "factoryCertificate",
oam_certificate_json);
}
cJSON* oam_credentials_json = IpSecureCredentialStatusToCjson(
openknx::LoadOamIpSecureCredentialStatus());
if (oam_credentials_json != nullptr) {
cJSON_AddItemToObject(oam_security_json, "ipSecureCredentials",
oam_credentials_json);
}
cJSON_AddItemToObject(security_json, "oamRouter", oam_security_json);
}
cJSON_AddItemToObject(knx_json, "security", security_json);
}
if (effective_knx.has_value()) {
@@ -2314,6 +2363,29 @@ struct GatewayBridgeService::ChannelRuntime {
effective_knx->ip_interface_individual_address);
cJSON_AddNumberToObject(knx_json, "individualAddress",
effective_knx->individual_address);
cJSON* oam_json = ToCjson(
GatewayKnxOamRouterConfigToValue(effective_knx->oam_router));
if (oam_json != nullptr) {
cJSON_AddItemToObject(knx_json, "oamRouter", oam_json);
}
cJSON* cloud_remote_json = cJSON_CreateObject();
if (cloud_remote_json != nullptr) {
const auto& cloud_remote = effective_knx->oam_router.cloud_remote;
cJSON_AddBoolToObject(cloud_remote_json, "prepared", true);
cJSON_AddBoolToObject(cloud_remote_json, "enabled", cloud_remote.enabled);
cJSON_AddStringToObject(cloud_remote_json, "mode", cloud_remote.mode.c_str());
cJSON_AddBoolToObject(cloud_remote_json, "requireSecureTunnel",
cloud_remote.require_secure_tunnel);
cJSON_AddBoolToObject(cloud_remote_json, "udpPunchEnabled",
cloud_remote.udp_punch_enabled);
cJSON_AddBoolToObject(cloud_remote_json, "relayEndpointConfigured",
!cloud_remote.relay_endpoint.empty());
cJSON_AddBoolToObject(cloud_remote_json, "mqttTopicPrefixConfigured",
!cloud_remote.mqtt_topic_prefix.empty());
cJSON_AddBoolToObject(cloud_remote_json, "authTokenRefConfigured",
!cloud_remote.auth_token_ref.empty());
cJSON_AddItemToObject(knx_json, "cloudKnxRemoteAccess", cloud_remote_json);
}
cJSON* serial_json = cJSON_CreateObject();
if (serial_json != nullptr) {
cJSON_AddNumberToObject(serial_json, "uartPort", effective_knx->tp_uart.uart_port);
@@ -3220,6 +3292,46 @@ struct GatewayBridgeService::ChannelRuntime {
}
return ESP_ERR_INVALID_ARG;
}
if (config.oam_router.enabled) {
if (config.oam_router.individual_address == 0 ||
config.oam_router.individual_address == 0xffff) {
if (error_message != nullptr) {
*error_message = "OAM KNX/IP router individual address must be a configured address";
}
return ESP_ERR_INVALID_ARG;
}
if (config.oam_router.tunnel_address_base == 0 ||
config.oam_router.tunnel_address_base == 0xffff ||
config.oam_router.tunnel_address_base > 0xffef) {
if (error_message != nullptr) {
*error_message = "OAM KNX/IP router tunnel address base must leave room for 16 tunnels";
}
return ESP_ERR_INVALID_ARG;
}
if (config.oam_router.individual_address == config.individual_address ||
config.oam_router.individual_address == config.ip_interface_individual_address ||
config.oam_router.tunnel_address_base == config.individual_address ||
config.oam_router.tunnel_address_base == config.ip_interface_individual_address) {
if (error_message != nullptr) {
*error_message = "OAM KNX/IP router addresses must differ from the shared IP interface and KNX-DALI gateway addresses";
}
return ESP_ERR_INVALID_ARG;
}
if (config.programming_button_gpio >= 0 &&
config.programming_button_gpio == config.oam_router.programming_button_gpio) {
if (error_message != nullptr) {
*error_message = "OAM and KNX-DALI programming buttons must use separate GPIOs";
}
return ESP_ERR_INVALID_ARG;
}
if (config.programming_led_gpio >= 0 &&
config.programming_led_gpio == config.oam_router.programming_led_gpio) {
if (error_message != nullptr) {
*error_message = "OAM and KNX-DALI programming LEDs must use separate GPIOs";
}
return ESP_ERR_INVALID_ARG;
}
}
if (!config.ip_router_enabled || !GatewayKnxConfigUsesTpUart(config)) {
return ESP_OK;
}
@@ -3360,6 +3472,7 @@ struct GatewayBridgeService::ChannelRuntime {
}
if (knx_router != nullptr) {
knx_router->setConfig(merged_config);
knx_router->setOamIpSecureCredentials(openknx::LoadOamIpSecureCredentialMaterial());
knx_router->setCommissioningOnly(false);
}
if (restart_router) {
@@ -4615,6 +4728,40 @@ GatewayBridgeHttpResponse GatewayBridgeService::handlePost(
}
return handleGet("status", gateway_id.value());
}
if (action == "knx_oam_programming_mode") {
cJSON* body_root = body.empty() ? nullptr : cJSON_ParseWithLength(body.data(), body.size());
if (body_root == nullptr) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "OAM KNX programming mode JSON is required");
}
const cJSON* enabled_item = cJSON_GetObjectItemCaseSensitive(body_root, "enabled");
if (!cJSON_IsBool(enabled_item)) {
cJSON_Delete(body_root);
return ErrorResponse(ESP_ERR_INVALID_ARG, "boolean enabled field is required");
}
const bool enabled = cJSON_IsTrue(enabled_item);
cJSON_Delete(body_root);
ChannelRuntime* owner = selectKnxEndpointRuntime();
if (owner == nullptr) {
return ErrorResponse(ESP_ERR_NOT_FOUND, "no KNX/IP endpoint owner is configured");
}
esp_err_t err = ESP_ERR_INVALID_STATE;
std::string detail = "KNX/IP router is unavailable";
{
LockGuard guard(owner->lock);
if (owner->knx_router != nullptr) {
err = owner->knx_router->setOamProgrammingMode(enabled);
detail = owner->knx_router->lastError();
}
owner->knx_last_error = err == ESP_OK ? std::string() : detail;
}
if (err != ESP_OK) {
return ErrorResponse(err, detail.empty() ? "failed to change OAM KNX programming mode"
: detail.c_str());
}
return handleGet("status", gateway_id.value());
}
if (action == "knx_security_read_factory_key") {
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS) && \
defined(CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED)
@@ -4807,6 +4954,225 @@ GatewayBridgeHttpResponse GatewayBridgeService::handlePost(
#else
return ErrorResponse(ESP_ERR_NOT_SUPPORTED,
"KNX security development endpoints are disabled");
#endif
}
if (action == "knx_oam_security_read_factory_key") {
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS)
cJSON* body_root = body.empty() ? nullptr : cJSON_ParseWithLength(body.data(), body.size());
if (body_root == nullptr) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "confirmation JSON is required");
}
const char* confirm = JsonString(body_root, "confirm");
const bool confirmed = confirm != nullptr &&
std::string_view(confirm) == "read-oam-factory-setup-key";
cJSON_Delete(body_root);
if (!confirmed) {
return ErrorResponse(ESP_ERR_INVALID_ARG,
"OAM factory setup key read confirmation is required");
}
cJSON* response = cJSON_CreateObject();
if (response == nullptr) {
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate OAM security response");
}
cJSON* fdsk_json = FactoryFdskInfoToCjson(openknx::LoadOamFactoryFdskInfo(), true);
if (fdsk_json != nullptr) {
cJSON_AddItemToObject(response, "factorySetupKey", fdsk_json);
}
return JsonOk(response);
#else
return ErrorResponse(ESP_ERR_NOT_SUPPORTED,
"KNX security development endpoints are disabled");
#endif
}
if (action == "knx_oam_security_generate_factory_key") {
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS)
cJSON* body_root = body.empty() ? nullptr : cJSON_ParseWithLength(body.data(), body.size());
if (body_root == nullptr) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "confirmation JSON is required");
}
const char* confirm = JsonString(body_root, "confirm");
const bool include_secret = JsonBool(body_root, "includeSecret", false);
const bool confirmed = confirm != nullptr &&
std::string_view(confirm) == "generate-oam-factory-setup-key";
cJSON_Delete(body_root);
if (!confirmed) {
return ErrorResponse(ESP_ERR_INVALID_ARG,
"OAM factory setup key generation confirmation is required");
}
openknx::FactoryFdskInfo info;
if (!openknx::GenerateOamFactoryFdsk(&info)) {
return ErrorResponse(ESP_FAIL, "failed to generate OAM factory setup key");
}
cJSON* response = cJSON_CreateObject();
if (response == nullptr) {
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate OAM security response");
}
cJSON* fdsk_json = FactoryFdskInfoToCjson(info, include_secret);
if (fdsk_json != nullptr) {
cJSON_AddItemToObject(response, "factorySetupKey", fdsk_json);
}
return JsonOk(response);
#else
return ErrorResponse(ESP_ERR_NOT_SUPPORTED,
"KNX security development endpoints are disabled");
#endif
}
if (action == "knx_oam_security_reset_factory_key") {
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS)
cJSON* body_root = body.empty() ? nullptr : cJSON_ParseWithLength(body.data(), body.size());
if (body_root == nullptr) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "confirmation JSON is required");
}
const char* confirm = JsonString(body_root, "confirm");
const bool confirmed = confirm != nullptr &&
std::string_view(confirm) == "reset-oam-factory-setup-key";
cJSON_Delete(body_root);
if (!confirmed) {
return ErrorResponse(ESP_ERR_INVALID_ARG,
"OAM factory setup key reset confirmation is required");
}
openknx::FactoryFdskInfo info;
if (!openknx::ResetOamFactoryFdskCache(&info)) {
return ErrorResponse(ESP_FAIL, "failed to reload OAM factory setup key");
}
cJSON* response = cJSON_CreateObject();
if (response == nullptr) {
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate OAM security response");
}
cJSON* fdsk_json = FactoryFdskInfoToCjson(info, false);
if (fdsk_json != nullptr) {
cJSON_AddItemToObject(response, "factorySetupKey", fdsk_json);
}
return JsonOk(response);
#else
return ErrorResponse(ESP_ERR_NOT_SUPPORTED,
"KNX security development endpoints are disabled");
#endif
}
if (action == "knx_oam_security_export_factory_certificate") {
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS)
cJSON* body_root = body.empty() ? nullptr : cJSON_ParseWithLength(body.data(), body.size());
if (body_root == nullptr) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "confirmation JSON is required");
}
const char* confirm = JsonString(body_root, "confirm");
const bool confirmed = confirm != nullptr &&
std::string_view(confirm) == "export-oam-factory-certificate";
cJSON_Delete(body_root);
if (!confirmed) {
return ErrorResponse(ESP_ERR_INVALID_ARG,
"OAM factory certificate export confirmation is required");
}
cJSON* response = cJSON_CreateObject();
if (response == nullptr) {
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate OAM security response");
}
cJSON* certificate_json = FactoryCertificateToCjson(
openknx::BuildOamFactoryCertificatePayload(), true);
if (certificate_json != nullptr) {
cJSON_AddItemToObject(response, "factoryCertificate", certificate_json);
}
return JsonOk(response);
#else
return ErrorResponse(ESP_ERR_NOT_SUPPORTED,
"KNX security development endpoints are disabled");
#endif
}
if (action == "knx_oam_security_upload_keyring") {
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS) && \
defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
cJSON* body_root = body.empty() ? nullptr : cJSON_ParseWithLength(body.data(), body.size());
if (body_root == nullptr) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "OAM keyring JSON is required");
}
const char* confirm = JsonString(body_root, "confirm");
const char* backbone_key = JsonString(body_root, "backboneKeyHex");
const char* device_auth_key = JsonString(body_root, "deviceAuthenticationKeyHex");
const bool activated = JsonBool(body_root, "activated", true);
const bool confirmed = confirm != nullptr &&
std::string_view(confirm) == "upload-oam-ip-secure-keyring";
std::vector<std::string> tunnel_keys;
const cJSON* tunnel_array = cJSON_GetObjectItemCaseSensitive(body_root, "tunnelUserKeysHex");
if (tunnel_array == nullptr) {
tunnel_array = cJSON_GetObjectItemCaseSensitive(body_root, "tunnelUserKeyHex");
}
if (cJSON_IsArray(tunnel_array)) {
const cJSON* item = nullptr;
cJSON_ArrayForEach(item, tunnel_array) {
if (cJSON_IsString(item) && item->valuestring != nullptr) {
tunnel_keys.emplace_back(item->valuestring);
}
}
} else if (cJSON_IsString(tunnel_array) && tunnel_array->valuestring != nullptr) {
tunnel_keys.emplace_back(tunnel_array->valuestring);
}
if (!confirmed || backbone_key == nullptr) {
cJSON_Delete(body_root);
return ErrorResponse(ESP_ERR_INVALID_ARG,
"OAM keyring upload confirmation and backboneKeyHex are required");
}
const std::string backbone_key_value(backbone_key);
const std::string device_auth_key_value(device_auth_key == nullptr ? "" : device_auth_key);
cJSON_Delete(body_root);
if (!openknx::WriteOamIpSecureKeyringHex(backbone_key_value, tunnel_keys,
device_auth_key_value, activated)) {
return ErrorResponse(ESP_ERR_INVALID_ARG,
"invalid OAM IP Secure keyring material");
}
if (runtime->knx_router != nullptr) {
runtime->knx_router->setOamIpSecureCredentials(
openknx::LoadOamIpSecureCredentialMaterial());
}
cJSON* response = cJSON_CreateObject();
if (response == nullptr) {
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate OAM keyring response");
}
cJSON* credentials_json = IpSecureCredentialStatusToCjson(
openknx::LoadOamIpSecureCredentialStatus());
if (credentials_json != nullptr) {
cJSON_AddItemToObject(response, "ipSecureCredentials", credentials_json);
}
return JsonOk(response);
#else
return ErrorResponse(ESP_ERR_NOT_SUPPORTED,
"KNXnet/IP Secure development endpoints are disabled");
#endif
}
if (action == "knx_oam_security_clear_keyring") {
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS) && \
defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
cJSON* body_root = body.empty() ? nullptr : cJSON_ParseWithLength(body.data(), body.size());
if (body_root == nullptr) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "confirmation JSON is required");
}
const char* confirm = JsonString(body_root, "confirm");
const bool confirmed = confirm != nullptr &&
std::string_view(confirm) == "clear-oam-ip-secure-keyring";
cJSON_Delete(body_root);
if (!confirmed) {
return ErrorResponse(ESP_ERR_INVALID_ARG,
"OAM keyring clear confirmation is required");
}
if (!openknx::ClearOamIpSecureKeyring()) {
return ErrorResponse(ESP_FAIL, "failed to clear OAM IP Secure keyring");
}
if (runtime->knx_router != nullptr) {
runtime->knx_router->setOamIpSecureCredentials(
openknx::LoadOamIpSecureCredentialMaterial());
}
cJSON* response = cJSON_CreateObject();
if (response == nullptr) {
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate OAM keyring response");
}
cJSON* credentials_json = IpSecureCredentialStatusToCjson(
openknx::LoadOamIpSecureCredentialStatus());
if (credentials_json != nullptr) {
cJSON_AddItemToObject(response, "ipSecureCredentials", credentials_json);
}
return JsonOk(response);
#else
return ErrorResponse(ESP_ERR_NOT_SUPPORTED,
"KNXnet/IP Secure development endpoints are disabled");
#endif
}
if (action == "bacnet_start") {
@@ -22,13 +22,21 @@ namespace {
constexpr const char* kTag = "openknx_sec";
constexpr const char* kNamespace = "knx_sec";
constexpr const char* kOamNamespace = "knx_oam_sec";
constexpr const char* kFactoryFdskKey = "factory_fdsk";
constexpr const char* kIpSecureBackboneKey = "ipsec_backbone";
constexpr const char* kIpSecureDeviceAuthKey = "ipsec_dev_auth";
constexpr const char* kIpSecureTunnelCountKey = "ipsec_tunnels";
constexpr const char* kIpSecureActivatedKey = "ipsec_active";
constexpr const char* kIpSecureRoutingSeqKey = "ipsec_route_seq";
constexpr size_t kFdskSize = 16;
constexpr size_t kSerialSize = 6;
constexpr size_t kFdskQrSize = 36;
constexpr const char* kProductIdentity = "REG1-Dali";
constexpr const char* kOamProductIdentity = "OpenKNX-IP-Router";
constexpr const char* kDevelopmentStorage = "base_mac_derived_plain_nvs_development";
constexpr char kFdskDerivationLabel[] = "DaliMaster REG1-Dali deterministic FDSK v1";
constexpr char kOamFdskDerivationLabel[] = "DaliMaster OAM-IP-Router deterministic FDSK v1";
constexpr uint8_t kCrc4Tab[16] = {
0x0, 0x3, 0x6, 0x5, 0xc, 0xf, 0xa, 0x9,
0xb, 0x8, 0xd, 0xe, 0x7, 0x4, 0x1, 0x2,
@@ -36,6 +44,37 @@ constexpr uint8_t kCrc4Tab[16] = {
constexpr char kBase32Alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
constexpr char kHexAlphabet[] = "0123456789ABCDEF";
struct FactoryFdskContext {
const char* nvsNamespace;
const char* productIdentity;
const char* derivationLabel;
uint16_t manufacturerId;
uint16_t applicationNumber;
uint8_t applicationVersion;
uint32_t serialMacIncrement;
bool clearOpenKnxCache;
};
constexpr FactoryFdskContext kReg1Context{
kNamespace,
kProductIdentity,
kFdskDerivationLabel,
gateway::knx_internal::kReg1DaliManufacturerId,
gateway::knx_internal::kReg1DaliApplicationNumber,
gateway::knx_internal::kReg1DaliApplicationVersion,
gateway::knx_internal::kReg1DaliSerialMacIncrement,
true};
constexpr FactoryFdskContext kOamContext{
kOamNamespace,
kOamProductIdentity,
kOamFdskDerivationLabel,
gateway::knx_internal::kOamRouterManufacturerId,
gateway::knx_internal::kOamRouterApplicationNumber,
gateway::knx_internal::kOamRouterApplicationVersion,
gateway::knx_internal::kOamRouterSerialMacIncrement,
false};
extern "C" void knx_platform_clear_cached_fdsk() __attribute__((weak));
std::string hexValue(uint32_t value, int width) {
@@ -120,7 +159,7 @@ bool parseHexKey(const std::string& value, uint8_t* out) {
return plausibleKey(out);
}
bool loadKnxSerialNumber(uint8_t* serial) {
bool loadKnxSerialNumber(const FactoryFdskContext& context, uint8_t* serial) {
if (serial == nullptr) {
return false;
}
@@ -129,22 +168,30 @@ bool loadKnxSerialNumber(uint8_t* serial) {
return false;
}
serial[0] = static_cast<uint8_t>(
(gateway::knx_internal::kReg1DaliManufacturerId >> 8) & 0xff);
serial[1] = static_cast<uint8_t>(
gateway::knx_internal::kReg1DaliManufacturerId & 0xff);
std::copy(mac.begin() + 2, mac.end(), serial + 2);
uint32_t suffix = (static_cast<uint32_t>(mac[2]) << 24) |
(static_cast<uint32_t>(mac[3]) << 16) |
(static_cast<uint32_t>(mac[4]) << 8) |
static_cast<uint32_t>(mac[5]);
suffix += context.serialMacIncrement;
serial[0] = static_cast<uint8_t>((context.manufacturerId >> 8) & 0xff);
serial[1] = static_cast<uint8_t>(context.manufacturerId & 0xff);
serial[2] = static_cast<uint8_t>((suffix >> 24) & 0xff);
serial[3] = static_cast<uint8_t>((suffix >> 16) & 0xff);
serial[4] = static_cast<uint8_t>((suffix >> 8) & 0xff);
serial[5] = static_cast<uint8_t>(suffix & 0xff);
return true;
}
bool deriveFactoryFdskFromSerial(const uint8_t* serial, uint8_t* key) {
bool deriveFactoryFdskFromSerial(const FactoryFdskContext& context,
const uint8_t* serial, uint8_t* key) {
if (serial == nullptr || key == nullptr) {
return false;
}
std::array<uint8_t, sizeof(kFdskDerivationLabel) - 1 + kSerialSize> material{};
std::copy(kFdskDerivationLabel, kFdskDerivationLabel + sizeof(kFdskDerivationLabel) - 1,
material.begin());
std::copy(serial, serial + kSerialSize, material.begin() + sizeof(kFdskDerivationLabel) - 1);
const size_t label_len = std::strlen(context.derivationLabel);
std::vector<uint8_t> material(label_len + kSerialSize);
std::copy(context.derivationLabel, context.derivationLabel + label_len, material.begin());
std::copy(serial, serial + kSerialSize, material.begin() + label_len);
std::array<uint8_t, 32> digest{};
if (mbedtls_sha256(material.data(), material.size(), digest.data(), 0) != 0) {
@@ -157,7 +204,7 @@ bool deriveFactoryFdskFromSerial(const uint8_t* serial, uint8_t* key) {
return plausibleKey(key);
}
void syncFactoryFdskToNvs(const uint8_t* data) {
void syncFactoryFdskToNvs(const FactoryFdskContext& context, const uint8_t* data) {
if (data == nullptr || !plausibleKey(data) || !ensureNvsReady()) {
return;
}
@@ -166,7 +213,7 @@ void syncFactoryFdskToNvs(const uint8_t* data) {
size_t stored_size = stored.size();
nvs_handle_t handle = 0;
esp_err_t err = nvs_open(kNamespace, NVS_READWRITE, &handle);
esp_err_t err = nvs_open(context.nvsNamespace, NVS_READWRITE, &handle);
if (err != ESP_OK) {
ESP_LOGW(kTag, "failed to open KNX security NVS namespace: %s", esp_err_to_name(err));
return;
@@ -186,7 +233,9 @@ void syncFactoryFdskToNvs(const uint8_t* data) {
ESP_LOGW(kTag, "failed to mirror deterministic KNX factory FDSK: %s", esp_err_to_name(err));
return;
}
clearOpenKnxFdskCache();
if (context.clearOpenKnxCache) {
clearOpenKnxFdskCache();
}
}
uint8_t crc4Array(const uint8_t* data, size_t len) {
@@ -275,27 +324,28 @@ std::string fnv1aHex(const std::string& value) {
namespace gateway::openknx {
bool LoadFactoryFdsk(uint8_t* data, size_t len) {
bool LoadFactoryFdskForContext(const FactoryFdskContext& context, uint8_t* data, size_t len) {
if (data == nullptr || len < kFdskSize) {
return false;
}
std::array<uint8_t, kSerialSize> serial{};
std::array<uint8_t, kFdskSize> key{};
if (!loadKnxSerialNumber(serial.data()) ||
!deriveFactoryFdskFromSerial(serial.data(), key.data())) {
if (!loadKnxSerialNumber(context, serial.data()) ||
!deriveFactoryFdskFromSerial(context, serial.data(), key.data())) {
return false;
}
std::memcpy(data, key.data(), kFdskSize);
syncFactoryFdskToNvs(key.data());
syncFactoryFdskToNvs(context, key.data());
return true;
}
FactoryFdskInfo LoadFactoryFdskInfo() {
FactoryFdskInfo LoadFactoryFdskInfoForContext(const FactoryFdskContext& context) {
FactoryFdskInfo info;
std::array<uint8_t, kFdskSize> key{};
std::array<uint8_t, kSerialSize> serial{};
if (!loadKnxSerialNumber(serial.data()) || !LoadFactoryFdsk(key.data(), key.size())) {
if (!loadKnxSerialNumber(context, serial.data()) ||
!LoadFactoryFdskForContext(context, key.data(), key.size())) {
return info;
}
@@ -306,31 +356,34 @@ FactoryFdskInfo LoadFactoryFdskInfo() {
return info;
}
bool GenerateFactoryFdsk(FactoryFdskInfo* info) {
bool GenerateFactoryFdskForContext(const FactoryFdskContext& context,
FactoryFdskInfo* info) {
std::array<uint8_t, kFdskSize> key{};
const bool stored = LoadFactoryFdsk(key.data(), key.size());
const bool stored = LoadFactoryFdskForContext(context, key.data(), key.size());
std::fill(key.begin(), key.end(), 0);
if (!stored) {
return false;
}
if (info != nullptr) {
*info = LoadFactoryFdskInfo();
*info = LoadFactoryFdskInfoForContext(context);
}
return true;
}
bool WriteFactoryFdskHex(const std::string& hex_key, FactoryFdskInfo* info) {
bool WriteFactoryFdskHexForContext(const FactoryFdskContext& context,
const std::string& hex_key,
FactoryFdskInfo* info) {
std::array<uint8_t, kFdskSize> key{};
if (!parseHexKey(hex_key, key.data())) {
return false;
}
std::array<uint8_t, kSerialSize> serial{};
std::array<uint8_t, kFdskSize> derived{};
const bool stored = loadKnxSerialNumber(serial.data()) &&
deriveFactoryFdskFromSerial(serial.data(), derived.data()) &&
const bool stored = loadKnxSerialNumber(context, serial.data()) &&
deriveFactoryFdskFromSerial(context, serial.data(), derived.data()) &&
std::equal(key.begin(), key.end(), derived.begin());
if (stored) {
syncFactoryFdskToNvs(derived.data());
syncFactoryFdskToNvs(context, derived.data());
}
std::fill(key.begin(), key.end(), 0);
std::fill(derived.begin(), derived.end(), 0);
@@ -338,33 +391,36 @@ bool WriteFactoryFdskHex(const std::string& hex_key, FactoryFdskInfo* info) {
return false;
}
if (info != nullptr) {
*info = LoadFactoryFdskInfo();
*info = LoadFactoryFdskInfoForContext(context);
}
return true;
}
bool ResetFactoryFdskCache(FactoryFdskInfo* info) {
clearOpenKnxFdskCache();
const auto loaded = LoadFactoryFdskInfo();
bool ResetFactoryFdskCacheForContext(const FactoryFdskContext& context,
FactoryFdskInfo* info) {
if (context.clearOpenKnxCache) {
clearOpenKnxFdskCache();
}
const auto loaded = LoadFactoryFdskInfoForContext(context);
if (info != nullptr) {
*info = loaded;
}
return loaded.available;
}
FactoryCertificatePayload BuildFactoryCertificatePayload() {
FactoryCertificatePayload BuildFactoryCertificatePayloadForContext(
const FactoryFdskContext& context) {
FactoryCertificatePayload payload;
const auto info = LoadFactoryFdskInfo();
const auto info = LoadFactoryFdskInfoForContext(context);
if (!info.available) {
return payload;
}
payload.available = true;
payload.productIdentity = kProductIdentity;
payload.manufacturerId = hexValue(gateway::knx_internal::kReg1DaliManufacturerId, 4);
payload.applicationNumber = hexValue(
gateway::knx_internal::kReg1DaliApplicationNumber, 2);
payload.applicationVersion = hexValue(
gateway::knx_internal::kReg1DaliApplicationVersion, 2);
payload.productIdentity = context.productIdentity;
payload.manufacturerId = hexValue(context.manufacturerId, 4);
payload.applicationNumber = hexValue(context.applicationNumber,
context.applicationNumber <= 0xff ? 2 : 4);
payload.applicationVersion = hexValue(context.applicationVersion, 2);
payload.serialNumber = info.serialNumber;
payload.fdskLabel = info.label;
payload.fdskQrCode = info.qrCode;
@@ -377,6 +433,241 @@ FactoryCertificatePayload BuildFactoryCertificatePayload() {
return payload;
}
std::string TunnelUserKeyName(uint8_t index) {
std::array<char, 18> buffer{};
std::snprintf(buffer.data(), buffer.size(), "ipsec_tunnel_%02u",
static_cast<unsigned>(index));
return buffer.data();
}
bool BlobKeyAvailable(nvs_handle_t handle, const char* key) {
std::array<uint8_t, kFdskSize> data{};
size_t len = data.size();
return nvs_get_blob(handle, key, data.data(), &len) == ESP_OK && len == data.size() &&
plausibleKey(data.data());
}
bool SetOptionalHexBlob(nvs_handle_t handle, const char* key, const std::string& hex) {
if (hex.empty()) {
nvs_erase_key(handle, key);
return true;
}
std::array<uint8_t, kFdskSize> data{};
if (!parseHexKey(hex, data.data())) {
return false;
}
const esp_err_t err = nvs_set_blob(handle, key, data.data(), data.size());
std::fill(data.begin(), data.end(), 0);
return err == ESP_OK;
}
bool LoadOptionalKey(nvs_handle_t handle, const char* key, std::array<uint8_t, kFdskSize>* out) {
if (out == nullptr) {
return false;
}
std::array<uint8_t, kFdskSize> data{};
size_t len = data.size();
if (nvs_get_blob(handle, key, data.data(), &len) != ESP_OK || len != data.size() ||
!plausibleKey(data.data())) {
return false;
}
*out = data;
return true;
}
bool LoadFactoryFdsk(uint8_t* data, size_t len) {
return LoadFactoryFdskForContext(kReg1Context, data, len);
}
FactoryFdskInfo LoadFactoryFdskInfo() {
return LoadFactoryFdskInfoForContext(kReg1Context);
}
bool GenerateFactoryFdsk(FactoryFdskInfo* info) {
return GenerateFactoryFdskForContext(kReg1Context, info);
}
bool WriteFactoryFdskHex(const std::string& hex_key, FactoryFdskInfo* info) {
return WriteFactoryFdskHexForContext(kReg1Context, hex_key, info);
}
bool ResetFactoryFdskCache(FactoryFdskInfo* info) {
return ResetFactoryFdskCacheForContext(kReg1Context, info);
}
FactoryCertificatePayload BuildFactoryCertificatePayload() {
return BuildFactoryCertificatePayloadForContext(kReg1Context);
}
bool LoadOamFactoryFdsk(uint8_t* data, size_t len) {
return LoadFactoryFdskForContext(kOamContext, data, len);
}
FactoryFdskInfo LoadOamFactoryFdskInfo() {
return LoadFactoryFdskInfoForContext(kOamContext);
}
bool GenerateOamFactoryFdsk(FactoryFdskInfo* info) {
return GenerateFactoryFdskForContext(kOamContext, info);
}
bool WriteOamFactoryFdskHex(const std::string& hex_key, FactoryFdskInfo* info) {
return WriteFactoryFdskHexForContext(kOamContext, hex_key, info);
}
bool ResetOamFactoryFdskCache(FactoryFdskInfo* info) {
return ResetFactoryFdskCacheForContext(kOamContext, info);
}
FactoryCertificatePayload BuildOamFactoryCertificatePayload() {
return BuildFactoryCertificatePayloadForContext(kOamContext);
}
IpSecureCredentialStatus LoadOamIpSecureCredentialStatus() {
IpSecureCredentialStatus status;
if (!ensureNvsReady()) {
return status;
}
nvs_handle_t handle = 0;
if (nvs_open(kOamNamespace, NVS_READONLY, &handle) != ESP_OK) {
return status;
}
uint8_t activated = 0;
if (nvs_get_u8(handle, kIpSecureActivatedKey, &activated) == ESP_OK) {
status.activated = activated != 0;
}
uint8_t tunnel_count = 0;
if (nvs_get_u8(handle, kIpSecureTunnelCountKey, &tunnel_count) == ESP_OK) {
status.tunnelUserCount = tunnel_count;
}
uint64_t routing_sequence = 0;
if (nvs_get_u64(handle, kIpSecureRoutingSeqKey, &routing_sequence) == ESP_OK) {
status.routingSequence = routing_sequence;
}
status.backboneKeyAvailable = BlobKeyAvailable(handle, kIpSecureBackboneKey);
status.deviceAuthenticationKeyAvailable = BlobKeyAvailable(handle, kIpSecureDeviceAuthKey);
nvs_close(handle);
return status;
}
::gateway::GatewayKnxIpSecureCredentialMaterial LoadOamIpSecureCredentialMaterial() {
::gateway::GatewayKnxIpSecureCredentialMaterial material;
if (!ensureNvsReady()) {
return material;
}
nvs_handle_t handle = 0;
if (nvs_open(kOamNamespace, NVS_READONLY, &handle) != ESP_OK) {
return material;
}
uint8_t activated = 0;
if (nvs_get_u8(handle, kIpSecureActivatedKey, &activated) == ESP_OK) {
material.activated = activated != 0;
}
material.backbone_key_available = LoadOptionalKey(handle, kIpSecureBackboneKey,
&material.backbone_key);
material.device_authentication_key_available =
LoadOptionalKey(handle, kIpSecureDeviceAuthKey,
&material.device_authentication_key);
uint8_t tunnel_count = 0;
if (nvs_get_u8(handle, kIpSecureTunnelCountKey, &tunnel_count) == ESP_OK) {
tunnel_count = std::min<uint8_t>(tunnel_count, 16);
material.tunnel_user_keys.reserve(tunnel_count);
for (uint8_t index = 0; index < tunnel_count; ++index) {
std::array<uint8_t, kFdskSize> key{};
const std::string key_name = TunnelUserKeyName(index);
if (LoadOptionalKey(handle, key_name.c_str(), &key)) {
material.tunnel_user_keys.push_back(key);
}
}
}
uint64_t routing_sequence = 0;
if (nvs_get_u64(handle, kIpSecureRoutingSeqKey, &routing_sequence) == ESP_OK) {
material.routing_sequence = routing_sequence;
}
nvs_close(handle);
return material;
}
bool WriteOamIpSecureKeyringHex(const std::string& backbone_key_hex,
const std::vector<std::string>& tunnel_user_key_hex,
const std::string& device_auth_key_hex,
bool activated) {
if (tunnel_user_key_hex.size() > 16 || !ensureNvsReady()) {
return false;
}
nvs_handle_t handle = 0;
if (nvs_open(kOamNamespace, NVS_READWRITE, &handle) != ESP_OK) {
return false;
}
bool ok = SetOptionalHexBlob(handle, kIpSecureBackboneKey, backbone_key_hex) &&
SetOptionalHexBlob(handle, kIpSecureDeviceAuthKey, device_auth_key_hex);
for (uint8_t index = 0; index < 16; ++index) {
const std::string key = TunnelUserKeyName(index);
nvs_erase_key(handle, key.c_str());
}
if (ok) {
for (size_t index = 0; index < tunnel_user_key_hex.size(); ++index) {
const std::string key = TunnelUserKeyName(static_cast<uint8_t>(index));
ok = SetOptionalHexBlob(handle, key.c_str(), tunnel_user_key_hex[index]);
if (!ok) {
break;
}
}
}
if (ok) {
ok = nvs_set_u8(handle, kIpSecureTunnelCountKey,
static_cast<uint8_t>(tunnel_user_key_hex.size())) == ESP_OK &&
nvs_set_u8(handle, kIpSecureActivatedKey, activated ? 1 : 0) == ESP_OK;
}
if (ok) {
uint64_t existing_sequence = 0;
if (nvs_get_u64(handle, kIpSecureRoutingSeqKey, &existing_sequence) != ESP_OK) {
ok = nvs_set_u64(handle, kIpSecureRoutingSeqKey, 0) == ESP_OK;
}
}
if (ok) {
ok = nvs_commit(handle) == ESP_OK;
}
nvs_close(handle);
return ok;
}
bool StoreOamIpSecureRoutingSequence(uint64_t sequence) {
if (!ensureNvsReady()) {
return false;
}
nvs_handle_t handle = 0;
if (nvs_open(kOamNamespace, NVS_READWRITE, &handle) != ESP_OK) {
return false;
}
const bool ok = nvs_set_u64(handle, kIpSecureRoutingSeqKey, sequence) == ESP_OK &&
nvs_commit(handle) == ESP_OK;
nvs_close(handle);
return ok;
}
bool ClearOamIpSecureKeyring() {
if (!ensureNvsReady()) {
return false;
}
nvs_handle_t handle = 0;
if (nvs_open(kOamNamespace, NVS_READWRITE, &handle) != ESP_OK) {
return false;
}
nvs_erase_key(handle, kIpSecureBackboneKey);
nvs_erase_key(handle, kIpSecureDeviceAuthKey);
nvs_erase_key(handle, kIpSecureTunnelCountKey);
nvs_erase_key(handle, kIpSecureActivatedKey);
nvs_erase_key(handle, kIpSecureRoutingSeqKey);
for (uint8_t index = 0; index < 16; ++index) {
const std::string key = TunnelUserKeyName(index);
nvs_erase_key(handle, key.c_str());
}
const bool ok = nvs_commit(handle) == ESP_OK;
nvs_close(handle);
return ok;
}
} // namespace gateway::openknx
extern "C" bool knx_platform_get_fdsk(uint8_t* data, size_t len) {
+3 -1
View File
@@ -6,10 +6,12 @@ idf_component_register(
"src/gateway_knx_router_openknx.cpp"
"src/gateway_knx_router_packets.cpp"
"src/gateway_knx_router_services.cpp"
"src/gateway_knx_secure_transport.cpp"
"src/oam_router_runtime.cpp"
"src/ets_device_runtime.cpp"
"src/ets_memory_loader.cpp"
INCLUDE_DIRS "include"
REQUIRES dali_cpp esp_driver_gpio esp_driver_uart esp_hw_support esp_netif freertos log lwip knx
REQUIRES dali_cpp esp_driver_gpio esp_driver_uart esp_hw_support esp_netif freertos log lwip knx mbedtls
)
set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17)
+103 -2
View File
@@ -27,6 +27,7 @@ namespace gateway {
namespace openknx {
class EtsDeviceRuntime;
class OamRouterRuntime;
class TpuartUartInterface;
}
@@ -59,6 +60,40 @@ struct GatewayKnxEtsAssociation {
uint16_t group_object_number{0};
};
struct GatewayKnxCloudRemoteConfig {
bool enabled{false};
std::string mode{"mqtt"};
std::string relay_endpoint;
std::string mqtt_topic_prefix;
std::string auth_token_ref;
bool require_secure_tunnel{true};
bool udp_punch_enabled{false};
};
struct GatewayKnxOamRouterConfig {
bool enabled{false};
bool ets_database_enabled{true};
bool secure_tunnel_enabled{true};
bool secure_routing_enabled{true};
uint16_t individual_address{0xff02};
uint16_t tunnel_address_base{0xff10};
int programming_button_gpio{-1};
bool programming_button_active_low{true};
int programming_led_gpio{-1};
bool programming_led_active_high{true};
GatewayKnxCloudRemoteConfig cloud_remote;
};
struct GatewayKnxIpSecureCredentialMaterial {
bool activated{false};
bool backbone_key_available{false};
std::array<uint8_t, 16> backbone_key{};
bool device_authentication_key_available{false};
std::array<uint8_t, 16> device_authentication_key{};
std::vector<std::array<uint8_t, 16>> tunnel_user_keys;
uint64_t routing_sequence{0};
};
struct GatewayKnxConfig {
bool dali_router_enabled{true};
bool ip_router_enabled{false};
@@ -76,6 +111,7 @@ struct GatewayKnxConfig {
bool programming_button_active_low{true};
int programming_led_gpio{-1};
bool programming_led_active_high{true};
GatewayKnxOamRouterConfig oam_router;
std::vector<GatewayKnxEtsAssociation> ets_associations;
GatewayKnxTpUartConfig tp_uart;
};
@@ -134,6 +170,9 @@ struct GatewayKnxReg1ScanOptions {
std::optional<GatewayKnxConfig> GatewayKnxConfigFromValue(const DaliValue* value);
DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config);
std::optional<GatewayKnxOamRouterConfig> GatewayKnxOamRouterConfigFromValue(
const DaliValue* value);
DaliValue GatewayKnxOamRouterConfigToValue(const GatewayKnxOamRouterConfig& config);
bool GatewayKnxConfigUsesTpUart(const GatewayKnxConfig& config);
const char* GatewayKnxMappingModeToString(GatewayKnxMappingMode mode);
@@ -228,6 +267,7 @@ class GatewayKnxTpIpRouter {
using GroupObjectWriteHandler = std::function<DaliBridgeResult(uint16_t group_object_number,
const uint8_t* data,
size_t len)>;
using RoutingSequenceStoreHandler = std::function<void(uint64_t sequence)>;
GatewayKnxTpIpRouter(GatewayKnxBridge& bridge,
std::string openknx_namespace = "openknx");
@@ -237,11 +277,16 @@ class GatewayKnxTpIpRouter {
void setCommissioningOnly(bool enabled);
void setGroupWriteHandler(GroupWriteHandler handler);
void setGroupObjectWriteHandler(GroupObjectWriteHandler handler);
void setOamIpSecureCredentials(const GatewayKnxIpSecureCredentialMaterial& credentials);
void setOamIpSecureRoutingSequenceStoreHandler(RoutingSequenceStoreHandler handler);
const GatewayKnxConfig& config() const;
bool tpUartOnline() const;
bool programmingMode();
esp_err_t setProgrammingMode(bool enabled);
esp_err_t toggleProgrammingMode();
bool oamProgrammingMode();
esp_err_t setOamProgrammingMode(bool enabled);
esp_err_t toggleOamProgrammingMode();
esp_err_t start(uint32_t task_stack_size, UBaseType_t task_priority);
esp_err_t stop();
@@ -285,6 +330,24 @@ class GatewayKnxTpIpRouter {
::sockaddr_in data_remote{};
std::vector<uint8_t> last_received_cemi;
std::vector<uint8_t> last_tunnel_confirmation_packet;
uint16_t secure_session_id{0};
bool oam_router_persona{false};
};
struct SecureSession {
bool active{false};
bool authenticated{false};
uint16_t session_id{0};
int tcp_sock{-1};
::sockaddr_in remote{};
std::array<uint8_t, 32> client_public_key{};
std::array<uint8_t, 32> server_public_key{};
std::array<uint8_t, 32> shared_secret{};
std::array<uint8_t, 16> session_key{};
uint64_t send_sequence{0};
uint64_t receive_sequence{0};
uint8_t user_id{0};
TickType_t last_activity_tick{0};
};
static void TaskEntry(void* arg);
@@ -297,6 +360,7 @@ class GatewayKnxTpIpRouter {
void handleTcpAccept();
void handleTcpClient(TcpClient& client);
void closeTcpClient(TcpClient& client);
void closeSecureSessionsForTcp(int sock);
std::unique_ptr<openknx::TpuartUartInterface> createOpenKnxTpUartInterface();
bool configureTpUart();
bool configureProgrammingGpio();
@@ -316,6 +380,12 @@ class GatewayKnxTpIpRouter {
void handleDisconnectRequest(const uint8_t* packet_data, size_t len, const ::sockaddr_in& remote);
void handleSecureService(uint16_t service, const uint8_t* body, size_t len,
const ::sockaddr_in& remote);
void handleSecureSessionRequest(const uint8_t* body, size_t len,
const ::sockaddr_in& remote);
void handleSecureWrapper(const uint8_t* body, size_t len,
const ::sockaddr_in& remote);
void handleSecureGroupSync(const uint8_t* body, size_t len,
const ::sockaddr_in& remote);
void sendTunnellingAck(uint8_t channel_id, uint8_t sequence, uint8_t status,
const ::sockaddr_in& remote);
void sendDeviceConfigurationAck(uint8_t channel_id, uint8_t sequence, uint8_t status,
@@ -335,9 +405,9 @@ class GatewayKnxTpIpRouter {
const ::sockaddr_in& remote, uint8_t connection_type,
uint16_t tunnel_address);
void sendRoutingIndication(const uint8_t* data, size_t len);
bool sendPacket(const std::vector<uint8_t>& packet, const ::sockaddr_in& remote) const;
bool sendPacket(const std::vector<uint8_t>& packet, const ::sockaddr_in& remote);
bool sendPacketToTunnelClient(const TunnelClient& client,
const std::vector<uint8_t>& packet) const;
const std::vector<uint8_t>& packet);
bool currentTransportAllowsTcpHpai() const;
std::optional<std::array<uint8_t, 8>> localHpaiForRemote(const ::sockaddr_in& remote,
bool tcp = false) const;
@@ -359,6 +429,22 @@ class GatewayKnxTpIpRouter {
const ::sockaddr_in& data_remote,
uint8_t connection_type);
void resetTunnelClient(TunnelClient& client);
SecureSession* findSecureSession(uint16_t session_id, const ::sockaddr_in& remote);
const SecureSession* findSecureSession(uint16_t session_id,
const ::sockaddr_in& remote) const;
SecureSession* allocateSecureSession(const ::sockaddr_in& remote);
SecureSession* activeSecureSession();
bool wrapSecurePacket(SecureSession& session, const std::vector<uint8_t>& inner,
std::vector<uint8_t>* wrapped);
bool wrapSecureRoutingPacket(const std::vector<uint8_t>& inner,
std::vector<uint8_t>* wrapped);
bool decryptSecureWrapper(SecureSession& session, const uint8_t* body, size_t len,
std::vector<uint8_t>* inner);
bool decryptSecureRoutingWrapper(const uint8_t* body, size_t len,
std::vector<uint8_t>* inner);
bool verifySecureSessionAuth(SecureSession& session, const uint8_t* packet,
size_t len, uint8_t* status);
bool secureCredentialsReady() const;
uint8_t nextTunnelChannelId() const;
uint16_t effectiveTunnelAddressForSlot(size_t slot) const;
void pruneStaleTunnelClients();
@@ -366,6 +452,10 @@ class GatewayKnxTpIpRouter {
TunnelClient* response_client, uint16_t response_service,
const uint8_t* suppress_routing_echo = nullptr,
size_t suppress_routing_echo_len = 0);
bool handleOamRouterTunnelFrame(const uint8_t* data, size_t len,
TunnelClient* response_client, uint16_t response_service,
const uint8_t* suppress_routing_echo = nullptr,
size_t suppress_routing_echo_len = 0);
bool handleOpenKnxBusFrame(const uint8_t* data, size_t len);
bool transmitOpenKnxTpFrame(const uint8_t* data, size_t len);
void selectOpenKnxNetworkInterface(const ::sockaddr_in& remote);
@@ -380,13 +470,16 @@ class GatewayKnxTpIpRouter {
void pollProgrammingButton();
void updateProgrammingLed();
void setProgrammingLed(bool on);
void setOamProgrammingLed(bool on);
GatewayKnxBridge& bridge_;
GroupWriteHandler group_write_handler_;
GroupObjectWriteHandler group_object_write_handler_;
RoutingSequenceStoreHandler routing_sequence_store_handler_;
std::string openknx_namespace_;
GatewayKnxConfig config_;
std::unique_ptr<openknx::EtsDeviceRuntime> ets_device_;
std::unique_ptr<openknx::OamRouterRuntime> oam_router_;
TaskHandle_t task_handle_{nullptr};
SemaphoreHandle_t openknx_lock_{nullptr};
SemaphoreHandle_t startup_semaphore_{nullptr};
@@ -403,14 +496,22 @@ class GatewayKnxTpIpRouter {
TickType_t network_refresh_tick_{0};
std::array<TcpClient, kMaxTcpClients> tcp_clients_{};
std::array<TunnelClient, kMaxTunnelClients> tunnel_clients_{};
std::array<SecureSession, kMaxTcpClients> secure_sessions_{};
std::unique_ptr<IpParameterObject> knx_ip_parameters_;
uint8_t last_tunnel_channel_id_{0};
uint16_t last_secure_session_id_{0};
uint16_t active_secure_session_id_{0};
GatewayKnxIpSecureCredentialMaterial oam_ip_secure_credentials_{};
bool tp_uart_online_{false};
bool commissioning_only_{false};
std::atomic_bool openknx_configured_{false};
bool programming_button_last_pressed_{false};
bool programming_led_state_{false};
TickType_t programming_button_last_toggle_tick_{0};
bool oam_programming_mode_{false};
bool oam_programming_button_last_pressed_{false};
bool oam_programming_led_state_{false};
TickType_t oam_programming_button_last_toggle_tick_{0};
std::string last_error_;
};
@@ -32,6 +32,22 @@ constexpr const char* kTag = "gateway_knx";
#define CONFIG_GATEWAY_KNX_OEM_HARDWARE_ID 0xA401
#endif
#ifndef CONFIG_GATEWAY_KNX_OAM_ROUTER_OEM_MANUFACTURER_ID
#define CONFIG_GATEWAY_KNX_OAM_ROUTER_OEM_MANUFACTURER_ID 0x00FA
#endif
#ifndef CONFIG_GATEWAY_KNX_OAM_ROUTER_APPLICATION_NUMBER
#define CONFIG_GATEWAY_KNX_OAM_ROUTER_APPLICATION_NUMBER 0xA11F
#endif
#ifndef CONFIG_GATEWAY_KNX_OAM_ROUTER_APPLICATION_VERSION
#define CONFIG_GATEWAY_KNX_OAM_ROUTER_APPLICATION_VERSION 0x07
#endif
#ifndef CONFIG_GATEWAY_KNX_OAM_ROUTER_HARDWARE_ID
#define CONFIG_GATEWAY_KNX_OAM_ROUTER_HARDWARE_ID 0x0001
#endif
inline constexpr uint16_t kReg1DaliManufacturerId =
static_cast<uint16_t>(CONFIG_GATEWAY_KNX_OEM_MANUFACTURER_ID);
inline constexpr uint16_t kReg1DaliHardwareId =
@@ -56,6 +72,33 @@ inline constexpr uint8_t kReg1DaliProgramVersion[5] = {
static_cast<uint8_t>(kReg1DaliApplicationNumber & 0xff),
kReg1DaliApplicationVersion};
inline constexpr uint32_t kReg1DaliSerialMacIncrement = 0;
inline constexpr uint32_t kOamRouterSerialMacIncrement = 1;
inline constexpr uint16_t kOamRouterDeviceDescriptor = 0x091A;
inline constexpr uint16_t kOamRouterManufacturerId =
static_cast<uint16_t>(CONFIG_GATEWAY_KNX_OAM_ROUTER_OEM_MANUFACTURER_ID);
inline constexpr uint16_t kOamRouterHardwareId =
static_cast<uint16_t>(CONFIG_GATEWAY_KNX_OAM_ROUTER_HARDWARE_ID);
inline constexpr uint16_t kOamRouterApplicationNumber =
static_cast<uint16_t>(CONFIG_GATEWAY_KNX_OAM_ROUTER_APPLICATION_NUMBER);
inline constexpr uint8_t kOamRouterApplicationVersion =
static_cast<uint8_t>(CONFIG_GATEWAY_KNX_OAM_ROUTER_APPLICATION_VERSION);
inline constexpr uint8_t kOamRouterHardwareType[6] = {
0x00,
0x00,
static_cast<uint8_t>((kOamRouterHardwareId >> 8) & 0xff),
static_cast<uint8_t>(kOamRouterHardwareId & 0xff),
kOamRouterApplicationVersion,
0x00};
inline constexpr uint8_t kOamRouterOrderNumber[10] = {
'I', 'P', '-', 'R', 'o', 'u', 't', 'e', 'r', 0};
inline constexpr uint8_t kOamRouterProgramVersion[5] = {
static_cast<uint8_t>((kOamRouterManufacturerId >> 8) & 0xff),
static_cast<uint8_t>(kOamRouterManufacturerId & 0xff),
static_cast<uint8_t>((kOamRouterApplicationNumber >> 8) & 0xff),
static_cast<uint8_t>(kOamRouterApplicationNumber & 0xff),
kOamRouterApplicationVersion};
// RAII semaphore guard.
class SemaphoreGuard {
public:
@@ -0,0 +1,63 @@
#pragma once
#include "esp_idf_platform.h"
#include "ets_memory_loader.h"
#include "esp_netif.h"
#include "freertos/FreeRTOS.h"
#include "knx/cemi_frame.h"
#include "knx/device_object.h"
#include "knx/platform.h"
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <vector>
#if defined(ENABLE_BAU091A_PERSONA)
#include "knx/bau091A.h"
#endif
namespace gateway::openknx {
class OamRouterRuntime {
public:
using CemiFrameSender = std::function<void(const uint8_t* data, size_t len)>;
OamRouterRuntime(std::string nvs_namespace,
uint16_t fallback_individual_address,
uint16_t tunnel_client_address = 0);
~OamRouterRuntime();
bool available() const;
uint16_t individualAddress() const;
uint16_t tunnelClientAddress() const;
bool configured() const;
bool programmingMode() const;
void setProgrammingMode(bool enabled);
void toggleProgrammingMode();
EtsMemorySnapshot snapshot() const;
DeviceObject* deviceObject();
Platform* platform();
void setNetworkInterface(esp_netif_t* netif);
bool handleTunnelFrame(const uint8_t* data, size_t len, CemiFrameSender sender);
void loop();
private:
static bool HandleOutboundCemiFrame(CemiFrame& frame, void* context);
static void EmitTunnelFrame(CemiFrame& frame, void* context);
static uint16_t DefaultTunnelClientAddress(uint16_t individual_address);
bool shouldConsumeTunnelFrame(CemiFrame& frame) const;
std::string nvs_namespace_;
CemiFrameSender sender_;
#if defined(ENABLE_BAU091A_PERSONA)
EspIdfPlatform platform_;
Bau091A device_;
#endif
};
} // namespace gateway::openknx
+119
View File
@@ -2,6 +2,115 @@
namespace gateway {
namespace {
GatewayKnxCloudRemoteConfig GatewayKnxCloudRemoteConfigFromValue(const DaliValue* value) {
GatewayKnxCloudRemoteConfig config;
if (value == nullptr || value->asObject() == nullptr) {
return config;
}
const auto& object = *value->asObject();
config.enabled = ObjectBoolAny(object, {"enabled", "cloudRemoteEnabled",
"cloud_remote_enabled"})
.value_or(config.enabled);
config.mode = ObjectStringAny(object, {"mode", "remoteMode", "remote_mode"})
.value_or(config.mode);
config.relay_endpoint = ObjectStringAny(object, {"relayEndpoint", "relay_endpoint",
"endpoint"})
.value_or(config.relay_endpoint);
config.mqtt_topic_prefix = ObjectStringAny(object, {"mqttTopicPrefix", "mqtt_topic_prefix",
"topicPrefix", "topic_prefix"})
.value_or(config.mqtt_topic_prefix);
config.auth_token_ref = ObjectStringAny(object, {"authTokenRef", "auth_token_ref",
"tokenRef", "token_ref"})
.value_or(config.auth_token_ref);
config.require_secure_tunnel = ObjectBoolAny(object, {"requireSecureTunnel",
"require_secure_tunnel"})
.value_or(config.require_secure_tunnel);
config.udp_punch_enabled = ObjectBoolAny(object, {"udpPunchEnabled", "udp_punch_enabled"})
.value_or(config.udp_punch_enabled);
return config;
}
DaliValue GatewayKnxCloudRemoteConfigToValue(const GatewayKnxCloudRemoteConfig& config) {
DaliValue::Object out;
out["enabled"] = config.enabled;
out["mode"] = config.mode;
out["relayEndpoint"] = config.relay_endpoint;
out["mqttTopicPrefix"] = config.mqtt_topic_prefix;
out["authTokenRef"] = config.auth_token_ref;
out["requireSecureTunnel"] = config.require_secure_tunnel;
out["udpPunchEnabled"] = config.udp_punch_enabled;
return DaliValue(std::move(out));
}
} // namespace
std::optional<GatewayKnxOamRouterConfig> GatewayKnxOamRouterConfigFromValue(
const DaliValue* value) {
if (value == nullptr || value->asObject() == nullptr) {
return std::nullopt;
}
const auto& object = *value->asObject();
GatewayKnxOamRouterConfig config;
config.enabled = ObjectBoolAny(object, {"enabled", "oamRouterEnabled",
"oam_router_enabled"})
.value_or(config.enabled);
config.ets_database_enabled = ObjectBoolAny(object, {"etsDatabaseEnabled",
"ets_database_enabled"})
.value_or(config.ets_database_enabled);
config.secure_tunnel_enabled = ObjectBoolAny(object, {"secureTunnelEnabled",
"secure_tunnel_enabled"})
.value_or(config.secure_tunnel_enabled);
config.secure_routing_enabled = ObjectBoolAny(object, {"secureRoutingEnabled",
"secure_routing_enabled"})
.value_or(config.secure_routing_enabled);
config.individual_address = static_cast<uint16_t>(std::clamp(
ObjectIntAny(object, {"individualAddress", "individual_address", "routerAddress",
"router_address"})
.value_or(config.individual_address),
0, 0xffff));
config.tunnel_address_base = static_cast<uint16_t>(std::clamp(
ObjectIntAny(object, {"tunnelAddressBase", "tunnel_address_base",
"tunnelBaseAddress", "tunnel_base_address"})
.value_or(config.tunnel_address_base),
0, 0xffff));
config.programming_button_gpio = std::clamp(
ObjectIntAny(object, {"programmingButtonGpio", "programming_button_gpio"})
.value_or(config.programming_button_gpio),
-1, 48);
config.programming_button_active_low =
ObjectBoolAny(object, {"programmingButtonActiveLow", "programming_button_active_low"})
.value_or(config.programming_button_active_low);
config.programming_led_gpio = std::clamp(
ObjectIntAny(object, {"programmingLedGpio", "programming_led_gpio"})
.value_or(config.programming_led_gpio),
-1, 48);
config.programming_led_active_high =
ObjectBoolAny(object, {"programmingLedActiveHigh", "programming_led_active_high"})
.value_or(config.programming_led_active_high);
config.cloud_remote = GatewayKnxCloudRemoteConfigFromValue(
ObjectValueAny(object, {"cloudRemote", "cloud_remote", "remoteAccess",
"remote_access"}));
return config;
}
DaliValue GatewayKnxOamRouterConfigToValue(const GatewayKnxOamRouterConfig& config) {
DaliValue::Object out;
out["enabled"] = config.enabled;
out["etsDatabaseEnabled"] = config.ets_database_enabled;
out["secureTunnelEnabled"] = config.secure_tunnel_enabled;
out["secureRoutingEnabled"] = config.secure_routing_enabled;
out["individualAddress"] = static_cast<int>(config.individual_address);
out["tunnelAddressBase"] = static_cast<int>(config.tunnel_address_base);
out["programmingButtonGpio"] = config.programming_button_gpio;
out["programmingButtonActiveLow"] = config.programming_button_active_low;
out["programmingLedGpio"] = config.programming_led_gpio;
out["programmingLedActiveHigh"] = config.programming_led_active_high;
out["cloudRemote"] = GatewayKnxCloudRemoteConfigToValue(config.cloud_remote);
return DaliValue(std::move(out));
}
std::optional<GatewayKnxConfig> GatewayKnxConfigFromValue(const DaliValue* value) {
if (value == nullptr || value->asObject() == nullptr) {
return std::nullopt;
@@ -65,6 +174,15 @@ std::optional<GatewayKnxConfig> GatewayKnxConfigFromValue(const DaliValue* value
config.programming_led_active_high =
ObjectBoolAny(object, {"programmingLedActiveHigh", "programming_led_active_high"})
.value_or(config.programming_led_active_high);
if (const auto oam_router = GatewayKnxOamRouterConfigFromValue(
ObjectValueAny(object, {"oamRouter", "oam_router", "routerApplication",
"router_application"}))) {
config.oam_router = oam_router.value();
} else {
config.oam_router.enabled = ObjectBoolAny(object, {"oamRouterEnabled",
"oam_router_enabled"})
.value_or(config.oam_router.enabled);
}
const auto* tp_uart = getObjectValue(object, "tpUart");
if (tp_uart == nullptr) {
@@ -117,6 +235,7 @@ DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config) {
out["programmingButtonActiveLow"] = config.programming_button_active_low;
out["programmingLedGpio"] = config.programming_led_gpio;
out["programmingLedActiveHigh"] = config.programming_led_active_high;
out["oamRouter"] = GatewayKnxOamRouterConfigToValue(config.oam_router);
DaliValue::Object serial;
serial["uartPort"] = config.tp_uart.uart_port;
serial["txPin"] = config.tp_uart.tx_pin;
@@ -12,6 +12,7 @@
#include "lwip/sockets.h"
#include "ets_device_runtime.h"
#include "gateway_knx_internal.h"
#include "oam_router_runtime.h"
#include "soc/uart_periph.h"
#include "tpuart_uart_interface.h"
@@ -105,6 +106,7 @@ constexpr uint8_t kKnxServiceFamilyCore = 0x02;
constexpr uint8_t kKnxServiceFamilyDeviceManagement = 0x03;
constexpr uint8_t kKnxServiceFamilyTunnelling = 0x04;
constexpr uint8_t kKnxServiceFamilyRouting = 0x05;
constexpr uint8_t kKnxServiceFamilySecurity = 0x06;
constexpr uint16_t kKnxIpOnlyDeviceDescriptor = 0x57b0;
constexpr uint16_t kKnxTpIpInterfaceDeviceDescriptor = 0x091a;
constexpr uint8_t kKnxIpAssignmentManual = 0x01;
@@ -848,6 +850,18 @@ bool MatchesOpenKnxLocalIndividualAddress(const CemiFrame& frame,
return dest == own_address || dest == client_address || (commissioning && dest == 0xffff);
}
bool MatchesOamRouterLocalIndividualAddress(const CemiFrame& frame,
const openknx::OamRouterRuntime& oam_router) {
if (frame.addressType() != IndividualAddress) {
return false;
}
const uint16_t dest = frame.destinationAddress();
const uint16_t own_address = oam_router.individualAddress();
const uint16_t client_address = oam_router.tunnelClientAddress();
const bool commissioning = !oam_router.configured() || oam_router.programmingMode();
return dest == own_address || dest == client_address || (commissioning && dest == 0xffff);
}
bool BuildLocalRoutingTunnelFrame(const uint8_t* data, size_t len,
std::vector<uint8_t>* local_frame) {
if (data == nullptr || local_frame == nullptr || len < 2) {
@@ -36,6 +36,16 @@ void GatewayKnxTpIpRouter::setGroupObjectWriteHandler(GroupObjectWriteHandler ha
group_object_write_handler_ = std::move(handler);
}
void GatewayKnxTpIpRouter::setOamIpSecureCredentials(
const GatewayKnxIpSecureCredentialMaterial& credentials) {
oam_ip_secure_credentials_ = credentials;
}
void GatewayKnxTpIpRouter::setOamIpSecureRoutingSequenceStoreHandler(
RoutingSequenceStoreHandler handler) {
routing_sequence_store_handler_ = std::move(handler);
}
const GatewayKnxConfig& GatewayKnxTpIpRouter::config() const { return config_; }
bool GatewayKnxTpIpRouter::tpUartOnline() const { return tp_uart_online_; }
@@ -69,6 +79,38 @@ esp_err_t GatewayKnxTpIpRouter::toggleProgrammingMode() {
return setProgrammingMode(!programmingMode());
}
bool GatewayKnxTpIpRouter::oamProgrammingMode() {
if (openknx_lock_ == nullptr) {
return false;
}
SemaphoreGuard guard(openknx_lock_);
return oam_router_ != nullptr ? oam_router_->programmingMode() : oam_programming_mode_;
}
esp_err_t GatewayKnxTpIpRouter::setOamProgrammingMode(bool enabled) {
if (openknx_lock_ == nullptr) {
last_error_ = "KNX runtime lock is unavailable";
return ESP_ERR_INVALID_STATE;
}
if (!config_.oam_router.enabled) {
last_error_ = "OAM KNX/IP router persona is disabled";
return ESP_ERR_NOT_SUPPORTED;
}
SemaphoreGuard guard(openknx_lock_);
oam_programming_mode_ = enabled;
if (oam_router_ != nullptr) {
oam_router_->setProgrammingMode(enabled);
}
setOamProgrammingLed(enabled);
ESP_LOGI(kTag, "OAM KNX/IP router programming mode %s namespace=%s",
enabled ? "enabled" : "disabled", openknx_namespace_.c_str());
return ESP_OK;
}
esp_err_t GatewayKnxTpIpRouter::toggleOamProgrammingMode() {
return setOamProgrammingMode(!oamProgrammingMode());
}
esp_err_t GatewayKnxTpIpRouter::start(uint32_t task_stack_size, UBaseType_t task_priority) {
if (started_ || task_handle_ != nullptr) {
return ESP_OK;
@@ -193,6 +235,19 @@ esp_err_t GatewayKnxTpIpRouter::initializeRuntime() {
bridge_.setRuntimeContext(ets_device_.get());
knx_ip_parameters_ = std::make_unique<IpParameterObject>(
ets_device_->deviceObject(), ets_device_->platform());
if (config_.oam_router.enabled) {
oam_router_ = std::make_unique<openknx::OamRouterRuntime>(
openknx_namespace_ + "_oam", config_.oam_router.individual_address,
config_.oam_router.tunnel_address_base);
if (oam_router_->available()) {
oam_router_->setProgrammingMode(oam_programming_mode_);
} else {
ESP_LOGW(kTag, "OAM router persona requested but BAU091A support is not compiled in");
oam_router_.reset();
}
} else {
oam_router_.reset();
}
openknx_configured_.store(ets_device_->configured());
ESP_LOGI(kTag,
"OpenKNX runtime namespace=%s configured=%d ipInterface=0x%04x "
@@ -200,6 +255,14 @@ esp_err_t GatewayKnxTpIpRouter::initializeRuntime() {
openknx_namespace_.c_str(), ets_device_->configured(),
effectiveIpInterfaceIndividualAddress(), ets_device_->individualAddress(),
ets_device_->tunnelClientAddress(), commissioning_only_);
if (oam_router_ != nullptr) {
ESP_LOGI(kTag,
"OAM router persona namespace=%s_oam configured=%d device=0x%04x tunnelClient=0x%04x secureTunnel=%d secureRouting=%d",
openknx_namespace_.c_str(), oam_router_->configured(),
oam_router_->individualAddress(), oam_router_->tunnelClientAddress(),
config_.oam_router.secure_tunnel_enabled,
config_.oam_router.secure_routing_enabled);
}
ets_device_->setFunctionPropertyHandlers(
[this](uint8_t object_index, uint8_t property_id, const uint8_t* data, size_t len,
std::vector<uint8_t>* response) {
@@ -300,6 +363,10 @@ void GatewayKnxTpIpRouter::taskLoop() {
if (ets_device_ != nullptr) {
pollProgrammingButton();
ets_device_->loop();
if (oam_router_ != nullptr) {
oam_router_->loop();
oam_programming_mode_ = oam_router_->programmingMode();
}
tp_uart_online_ = ets_device_->tpUartOnline();
updateProgrammingLed();
}
@@ -376,8 +443,11 @@ void GatewayKnxTpIpRouter::finishTask() {
{
SemaphoreGuard guard(openknx_lock_);
setProgrammingLed(false);
setOamProgrammingLed(false);
oam_programming_mode_ = false;
knx_ip_parameters_.reset();
bridge_.setRuntimeContext(nullptr);
oam_router_.reset();
ets_device_.reset();
openknx_configured_.store(false);
}
@@ -387,32 +457,57 @@ void GatewayKnxTpIpRouter::finishTask() {
}
void GatewayKnxTpIpRouter::pollProgrammingButton() {
if (config_.programming_button_gpio < 0 || ets_device_ == nullptr) {
const TickType_t now = xTaskGetTickCount();
if (config_.programming_button_gpio >= 0 && ets_device_ != nullptr) {
const int level = gpio_get_level(static_cast<gpio_num_t>(config_.programming_button_gpio));
const bool pressed = config_.programming_button_active_low ? level == 0 : level != 0;
if (pressed && !programming_button_last_pressed_ &&
now - programming_button_last_toggle_tick_ >= pdMS_TO_TICKS(200)) {
ets_device_->toggleProgrammingMode();
ESP_LOGI(kTag, "KNX programming mode %s namespace=%s",
ets_device_->programmingMode() ? "enabled" : "disabled",
openknx_namespace_.c_str());
programming_button_last_toggle_tick_ = now;
}
programming_button_last_pressed_ = pressed;
}
if (!config_.oam_router.enabled || config_.oam_router.programming_button_gpio < 0) {
return;
}
const int level = gpio_get_level(static_cast<gpio_num_t>(config_.programming_button_gpio));
const bool pressed = config_.programming_button_active_low ? level == 0 : level != 0;
const TickType_t now = xTaskGetTickCount();
if (pressed && !programming_button_last_pressed_ &&
now - programming_button_last_toggle_tick_ >= pdMS_TO_TICKS(200)) {
ets_device_->toggleProgrammingMode();
ESP_LOGI(kTag, "KNX programming mode %s namespace=%s",
ets_device_->programmingMode() ? "enabled" : "disabled",
const int oam_level = gpio_get_level(
static_cast<gpio_num_t>(config_.oam_router.programming_button_gpio));
const bool oam_pressed = config_.oam_router.programming_button_active_low
? oam_level == 0
: oam_level != 0;
if (oam_pressed && !oam_programming_button_last_pressed_ &&
now - oam_programming_button_last_toggle_tick_ >= pdMS_TO_TICKS(200)) {
oam_programming_mode_ = !oam_programming_mode_;
if (oam_router_ != nullptr) {
oam_router_->setProgrammingMode(oam_programming_mode_);
}
setOamProgrammingLed(oam_programming_mode_);
ESP_LOGI(kTag, "OAM KNX/IP router programming mode %s namespace=%s",
oam_programming_mode_ ? "enabled" : "disabled",
openknx_namespace_.c_str());
programming_button_last_toggle_tick_ = now;
oam_programming_button_last_toggle_tick_ = now;
}
programming_button_last_pressed_ = pressed;
oam_programming_button_last_pressed_ = oam_pressed;
}
void GatewayKnxTpIpRouter::updateProgrammingLed() {
if (config_.programming_led_gpio < 0 || ets_device_ == nullptr) {
return;
if (config_.programming_led_gpio >= 0 && ets_device_ != nullptr) {
const bool programming_mode = ets_device_->programmingMode();
if (programming_mode != programming_led_state_) {
setProgrammingLed(programming_mode);
}
}
const bool programming_mode = ets_device_->programmingMode();
if (programming_mode == programming_led_state_) {
return;
if (config_.oam_router.enabled && config_.oam_router.programming_led_gpio >= 0 &&
oam_programming_mode_ != oam_programming_led_state_) {
setOamProgrammingLed(oam_programming_mode_);
}
setProgrammingLed(programming_mode);
}
void GatewayKnxTpIpRouter::setProgrammingLed(bool on) {
@@ -425,6 +520,17 @@ void GatewayKnxTpIpRouter::setProgrammingLed(bool on) {
programming_led_state_ = on;
}
void GatewayKnxTpIpRouter::setOamProgrammingLed(bool on) {
if (config_.oam_router.programming_led_gpio < 0) {
oam_programming_led_state_ = on;
return;
}
const bool level = config_.oam_router.programming_led_active_high ? on : !on;
gpio_set_level(static_cast<gpio_num_t>(config_.oam_router.programming_led_gpio),
level ? 1 : 0);
oam_programming_led_state_ = on;
}
void GatewayKnxTpIpRouter::closeSockets() {
if (udp_sock_ >= 0) {
shutdown(udp_sock_, SHUT_RDWR);
@@ -609,6 +715,7 @@ void GatewayKnxTpIpRouter::closeTcpClient(TcpClient& client) {
resetTunnelClient(tunnel);
}
}
closeSecureSessionsForTcp(sock);
if (active_tcp_sock_ == sock) {
active_tcp_sock_ = -1;
}
@@ -754,6 +861,9 @@ bool GatewayKnxTpIpRouter::configureProgrammingGpio() {
programming_button_last_pressed_ = false;
programming_button_last_toggle_tick_ = 0;
programming_led_state_ = false;
oam_programming_button_last_pressed_ = false;
oam_programming_button_last_toggle_tick_ = 0;
oam_programming_led_state_ = false;
if (config_.programming_button_gpio >= 0) {
gpio_config_t button_config{};
@@ -792,6 +902,47 @@ bool GatewayKnxTpIpRouter::configureProgrammingGpio() {
setProgrammingLed(false);
}
if (config_.oam_router.enabled && config_.oam_router.programming_button_gpio >= 0) {
gpio_config_t button_config{};
button_config.pin_bit_mask =
1ULL << static_cast<uint32_t>(config_.oam_router.programming_button_gpio);
button_config.mode = GPIO_MODE_INPUT;
button_config.pull_up_en = config_.oam_router.programming_button_active_low
? GPIO_PULLUP_ENABLE
: GPIO_PULLUP_DISABLE;
button_config.pull_down_en = config_.oam_router.programming_button_active_low
? GPIO_PULLDOWN_DISABLE
: GPIO_PULLDOWN_ENABLE;
button_config.intr_type = GPIO_INTR_DISABLE;
const esp_err_t err = gpio_config(&button_config);
if (err != ESP_OK) {
last_error_ = EspErrDetail("failed to configure OAM KNX programming button GPIO" +
std::to_string(config_.oam_router.programming_button_gpio),
err);
ESP_LOGE(kTag, "%s", last_error_.c_str());
return false;
}
}
if (config_.oam_router.enabled && config_.oam_router.programming_led_gpio >= 0) {
gpio_config_t led_config{};
led_config.pin_bit_mask =
1ULL << static_cast<uint32_t>(config_.oam_router.programming_led_gpio);
led_config.mode = GPIO_MODE_OUTPUT;
led_config.pull_up_en = GPIO_PULLUP_DISABLE;
led_config.pull_down_en = GPIO_PULLDOWN_DISABLE;
led_config.intr_type = GPIO_INTR_DISABLE;
const esp_err_t err = gpio_config(&led_config);
if (err != ESP_OK) {
last_error_ = EspErrDetail("failed to configure OAM KNX programming LED GPIO" +
std::to_string(config_.oam_router.programming_led_gpio),
err);
ESP_LOGE(kTag, "%s", last_error_.c_str());
return false;
}
setOamProgrammingLed(false);
}
return true;
}
@@ -8,6 +8,9 @@ void GatewayKnxTpIpRouter::selectOpenKnxNetworkInterface(const sockaddr_in& remo
if (ets_device_ != nullptr) {
ets_device_->setNetworkInterface(netif.has_value() ? netif->netif : nullptr);
}
if (oam_router_ != nullptr) {
oam_router_->setNetworkInterface(netif.has_value() ? netif->netif : nullptr);
}
}
bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t len,
@@ -15,6 +18,20 @@ bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t
uint16_t response_service,
const uint8_t* suppress_routing_echo,
size_t suppress_routing_echo_len) {
bool route_to_oam = response_client != nullptr && response_client->oam_router_persona;
if (!route_to_oam && data != nullptr && len >= 2) {
std::vector<uint8_t> frame_data(data, data + len);
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(frame_data.size()));
if (frame.valid() && oam_router_ != nullptr &&
MatchesOamRouterLocalIndividualAddress(frame, *oam_router_)) {
route_to_oam = true;
}
}
if (route_to_oam) {
return handleOamRouterTunnelFrame(data, len, response_client, response_service,
suppress_routing_echo, suppress_routing_echo_len);
}
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ == nullptr) {
return false;
@@ -87,6 +104,82 @@ bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t
return consumed;
}
bool GatewayKnxTpIpRouter::handleOamRouterTunnelFrame(const uint8_t* data, size_t len,
TunnelClient* response_client,
uint16_t response_service,
const uint8_t* suppress_routing_echo,
size_t suppress_routing_echo_len) {
SemaphoreGuard guard(openknx_lock_);
if (oam_router_ == nullptr) {
return false;
}
std::vector<uint8_t> tunnel_confirmation;
const bool needs_tunnel_confirmation =
response_client != nullptr && response_client->connected &&
response_service == kServiceTunnellingRequest &&
BuildTunnelConfirmationFrame(data, len, &tunnel_confirmation);
bool sent_tunnel_confirmation = false;
const bool consumed = oam_router_->handleTunnelFrame(
data, len,
[this, response_client, response_service, needs_tunnel_confirmation,
&tunnel_confirmation, &sent_tunnel_confirmation,
suppress_routing_echo, suppress_routing_echo_len](const uint8_t* response,
size_t response_len) {
if (response == nullptr || response_len == 0) {
return;
}
const bool routing_context =
response_client == nullptr && response_service == kServiceRoutingIndication;
const auto message_code = CemiMessageCode(response, response_len);
if (routing_context && suppress_routing_echo != nullptr &&
IsLocalRoutingEchoIndication(response, response_len, suppress_routing_echo,
suppress_routing_echo_len)) {
return;
}
if (needs_tunnel_confirmation && !sent_tunnel_confirmation &&
message_code.has_value() && message_code.value() != L_data_con) {
sent_tunnel_confirmation = sendCemiFrameToClient(
*response_client, kServiceTunnellingRequest,
tunnel_confirmation.data(), tunnel_confirmation.size());
}
const uint16_t service = KnxIpServiceForCemi(response, response_len, response_service);
if (service == kServiceDeviceConfigurationRequest) {
if (response_client != nullptr && response_client->connected) {
sendCemiFrameToClient(*response_client, service, response, response_len);
} else if (routing_context) {
sendRoutingIndication(response, response_len);
}
return;
}
if (message_code.has_value() && message_code.value() == L_data_con) {
if (routing_context) {
return;
}
if (response_client != nullptr && response_client->connected) {
sent_tunnel_confirmation =
sendCemiFrameToClient(*response_client, service, response, response_len) ||
sent_tunnel_confirmation;
}
return;
}
if (routing_context) {
sendRoutingIndication(response, response_len);
return;
}
if (response_client != nullptr && response_client->connected) {
sendCemiFrameToClient(*response_client, service, response, response_len);
return;
}
sendTunnelIndication(response, response_len);
});
if (needs_tunnel_confirmation && consumed && !sent_tunnel_confirmation) {
sendCemiFrameToClient(*response_client, kServiceTunnellingRequest,
tunnel_confirmation.data(), tunnel_confirmation.size());
}
return consumed;
}
bool GatewayKnxTpIpRouter::transmitOpenKnxTpFrame(const uint8_t* data, size_t len) {
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ == nullptr) {
@@ -33,10 +33,20 @@ void GatewayKnxTpIpRouter::sendSecureSessionStatus(uint8_t status, const sockadd
}
bool GatewayKnxTpIpRouter::sendPacket(const std::vector<uint8_t>& packet,
const sockaddr_in& remote) const {
const sockaddr_in& remote) {
if (packet.empty()) {
return false;
}
if (SecureSession* session = activeSecureSession(); session != nullptr) {
std::vector<uint8_t> wrapped;
if (wrapSecurePacket(*session, packet, &wrapped)) {
if (active_tcp_sock_ >= 0) {
return SendStream(active_tcp_sock_, wrapped.data(), wrapped.size());
}
return udp_sock_ >= 0 && SendAll(udp_sock_, wrapped.data(), wrapped.size(), remote);
}
return false;
}
if (active_tcp_sock_ >= 0) {
return SendStream(active_tcp_sock_, packet.data(), packet.size());
}
@@ -44,10 +54,32 @@ bool GatewayKnxTpIpRouter::sendPacket(const std::vector<uint8_t>& packet,
}
bool GatewayKnxTpIpRouter::sendPacketToTunnelClient(
const TunnelClient& client, const std::vector<uint8_t>& packet) const {
const TunnelClient& client, const std::vector<uint8_t>& packet) {
if (packet.empty()) {
return false;
}
if (client.secure_session_id != 0) {
SecureSession* session = nullptr;
for (auto& candidate : secure_sessions_) {
if (candidate.active && candidate.authenticated &&
candidate.session_id == client.secure_session_id) {
session = &candidate;
break;
}
}
if (session != nullptr) {
std::vector<uint8_t> wrapped;
if (wrapSecurePacket(*session, packet, &wrapped)) {
if (client.tcp_sock >= 0) {
return SendStream(client.tcp_sock, wrapped.data(), wrapped.size());
}
return udp_sock_ >= 0 && SendAll(udp_sock_, wrapped.data(), wrapped.size(),
client.data_remote);
}
return false;
}
return false;
}
if (client.tcp_sock >= 0) {
return SendStream(client.tcp_sock, packet.data(), packet.size());
}
@@ -211,6 +243,15 @@ std::vector<uint8_t> GatewayKnxTpIpRouter::buildSupportedServiceDib() const {
if (config_.multicast_enabled) {
services.emplace_back(kKnxServiceFamilyRouting, 1);
}
#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
const bool secure_routing_ready =
config_.oam_router.enabled && config_.oam_router.secure_routing_enabled &&
oam_ip_secure_credentials_.activated &&
oam_ip_secure_credentials_.backbone_key_available;
if (secureCredentialsReady() || secure_routing_ready) {
services.emplace_back(kKnxServiceFamilySecurity, 1);
}
#endif
std::vector<uint8_t> dib(2 + services.size() * 2U, 0);
dib[0] = static_cast<uint8_t>(dib.size());
dib[1] = kKnxDibSupportedServices;
@@ -399,7 +440,19 @@ void GatewayKnxTpIpRouter::sendRoutingIndication(const uint8_t* data, size_t len
return;
}
KnxIpRoutingIndication routing(frame);
const std::vector<uint8_t> packet(routing.data(), routing.data() + routing.totalLength());
std::vector<uint8_t> packet(routing.data(), routing.data() + routing.totalLength());
#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
if (config_.oam_router.enabled && config_.oam_router.secure_routing_enabled) {
std::vector<uint8_t> secure_packet;
if (wrapSecureRoutingPacket(packet, &secure_packet)) {
packet = std::move(secure_packet);
} else if (oam_ip_secure_credentials_.activated &&
oam_ip_secure_credentials_.backbone_key_available) {
ESP_LOGW(kTag, "failed to wrap KNXnet/IP Secure routing indication");
return;
}
}
#endif
const auto netifs = ActiveKnxNetifs();
if (netifs.empty()) {
SendAll(udp_sock_, packet.data(), packet.size(), remote);
@@ -184,7 +184,11 @@ void GatewayKnxTpIpRouter::handleRoutingIndication(const uint8_t* packet_data, s
const uint8_t* cemi = frame.data();
const size_t cemi_len = frame.dataLength();
bool consumed_by_local_application = false;
if (ets_device_ != nullptr && MatchesOpenKnxLocalIndividualAddress(frame, *ets_device_)) {
const bool addressed_to_oam =
oam_router_ != nullptr && MatchesOamRouterLocalIndividualAddress(frame, *oam_router_);
const bool addressed_to_reg1 =
ets_device_ != nullptr && MatchesOpenKnxLocalIndividualAddress(frame, *ets_device_);
if (addressed_to_oam || addressed_to_reg1) {
std::vector<uint8_t> local_tunnel_frame;
if (BuildLocalRoutingTunnelFrame(cemi, cemi_len, &local_tunnel_frame)) {
consumed_by_local_application = handleOpenKnxTunnelFrame(
@@ -300,11 +304,26 @@ GatewayKnxTpIpRouter::TunnelClient* GatewayKnxTpIpRouter::allocateTunnelClient(
free_client->connection_type = connection_type;
free_client->received_sequence = 255;
free_client->send_sequence = 0;
free_client->individual_address = effectiveTunnelAddressForSlot(free_index);
const bool oam_router_persona =
config_.oam_router.enabled && config_.oam_router.secure_tunnel_enabled &&
active_secure_session_id_ != 0;
if (oam_router_persona) {
const uint16_t first = config_.oam_router.tunnel_address_base;
const uint16_t line = first & 0xff00;
uint16_t device = static_cast<uint16_t>((first & 0x00ff) + free_index);
if (device == 0 || device > 0xff) {
device = static_cast<uint16_t>(1 + free_index);
}
free_client->individual_address = static_cast<uint16_t>(line | (device & 0x00ff));
} else {
free_client->individual_address = effectiveTunnelAddressForSlot(free_index);
}
free_client->last_activity_tick = xTaskGetTickCount();
free_client->control_remote = control_remote;
free_client->data_remote = data_remote;
free_client->tcp_sock = active_tcp_sock_;
free_client->secure_session_id = active_secure_session_id_;
free_client->oam_router_persona = oam_router_persona;
last_tunnel_channel_id_ = channel_id;
return free_client;
}
@@ -636,16 +655,18 @@ void GatewayKnxTpIpRouter::handleSecureService(uint16_t service, const uint8_t*
#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
switch (service) {
case kServiceSecureSessionRequest:
handleSecureSessionRequest(body, len, remote);
break;
case kServiceSecureSessionAuth:
ESP_LOGW(kTag, "KNXnet/IP Secure service 0x%04x rejected: secure sessions are not provisioned", service);
ESP_LOGW(kTag, "KNXnet/IP Secure auth from %s rejected outside a secure wrapper",
EndpointString(remote).c_str());
sendSecureSessionStatus(kKnxSecureStatusAuthFailed, remote);
break;
case kServiceSecureWrapper:
ESP_LOGW(kTag, "KNXnet/IP Secure wrapper rejected: no authenticated secure session");
sendSecureSessionStatus(kKnxSecureStatusUnauthenticated, remote);
handleSecureWrapper(body, len, remote);
break;
case kServiceSecureGroupSync:
ESP_LOGD(kTag, "KNXnet/IP Secure group sync ignored until secure routing is provisioned");
handleSecureGroupSync(body, len, remote);
break;
default:
ESP_LOGD(kTag, "KNXnet/IP Secure service 0x%04x ignored", service);
@@ -0,0 +1,780 @@
#include "gateway_knx_private.hpp"
#include "esp_random.h"
#include "mbedtls/aes.h"
#include "mbedtls/ecp.h"
#include "mbedtls/sha256.h"
namespace gateway {
namespace {
constexpr size_t kSecurePublicKeyLen = 32;
constexpr size_t kSecureKeyLen = 16;
constexpr size_t kSecureSeqLen = 6;
constexpr size_t kSecureSerialLen = 6;
constexpr size_t kSecureMacLen = 16;
constexpr size_t kSecureWrapperBodyOverhead = 2 + kSecureSeqLen + kSecureSerialLen + 2 + kSecureMacLen;
constexpr std::array<uint8_t, 16> kAuthCtrIv{0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xff, 0x00};
int SecureRandom(void*, unsigned char* output, size_t len) {
esp_fill_random(output, len);
return 0;
}
void WriteSeq48(uint8_t* data, uint64_t value) {
data[0] = static_cast<uint8_t>((value >> 40) & 0xff);
data[1] = static_cast<uint8_t>((value >> 32) & 0xff);
data[2] = static_cast<uint8_t>((value >> 24) & 0xff);
data[3] = static_cast<uint8_t>((value >> 16) & 0xff);
data[4] = static_cast<uint8_t>((value >> 8) & 0xff);
data[5] = static_cast<uint8_t>(value & 0xff);
}
uint64_t ReadSeq48(const uint8_t* data) {
uint64_t value = 0;
for (size_t index = 0; index < kSecureSeqLen; ++index) {
value = (value << 8) | data[index];
}
return value;
}
std::array<uint8_t, kSecureSerialLen> OamRouterSerial() {
std::array<uint8_t, kSecureSerialLen> serial{};
uint8_t mac[6]{};
if (!ReadBaseMac(mac)) {
return serial;
}
uint32_t suffix = (static_cast<uint32_t>(mac[2]) << 24) |
(static_cast<uint32_t>(mac[3]) << 16) |
(static_cast<uint32_t>(mac[4]) << 8) |
static_cast<uint32_t>(mac[5]);
suffix += knx_internal::kOamRouterSerialMacIncrement;
serial[0] = static_cast<uint8_t>((knx_internal::kOamRouterManufacturerId >> 8) & 0xff);
serial[1] = static_cast<uint8_t>(knx_internal::kOamRouterManufacturerId & 0xff);
serial[2] = static_cast<uint8_t>((suffix >> 24) & 0xff);
serial[3] = static_cast<uint8_t>((suffix >> 16) & 0xff);
serial[4] = static_cast<uint8_t>((suffix >> 8) & 0xff);
serial[5] = static_cast<uint8_t>(suffix & 0xff);
return serial;
}
bool AesCbcMac(const std::array<uint8_t, kSecureKeyLen>& key,
const std::vector<uint8_t>& additional_data,
const std::vector<uint8_t>& payload,
const std::array<uint8_t, kSecureMacLen>& block0,
std::array<uint8_t, kSecureMacLen>* mac) {
if (mac == nullptr || additional_data.size() > 0xffff) {
return false;
}
std::vector<uint8_t> blocks;
blocks.reserve(block0.size() + 2 + additional_data.size() + payload.size() + kSecureMacLen);
blocks.insert(blocks.end(), block0.begin(), block0.end());
blocks.push_back(static_cast<uint8_t>((additional_data.size() >> 8) & 0xff));
blocks.push_back(static_cast<uint8_t>(additional_data.size() & 0xff));
blocks.insert(blocks.end(), additional_data.begin(), additional_data.end());
blocks.insert(blocks.end(), payload.begin(), payload.end());
const size_t remainder = blocks.size() % kSecureMacLen;
if (remainder != 0) {
blocks.insert(blocks.end(), kSecureMacLen - remainder, 0x00);
}
std::array<uint8_t, kSecureMacLen> iv{};
mbedtls_aes_context aes;
mbedtls_aes_init(&aes);
const int set_key = mbedtls_aes_setkey_enc(&aes, key.data(), 128);
int crypt = 0;
if (set_key == 0) {
crypt = mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_ENCRYPT, blocks.size(),
iv.data(), blocks.data(), blocks.data());
}
mbedtls_aes_free(&aes);
if (set_key != 0 || crypt != 0) {
return false;
}
std::copy(blocks.end() - kSecureMacLen, blocks.end(), mac->begin());
return true;
}
bool AesCtrTransform(const std::array<uint8_t, kSecureKeyLen>& key,
const std::array<uint8_t, kSecureMacLen>& counter,
const uint8_t* input, size_t len,
std::vector<uint8_t>* output) {
if (output == nullptr || (input == nullptr && len != 0)) {
return false;
}
output->assign(len, 0);
std::array<uint8_t, kSecureMacLen> nonce_counter = counter;
std::array<uint8_t, kSecureMacLen> stream_block{};
size_t nc_off = 0;
mbedtls_aes_context aes;
mbedtls_aes_init(&aes);
const int set_key = mbedtls_aes_setkey_enc(&aes, key.data(), 128);
int crypt = 0;
if (set_key == 0 && len > 0) {
crypt = mbedtls_aes_crypt_ctr(&aes, len, &nc_off, nonce_counter.data(),
stream_block.data(), input, output->data());
}
mbedtls_aes_free(&aes);
return set_key == 0 && crypt == 0;
}
bool GenerateSessionKey(const std::array<uint8_t, kSecurePublicKeyLen>& client_public,
std::array<uint8_t, kSecurePublicKeyLen>* server_public,
std::array<uint8_t, kSecurePublicKeyLen>* shared_secret,
std::array<uint8_t, kSecureKeyLen>* session_key) {
if (server_public == nullptr || shared_secret == nullptr || session_key == nullptr) {
return false;
}
mbedtls_ecp_group group;
mbedtls_mpi private_key;
mbedtls_ecp_point public_key;
mbedtls_ecp_point peer_public_key;
mbedtls_ecp_point shared_point;
mbedtls_ecp_group_init(&group);
mbedtls_mpi_init(&private_key);
mbedtls_ecp_point_init(&public_key);
mbedtls_ecp_point_init(&peer_public_key);
mbedtls_ecp_point_init(&shared_point);
int ret = mbedtls_ecp_group_load(&group, MBEDTLS_ECP_DP_CURVE25519);
if (ret == 0) {
ret = mbedtls_ecp_gen_privkey(&group, &private_key, SecureRandom, nullptr);
}
if (ret == 0) {
ret = mbedtls_ecp_mul(&group, &public_key, &private_key, &group.G, SecureRandom, nullptr);
}
if (ret == 0) {
ret = mbedtls_ecp_point_read_binary(&group, &peer_public_key, client_public.data(),
client_public.size());
}
if (ret == 0) {
ret = mbedtls_ecp_mul(&group, &shared_point, &private_key, &peer_public_key,
SecureRandom, nullptr);
}
size_t public_len = 0;
if (ret == 0) {
ret = mbedtls_ecp_point_write_binary(&group, &public_key, MBEDTLS_ECP_PF_UNCOMPRESSED,
&public_len, server_public->data(),
server_public->size());
}
size_t secret_len = 0;
if (ret == 0) {
ret = mbedtls_ecp_point_write_binary(&group, &shared_point, MBEDTLS_ECP_PF_UNCOMPRESSED,
&secret_len, shared_secret->data(),
shared_secret->size());
}
mbedtls_ecp_point_free(&shared_point);
mbedtls_ecp_point_free(&peer_public_key);
mbedtls_ecp_point_free(&public_key);
mbedtls_mpi_free(&private_key);
mbedtls_ecp_group_free(&group);
if (ret != 0 || public_len != server_public->size() || secret_len != shared_secret->size()) {
return false;
}
std::array<uint8_t, 32> digest{};
if (mbedtls_sha256(shared_secret->data(), shared_secret->size(), digest.data(), 0) != 0) {
return false;
}
std::copy(digest.begin(), digest.begin() + session_key->size(), session_key->begin());
return true;
}
} // namespace
bool GatewayKnxTpIpRouter::secureCredentialsReady() const {
#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
return config_.oam_router.enabled && config_.oam_router.secure_tunnel_enabled &&
oam_ip_secure_credentials_.activated &&
!oam_ip_secure_credentials_.tunnel_user_keys.empty();
#else
return false;
#endif
}
GatewayKnxTpIpRouter::SecureSession* GatewayKnxTpIpRouter::findSecureSession(
uint16_t session_id, const sockaddr_in& remote) {
if (session_id == 0) {
return nullptr;
}
for (auto& session : secure_sessions_) {
if (session.active && session.session_id == session_id &&
EndpointEquals(session.remote, remote)) {
return &session;
}
}
return nullptr;
}
const GatewayKnxTpIpRouter::SecureSession* GatewayKnxTpIpRouter::findSecureSession(
uint16_t session_id, const sockaddr_in& remote) const {
if (session_id == 0) {
return nullptr;
}
for (const auto& session : secure_sessions_) {
if (session.active && session.session_id == session_id &&
EndpointEquals(session.remote, remote)) {
return &session;
}
}
return nullptr;
}
GatewayKnxTpIpRouter::SecureSession* GatewayKnxTpIpRouter::allocateSecureSession(
const sockaddr_in& remote) {
SecureSession* slot = nullptr;
for (auto& session : secure_sessions_) {
if (session.active && EndpointEquals(session.remote, remote)) {
slot = &session;
break;
}
if (!session.active && slot == nullptr) {
slot = &session;
}
}
if (slot == nullptr) {
return nullptr;
}
*slot = SecureSession{};
slot->active = true;
slot->remote = remote;
slot->tcp_sock = active_tcp_sock_;
for (uint32_t attempt = 0; attempt < 0xffff; ++attempt) {
last_secure_session_id_ = static_cast<uint16_t>(last_secure_session_id_ + 1);
if (last_secure_session_id_ == 0) {
last_secure_session_id_ = 1;
}
if (findSecureSession(last_secure_session_id_, remote) == nullptr) {
slot->session_id = last_secure_session_id_;
break;
}
}
slot->last_activity_tick = xTaskGetTickCount();
return slot->session_id == 0 ? nullptr : slot;
}
GatewayKnxTpIpRouter::SecureSession* GatewayKnxTpIpRouter::activeSecureSession() {
if (active_secure_session_id_ == 0) {
return nullptr;
}
for (auto& session : secure_sessions_) {
if (session.active && session.session_id == active_secure_session_id_) {
return &session;
}
}
return nullptr;
}
void GatewayKnxTpIpRouter::closeSecureSessionsForTcp(int sock) {
if (sock < 0) {
return;
}
for (auto& session : secure_sessions_) {
if (session.active && session.tcp_sock == sock) {
session = SecureSession{};
}
}
}
bool GatewayKnxTpIpRouter::wrapSecurePacket(SecureSession& session,
const std::vector<uint8_t>& inner,
std::vector<uint8_t>* wrapped) {
if (wrapped == nullptr || inner.empty() || inner.size() > 0xffff) {
return false;
}
const size_t total_len = kKnxNetIpHeaderSize + kSecureWrapperBodyOverhead + inner.size();
if (total_len > 0xffff) {
return false;
}
const auto serial = OamRouterSerial();
std::array<uint8_t, kSecureSeqLen> sequence{};
WriteSeq48(sequence.data(), session.send_sequence++ & 0xffffffffffffULL);
std::array<uint8_t, 2> tag{0x00, 0x00};
std::vector<uint8_t> header(kKnxNetIpHeaderSize, 0);
header[0] = kKnxNetIpHeaderSize;
header[1] = kKnxNetIpVersion10;
WriteBe16(header.data() + 2, kServiceSecureWrapper);
WriteBe16(header.data() + 4, static_cast<uint16_t>(total_len));
std::vector<uint8_t> additional = header;
additional.push_back(static_cast<uint8_t>((session.session_id >> 8) & 0xff));
additional.push_back(static_cast<uint8_t>(session.session_id & 0xff));
std::array<uint8_t, kSecureMacLen> block0{};
std::copy(sequence.begin(), sequence.end(), block0.begin());
std::copy(serial.begin(), serial.end(), block0.begin() + kSecureSeqLen);
block0[12] = tag[0];
block0[13] = tag[1];
WriteBe16(block0.data() + 14, static_cast<uint16_t>(inner.size()));
std::array<uint8_t, kSecureMacLen> mac_cbc{};
if (!AesCbcMac(session.session_key, additional, inner, block0, &mac_cbc)) {
return false;
}
std::array<uint8_t, kSecureMacLen> counter{};
std::copy(sequence.begin(), sequence.end(), counter.begin());
std::copy(serial.begin(), serial.end(), counter.begin() + kSecureSeqLen);
counter[12] = tag[0];
counter[13] = tag[1];
counter[14] = 0xff;
counter[15] = 0x00;
std::vector<uint8_t> enc_mac;
if (!AesCtrTransform(session.session_key, counter, mac_cbc.data(), mac_cbc.size(),
&enc_mac)) {
return false;
}
std::vector<uint8_t> enc_payload;
if (!AesCtrTransform(session.session_key, counter, inner.data(), inner.size(),
&enc_payload)) {
return false;
}
wrapped->clear();
wrapped->reserve(total_len);
wrapped->insert(wrapped->end(), header.begin(), header.end());
wrapped->push_back(static_cast<uint8_t>((session.session_id >> 8) & 0xff));
wrapped->push_back(static_cast<uint8_t>(session.session_id & 0xff));
wrapped->insert(wrapped->end(), sequence.begin(), sequence.end());
wrapped->insert(wrapped->end(), serial.begin(), serial.end());
wrapped->insert(wrapped->end(), tag.begin(), tag.end());
wrapped->insert(wrapped->end(), enc_payload.begin(), enc_payload.end());
wrapped->insert(wrapped->end(), enc_mac.begin(), enc_mac.end());
session.last_activity_tick = xTaskGetTickCount();
return true;
}
bool GatewayKnxTpIpRouter::wrapSecureRoutingPacket(const std::vector<uint8_t>& inner,
std::vector<uint8_t>* wrapped) {
#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
if (wrapped == nullptr || inner.empty() || inner.size() > 0xffff ||
!config_.oam_router.enabled || !config_.oam_router.secure_routing_enabled ||
!oam_ip_secure_credentials_.activated ||
!oam_ip_secure_credentials_.backbone_key_available) {
return false;
}
const size_t total_len = kKnxNetIpHeaderSize + kSecureWrapperBodyOverhead + inner.size();
if (total_len > 0xffff) {
return false;
}
const uint16_t routing_session_id = 0;
const auto serial = OamRouterSerial();
const uint64_t sequence_value = oam_ip_secure_credentials_.routing_sequence;
std::array<uint8_t, kSecureSeqLen> sequence{};
WriteSeq48(sequence.data(), sequence_value & 0xffffffffffffULL);
std::array<uint8_t, 2> tag{0x00, 0x00};
std::vector<uint8_t> header(kKnxNetIpHeaderSize, 0);
header[0] = kKnxNetIpHeaderSize;
header[1] = kKnxNetIpVersion10;
WriteBe16(header.data() + 2, kServiceSecureWrapper);
WriteBe16(header.data() + 4, static_cast<uint16_t>(total_len));
std::vector<uint8_t> additional = header;
additional.push_back(0x00);
additional.push_back(0x00);
std::array<uint8_t, kSecureMacLen> block0{};
std::copy(sequence.begin(), sequence.end(), block0.begin());
std::copy(serial.begin(), serial.end(), block0.begin() + kSecureSeqLen);
block0[12] = tag[0];
block0[13] = tag[1];
WriteBe16(block0.data() + 14, static_cast<uint16_t>(inner.size()));
std::array<uint8_t, kSecureMacLen> mac_cbc{};
if (!AesCbcMac(oam_ip_secure_credentials_.backbone_key, additional, inner, block0,
&mac_cbc)) {
return false;
}
std::array<uint8_t, kSecureMacLen> counter{};
std::copy(sequence.begin(), sequence.end(), counter.begin());
std::copy(serial.begin(), serial.end(), counter.begin() + kSecureSeqLen);
counter[12] = tag[0];
counter[13] = tag[1];
counter[14] = 0xff;
counter[15] = 0x00;
std::vector<uint8_t> enc_mac;
if (!AesCtrTransform(oam_ip_secure_credentials_.backbone_key, counter,
mac_cbc.data(), mac_cbc.size(), &enc_mac)) {
return false;
}
std::vector<uint8_t> enc_payload;
if (!AesCtrTransform(oam_ip_secure_credentials_.backbone_key, counter,
inner.data(), inner.size(), &enc_payload)) {
return false;
}
wrapped->clear();
wrapped->reserve(total_len);
wrapped->insert(wrapped->end(), header.begin(), header.end());
wrapped->push_back(static_cast<uint8_t>((routing_session_id >> 8) & 0xff));
wrapped->push_back(static_cast<uint8_t>(routing_session_id & 0xff));
wrapped->insert(wrapped->end(), sequence.begin(), sequence.end());
wrapped->insert(wrapped->end(), serial.begin(), serial.end());
wrapped->insert(wrapped->end(), tag.begin(), tag.end());
wrapped->insert(wrapped->end(), enc_payload.begin(), enc_payload.end());
wrapped->insert(wrapped->end(), enc_mac.begin(), enc_mac.end());
oam_ip_secure_credentials_.routing_sequence = sequence_value + 1;
if (routing_sequence_store_handler_) {
routing_sequence_store_handler_(oam_ip_secure_credentials_.routing_sequence);
}
return true;
#else
(void)inner;
(void)wrapped;
return false;
#endif
}
bool GatewayKnxTpIpRouter::decryptSecureWrapper(SecureSession& session,
const uint8_t* body, size_t len,
std::vector<uint8_t>* inner) {
if (body == nullptr || inner == nullptr || len < kSecureWrapperBodyOverhead) {
return false;
}
const uint16_t session_id = ReadBe16(body);
if (session_id != session.session_id) {
return false;
}
const uint8_t* sequence = body + 2;
const uint8_t* serial = body + 8;
const uint8_t* tag = body + 14;
const uint8_t* encrypted_payload = body + 16;
const size_t encrypted_payload_len = len - kSecureWrapperBodyOverhead;
const uint8_t* encrypted_mac = body + len - kSecureMacLen;
const uint64_t incoming_sequence = ReadSeq48(sequence);
if (incoming_sequence < session.receive_sequence) {
return false;
}
std::array<uint8_t, kSecureMacLen> counter{};
std::copy(sequence, sequence + kSecureSeqLen, counter.begin());
std::copy(serial, serial + kSecureSerialLen, counter.begin() + kSecureSeqLen);
counter[12] = tag[0];
counter[13] = tag[1];
counter[14] = 0xff;
counter[15] = 0x00;
std::vector<uint8_t> mac_tr;
if (!AesCtrTransform(session.session_key, counter, encrypted_mac, kSecureMacLen, &mac_tr)) {
return false;
}
if (!AesCtrTransform(session.session_key, counter, encrypted_payload,
encrypted_payload_len, inner)) {
return false;
}
std::vector<uint8_t> header(kKnxNetIpHeaderSize, 0);
header[0] = kKnxNetIpHeaderSize;
header[1] = kKnxNetIpVersion10;
WriteBe16(header.data() + 2, kServiceSecureWrapper);
WriteBe16(header.data() + 4, static_cast<uint16_t>(kKnxNetIpHeaderSize + len));
std::vector<uint8_t> additional = header;
additional.push_back(static_cast<uint8_t>((session_id >> 8) & 0xff));
additional.push_back(static_cast<uint8_t>(session_id & 0xff));
std::array<uint8_t, kSecureMacLen> block0{};
std::copy(sequence, sequence + kSecureSeqLen, block0.begin());
std::copy(serial, serial + kSecureSerialLen, block0.begin() + kSecureSeqLen);
block0[12] = tag[0];
block0[13] = tag[1];
WriteBe16(block0.data() + 14, static_cast<uint16_t>(inner->size()));
std::array<uint8_t, kSecureMacLen> mac_cbc{};
if (!AesCbcMac(session.session_key, additional, *inner, block0, &mac_cbc) ||
mac_tr.size() != mac_cbc.size() ||
!std::equal(mac_cbc.begin(), mac_cbc.end(), mac_tr.begin())) {
return false;
}
session.receive_sequence = incoming_sequence + 1;
session.last_activity_tick = xTaskGetTickCount();
return true;
}
bool GatewayKnxTpIpRouter::decryptSecureRoutingWrapper(const uint8_t* body,
size_t len,
std::vector<uint8_t>* inner) {
#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
if (body == nullptr || inner == nullptr || len < kSecureWrapperBodyOverhead ||
!config_.oam_router.enabled || !config_.oam_router.secure_routing_enabled ||
!oam_ip_secure_credentials_.activated ||
!oam_ip_secure_credentials_.backbone_key_available) {
return false;
}
const uint16_t session_id = ReadBe16(body);
if (session_id != 0) {
return false;
}
const uint8_t* sequence = body + 2;
const uint8_t* serial = body + 8;
const uint8_t* tag = body + 14;
const uint8_t* encrypted_payload = body + 16;
const size_t encrypted_payload_len = len - kSecureWrapperBodyOverhead;
const uint8_t* encrypted_mac = body + len - kSecureMacLen;
const uint64_t incoming_sequence = ReadSeq48(sequence);
std::array<uint8_t, kSecureMacLen> counter{};
std::copy(sequence, sequence + kSecureSeqLen, counter.begin());
std::copy(serial, serial + kSecureSerialLen, counter.begin() + kSecureSeqLen);
counter[12] = tag[0];
counter[13] = tag[1];
counter[14] = 0xff;
counter[15] = 0x00;
std::vector<uint8_t> mac_tr;
if (!AesCtrTransform(oam_ip_secure_credentials_.backbone_key, counter,
encrypted_mac, kSecureMacLen, &mac_tr)) {
return false;
}
if (!AesCtrTransform(oam_ip_secure_credentials_.backbone_key, counter,
encrypted_payload, encrypted_payload_len, inner)) {
return false;
}
std::vector<uint8_t> header(kKnxNetIpHeaderSize, 0);
header[0] = kKnxNetIpHeaderSize;
header[1] = kKnxNetIpVersion10;
WriteBe16(header.data() + 2, kServiceSecureWrapper);
WriteBe16(header.data() + 4, static_cast<uint16_t>(kKnxNetIpHeaderSize + len));
std::vector<uint8_t> additional = header;
additional.push_back(0x00);
additional.push_back(0x00);
std::array<uint8_t, kSecureMacLen> block0{};
std::copy(sequence, sequence + kSecureSeqLen, block0.begin());
std::copy(serial, serial + kSecureSerialLen, block0.begin() + kSecureSeqLen);
block0[12] = tag[0];
block0[13] = tag[1];
WriteBe16(block0.data() + 14, static_cast<uint16_t>(inner->size()));
std::array<uint8_t, kSecureMacLen> mac_cbc{};
if (!AesCbcMac(oam_ip_secure_credentials_.backbone_key, additional, *inner, block0,
&mac_cbc) ||
mac_tr.size() != mac_cbc.size() ||
!std::equal(mac_cbc.begin(), mac_cbc.end(), mac_tr.begin())) {
return false;
}
oam_ip_secure_credentials_.routing_sequence =
std::max(oam_ip_secure_credentials_.routing_sequence, incoming_sequence + 1);
if (routing_sequence_store_handler_) {
routing_sequence_store_handler_(oam_ip_secure_credentials_.routing_sequence);
}
return true;
#else
(void)body;
(void)len;
(void)inner;
return false;
#endif
}
bool GatewayKnxTpIpRouter::verifySecureSessionAuth(SecureSession& session,
const uint8_t* packet,
size_t len, uint8_t* status) {
if (status != nullptr) {
*status = kKnxSecureStatusAuthFailed;
}
uint16_t service = 0;
uint16_t total_len = 0;
if (!ParseKnxNetIpHeader(packet, len, &service, &total_len) ||
service != kServiceSecureSessionAuth || total_len < kKnxNetIpHeaderSize + 18) {
return false;
}
const uint8_t user_id = packet[7];
const uint8_t* received_mac = packet + 8;
const std::array<uint8_t, kSecureKeyLen>* user_key = nullptr;
if (user_id > 0 && user_id - 1 < oam_ip_secure_credentials_.tunnel_user_keys.size()) {
user_key = &oam_ip_secure_credentials_.tunnel_user_keys[user_id - 1];
} else if (oam_ip_secure_credentials_.tunnel_user_keys.size() == 1) {
user_key = &oam_ip_secure_credentials_.tunnel_user_keys.front();
}
if (user_key == nullptr) {
return false;
}
std::vector<uint8_t> additional;
additional.reserve(6 + 2 + kSecurePublicKeyLen);
additional.insert(additional.end(), packet, packet + 6);
additional.push_back(packet[6]);
additional.push_back(user_id);
for (size_t index = 0; index < kSecurePublicKeyLen; ++index) {
additional.push_back(session.client_public_key[index] ^ session.server_public_key[index]);
}
std::array<uint8_t, kSecureMacLen> block0{};
std::array<uint8_t, kSecureMacLen> mac_cbc{};
if (!AesCbcMac(*user_key, additional, {}, block0, &mac_cbc)) {
return false;
}
std::vector<uint8_t> transformed_mac;
if (!AesCtrTransform(*user_key, kAuthCtrIv, mac_cbc.data(), mac_cbc.size(),
&transformed_mac) ||
transformed_mac.size() != kSecureMacLen ||
!std::equal(transformed_mac.begin(), transformed_mac.end(), received_mac)) {
return false;
}
session.authenticated = true;
session.user_id = user_id;
if (status != nullptr) {
*status = kKnxNoError;
}
return true;
}
void GatewayKnxTpIpRouter::handleSecureSessionRequest(const uint8_t* body,
size_t len,
const sockaddr_in& remote) {
#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
if (!secureCredentialsReady()) {
ESP_LOGW(kTag, "reject KNXnet/IP Secure session from %s: OAM IP Secure credentials are not active",
EndpointString(remote).c_str());
sendSecureSessionStatus(kKnxSecureStatusAuthFailed, remote);
return;
}
if (body == nullptr || len < 8 + kSecurePublicKeyLen) {
sendSecureSessionStatus(kKnxSecureStatusAuthFailed, remote);
return;
}
SecureSession* session = allocateSecureSession(remote);
if (session == nullptr) {
sendSecureSessionStatus(kKnxSecureStatusAuthFailed, remote);
return;
}
std::copy(body + 8, body + 8 + kSecurePublicKeyLen, session->client_public_key.begin());
if (!GenerateSessionKey(session->client_public_key, &session->server_public_key,
&session->shared_secret, &session->session_key)) {
*session = SecureSession{};
sendSecureSessionStatus(kKnxSecureStatusAuthFailed, remote);
return;
}
std::vector<uint8_t> response_body;
response_body.reserve(2 + kSecurePublicKeyLen);
response_body.push_back(static_cast<uint8_t>((session->session_id >> 8) & 0xff));
response_body.push_back(static_cast<uint8_t>(session->session_id & 0xff));
response_body.insert(response_body.end(), session->server_public_key.begin(),
session->server_public_key.end());
sendPacket(OpenKnxIpPacket(kServiceSecureSessionResponse, response_body), remote);
ESP_LOGI(kTag, "accepted KNXnet/IP Secure session request sid=%u from %s",
static_cast<unsigned>(session->session_id), EndpointString(remote).c_str());
#else
(void)body;
(void)len;
(void)remote;
#endif
}
void GatewayKnxTpIpRouter::handleSecureWrapper(const uint8_t* body, size_t len,
const sockaddr_in& remote) {
#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
if (body == nullptr || len < kSecureWrapperBodyOverhead) {
sendSecureSessionStatus(kKnxSecureStatusUnauthenticated, remote);
return;
}
const uint16_t session_id = ReadBe16(body);
if (session_id == 0) {
std::vector<uint8_t> inner;
if (!decryptSecureRoutingWrapper(body, len, &inner)) {
return;
}
handleUdpDatagram(inner.data(), inner.size(), remote);
return;
}
SecureSession* session = findSecureSession(session_id, remote);
if (session == nullptr) {
sendSecureSessionStatus(kKnxSecureStatusUnauthenticated, remote);
return;
}
std::vector<uint8_t> inner;
if (!decryptSecureWrapper(*session, body, len, &inner)) {
sendSecureSessionStatus(0x04, remote);
return;
}
uint16_t service = 0;
uint16_t total_len = 0;
if (!ParseKnxNetIpHeader(inner.data(), inner.size(), &service, &total_len)) {
sendSecureSessionStatus(kKnxSecureStatusUnauthenticated, remote);
return;
}
const uint16_t previous_session = active_secure_session_id_;
active_secure_session_id_ = session->session_id;
if (service == kServiceSecureSessionAuth) {
uint8_t status = kKnxSecureStatusAuthFailed;
verifySecureSessionAuth(*session, inner.data(), inner.size(), &status);
sendSecureSessionStatus(status, remote);
active_secure_session_id_ = previous_session;
return;
}
if (!session->authenticated) {
sendSecureSessionStatus(kKnxSecureStatusUnauthenticated, remote);
active_secure_session_id_ = previous_session;
return;
}
handleUdpDatagram(inner.data(), inner.size(), remote);
active_secure_session_id_ = previous_session;
#else
(void)body;
(void)len;
(void)remote;
#endif
}
void GatewayKnxTpIpRouter::handleSecureGroupSync(const uint8_t* body, size_t len,
const sockaddr_in& remote) {
#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
if (body == nullptr || len < kSecureSeqLen + kSecureSerialLen + 2 + kSecureMacLen ||
!oam_ip_secure_credentials_.activated ||
!oam_ip_secure_credentials_.backbone_key_available) {
return;
}
const uint8_t* sequence = body;
const uint8_t* serial = body + kSecureSeqLen;
const uint8_t* tag = body + kSecureSeqLen + kSecureSerialLen;
const uint8_t* encrypted_mac = tag + 2;
std::array<uint8_t, kSecureMacLen> counter{};
std::copy(sequence, sequence + kSecureSeqLen, counter.begin());
std::copy(serial, serial + kSecureSerialLen, counter.begin() + kSecureSeqLen);
counter[12] = tag[0];
counter[13] = tag[1];
counter[14] = 0xff;
counter[15] = 0x00;
std::vector<uint8_t> mac_tr;
if (!AesCtrTransform(oam_ip_secure_credentials_.backbone_key, counter, encrypted_mac,
kSecureMacLen, &mac_tr)) {
return;
}
std::vector<uint8_t> header(kKnxNetIpHeaderSize, 0);
header[0] = kKnxNetIpHeaderSize;
header[1] = kKnxNetIpVersion10;
WriteBe16(header.data() + 2, kServiceSecureGroupSync);
WriteBe16(header.data() + 4, static_cast<uint16_t>(kKnxNetIpHeaderSize + len));
std::array<uint8_t, kSecureMacLen> block0{};
std::copy(sequence, sequence + kSecureSeqLen, block0.begin());
std::copy(serial, serial + kSecureSerialLen, block0.begin() + kSecureSeqLen);
block0[12] = tag[0];
block0[13] = tag[1];
std::array<uint8_t, kSecureMacLen> mac_cbc{};
if (AesCbcMac(oam_ip_secure_credentials_.backbone_key, header, {}, block0, &mac_cbc) &&
mac_tr.size() == mac_cbc.size() &&
std::equal(mac_cbc.begin(), mac_cbc.end(), mac_tr.begin())) {
oam_ip_secure_credentials_.routing_sequence =
std::max(oam_ip_secure_credentials_.routing_sequence, ReadSeq48(sequence) + 1);
if (routing_sequence_store_handler_) {
routing_sequence_store_handler_(oam_ip_secure_credentials_.routing_sequence);
}
ESP_LOGD(kTag, "authenticated KNXnet/IP Secure group sync from %s",
EndpointString(remote).c_str());
}
#else
(void)body;
(void)len;
(void)remote;
#endif
}
} // namespace gateway
@@ -0,0 +1,278 @@
#include "oam_router_runtime.h"
#include "gateway_knx_internal.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "knx/cemi_server.h"
#include "knx/property.h"
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <utility>
namespace gateway::openknx {
namespace {
constexpr uint16_t kInvalidIndividualAddress = 0xffff;
constexpr uint16_t kKnxUnconfiguredBroadcastAddress = 0xffff;
bool IsUsableIndividualAddress(uint16_t address) {
return address != 0 && address != kInvalidIndividualAddress;
}
uint32_t OamBauNumberFromBaseMac() {
uint8_t mac[6]{};
if (esp_efuse_mac_get_default(mac) != ESP_OK && esp_read_mac(mac, ESP_MAC_WIFI_STA) != ESP_OK) {
return 0;
}
uint32_t suffix = (static_cast<uint32_t>(mac[2]) << 24) |
(static_cast<uint32_t>(mac[3]) << 16) |
(static_cast<uint32_t>(mac[4]) << 8) |
static_cast<uint32_t>(mac[5]);
return suffix + knx_internal::kOamRouterSerialMacIncrement;
}
#if defined(ENABLE_BAU091A_PERSONA)
void ApplyOamRouterIdentity(Bau091A& device) {
device.deviceObject().manufacturerId(knx_internal::kOamRouterManufacturerId);
device.deviceObject().bauNumber(OamBauNumberFromBaseMac());
device.deviceObject().hardwareType(knx_internal::kOamRouterHardwareType);
device.deviceObject().orderNumber(knx_internal::kOamRouterOrderNumber);
if (auto* property = device.parameters().property(PID_PROG_VERSION); property != nullptr) {
property->write(knx_internal::kOamRouterProgramVersion);
}
}
#endif
} // namespace
OamRouterRuntime::OamRouterRuntime(std::string nvs_namespace,
uint16_t fallback_individual_address,
uint16_t tunnel_client_address)
: nvs_namespace_(std::move(nvs_namespace))
#if defined(ENABLE_BAU091A_PERSONA)
,
platform_(nullptr, nvs_namespace_.c_str()),
device_(platform_)
#endif
{
#if defined(ENABLE_BAU091A_PERSONA)
platform_.outboundCemiFrameCallback(&OamRouterRuntime::HandleOutboundCemiFrame, this);
ApplyOamRouterIdentity(device_);
if (IsUsableIndividualAddress(fallback_individual_address)) {
device_.deviceObject().individualAddress(fallback_individual_address);
}
ESP_LOGI("gateway_knx", "OAM OpenKNX loading memory namespace=%s", nvs_namespace_.c_str());
device_.readMemory();
ApplyOamRouterIdentity(device_);
if (!IsUsableIndividualAddress(device_.deviceObject().individualAddress()) &&
IsUsableIndividualAddress(fallback_individual_address)) {
device_.deviceObject().individualAddress(fallback_individual_address);
}
#ifdef USE_CEMI_SERVER
if (auto* server = device_.getCemiServer()) {
server->clientAddress(IsUsableIndividualAddress(tunnel_client_address)
? tunnel_client_address
: DefaultTunnelClientAddress(
device_.deviceObject().individualAddress()));
server->deviceAddressPropertiesTargetClient(false);
server->tunnelFrameCallback(&OamRouterRuntime::EmitTunnelFrame, this);
}
#endif
uint8_t program_version[5]{};
if (auto* property = device_.parameters().property(PID_PROG_VERSION); property != nullptr) {
property->read(program_version);
}
ESP_LOGI("gateway_knx",
"OAM router runtime namespace=%s configured=%d manufacturer=0x%04x mask=0x%04x device=0x%04x tunnelClient=0x%04x progVersion=%02X %02X %02X %02X %02X",
nvs_namespace_.c_str(), device_.configured(), device_.deviceObject().manufacturerId(),
device_.deviceObject().maskVersion(), device_.deviceObject().individualAddress(),
tunnelClientAddress(), program_version[0], program_version[1], program_version[2],
program_version[3], program_version[4]);
#else
(void)fallback_individual_address;
(void)tunnel_client_address;
#endif
}
OamRouterRuntime::~OamRouterRuntime() {
#if defined(ENABLE_BAU091A_PERSONA)
platform_.outboundCemiFrameCallback(nullptr, nullptr);
#ifdef USE_CEMI_SERVER
if (auto* server = device_.getCemiServer()) {
server->tunnelFrameCallback(nullptr, nullptr);
}
#endif
#endif
}
bool OamRouterRuntime::available() const {
#if defined(ENABLE_BAU091A_PERSONA)
return true;
#else
return false;
#endif
}
uint16_t OamRouterRuntime::individualAddress() const {
#if defined(ENABLE_BAU091A_PERSONA)
return const_cast<Bau091A&>(device_).deviceObject().individualAddress();
#else
return 0xffff;
#endif
}
uint16_t OamRouterRuntime::tunnelClientAddress() const {
#if defined(ENABLE_BAU091A_PERSONA) && defined(USE_CEMI_SERVER)
if (auto* server = const_cast<Bau091A&>(device_).getCemiServer()) {
return server->clientAddress();
}
#endif
return DefaultTunnelClientAddress(individualAddress());
}
bool OamRouterRuntime::configured() const {
#if defined(ENABLE_BAU091A_PERSONA)
return const_cast<Bau091A&>(device_).configured();
#else
return false;
#endif
}
bool OamRouterRuntime::programmingMode() const {
#if defined(ENABLE_BAU091A_PERSONA)
return const_cast<Bau091A&>(device_).deviceObject().progMode();
#else
return false;
#endif
}
void OamRouterRuntime::setProgrammingMode(bool enabled) {
#if defined(ENABLE_BAU091A_PERSONA)
device_.deviceObject().progMode(enabled);
#else
(void)enabled;
#endif
}
void OamRouterRuntime::toggleProgrammingMode() { setProgrammingMode(!programmingMode()); }
EtsMemorySnapshot OamRouterRuntime::snapshot() const {
EtsMemorySnapshot out;
#if defined(ENABLE_BAU091A_PERSONA)
auto& device = const_cast<Bau091A&>(device_);
out.configured = device.configured();
out.individual_address = device.deviceObject().individualAddress();
#endif
return out;
}
DeviceObject* OamRouterRuntime::deviceObject() {
#if defined(ENABLE_BAU091A_PERSONA)
return &device_.deviceObject();
#else
return nullptr;
#endif
}
Platform* OamRouterRuntime::platform() {
#if defined(ENABLE_BAU091A_PERSONA)
return &platform_;
#else
return nullptr;
#endif
}
void OamRouterRuntime::setNetworkInterface(esp_netif_t* netif) {
#if defined(ENABLE_BAU091A_PERSONA)
platform_.networkInterface(netif);
#else
(void)netif;
#endif
}
bool OamRouterRuntime::handleTunnelFrame(const uint8_t* data, size_t len,
CemiFrameSender sender) {
#if defined(ENABLE_BAU091A_PERSONA) && defined(USE_CEMI_SERVER)
auto* server = device_.getCemiServer();
if (server == nullptr || data == nullptr || len < 2) {
return false;
}
std::vector<uint8_t> frame_data(data, data + len);
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(frame_data.size()));
if (!shouldConsumeTunnelFrame(frame)) {
return false;
}
sender_ = std::move(sender);
server->frameReceived(frame);
loop();
sender_ = nullptr;
return true;
#else
(void)data;
(void)len;
(void)sender;
return false;
#endif
}
void OamRouterRuntime::loop() {
#if defined(ENABLE_BAU091A_PERSONA)
device_.loop();
#endif
}
bool OamRouterRuntime::HandleOutboundCemiFrame(CemiFrame& frame, void* context) {
auto* self = static_cast<OamRouterRuntime*>(context);
if (self == nullptr || !self->sender_) {
return false;
}
self->sender_(frame.data(), frame.dataLength());
return true;
}
void OamRouterRuntime::EmitTunnelFrame(CemiFrame& frame, void* context) {
auto* self = static_cast<OamRouterRuntime*>(context);
if (self == nullptr || !self->sender_) {
return;
}
self->sender_(frame.data(), frame.dataLength());
}
uint16_t OamRouterRuntime::DefaultTunnelClientAddress(uint16_t individual_address) {
if (!IsUsableIndividualAddress(individual_address)) {
return 0x1102;
}
const uint16_t line_base = individual_address & 0xff00;
uint16_t device = static_cast<uint16_t>((individual_address & 0x00ff) + 1);
if (device == 0 || device > 0xff) {
device = 2;
}
return static_cast<uint16_t>(line_base | device);
}
bool OamRouterRuntime::shouldConsumeTunnelFrame(CemiFrame& frame) const {
switch (frame.messageCode()) {
case M_PropRead_req:
case M_PropWrite_req:
case M_Reset_req:
case M_FuncPropCommand_req:
case M_FuncPropStateRead_req:
return true;
case L_data_req: {
const uint16_t dest = frame.destinationAddress();
const bool commissioning = !configured() || programmingMode();
if (frame.addressType() == IndividualAddress) {
return dest == individualAddress() || dest == tunnelClientAddress() ||
(commissioning && dest == kKnxUnconfiguredBroadcastAddress);
}
return false;
}
default:
return false;
}
}
} // namespace gateway::openknx