feat(gateway): implement KNX security features including secure session handling and factory certificate management

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-12 21:29:40 +08:00
parent 888d021343
commit df1dd472cc
7 changed files with 509 additions and 3 deletions
@@ -82,6 +82,11 @@ struct DaliKnxStatusUpdate {
using BridgeDiscoveryInventory = std::map<int, BridgeDiscoveryEntry>;
extern "C" uint8_t knx_platform_copy_security_failures(uint8_t* counters, size_t countersLen,
uint8_t* records, size_t recordSize,
size_t maxRecords) __attribute__((weak));
extern "C" void knx_platform_clear_security_failures() __attribute__((weak));
class LockGuard {
public:
explicit LockGuard(SemaphoreHandle_t lock) : lock_(lock) {
@@ -150,11 +155,90 @@ cJSON* FactoryFdskInfoToCjson(const openknx::FactoryFdskInfo& fdsk_info,
return root;
}
cJSON* FactoryCertificateToCjson(const openknx::FactoryCertificatePayload& certificate,
bool include_secret_strings) {
cJSON* root = cJSON_CreateObject();
if (root == nullptr) {
return nullptr;
}
cJSON_AddBoolToObject(root, "available", certificate.available);
if (!certificate.available) {
return root;
}
cJSON_AddStringToObject(root, "productIdentity", certificate.productIdentity.c_str());
cJSON_AddStringToObject(root, "manufacturerId", certificate.manufacturerId.c_str());
cJSON_AddStringToObject(root, "applicationNumber", certificate.applicationNumber.c_str());
cJSON_AddStringToObject(root, "applicationVersion", certificate.applicationVersion.c_str());
cJSON_AddStringToObject(root, "serialNumber", certificate.serialNumber.c_str());
cJSON_AddStringToObject(root, "storage", certificate.storage.c_str());
cJSON_AddStringToObject(root, "createdAt", certificate.createdAt.c_str());
cJSON_AddStringToObject(root, "checksum", certificate.checksum.c_str());
cJSON_AddNumberToObject(root, "payloadLength",
static_cast<double>(certificate.fdskLabel.size() +
certificate.fdskQrCode.size()));
if (include_secret_strings) {
cJSON_AddStringToObject(root, "fdskLabel", certificate.fdskLabel.c_str());
cJSON_AddStringToObject(root, "fdskQrCode", certificate.fdskQrCode.c_str());
}
return root;
}
cJSON* SecurityFailuresToCjson() {
cJSON* root = cJSON_CreateObject();
if (root == nullptr) {
return nullptr;
}
cJSON* counters_json = cJSON_CreateArray();
cJSON* log_json = cJSON_CreateArray();
std::array<uint8_t, 8> counters{};
std::array<uint8_t, 8 * 8> records{};
uint8_t count = 0;
#if defined(CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED)
if (knx_platform_copy_security_failures != nullptr) {
count = knx_platform_copy_security_failures(counters.data(), counters.size(), records.data(), 8, 8);
}
#endif
if (counters_json != nullptr) {
for (uint8_t value : counters) {
cJSON_AddItemToArray(counters_json, cJSON_CreateNumber(static_cast<double>(value)));
}
cJSON_AddItemToObject(root, "counters", counters_json);
}
if (log_json != nullptr) {
for (uint8_t index = 0; index < count && index < 8; ++index) {
const size_t offset = static_cast<size_t>(index) * 8U;
cJSON* entry = cJSON_CreateObject();
if (entry == nullptr) {
continue;
}
cJSON_AddNumberToObject(entry, "class", records[offset]);
cJSON_AddNumberToObject(entry, "detail", records[offset + 1]);
cJSON_AddNumberToObject(entry, "source",
static_cast<double>((records[offset + 2] << 8) | records[offset + 3]));
cJSON_AddNumberToObject(entry, "destination",
static_cast<double>((records[offset + 4] << 8) | records[offset + 5]));
cJSON_AddNumberToObject(entry, "count", records[offset + 6]);
cJSON_AddNumberToObject(entry, "loadState", records[offset + 7]);
cJSON_AddItemToArray(log_json, entry);
}
cJSON_AddItemToObject(root, "log", log_json);
}
return root;
}
const char* JsonString(const cJSON* parent, const char* name) {
const cJSON* item = cJSON_GetObjectItemCaseSensitive(parent, name);
return cJSON_IsString(item) && item->valuestring != nullptr ? item->valuestring : nullptr;
}
[[maybe_unused]] bool JsonBool(const cJSON* parent, const char* name, bool fallback = false) {
const cJSON* item = cJSON_GetObjectItemCaseSensitive(parent, name);
if (cJSON_IsBool(item)) {
return cJSON_IsTrue(item);
}
return fallback;
}
std::optional<int> JsonInt(const cJSON* parent, const char* name) {
const cJSON* item = cJSON_GetObjectItemCaseSensitive(parent, name);
if (!cJSON_IsNumber(item)) {
@@ -2127,7 +2211,12 @@ struct GatewayBridgeService::ChannelRuntime {
#else
cJSON_AddBoolToObject(security_json, "knxnetIpSecureCompiled", false);
#endif
cJSON_AddBoolToObject(security_json, "knxnetIpSecureImplemented", false);
#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
cJSON_AddBoolToObject(security_json, "knxnetIpSecureServicesRecognized", true);
#else
cJSON_AddBoolToObject(security_json, "knxnetIpSecureServicesRecognized", false);
#endif
cJSON_AddBoolToObject(security_json, "knxnetIpSecureImplemented", false);
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS)
cJSON_AddBoolToObject(security_json, "developmentEndpointsEnabled", true);
#else
@@ -2146,6 +2235,15 @@ struct GatewayBridgeService::ChannelRuntime {
if (fdsk_json != nullptr) {
cJSON_AddItemToObject(security_json, "factorySetupKey", fdsk_json);
}
cJSON* certificate_json = FactoryCertificateToCjson(
openknx::BuildFactoryCertificatePayload(), false);
if (certificate_json != nullptr) {
cJSON_AddItemToObject(security_json, "factoryCertificate", certificate_json);
}
cJSON* failures_json = SecurityFailuresToCjson();
if (failures_json != nullptr) {
cJSON_AddItemToObject(security_json, "failures", failures_json);
}
#endif
cJSON_AddItemToObject(knx_json, "security", security_json);
}
@@ -4163,6 +4261,170 @@ GatewayBridgeHttpResponse GatewayBridgeService::handlePost(
#else
return ErrorResponse(ESP_ERR_NOT_SUPPORTED,
"KNX security development endpoints are disabled");
#endif
}
if (action == "knx_security_generate_factory_key") {
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS) && \
defined(CONFIG_GATEWAY_KNX_DATA_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 include_secret = JsonBool(body_root, "includeSecret", false);
const bool confirmed = confirm != nullptr &&
std::string_view(confirm) == "generate-factory-setup-key";
cJSON_Delete(body_root);
if (!confirmed) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "factory setup key generation confirmation is required");
}
openknx::FactoryFdskInfo info;
if (!openknx::GenerateFactoryFdsk(&info)) {
return ErrorResponse(ESP_FAIL, "failed to generate factory setup key");
}
cJSON* response = cJSON_CreateObject();
if (response == nullptr) {
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate 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_security_write_factory_key") {
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS) && \
defined(CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED)
cJSON* body_root = body.empty() ? nullptr : cJSON_ParseWithLength(body.data(), body.size());
if (body_root == nullptr) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "factory setup key JSON is required");
}
const char* confirm = JsonString(body_root, "confirm");
const char* key_hex = JsonString(body_root, "keyHex");
const bool include_secret = JsonBool(body_root, "includeSecret", false);
const bool confirmed = confirm != nullptr &&
std::string_view(confirm) == "write-factory-setup-key";
if (!confirmed || key_hex == nullptr) {
cJSON_Delete(body_root);
return ErrorResponse(ESP_ERR_INVALID_ARG,
"factory setup key write confirmation and keyHex are required");
}
const std::string key_value(key_hex);
cJSON_Delete(body_root);
openknx::FactoryFdskInfo info;
if (!openknx::WriteFactoryFdskHex(key_value, &info)) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "invalid factory setup key hex value");
}
cJSON* response = cJSON_CreateObject();
if (response == nullptr) {
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate 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_security_reset_factory_key") {
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS) && \
defined(CONFIG_GATEWAY_KNX_DATA_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) == "reset-factory-setup-key";
cJSON_Delete(body_root);
if (!confirmed) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "factory setup key reset confirmation is required");
}
openknx::FactoryFdskInfo info;
if (!openknx::ResetFactoryFdskCache(&info)) {
return ErrorResponse(ESP_FAIL, "failed to reload factory setup key");
}
cJSON* response = cJSON_CreateObject();
if (response == nullptr) {
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate 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_security_export_factory_certificate") {
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS) && \
defined(CONFIG_GATEWAY_KNX_DATA_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) == "export-factory-certificate";
cJSON_Delete(body_root);
if (!confirmed) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "factory certificate export confirmation is required");
}
cJSON* response = cJSON_CreateObject();
if (response == nullptr) {
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate security response");
}
cJSON* certificate_json = FactoryCertificateToCjson(
openknx::BuildFactoryCertificatePayload(), true);
if (certificate_json == nullptr) {
cJSON_Delete(response);
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate factory certificate response");
}
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_security_clear_failures") {
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS) && \
defined(CONFIG_GATEWAY_KNX_DATA_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-security-failures";
cJSON_Delete(body_root);
if (!confirmed) {
return ErrorResponse(ESP_ERR_INVALID_ARG, "security failure clear confirmation is required");
}
if (knx_platform_clear_security_failures != nullptr) {
knx_platform_clear_security_failures();
}
cJSON* response = cJSON_CreateObject();
if (response == nullptr) {
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate security response");
}
cJSON* failures_json = SecurityFailuresToCjson();
if (failures_json != nullptr) {
cJSON_AddItemToObject(response, "failures", failures_json);
}
return JsonOk(response);
#else
return ErrorResponse(ESP_ERR_NOT_SUPPORTED,
"KNX security development endpoints are disabled");
#endif
}
if (action == "bacnet_start") {
@@ -216,8 +216,11 @@ class GatewayKnxTpIpRouter {
void handleConnectionStateRequest(const uint8_t* body, size_t len,
const ::sockaddr_in& remote);
void handleDisconnectRequest(const uint8_t* body, size_t len, const ::sockaddr_in& remote);
void handleSecureService(uint16_t service, 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 sendSecureSessionStatus(uint8_t status, const ::sockaddr_in& remote);
void sendTunnelIndication(const uint8_t* data, size_t len);
void sendConnectionStateResponse(uint8_t channel_id, uint8_t status,
const ::sockaddr_in& remote);
@@ -35,6 +35,12 @@ constexpr uint16_t kServiceDisconnectResponse = 0x020a;
constexpr uint16_t kServiceTunnellingRequest = 0x0420;
constexpr uint16_t kServiceTunnellingAck = 0x0421;
constexpr uint16_t kServiceRoutingIndication = 0x0530;
constexpr uint16_t kServiceSecureWrapper = 0x0950;
constexpr uint16_t kServiceSecureSessionRequest = 0x0951;
constexpr uint16_t kServiceSecureSessionResponse = 0x0952;
constexpr uint16_t kServiceSecureSessionAuth = 0x0953;
constexpr uint16_t kServiceSecureSessionStatus = 0x0954;
constexpr uint16_t kServiceSecureGroupSync = 0x0955;
constexpr uint8_t kKnxNetIpHeaderSize = 0x06;
constexpr uint8_t kKnxNetIpVersion10 = 0x10;
constexpr uint8_t kKnxNoError = 0x00;
@@ -42,6 +48,8 @@ constexpr uint8_t kKnxErrorConnectionId = 0x21;
constexpr uint8_t kKnxErrorConnectionType = 0x22;
constexpr uint8_t kKnxErrorNoMoreConnections = 0x24;
constexpr uint8_t kKnxErrorSequenceNumber = 0x04;
constexpr uint8_t kKnxSecureStatusAuthFailed = 0x01;
constexpr uint8_t kKnxSecureStatusUnauthenticated = 0x02;
constexpr uint8_t kKnxConnectionTypeTunnel = 0x04;
constexpr uint8_t kKnxTunnelLayerLink = 0x02;
constexpr uint8_t kTpUartResetRequest = 0x01;
@@ -507,6 +515,20 @@ bool ParseKnxNetIpHeader(const uint8_t* data, size_t len, uint16_t* service,
return *total_len >= 6 && *total_len <= len;
}
bool IsKnxNetIpSecureService(uint16_t service) {
switch (service) {
case kServiceSecureWrapper:
case kServiceSecureSessionRequest:
case kServiceSecureSessionResponse:
case kServiceSecureSessionAuth:
case kServiceSecureSessionStatus:
case kServiceSecureGroupSync:
return true;
default:
return false;
}
}
bool IsExtendedTpFrame(const uint8_t* data, size_t len) {
return len > 0 && (data[0] & 0xD3) == 0x10;
}
@@ -1875,6 +1897,10 @@ void GatewayKnxTpIpRouter::handleUdpDatagram(const uint8_t* data, size_t len,
}
const uint8_t* body = data + 6;
const size_t body_len = total_len - 6;
if (IsKnxNetIpSecureService(service)) {
handleSecureService(service, body, body_len, remote);
return;
}
switch (service) {
case kServiceRoutingIndication:
if (config_.multicast_enabled) {
@@ -2001,6 +2027,34 @@ void GatewayKnxTpIpRouter::handleDisconnectRequest(const uint8_t* body, size_t l
sendDisconnectResponse(channel_id, status, remote);
}
void GatewayKnxTpIpRouter::handleSecureService(uint16_t service, const uint8_t* body,
size_t len, const sockaddr_in& remote) {
#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
switch (service) {
case kServiceSecureSessionRequest:
case kServiceSecureSessionAuth:
ESP_LOGW(kTag, "KNXnet/IP Secure service 0x%04x rejected: secure sessions are not provisioned", service);
sendSecureSessionStatus(kKnxSecureStatusAuthFailed, remote);
break;
case kServiceSecureWrapper:
ESP_LOGW(kTag, "KNXnet/IP Secure wrapper rejected: no authenticated secure session");
sendSecureSessionStatus(kKnxSecureStatusUnauthenticated, remote);
break;
case kServiceSecureGroupSync:
ESP_LOGD(kTag, "KNXnet/IP Secure group sync ignored until secure routing is provisioned");
break;
default:
ESP_LOGD(kTag, "KNXnet/IP Secure service 0x%04x ignored", service);
break;
}
#else
(void)service;
(void)body;
(void)len;
(void)remote;
#endif
}
void GatewayKnxTpIpRouter::sendTunnellingAck(uint8_t channel_id, uint8_t sequence,
uint8_t status, const sockaddr_in& remote) {
const std::vector<uint8_t> body{0x04, channel_id, sequence, status};
@@ -2008,6 +2062,12 @@ void GatewayKnxTpIpRouter::sendTunnellingAck(uint8_t channel_id, uint8_t sequenc
SendAll(udp_sock_, packet.data(), packet.size(), remote);
}
void GatewayKnxTpIpRouter::sendSecureSessionStatus(uint8_t status, const sockaddr_in& remote) {
const std::vector<uint8_t> body{status, 0x00};
const auto packet = KnxNetIpPacket(kServiceSecureSessionStatus, body);
SendAll(udp_sock_, packet.data(), packet.size(), remote);
}
void GatewayKnxTpIpRouter::sendTunnelIndication(const uint8_t* data, size_t len) {
if (!tunnel_connected_ || udp_sock_ < 0 || data == nullptr || len == 0) {
return;
@@ -13,7 +13,25 @@ struct FactoryFdskInfo {
std::string qrCode;
};
struct FactoryCertificatePayload {
bool available{false};
std::string productIdentity;
std::string manufacturerId;
std::string applicationNumber;
std::string applicationVersion;
std::string serialNumber;
std::string fdskLabel;
std::string fdskQrCode;
std::string storage;
std::string createdAt;
std::string checksum;
};
bool LoadFactoryFdsk(uint8_t* data, size_t len);
FactoryFdskInfo LoadFactoryFdskInfo();
bool GenerateFactoryFdsk(FactoryFdskInfo* info = nullptr);
bool WriteFactoryFdskHex(const std::string& hex_key, FactoryFdskInfo* info = nullptr);
bool ResetFactoryFdskCache(FactoryFdskInfo* info = nullptr);
FactoryCertificatePayload BuildFactoryCertificatePayload();
} // namespace gateway::openknx
@@ -3,6 +3,7 @@
#include "esp_log.h"
#include "esp_mac.h"
#include "esp_random.h"
#include "esp_timer.h"
#include "nvs.h"
#include "nvs_flash.h"
@@ -10,6 +11,7 @@
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <string>
namespace {
@@ -20,6 +22,11 @@ constexpr const char* kFactoryFdskKey = "factory_fdsk";
constexpr size_t kFdskSize = 16;
constexpr size_t kSerialSize = 6;
constexpr size_t kFdskQrSize = 36;
constexpr const char* kProductIdentity = "REG1-Dali";
constexpr const char* kManufacturerId = "00A4";
constexpr const char* kApplicationNumber = "01";
constexpr const char* kApplicationVersion = "05";
constexpr const char* kDevelopmentStorage = "plain_nvs_development";
constexpr uint8_t kCrc4Tab[16] = {
0x0, 0x3, 0x6, 0x5, 0xc, 0xf, 0xa, 0x9,
0xb, 0x8, 0xd, 0xe, 0x7, 0x4, 0x1, 0x2,
@@ -27,6 +34,8 @@ constexpr uint8_t kCrc4Tab[16] = {
constexpr char kBase32Alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
constexpr char kHexAlphabet[] = "0123456789ABCDEF";
extern "C" void knx_platform_clear_cached_fdsk() __attribute__((weak));
bool ensureNvsReady() {
const esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
@@ -54,6 +63,75 @@ void generateKey(uint8_t* data) {
} while (!plausibleKey(data));
}
void clearOpenKnxFdskCache() {
if (knx_platform_clear_cached_fdsk != nullptr) {
knx_platform_clear_cached_fdsk();
}
}
int fromHexDigit(char value) {
if (value >= '0' && value <= '9') {
return value - '0';
}
if (value >= 'a' && value <= 'f') {
return value - 'a' + 10;
}
if (value >= 'A' && value <= 'F') {
return value - 'A' + 10;
}
return -1;
}
bool parseHexKey(const std::string& value, uint8_t* out) {
std::string digits;
digits.reserve(value.size());
for (char ch : value) {
if (ch == ':' || ch == '-' || ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r') {
continue;
}
if (fromHexDigit(ch) < 0) {
return false;
}
digits.push_back(ch);
}
if (digits.size() != kFdskSize * 2U) {
return false;
}
for (size_t index = 0; index < kFdskSize; ++index) {
const int hi = fromHexDigit(digits[index * 2U]);
const int lo = fromHexDigit(digits[index * 2U + 1U]);
if (hi < 0 || lo < 0) {
return false;
}
out[index] = static_cast<uint8_t>((hi << 4) | lo);
}
return plausibleKey(out);
}
bool storeFactoryFdsk(const uint8_t* data) {
if (data == nullptr || !plausibleKey(data) || !ensureNvsReady()) {
return false;
}
nvs_handle_t handle = 0;
esp_err_t err = nvs_open(kNamespace, NVS_READWRITE, &handle);
if (err != ESP_OK) {
ESP_LOGW(kTag, "failed to open KNX security NVS namespace: %s", esp_err_to_name(err));
return false;
}
err = nvs_set_blob(handle, kFactoryFdskKey, data, kFdskSize);
if (err == ESP_OK) {
err = nvs_commit(handle);
}
nvs_close(handle);
if (err != ESP_OK) {
ESP_LOGW(kTag, "failed to store KNX factory FDSK: %s", esp_err_to_name(err));
return false;
}
clearOpenKnxFdskCache();
return true;
}
uint8_t crc4Array(const uint8_t* data, size_t len) {
uint8_t crc = 0;
for (size_t i = 0; i < len; ++i) {
@@ -121,6 +199,21 @@ std::string formatFdskLabel(const std::string& qr_code) {
return label;
}
std::string fnv1aHex(const std::string& value) {
uint32_t hash = 2166136261u;
for (unsigned char ch : value) {
hash ^= ch;
hash *= 16777619u;
}
std::array<uint8_t, 4> bytes{
static_cast<uint8_t>((hash >> 24) & 0xff),
static_cast<uint8_t>((hash >> 16) & 0xff),
static_cast<uint8_t>((hash >> 8) & 0xff),
static_cast<uint8_t>(hash & 0xff),
};
return toHex(bytes.data(), bytes.size());
}
} // namespace
namespace gateway::openknx {
@@ -174,6 +267,68 @@ FactoryFdskInfo LoadFactoryFdskInfo() {
return info;
}
bool GenerateFactoryFdsk(FactoryFdskInfo* info) {
std::array<uint8_t, kFdskSize> key{};
generateKey(key.data());
const bool stored = storeFactoryFdsk(key.data());
std::fill(key.begin(), key.end(), 0);
if (!stored) {
return false;
}
if (info != nullptr) {
*info = LoadFactoryFdskInfo();
}
return true;
}
bool WriteFactoryFdskHex(const std::string& hex_key, FactoryFdskInfo* info) {
std::array<uint8_t, kFdskSize> key{};
if (!parseHexKey(hex_key, key.data())) {
return false;
}
const bool stored = storeFactoryFdsk(key.data());
std::fill(key.begin(), key.end(), 0);
if (!stored) {
return false;
}
if (info != nullptr) {
*info = LoadFactoryFdskInfo();
}
return true;
}
bool ResetFactoryFdskCache(FactoryFdskInfo* info) {
clearOpenKnxFdskCache();
const auto loaded = LoadFactoryFdskInfo();
if (info != nullptr) {
*info = loaded;
}
return loaded.available;
}
FactoryCertificatePayload BuildFactoryCertificatePayload() {
FactoryCertificatePayload payload;
const auto info = LoadFactoryFdskInfo();
if (!info.available) {
return payload;
}
payload.available = true;
payload.productIdentity = kProductIdentity;
payload.manufacturerId = kManufacturerId;
payload.applicationNumber = kApplicationNumber;
payload.applicationVersion = kApplicationVersion;
payload.serialNumber = info.serialNumber;
payload.fdskLabel = info.label;
payload.fdskQrCode = info.qrCode;
payload.storage = kDevelopmentStorage;
payload.createdAt = "uptime_us:" + std::to_string(esp_timer_get_time());
payload.checksum = fnv1aHex(payload.productIdentity + "|" + payload.manufacturerId + "|" +
payload.applicationNumber + "|" + payload.applicationVersion + "|" +
payload.serialNumber + "|" + payload.fdskLabel + "|" +
payload.fdskQrCode + "|" + payload.createdAt);
return payload;
}
} // namespace gateway::openknx
extern "C" bool knx_platform_get_fdsk(uint8_t* data, size_t len) {