#include "security_storage.h" #include "gateway_knx_internal.h" #include "esp_log.h" #include "esp_mac.h" #include "esp_timer.h" #include "mbedtls/sha256.h" #include "nvs.h" #include "nvs_flash.h" #include #include #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* kDevelopmentStorage = "base_mac_derived_plain_nvs_development"; constexpr char kFdskDerivationLabel[] = "DaliMaster REG1-Dali deterministic FDSK v1"; 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)); std::string hexValue(uint32_t value, int width) { std::array buffer{}; std::snprintf(buffer.data(), buffer.size(), "%0*" PRIX32, width, value); return buffer.data(); } 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; } bool readBaseMac(uint8_t* data) { if (data == nullptr) { return false; } if (esp_efuse_mac_get_default(data) == ESP_OK) { return true; } return esp_read_mac(data, ESP_MAC_WIFI_STA) == ESP_OK; } 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 loadKnxSerialNumber(uint8_t* serial) { if (serial == nullptr) { return false; } std::array mac{}; if (!readBaseMac(mac.data())) { return false; } serial[0] = static_cast( (gateway::knx_internal::kReg1DaliManufacturerId >> 8) & 0xff); serial[1] = static_cast( gateway::knx_internal::kReg1DaliManufacturerId & 0xff); std::copy(mac.begin() + 2, mac.end(), serial + 2); return true; } bool deriveFactoryFdskFromSerial(const uint8_t* serial, uint8_t* key) { if (serial == nullptr || key == nullptr) { return false; } std::array material{}; std::copy(kFdskDerivationLabel, kFdskDerivationLabel + sizeof(kFdskDerivationLabel) - 1, material.begin()); std::copy(serial, serial + kSerialSize, material.begin() + sizeof(kFdskDerivationLabel) - 1); std::array digest{}; if (mbedtls_sha256(material.data(), material.size(), digest.data(), 0) != 0) { return false; } std::copy(digest.begin(), digest.begin() + kFdskSize, key); if (!plausibleKey(key)) { key[kFdskSize - 1] ^= 0xA5; } return plausibleKey(key); } void syncFactoryFdskToNvs(const uint8_t* data) { if (data == nullptr || !plausibleKey(data) || !ensureNvsReady()) { return; } std::array stored{}; size_t stored_size = stored.size(); 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; } err = nvs_get_blob(handle, kFactoryFdskKey, stored.data(), &stored_size); if (err == ESP_OK && stored_size == stored.size() && std::equal(stored.begin(), stored.end(), data)) { nvs_close(handle); return; } 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 mirror deterministic KNX factory FDSK: %s", esp_err_to_name(err)); return; } clearOpenKnxFdskCache(); } 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) { return false; } std::array serial{}; std::array key{}; if (!loadKnxSerialNumber(serial.data()) || !deriveFactoryFdskFromSerial(serial.data(), key.data())) { return false; } std::memcpy(data, key.data(), kFdskSize); syncFactoryFdskToNvs(key.data()); return true; } FactoryFdskInfo LoadFactoryFdskInfo() { FactoryFdskInfo info; std::array key{}; std::array serial{}; if (!loadKnxSerialNumber(serial.data()) || !LoadFactoryFdsk(key.data(), key.size())) { 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{}; const bool stored = LoadFactoryFdsk(key.data(), key.size()); 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; } std::array serial{}; std::array derived{}; const bool stored = loadKnxSerialNumber(serial.data()) && deriveFactoryFdskFromSerial(serial.data(), derived.data()) && std::equal(key.begin(), key.end(), derived.begin()); if (stored) { syncFactoryFdskToNvs(derived.data()); } std::fill(key.begin(), key.end(), 0); std::fill(derived.begin(), derived.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 = hexValue(gateway::knx_internal::kReg1DaliManufacturerId, 4); payload.applicationNumber = hexValue( gateway::knx_internal::kReg1DaliApplicationNumber, 2); payload.applicationVersion = hexValue( gateway::knx_internal::kReg1DaliApplicationVersion, 2); 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); }