#include "openknx_idf/security_storage.h" #include "esp_log.h" #include "esp_mac.h" #include "esp_random.h" #include "esp_timer.h" #include "nvs.h" #include "nvs_flash.h" #include #include #include #include #include #include namespace { constexpr const char* kTag = "openknx_sec"; constexpr const char* kNamespace = "knx_sec"; 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, }; 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) { if (nvs_flash_erase() != ESP_OK) { return false; } return nvs_flash_init() == ESP_OK; } return err == ESP_OK || err == ESP_ERR_INVALID_STATE; } bool plausibleKey(const uint8_t* data) { const bool all_zero = std::all_of(data, data + kFdskSize, [](uint8_t value) { return value == 0x00; }); const bool all_ff = std::all_of(data, data + kFdskSize, [](uint8_t value) { return value == 0xff; }); return !all_zero && !all_ff; } void generateKey(uint8_t* data) { do { esp_fill_random(data, kFdskSize); } 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((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) { crc = kCrc4Tab[crc ^ (data[i] >> 4)]; crc = kCrc4Tab[crc ^ (data[i] & 0x0f)]; } return crc; } std::string toBase32NoPadding(const uint8_t* data, size_t len) { std::string result; result.reserve(((len * 8) + 4) / 5); uint32_t buffer = 0; int bits_left = 0; for (size_t i = 0; i < len; ++i) { buffer = (buffer << 8) | data[i]; bits_left += 8; while (bits_left >= 5) { const uint8_t index = static_cast((buffer >> (bits_left - 5)) & 0x1f); result.push_back(kBase32Alphabet[index]); bits_left -= 5; } } if (bits_left > 0) { const uint8_t index = static_cast((buffer << (5 - bits_left)) & 0x1f); result.push_back(kBase32Alphabet[index]); } return result; } std::string toHex(const uint8_t* data, size_t len) { std::string result; result.reserve(len * 2); for (size_t i = 0; i < len; ++i) { result.push_back(kHexAlphabet[(data[i] >> 4) & 0x0f]); result.push_back(kHexAlphabet[data[i] & 0x0f]); } return result; } std::string generateFdskQrCode(const uint8_t* serial, const uint8_t* key) { std::array buffer{}; std::copy(serial, serial + kSerialSize, buffer.begin()); std::copy(key, key + kFdskSize, buffer.begin() + kSerialSize); buffer[kSerialSize + kFdskSize] = static_cast((crc4Array(buffer.data(), buffer.size() - 1) << 4) & 0xff); std::string encoded = toBase32NoPadding(buffer.data(), buffer.size()); if (encoded.size() > kFdskQrSize) { encoded.resize(kFdskQrSize); } return encoded; } std::string formatFdskLabel(const std::string& qr_code) { std::string label; label.reserve(qr_code.size() + (qr_code.size() / 6)); for (size_t i = 0; i < qr_code.size(); ++i) { if (i != 0 && (i % 6) == 0) { label.push_back('-'); } label.push_back(qr_code[i]); } return label; } std::string fnv1aHex(const std::string& value) { uint32_t hash = 2166136261u; for (unsigned char ch : value) { hash ^= ch; hash *= 16777619u; } std::array bytes{ static_cast((hash >> 24) & 0xff), static_cast((hash >> 16) & 0xff), static_cast((hash >> 8) & 0xff), static_cast(hash & 0xff), }; return toHex(bytes.data(), bytes.size()); } } // namespace namespace gateway::openknx { bool LoadFactoryFdsk(uint8_t* data, size_t len) { if (data == nullptr || len < kFdskSize || !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; } size_t stored_size = kFdskSize; err = nvs_get_blob(handle, kFactoryFdskKey, data, &stored_size); if (err == ESP_OK && stored_size == kFdskSize && plausibleKey(data)) { nvs_close(handle); return true; } generateKey(data); 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 generated KNX factory FDSK: %s", esp_err_to_name(err)); return false; } return true; } FactoryFdskInfo LoadFactoryFdskInfo() { FactoryFdskInfo info; std::array key{}; std::array serial{}; if (!LoadFactoryFdsk(key.data(), key.size()) || esp_read_mac(serial.data(), ESP_MAC_WIFI_STA) != ESP_OK) { return info; } info.available = true; info.serialNumber = toHex(serial.data(), serial.size()); info.qrCode = generateFdskQrCode(serial.data(), key.data()); info.label = formatFdskLabel(info.qrCode); return info; } bool GenerateFactoryFdsk(FactoryFdskInfo* info) { std::array 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 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) { return gateway::openknx::LoadFactoryFdsk(data, len); }