df1dd472cc
Signed-off-by: Tony <tonylu@tony-cloud.com>
337 lines
9.4 KiB
C++
337 lines
9.4 KiB
C++
#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 <algorithm>
|
|
#include <array>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <string>
|
|
|
|
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<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) {
|
|
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<uint8_t>((buffer >> (bits_left - 5)) & 0x1f);
|
|
result.push_back(kBase32Alphabet[index]);
|
|
bits_left -= 5;
|
|
}
|
|
}
|
|
|
|
if (bits_left > 0) {
|
|
const uint8_t index = static_cast<uint8_t>((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<uint8_t, kSerialSize + kFdskSize + 1> buffer{};
|
|
std::copy(serial, serial + kSerialSize, buffer.begin());
|
|
std::copy(key, key + kFdskSize, buffer.begin() + kSerialSize);
|
|
buffer[kSerialSize + kFdskSize] = static_cast<uint8_t>((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<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 {
|
|
|
|
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<uint8_t, kFdskSize> key{};
|
|
std::array<uint8_t, kSerialSize> 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<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) {
|
|
return gateway::openknx::LoadFactoryFdsk(data, len);
|
|
}
|