From 0a0d0b9ea7b0f44ee3ab57803f959b5ac4478b21 Mon Sep 17 00:00:00 2001 From: iranl Date: Tue, 11 Feb 2025 17:00:56 +0100 Subject: [PATCH 01/19] No TOTP if not time synced --- src/NukiNetwork.cpp | 2 +- src/WebCfgServer.cpp | 27 +++++++++------------------ 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/src/NukiNetwork.cpp b/src/NukiNetwork.cpp index a010aee..8dfd06e 100644 --- a/src/NukiNetwork.cpp +++ b/src/NukiNetwork.cpp @@ -1093,7 +1093,7 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns { if(_preferences->getBool(preference_cred_duo_approval, false) && (_importExport->getTOTPEnabled() || _importExport->getDuoEnabled())) { - if(_importExport->getTOTPEnabled() && !doc["totp"].isNull()) + if(timeSynced && _importExport->getTOTPEnabled() && !doc["totp"].isNull()) { String jsonTotp = doc["totp"]; diff --git a/src/WebCfgServer.cpp b/src/WebCfgServer.cpp index 3598eb2..80bba13 100644 --- a/src/WebCfgServer.cpp +++ b/src/WebCfgServer.cpp @@ -629,7 +629,7 @@ void WebCfgServer::initialize() _importExport->_sessionsOpts[request->client()->localIP().toString() + "approve"] = false; return sendSettings(request, resp); } - else if(request->hasParam("totpkey") && _importExport->getTOTPEnabled()) + else if(timeSynced && request->hasParam("totpkey") && _importExport->getTOTPEnabled()) { const PsychicWebParameter* pass = request->getParam("totpkey"); if(pass->value() != "") @@ -853,7 +853,7 @@ void WebCfgServer::initialize() if(!_importExport->_sessionsOpts[request->client()->localIP().toString() + "approve"]) { bool approved = false; - if(request->hasParam("totpkey") && _importExport->getTOTPEnabled()) + if(timeSynced && request->hasParam("totpkey") && _importExport->getTOTPEnabled()) { const PsychicWebParameter* pass = request->getParam("totpkey"); if(pass->value() != "") @@ -1880,18 +1880,15 @@ esp_err_t WebCfgServer::buildLoginHtml(PsychicRequest *request, PsychicResponse* esp_err_t WebCfgServer::buildTOTPHtml(PsychicRequest *request, PsychicResponse* resp, int type) { + if (!timeSynced) + { + return buildConfirmHtml(request, resp, "NTP time not synced yet, TOTP not available, please wait for NTP to sync", 3, true); + } + PsychicStreamResponse response(resp, "text/html"); response.beginSend(); response.print(""); response.print(""); - /* - if (!timeSynced) - { - char millis[20]; - itoa(espMillis(), millis, 10); - response.print((String)""); - } - */ response.print("

Nuki Hub TOTP

"); String typeText = "Login"; @@ -1931,12 +1928,6 @@ esp_err_t WebCfgServer::buildTOTPHtml(PsychicRequest *request, PsychicResponse* response.print("
"); response.print(""); - /* - if (!timeSynced) - { - response.print(""); - } - */ response.print("
"); + response.print("
"); + return response.endSend(); +} + esp_err_t WebCfgServer::buildDuoCheckHtml(PsychicRequest *request, PsychicResponse* resp) { char valueStr[2]; @@ -1986,7 +2069,7 @@ esp_err_t WebCfgServer::buildDuoHtml(PsychicRequest *request, PsychicResponse* r { if (!timeSynced) { - return buildConfirmHtml(request, resp, "NTP time not synced yet, Duo not available, please wait for NTP to sync", 3, true); + return buildConfirmHtml(request, resp, "NTP time not synced yet, Duo not available, please wait for NTP to sync or use one-time bypass", 3, true); } String duoText; @@ -2130,6 +2213,52 @@ bool WebCfgServer::processLogin(PsychicRequest *request, PsychicResponse* resp) return false; } +bool WebCfgServer::processBypass(PsychicRequest *request, PsychicResponse* resp) +{ + if(!timeSynced && request->hasParam("bypass")) + { + const PsychicWebParameter* pass = request->getParam("bypass"); + if(pass->value() != "") + { + String bypass = pass->value(); + if (_importExport->checkBypass(bypass)) + { + char buffer[33]; + int i; + for (i = 0; i < 4; i++) { + sprintf(buffer + (i * 8), "%08lx", (unsigned long int)esp_random()); + } + + if (!_isSSL) + { + resp->setCookie("bypassId", buffer, 3600, "HttpOnly"); + } + else + { + resp->setCookie("bypassId", buffer, 3600, "Secure; HttpOnly"); + } + + struct timeval time; + gettimeofday(&time, NULL); + int64_t time_us = (int64_t)time.tv_sec * 1000000L + (int64_t)time.tv_usec; + _importExport->_bypassSessions[buffer] = time_us + ((int64_t)3600*1000000L); + + char randomstr2[33]; + randomSeed(analogRead(0)); + char chars[] = {'1', '2', '3','4', '5', '6','7', '8', '9', '0', 'A', 'B', 'C', 'D','E', 'F', 'G','H', 'I', 'J','K', 'L', 'M', 'N', 'O','P', 'Q','R', 'S', 'T','U', 'V', 'W','X', 'Y', 'Z'}; + for(int i = 0;i < 32; i++){ + randomstr2[i] = chars[random(36)]; + } + randomstr2[32] = '\0'; + _preferences->putString(preference_bypass_secret, randomstr2); + + return true; + } + } + } + return false; +} + bool WebCfgServer::processTOTP(PsychicRequest *request, PsychicResponse* resp) { if(timeSynced && request->hasParam("totpkey")) @@ -4151,6 +4280,19 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S } } } + else if(key == "CREDBYPASS") + { + if(value != "*") + { + if(_preferences->getString(preference_bypass_secret, "") != value) + { + _preferences->putString(preference_bypass_secret, value); + Log->print("Setting changed: "); + Log->println(key); + configChanged = true; + } + } + } else if(key == "NUKIPIN" && _nuki != nullptr) { if(value == "#") @@ -4708,6 +4850,13 @@ esp_err_t WebCfgServer::buildCredHtml(PsychicRequest *request, PsychicResponse* randomstr[i] = chars[random(32)]; } randomstr[16] = '\0'; + char randomstr2[33]; + randomSeed(analogRead(0)); + char chars2[] = {'1', '2', '3','4', '5', '6','7', '8', '9', '0', 'A', 'B', 'C', 'D','E', 'F', 'G','H', 'I', 'J','K', 'L', 'M', 'N', 'O','P', 'Q','R', 'S', 'T','U', 'V', 'W','X', 'Y', 'Z'}; + for(int i = 0;i < 32; i++){ + randomstr2[i] = chars2[random(36)]; + } + randomstr2[32] = '\0'; PsychicStreamResponse response(resp, "text/html"); response.beginSend(); @@ -4738,6 +4887,10 @@ esp_err_t WebCfgServer::buildCredHtml(PsychicRequest *request, PsychicResponse* response.print(""); + printInputField(&response, "CREDBYPASS", "One-time MFA Bypass", "*", 32, "", true, false); + response.print(""); printInputField(&response, "CREDLFTM", "Session validity (in seconds)", _preferences->getInt(preference_cred_session_lifetime, 3600), 12, ""); printInputField(&response, "CREDLFTMRMBR", "Session validity remember (in hours)", _preferences->getInt(preference_cred_session_lifetime_remember, 720), 12, ""); printInputField(&response, "CREDDUOLFTM", "Duo Session validity (in seconds)", _preferences->getInt(preference_cred_session_lifetime_duo, 3600), 12, ""); diff --git a/src/WebCfgServer.h b/src/WebCfgServer.h index 6e3e763..fbc5bdb 100644 --- a/src/WebCfgServer.h +++ b/src/WebCfgServer.h @@ -111,9 +111,11 @@ private: bool isAuthenticated(PsychicRequest *request, int type = 0); bool processLogin(PsychicRequest *request, PsychicResponse* resp); bool processTOTP(PsychicRequest *request, PsychicResponse* resp); + bool processBypass(PsychicRequest *request, PsychicResponse* resp); int doAuthentication(PsychicRequest *request); esp_err_t buildCoredumpHtml(PsychicRequest *request, PsychicResponse* resp); esp_err_t buildLoginHtml(PsychicRequest *request, PsychicResponse* resp); + esp_err_t buildBypassHtml(PsychicRequest *request, PsychicResponse* resp); esp_err_t buildTOTPHtml(PsychicRequest *request, PsychicResponse* resp, int type); esp_err_t buildDuoHtml(PsychicRequest *request, PsychicResponse* resp, int type); esp_err_t buildDuoCheckHtml(PsychicRequest *request, PsychicResponse* resp); @@ -150,6 +152,7 @@ private: JsonDocument _httpSessions; bool _duoEnabled = false; bool _bypassGPIO = false; + bool _newBypass = false; int _bypassGPIOHigh = -1; int _bypassGPIOLow = -1; }; diff --git a/src/main.cpp b/src/main.cpp index 142450f..3be50bc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -546,7 +546,7 @@ void logCoreDump() file.printf("%s\r\n", NUKI_HUB_HW); file.printf("%s\r\n", NUKI_HUB_BUILD); } - + Serial.printf("%s\r\n", NUKI_HUB_HW); Serial.printf("%s\r\n", NUKI_HUB_BUILD); @@ -581,7 +581,7 @@ void logCoreDump() file.printf("%s", str_dst); } } - + Serial.println(""); if (file) { @@ -669,7 +669,7 @@ void setup() { preferences->putString(preference_updater_date, NUKI_HUB_DATE); } - + importExport = new ImportExport(preferences); network = new NukiNetwork(preferences); @@ -796,7 +796,7 @@ void setup() Log->print(gpioDesc.c_str()); const String mqttLockPath = preferences->getString(preference_mqtt_lock_path); - + importExport = new ImportExport(preferences); network = new NukiNetwork(preferences, gpio, mqttLockPath, CharBuffer::get(), buffer_size, importExport); From 92a6ca5b354b1bce9d49317eba7a5414b2a91c91 Mon Sep 17 00:00:00 2001 From: iranl Date: Fri, 14 Feb 2025 20:32:15 +0100 Subject: [PATCH 06/19] Add Admin key --- README.md | 2 + src/Config.h | 2 +- src/ImportExport.cpp | 4 ++ src/PreferencesKeys.h | 6 +- src/WebCfgServer.cpp | 134 +++++++++++++++++++++++++++++++++++++----- src/WebCfgServer.h | 2 +- 6 files changed, 131 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index b683a0e..23fcb54 100644 --- a/README.md +++ b/README.md @@ -333,6 +333,8 @@ Note: All of the following requires the Nuki security code / PIN to be set, see - Duo secret key: Set to the Duo secret key - Duo user: Set to the Duo user that you want to receive the push notification - TOTP Secret Key: Set a TOTP secret key to enable TOTP MFA. Enter the TOTP secret key in an authenticator application (Password manager, Microsoft/Google Authenticator etc.) to generate TOTP codes. +- One-time MFA Bypass: Set a 32 character long alphanumeric string that can be used as a one-time MFA bypass when the ESP32 is unable to sync it's time and TOTP and Duo are unavailable as a result. +- Admin key: Set a 32 character long alphanumeric string that can be used in combination with a TOTP code to export and import settings without needing to log in (for use with automated systems). - Session validity (in seconds): Session validity to use with form authentication when the "Remember me" checkbox is disabled, default 3600 seconds. - Session validity remember (in hours): Session validity to use with form authentication when the "Remember me" checkbox is enabled, default 720 hours. - Duo Session validity (in seconds): Session validity to use with Duo authentication when the "Remember me" checkbox is disabled, default 3600 seconds. diff --git a/src/Config.h b/src/Config.h index cbc09ea..5f64dce 100644 --- a/src/Config.h +++ b/src/Config.h @@ -5,7 +5,7 @@ #define NUKI_HUB_VERSION "9.09" #define NUKI_HUB_VERSION_INT (uint32_t)909 #define NUKI_HUB_BUILD "unknownbuildnr" -#define NUKI_HUB_DATE "2025-02-13" +#define NUKI_HUB_DATE "2025-02-14" #define GITHUB_LATEST_RELEASE_URL (char*)"https://github.com/technyon/nuki_hub/releases/latest" #define GITHUB_OTA_MANIFEST_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/manifest.json" diff --git a/src/ImportExport.cpp b/src/ImportExport.cpp index 8d307c2..049be5c 100644 --- a/src/ImportExport.cpp +++ b/src/ImportExport.cpp @@ -477,6 +477,10 @@ void ImportExport::exportNukiHubJson(JsonDocument &json, bool redacted, bool pai { continue; } + if(strcmp(key, preference_admin_secret) == 0) + { + continue; + } if(!redacted) if(std::find(redactedPrefs.begin(), redactedPrefs.end(), key) != redactedPrefs.end()) { continue; diff --git a/src/PreferencesKeys.h b/src/PreferencesKeys.h index 6722fce..9cfcdef 100644 --- a/src/PreferencesKeys.h +++ b/src/PreferencesKeys.h @@ -97,6 +97,7 @@ #define preference_config_from_mqtt (char*)"nhCntrlEnabled" #define preference_totp_secret (char*)"totpsecret" #define preference_bypass_secret (char*)"bypassecret" +#define preference_admin_secret (char*)"adminsecret" // CHANGE DOES NOT REQUIRE REBOOT TO TAKE EFFECT #define preference_find_best_rssi (char*)"nwbestrssi" @@ -536,13 +537,14 @@ private: preference_cred_duo_host, preference_cred_duo_ikey, preference_cred_duo_skey, preference_cred_duo_user, preference_cred_duo_enabled, preference_https_fqdn, preference_bypass_proxy, preference_cred_session_lifetime, preference_cred_session_lifetime_remember, preference_cred_session_lifetime_duo, preference_cred_session_lifetime_duo_remember, preference_cred_duo_approval, preference_cred_bypass_boot_btn_enabled, preference_cred_bypass_gpio_high, preference_cred_bypass_gpio_low, preference_publish_config, - preference_config_from_mqtt, preference_totp_secret, preference_cred_session_lifetime_totp, preference_cred_session_lifetime_totp_remember, preference_bypass_secret + preference_config_from_mqtt, preference_totp_secret, preference_cred_session_lifetime_totp, preference_cred_session_lifetime_totp_remember, preference_bypass_secret, + preference_admin_secret }; std::vector _redact = { preference_mqtt_user, preference_mqtt_password, preference_cred_user, preference_cred_password, preference_nuki_id_lock, preference_nuki_id_opener, preference_wifi_pass, preference_lock_gemini_pin, preference_cred_duo_host, preference_cred_duo_ikey, preference_cred_duo_skey, preference_cred_duo_user, preference_bypass_proxy, - preference_totp_secret, preference_bypass_secret + preference_totp_secret, preference_bypass_secret, preference_admin_secret }; std::vector _boolPrefs = { diff --git a/src/WebCfgServer.cpp b/src/WebCfgServer.cpp index bb14218..77d492f 100644 --- a/src/WebCfgServer.cpp +++ b/src/WebCfgServer.cpp @@ -531,10 +531,38 @@ void WebCfgServer::initialize() value = p->value(); } } - int authReq = doAuthentication(request); - if (value != "status" && value != "login" && value != "duocheck" && value != "bypass") + bool adminKeyValid = false; + if(value == "export" && timeSynced && request->hasParam("adminkey") && request->hasParam("totpkey") && _importExport->getTOTPEnabled()) { + String value2 = ""; + if(request->hasParam("adminkey")) + { + const PsychicWebParameter* p = request->getParam("adminkey"); + if(p->value() != "") + { + value2 = p->value(); + } + } + String value3 = ""; + if(request->hasParam("totpkey")) + { + const PsychicWebParameter* p = request->getParam("totpkey"); + if(p->value() != "") + { + value3 = p->value(); + } + } + if (value2.length() > 0 && value2 == _preferences->getString(preference_admin_secret, "") && _importExport->checkTOTP(&value3)) + { + adminKeyValid = true; + } + } + + if (!adminKeyValid && value != "status" && value != "login" && value != "duocheck" && value != "bypass") + { + int authReq = doAuthentication(request); + switch (authReq) { case 0: @@ -569,14 +597,16 @@ void WebCfgServer::initialize() break; } } - else if (value == "status" && authReq != 4) + else if (value == "status") { - resp->setCode(200); - resp->setContentType("application/json"); - resp->setContent("{}"); - return resp->send(); + if (doAuthentication(request) != 4) + { + resp->setCode(200); + resp->setContentType("application/json"); + resp->setContent("{}"); + return resp->send(); + } } - if (value == "login") { return buildLoginHtml(request, resp); @@ -592,7 +622,7 @@ void WebCfgServer::initialize() else if (value == "newbypass" && _newBypass) { _newBypass = false; - return buildConfirmHtml(request, resp, "Logged in using Bypass. New bypass: " + _preferences->getString(preference_bypass_secret, ""), 3, false); + return buildConfirmHtml(request, resp, "Logged in using Bypass. New bypass: " + _preferences->getString(preference_bypass_secret, "") + "

Home page", 3, false); } else if (value == "logout") { @@ -659,6 +689,11 @@ void WebCfgServer::initialize() return sendSettings(request, resp); } + if(adminKeyValid) + { + return sendSettings(request, resp, true); + } + if(_importExport->_sessionsOpts[request->client()->localIP().toString() + "approve"]) { _importExport->_sessionsOpts[request->client()->localIP().toString() + "approve"] = false; @@ -851,7 +886,33 @@ void WebCfgServer::initialize() } } - if (value != "login" && value != "totp" && value != "bypass") + bool adminKeyValid = false; + if(value == "import" && timeSynced && request->hasParam("adminkey") && request->hasParam("totpkey") && _importExport->getTOTPEnabled()) + { + String value2 = ""; + if(request->hasParam("adminkey")) + { + const PsychicWebParameter* p = request->getParam("adminkey"); + if(p->value() != "") + { + value2 = p->value(); + } + } + String value3 = ""; + if(request->hasParam("totpkey")) + { + const PsychicWebParameter* p = request->getParam("totpkey"); + if(p->value() != "") + { + value3 = p->value(); + } + } + if (value2.length() > 0 && value2 == _preferences->getString(preference_admin_secret, "") && _importExport->checkTOTP(&value3)) + { + adminKeyValid = true; + } + } + if(!adminKeyValid && value != "login" && value != "totp" && value != "bypass") { int authReq = doAuthentication(request); @@ -1026,7 +1087,23 @@ void WebCfgServer::initialize() { String message = ""; bool restart = processImport(request, resp, message); - return buildConfirmHtml(request, resp, message, 3, true); + + if(adminKeyValid) + { + resp->setCode(200); + resp->setContentType("application/json"); + resp->setContent("{ \"result\": \"success\"}"); + esp_err_t res = resp->send(); + if(restart) + { + restartEsp(RestartReason::RequestedViaWebServer); + } + return res; + } + else + { + return buildConfirmHtml(request, resp, message, 3, true); + } } #endif else @@ -2303,7 +2380,7 @@ bool WebCfgServer::processTOTP(PsychicRequest *request, PsychicResponse* resp) } #ifndef NUKI_HUB_UPDATER -esp_err_t WebCfgServer::sendSettings(PsychicRequest *request, PsychicResponse* resp) +esp_err_t WebCfgServer::sendSettings(PsychicRequest *request, PsychicResponse* resp, bool adminKey) { JsonDocument json; String jsonPretty; @@ -2352,7 +2429,10 @@ esp_err_t WebCfgServer::sendSettings(PsychicRequest *request, PsychicResponse* r serializeJsonPretty(json, jsonPretty); char buf[26 + name.length()]; snprintf(buf, sizeof(buf), "attachment; filename=\"%s\"", name.c_str()); - resp->addHeader("Content-Disposition", buf); + if(!adminKey) + { + resp->addHeader("Content-Disposition", buf); + } resp->setCode(200); resp->setContentType("application/json"); resp->setContent(jsonPretty.c_str()); @@ -4293,6 +4373,19 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S } } } + else if(key == "CREDADMIN") + { + if(value != "*") + { + if(_preferences->getString(preference_admin_secret, "") != value) + { + _preferences->putString(preference_admin_secret, value); + Log->print("Setting changed: "); + Log->println(key); + configChanged = true; + } + } + } else if(key == "NUKIPIN" && _nuki != nullptr) { if(value == "#") @@ -4843,20 +4936,27 @@ esp_err_t WebCfgServer::buildHtml(PsychicRequest *request, PsychicResponse* resp esp_err_t WebCfgServer::buildCredHtml(PsychicRequest *request, PsychicResponse* resp) { + char chars[] = {'2', '3','4', '5', '6','7', 'A', 'B', 'C', 'D','E', 'F', 'G','H', 'I', 'J','K', 'L', 'M', 'N', 'O','P', 'Q','R', 'S', 'T','U', 'V', 'W','X', 'Y', 'Z'}; + char chars2[] = {'1', '2', '3','4', '5', '6','7', '8', '9', '0', 'A', 'B', 'C', 'D','E', 'F', 'G','H', 'I', 'J','K', 'L', 'M', 'N', 'O','P', 'Q','R', 'S', 'T','U', 'V', 'W','X', 'Y', 'Z'}; + char randomstr[17]; randomSeed(analogRead(0)); - char chars[] = {'2', '3','4', '5', '6','7', 'A', 'B', 'C', 'D','E', 'F', 'G','H', 'I', 'J','K', 'L', 'M', 'N', 'O','P', 'Q','R', 'S', 'T','U', 'V', 'W','X', 'Y', 'Z'}; for(int i = 0;i < 16; i++){ randomstr[i] = chars[random(32)]; } randomstr[16] = '\0'; char randomstr2[33]; randomSeed(analogRead(0)); - char chars2[] = {'1', '2', '3','4', '5', '6','7', '8', '9', '0', 'A', 'B', 'C', 'D','E', 'F', 'G','H', 'I', 'J','K', 'L', 'M', 'N', 'O','P', 'Q','R', 'S', 'T','U', 'V', 'W','X', 'Y', 'Z'}; for(int i = 0;i < 32; i++){ randomstr2[i] = chars2[random(36)]; } randomstr2[32] = '\0'; + char randomstr3[33]; + randomSeed(analogRead(0)); + for(int i = 0;i < 32; i++){ + randomstr3[i] = chars2[random(36)]; + } + randomstr3[32] = '\0'; PsychicStreamResponse response(resp, "text/html"); response.beginSend(); @@ -4891,6 +4991,10 @@ esp_err_t WebCfgServer::buildCredHtml(PsychicRequest *request, PsychicResponse* response.print(""); + printInputField(&response, "CREDADMIN", "Admin key", "*", 32, "", true, false); + response.print(""); printInputField(&response, "CREDLFTM", "Session validity (in seconds)", _preferences->getInt(preference_cred_session_lifetime, 3600), 12, ""); printInputField(&response, "CREDLFTMRMBR", "Session validity remember (in hours)", _preferences->getInt(preference_cred_session_lifetime_remember, 720), 12, ""); printInputField(&response, "CREDDUOLFTM", "Duo Session validity (in seconds)", _preferences->getInt(preference_cred_session_lifetime_duo, 3600), 12, ""); diff --git a/src/WebCfgServer.h b/src/WebCfgServer.h index fbc5bdb..04a7224 100644 --- a/src/WebCfgServer.h +++ b/src/WebCfgServer.h @@ -52,7 +52,7 @@ public: private: #ifndef NUKI_HUB_UPDATER - esp_err_t sendSettings(PsychicRequest *request, PsychicResponse* resp); + esp_err_t sendSettings(PsychicRequest *request, PsychicResponse* resp, bool adminKey = false); bool processArgs(PsychicRequest *request, PsychicResponse* resp, String& message); bool processImport(PsychicRequest *request, PsychicResponse* resp, String& message); void processGpioArgs(PsychicRequest *request, PsychicResponse* resp); From 6d14a1dcdd8bee26268fbd060ed34244598eb2f9 Mon Sep 17 00:00:00 2001 From: iranl Date: Tue, 11 Feb 2025 22:37:00 +0100 Subject: [PATCH 07/19] Bypass MFA --- README.md | 4 +- src/Config.h | 2 +- src/ImportExport.cpp | 49 ++++++++++-- src/ImportExport.h | 9 ++- src/NukiNetwork.cpp | 37 +++++---- src/NukiWrapper.cpp | 12 +-- src/PreferencesKeys.h | 5 +- src/WebCfgServer.cpp | 171 +++++++++++++++++++++++++++++++++++++++--- src/WebCfgServer.h | 3 + src/main.cpp | 8 +- 10 files changed, 254 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 020dcb6..3579053 100644 --- a/README.md +++ b/README.md @@ -527,7 +527,7 @@ Note that the following options can break Nuki Hub and cause bootloops that will Consider this when deciding if you want to enable the following functionality: -- Any application/actor that has read access to `nukihub/configuration/action` and `nukihub/configuration/json` can view your changes and exports. +- Any application/actor that has read access to `nukihub/configuration/action` and `nukihub/configuration/json` can view your changes and exports. - If you have not enabled the setting to require MFA when changing settings any application/actor that has write access to `nukihub/configuration/action` can change Nuki Hub settings (including pairing data and credentials) ### Export Nuki Hub settings over MQTT @@ -562,7 +562,7 @@ After the import is complete the ESP32 will reboot. If you have enabled `Require MFA (Duo/TOTP) authentication for all sensitive Nuki Hub operations (changing/exporting settings)` you will need to either provide a currently valid TOTP code as part of the sent JSON in the `totp` node or approve the Duo Push before the settings will be changed/imported. -Note: When importing settings using MQTT there are less/no checks on the values entered. These checks are only available when changing settings through the WebConfigurator. +Note: When importing settings using MQTT there are less/no checks on the values entered. These checks are only available when changing settings through the WebConfigurator. Consider testing your configuration values by changing them in the Web Configurator before trying to use MQTT to change configuration. A general explanation of the values that can be imported can be found in the [PreferencesKeys.h](/src/PreferencesKeys.h) file diff --git a/src/Config.h b/src/Config.h index 711efab..cbc09ea 100644 --- a/src/Config.h +++ b/src/Config.h @@ -5,7 +5,7 @@ #define NUKI_HUB_VERSION "9.09" #define NUKI_HUB_VERSION_INT (uint32_t)909 #define NUKI_HUB_BUILD "unknownbuildnr" -#define NUKI_HUB_DATE "2025-02-11" +#define NUKI_HUB_DATE "2025-02-13" #define GITHUB_LATEST_RELEASE_URL (char*)"https://github.com/technyon/nuki_hub/releases/latest" #define GITHUB_OTA_MANIFEST_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/manifest.json" diff --git a/src/ImportExport.cpp b/src/ImportExport.cpp index 21d46e6..8d307c2 100644 --- a/src/ImportExport.cpp +++ b/src/ImportExport.cpp @@ -39,6 +39,8 @@ void ImportExport::readSettings() _totpKey = _preferences->getString(preference_totp_secret, ""); _totpEnabled = _totpKey.length() > 0; + _bypassKey = _preferences->getString(preference_bypass_secret, ""); + _bypassEnabled = _bypassKey.length() > 0; } bool ImportExport::getDuoEnabled() @@ -51,6 +53,11 @@ bool ImportExport::getTOTPEnabled() return _totpEnabled; } +bool ImportExport::getBypassEnabled() +{ + return _bypassEnabled; +} + bool ImportExport::getBypassGPIOEnabled() { return _bypassGPIO; @@ -132,7 +139,7 @@ int ImportExport::checkDuoAuth(PsychicRequest *request) const char* duo_ikey = _duoIkey.c_str(); const char* duo_skey = _duoSkey.c_str(); const char* duo_user = _duoUser.c_str(); - + int type = 0; if(request->hasParam("type")) { @@ -168,7 +175,7 @@ int ImportExport::checkDuoAuth(PsychicRequest *request) _duoTransactionId = ""; _duoCheckIP = ""; _duoCheckId = ""; - + if(type==0) { int64_t durationLength = 60*60*_preferences->getInt(preference_cred_session_lifetime_duo_remember, 720); @@ -200,7 +207,7 @@ int ImportExport::checkDuoAuth(PsychicRequest *request) _duoTransactionId = ""; _duoCheckIP = ""; _duoCheckId = ""; - + if(type==0) { if (_preferences->getBool(preference_mfa_reconfigure, false)) @@ -278,9 +285,9 @@ bool ImportExport::checkTOTP(String* totpKey) } _lastCodeCheck = espMillis(); - - String key(totpKey->c_str()); - + + String key(totpKey->c_str()); + time_t now; time(&now); int totpTime = -60; @@ -288,7 +295,7 @@ bool ImportExport::checkTOTP(String* totpKey) while (totpTime <= 60) { String key2(TOTP::currentOTP(now, _totpKey, 30, 6, totpTime)->c_str()); - + if(key.toInt() == key2.toInt()) { _invalidCount = 0; @@ -303,6 +310,30 @@ bool ImportExport::checkTOTP(String* totpKey) return false; } +bool ImportExport::checkBypass(String bypass) +{ + if(_bypassEnabled) + { + if((pow(_invalidCount2, 5) + _lastCodeCheck2) > espMillis()) + { + _lastCodeCheck2 = espMillis(); + return false; + } + + _lastCodeCheck2 = espMillis(); + + if(bypass == _bypassKey) + { + _invalidCount2 = 0; + Log->println("Successful Bypass MFA Auth"); + return true; + } + _invalidCount2++; + Log->println("Failed Bypass MFA Auth"); + } + return false; +} + void ImportExport::exportHttpsJson(JsonDocument &json) { if (!SPIFFS.begin(true)) { @@ -442,6 +473,10 @@ void ImportExport::exportNukiHubJson(JsonDocument &json, bool redacted, bool pai { continue; } + if(strcmp(key, preference_bypass_secret) == 0) + { + continue; + } if(!redacted) if(std::find(redactedPrefs.begin(), redactedPrefs.end(), key) != redactedPrefs.end()) { continue; diff --git a/src/ImportExport.h b/src/ImportExport.h index 302974b..7d04516 100644 --- a/src/ImportExport.h +++ b/src/ImportExport.h @@ -16,7 +16,9 @@ public: int checkDuoApprove(); bool startDuoAuth(char* pushType = (char*)""); bool getTOTPEnabled(); + bool getBypassEnabled(); bool checkTOTP(String* totpKey); + bool checkBypass(String bypass); bool getDuoEnabled(); bool getBypassGPIOEnabled(); int getBypassGPIOHigh(); @@ -27,13 +29,17 @@ public: JsonDocument _duoSessions; JsonDocument _totpSessions; JsonDocument _sessionsOpts; + JsonDocument _bypassSessions; int64_t _lastCodeCheck = 0; + int64_t _lastCodeCheck2 = 0; int _invalidCount = 0; + int _invalidCount2 = 0; private: void saveSessions(); Preferences* _preferences; struct tm timeinfo; bool _totpEnabled = false; + bool _bypassEnabled = false; bool _duoActiveRequest; bool _duoEnabled = false; bool _bypassGPIO = false; @@ -47,6 +53,7 @@ private: String _duoUser; String _duoCheckId; String _duoCheckIP; - String _totpKey; + String _totpKey; + String _bypassKey; }; diff --git a/src/NukiNetwork.cpp b/src/NukiNetwork.cpp index 608ff6a..8684d4a 100644 --- a/src/NukiNetwork.cpp +++ b/src/NukiNetwork.cpp @@ -384,12 +384,17 @@ bool NukiNetwork::update() wdt_hal_write_protect_enable(&rtc_wdt_ctx); int64_t ts = espMillis(); _device->update(); - + if(_importExport->getTOTPEnabled() && _importExport->_invalidCount > 0 && (ts - (120000 * _importExport->_invalidCount)) > _importExport->_lastCodeCheck) { _importExport->_invalidCount--; } + if(_importExport->getBypassEnabled() && _importExport->_invalidCount2 > 0 && (ts - (120000 * _importExport->_invalidCount2)) > _importExport->_lastCodeCheck2) + { + _importExport->_invalidCount2--; + } + if(disableNetwork || !_mqttEnabled || _device->isApOpen()) { return false; @@ -477,7 +482,7 @@ bool NukiNetwork::update() _lastRssi = rssi; } } - + if(_overwriteNukiHubConfigTS > 0 && espMillis() > _overwriteNukiHubConfigTS) { publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_json, "--", true); @@ -1101,7 +1106,7 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns if(timeSynced && _importExport->getTOTPEnabled() && !doc["totp"].isNull()) { String jsonTotp = doc["totp"]; - + if (!_importExport->checkTOTP(&jsonTotp)) { publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action_command_result, "{\"error\": \"totpIncorrect\"}", false); publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action, "--", true); @@ -1114,15 +1119,19 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action, "--", true); return; } - else if (_importExport->startDuoAuth((char*)"Approve Nuki Hub setting change")) + else { + bool duoRes = _importExport->startDuoAuth((char*)"Approve Nuki Hub setting change"); int duoResult = 2; - while (duoResult == 2) + if (duoRes) { - duoResult = _importExport->checkDuoApprove(); - delay(2000); - esp_task_wdt_reset(); + while (duoResult == 2) + { + duoResult = _importExport->checkDuoApprove(); + delay(2000); + esp_task_wdt_reset(); + } } if (duoResult != 1) @@ -1133,18 +1142,18 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns } } } - + if(!doc["exportHTTPS"].isNull() && _device->isEncrypted()) { if(_preferences->getBool(preference_publish_config, false)) { - if(_device->isEncrypted()) + if(_device->isEncrypted()) { JsonDocument json; _importExport->exportHttpsJson(json); serializeJson(json, _buffer, _bufferSize); publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_json, _buffer, false); - + if (doc["exportHTTPS"].as() > 0) { _overwriteNukiHubConfigTS = espMillis() + (doc["exportHTTPS"].as() * 1000); @@ -1164,7 +1173,7 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns { if(_preferences->getBool(preference_publish_config, false)) { - if(_device->isEncrypted()) + if(_device->isEncrypted()) { JsonDocument json; _importExport->exportMqttsJson(json); @@ -1193,7 +1202,7 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns bool redacted = false; if(!doc["redacted"].isNull()) { - if(_device->isEncrypted()) + if(_device->isEncrypted()) { redacted = true; } @@ -1205,7 +1214,7 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns bool pairing = false; if(!doc["pairing"].isNull()) { - if(_device->isEncrypted()) + if(_device->isEncrypted()) { pairing = true; } diff --git a/src/NukiWrapper.cpp b/src/NukiWrapper.cpp index 7ef9542..3be7a82 100644 --- a/src/NukiWrapper.cpp +++ b/src/NukiWrapper.cpp @@ -62,7 +62,7 @@ void NukiWrapper::initialize() _nukiLock.setDebugHexData(_preferences->getBool(preference_debug_hex_data, false)); _nukiLock.setDebugCommand(_preferences->getBool(preference_debug_command, false)); _nukiLock.registerLogger(Log); - + if (_preferences->getInt(preference_lock_gemini_pin, 0) > 0 && _preferences->getBool(preference_lock_gemini_enabled, false)) { _nukiLock.saveUltraPincode(_preferences->getInt(preference_lock_gemini_pin, 0), false); @@ -90,7 +90,7 @@ void NukiWrapper::readSettings() #else if(pwrLvl >= 20) { - powerLevel = ESP_PWR_LVL_P20; + powerLevel = ESP_PWR_LVL_P20; } else if(pwrLvl >= 18) { @@ -486,7 +486,7 @@ void NukiWrapper::unpair() nukiBlePref.clear(); nukiBlePref.end(); _deviceId->assignNewId(); - if (!_forceId) + if (!_forceId) { _preferences->remove(preference_nuki_id_lock); } @@ -4302,18 +4302,18 @@ void NukiWrapper::updateTime() Log->println("No valid PIN set"); return; } - + time_t now; tm tm; time(&now); localtime_r(&now, &tm); - + if (int(tm.tm_year + 1900) < int(2025)) { Log->println("NTP Time not valid, not updating Nuki device"); return; } - + Nuki::TimeValue nukiTime; nukiTime.year = tm.tm_year + 1900; nukiTime.month = tm.tm_mon + 1; diff --git a/src/PreferencesKeys.h b/src/PreferencesKeys.h index 6658dff..6722fce 100644 --- a/src/PreferencesKeys.h +++ b/src/PreferencesKeys.h @@ -96,6 +96,7 @@ #define preference_publish_config (char*)"nhPubConfig" #define preference_config_from_mqtt (char*)"nhCntrlEnabled" #define preference_totp_secret (char*)"totpsecret" +#define preference_bypass_secret (char*)"bypassecret" // CHANGE DOES NOT REQUIRE REBOOT TO TAKE EFFECT #define preference_find_best_rssi (char*)"nwbestrssi" @@ -535,13 +536,13 @@ private: preference_cred_duo_host, preference_cred_duo_ikey, preference_cred_duo_skey, preference_cred_duo_user, preference_cred_duo_enabled, preference_https_fqdn, preference_bypass_proxy, preference_cred_session_lifetime, preference_cred_session_lifetime_remember, preference_cred_session_lifetime_duo, preference_cred_session_lifetime_duo_remember, preference_cred_duo_approval, preference_cred_bypass_boot_btn_enabled, preference_cred_bypass_gpio_high, preference_cred_bypass_gpio_low, preference_publish_config, - preference_config_from_mqtt, preference_totp_secret, preference_cred_session_lifetime_totp, preference_cred_session_lifetime_totp_remember + preference_config_from_mqtt, preference_totp_secret, preference_cred_session_lifetime_totp, preference_cred_session_lifetime_totp_remember, preference_bypass_secret }; std::vector _redact = { preference_mqtt_user, preference_mqtt_password, preference_cred_user, preference_cred_password, preference_nuki_id_lock, preference_nuki_id_opener, preference_wifi_pass, preference_lock_gemini_pin, preference_cred_duo_host, preference_cred_duo_ikey, preference_cred_duo_skey, preference_cred_duo_user, preference_bypass_proxy, - preference_totp_secret + preference_totp_secret, preference_bypass_secret }; std::vector _boolPrefs = { diff --git a/src/WebCfgServer.cpp b/src/WebCfgServer.cpp index d9f8843..bb14218 100644 --- a/src/WebCfgServer.cpp +++ b/src/WebCfgServer.cpp @@ -102,18 +102,22 @@ bool WebCfgServer::isAuthenticated(PsychicRequest *request, int type) { cookieKey = "totpId"; } + else if (type == 3) + { + cookieKey = "bypassId"; + } if (request->hasCookie(cookieKey.c_str())) { String cookie = request->getCookie(cookieKey.c_str()); - if ((type == 0 && _httpSessions[cookie].is()) || (type == 1 && _importExport->_duoSessions[cookie].is()) || (type == 2 && _importExport->_totpSessions[cookie].is())) + if ((type == 0 && _httpSessions[cookie].is()) || (type == 1 && _importExport->_duoSessions[cookie].is()) || (type == 2 && _importExport->_totpSessions[cookie].is()) || (type == 3 && _importExport->_bypassSessions[cookie].is())) { struct timeval time; gettimeofday(&time, NULL); int64_t time_us = (int64_t)time.tv_sec * 1000000L + (int64_t)time.tv_usec; - if ((type == 0 && _httpSessions[cookie].as() > time_us) || (type == 1 && _importExport->_duoSessions[cookie].as() > time_us) || (type == 2 && _importExport->_totpSessions[cookie].as() > time_us)) + if ((type == 0 && _httpSessions[cookie].as() > time_us) || (type == 1 && _importExport->_duoSessions[cookie].as() > time_us) || (type == 2 && _importExport->_totpSessions[cookie].as() > time_us) || (type == 3 && _importExport->_bypassSessions[cookie].as() > time_us)) { return true; } @@ -194,6 +198,23 @@ esp_err_t WebCfgServer::logoutSession(PsychicRequest *request, PsychicResponse* } } + if (_importExport->getBypassEnabled()) + { + if (!_isSSL) + { + resp->setCookie("bypassId", "", 0, "HttpOnly"); + } + else + { + resp->setCookie("bypassId", "", 0, "Secure; HttpOnly"); + } + + if (request->hasCookie("bypassId")) { + String cookie2 = request->getCookie("bypassId"); + _importExport->_bypassSessions.remove(cookie2); + } + } + return buildConfirmHtml(request, resp, "Logging out", 3, true); } @@ -369,6 +390,11 @@ int WebCfgServer::doAuthentication(PsychicRequest *request) _importExport->_sessionsOpts[request->client()->localIP().toString() + "totp"] = true; return 4; } + else if(!timeSynced && _importExport->getBypassEnabled() && isAuthenticated(request, 3)) + { + _importExport->_sessionsOpts[request->client()->localIP().toString() + "totp"] = false; + return 4; + } Log->println("Authentication Failed"); @@ -507,7 +533,7 @@ void WebCfgServer::initialize() } int authReq = doAuthentication(request); - if (value != "status" && value != "login" && value != "duocheck") + if (value != "status" && value != "login" && value != "duocheck" && value != "bypass") { switch (authReq) { @@ -559,6 +585,15 @@ void WebCfgServer::initialize() { return buildTOTPHtml(request, resp, 0); } + else if (value == "bypass") + { + return buildBypassHtml(request, resp); + } + else if (value == "newbypass" && _newBypass) + { + _newBypass = false; + return buildConfirmHtml(request, resp, "Logged in using Bypass. New bypass: " + _preferences->getString(preference_bypass_secret, ""), 3, false); + } else if (value == "logout") { return logoutSession(request, resp); @@ -816,7 +851,7 @@ void WebCfgServer::initialize() } } - if (value != "login" && value != "totp") + if (value != "login" && value != "totp" && value != "bypass") { int authReq = doAuthentication(request); @@ -866,6 +901,11 @@ void WebCfgServer::initialize() } } } + else if(!timeSynced && _importExport->getBypassEnabled() && isAuthenticated(request, 3)) + { + _importExport->_sessionsOpts[request->client()->localIP().toString() + "approve"] = false; + approved = true; + } if (!approved) { @@ -926,6 +966,23 @@ void WebCfgServer::initialize() return resp->redirect("/get?page=totp"); } } + else if (value == "bypass") + { + bool loggedIn = processBypass(request, resp); + if (loggedIn) + { + resp->setCode(302); + resp->addHeader("Cache-Control", "no-cache"); + _newBypass = true; + return resp->redirect("/get?page=newbypass"); + } + else + { + resp->setCode(302); + resp->addHeader("Cache-Control", "no-cache"); + return resp->redirect("/"); + } + } #ifndef NUKI_HUB_UPDATER else if (value == "savecfg") { @@ -1882,14 +1939,14 @@ esp_err_t WebCfgServer::buildTOTPHtml(PsychicRequest *request, PsychicResponse* { if (!timeSynced) { - return buildConfirmHtml(request, resp, "NTP time not synced yet, TOTP not available, please wait for NTP to sync", 3, true); + return buildConfirmHtml(request, resp, "NTP time not synced yet, TOTP not available, please wait for NTP to sync or use one-time bypass", 3, true); } - + if((pow(_importExport->_invalidCount, 5) + _importExport->_lastCodeCheck) > espMillis()) { - return buildConfirmHtml(request, resp, "Too many invalid TOTP tries, please wait before retrying", 3, true); + return buildConfirmHtml(request, resp, "Too many invalid TOTP tries, please wait before retrying or use one-time bypass", 3, true); } - + PsychicStreamResponse response(resp, "text/html"); response.beginSend(); response.print(""); @@ -1943,6 +2000,32 @@ esp_err_t WebCfgServer::buildTOTPHtml(PsychicRequest *request, PsychicResponse* return response.endSend(); } +esp_err_t WebCfgServer::buildBypassHtml(PsychicRequest *request, PsychicResponse* resp) +{ + if (timeSynced) + { + return buildConfirmHtml(request, resp, "One-time bypass is only available if NTP time is not synced", 3, true); + } + + if((pow(_importExport->_invalidCount2, 5) + _importExport->_lastCodeCheck2) > espMillis()) + { + return buildConfirmHtml(request, resp, "Too many invalid bypass tries, please wait before retrying", 3, true); + } + + PsychicStreamResponse response(resp, "text/html"); + response.beginSend(); + response.print(""); + response.print(""); + response.print("

Nuki Hub One-time Bypass

"); + response.print("
"); + response.print("
"); + response.print(""); + response.print("
"); + response.print("
"); + return response.endSend(); +} + esp_err_t WebCfgServer::buildDuoCheckHtml(PsychicRequest *request, PsychicResponse* resp) { char valueStr[2]; @@ -1986,7 +2069,7 @@ esp_err_t WebCfgServer::buildDuoHtml(PsychicRequest *request, PsychicResponse* r { if (!timeSynced) { - return buildConfirmHtml(request, resp, "NTP time not synced yet, Duo not available, please wait for NTP to sync", 3, true); + return buildConfirmHtml(request, resp, "NTP time not synced yet, Duo not available, please wait for NTP to sync or use one-time bypass", 3, true); } String duoText; @@ -2130,6 +2213,52 @@ bool WebCfgServer::processLogin(PsychicRequest *request, PsychicResponse* resp) return false; } +bool WebCfgServer::processBypass(PsychicRequest *request, PsychicResponse* resp) +{ + if(!timeSynced && request->hasParam("bypass")) + { + const PsychicWebParameter* pass = request->getParam("bypass"); + if(pass->value() != "") + { + String bypass = pass->value(); + if (_importExport->checkBypass(bypass)) + { + char buffer[33]; + int i; + for (i = 0; i < 4; i++) { + sprintf(buffer + (i * 8), "%08lx", (unsigned long int)esp_random()); + } + + if (!_isSSL) + { + resp->setCookie("bypassId", buffer, 3600, "HttpOnly"); + } + else + { + resp->setCookie("bypassId", buffer, 3600, "Secure; HttpOnly"); + } + + struct timeval time; + gettimeofday(&time, NULL); + int64_t time_us = (int64_t)time.tv_sec * 1000000L + (int64_t)time.tv_usec; + _importExport->_bypassSessions[buffer] = time_us + ((int64_t)3600*1000000L); + + char randomstr2[33]; + randomSeed(analogRead(0)); + char chars[] = {'1', '2', '3','4', '5', '6','7', '8', '9', '0', 'A', 'B', 'C', 'D','E', 'F', 'G','H', 'I', 'J','K', 'L', 'M', 'N', 'O','P', 'Q','R', 'S', 'T','U', 'V', 'W','X', 'Y', 'Z'}; + for(int i = 0;i < 32; i++){ + randomstr2[i] = chars[random(36)]; + } + randomstr2[32] = '\0'; + _preferences->putString(preference_bypass_secret, randomstr2); + + return true; + } + } + } + return false; +} + bool WebCfgServer::processTOTP(PsychicRequest *request, PsychicResponse* resp) { if(timeSynced && request->hasParam("totpkey")) @@ -4151,6 +4280,19 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S } } } + else if(key == "CREDBYPASS") + { + if(value != "*") + { + if(_preferences->getString(preference_bypass_secret, "") != value) + { + _preferences->putString(preference_bypass_secret, value); + Log->print("Setting changed: "); + Log->println(key); + configChanged = true; + } + } + } else if(key == "NUKIPIN" && _nuki != nullptr) { if(value == "#") @@ -4708,6 +4850,13 @@ esp_err_t WebCfgServer::buildCredHtml(PsychicRequest *request, PsychicResponse* randomstr[i] = chars[random(32)]; } randomstr[16] = '\0'; + char randomstr2[33]; + randomSeed(analogRead(0)); + char chars2[] = {'1', '2', '3','4', '5', '6','7', '8', '9', '0', 'A', 'B', 'C', 'D','E', 'F', 'G','H', 'I', 'J','K', 'L', 'M', 'N', 'O','P', 'Q','R', 'S', 'T','U', 'V', 'W','X', 'Y', 'Z'}; + for(int i = 0;i < 32; i++){ + randomstr2[i] = chars2[random(36)]; + } + randomstr2[32] = '\0'; PsychicStreamResponse response(resp, "text/html"); response.beginSend(); @@ -4738,6 +4887,10 @@ esp_err_t WebCfgServer::buildCredHtml(PsychicRequest *request, PsychicResponse* response.print(""); + printInputField(&response, "CREDBYPASS", "One-time MFA Bypass", "*", 32, "", true, false); + response.print(""); printInputField(&response, "CREDLFTM", "Session validity (in seconds)", _preferences->getInt(preference_cred_session_lifetime, 3600), 12, ""); printInputField(&response, "CREDLFTMRMBR", "Session validity remember (in hours)", _preferences->getInt(preference_cred_session_lifetime_remember, 720), 12, ""); printInputField(&response, "CREDDUOLFTM", "Duo Session validity (in seconds)", _preferences->getInt(preference_cred_session_lifetime_duo, 3600), 12, ""); diff --git a/src/WebCfgServer.h b/src/WebCfgServer.h index 6e3e763..fbc5bdb 100644 --- a/src/WebCfgServer.h +++ b/src/WebCfgServer.h @@ -111,9 +111,11 @@ private: bool isAuthenticated(PsychicRequest *request, int type = 0); bool processLogin(PsychicRequest *request, PsychicResponse* resp); bool processTOTP(PsychicRequest *request, PsychicResponse* resp); + bool processBypass(PsychicRequest *request, PsychicResponse* resp); int doAuthentication(PsychicRequest *request); esp_err_t buildCoredumpHtml(PsychicRequest *request, PsychicResponse* resp); esp_err_t buildLoginHtml(PsychicRequest *request, PsychicResponse* resp); + esp_err_t buildBypassHtml(PsychicRequest *request, PsychicResponse* resp); esp_err_t buildTOTPHtml(PsychicRequest *request, PsychicResponse* resp, int type); esp_err_t buildDuoHtml(PsychicRequest *request, PsychicResponse* resp, int type); esp_err_t buildDuoCheckHtml(PsychicRequest *request, PsychicResponse* resp); @@ -150,6 +152,7 @@ private: JsonDocument _httpSessions; bool _duoEnabled = false; bool _bypassGPIO = false; + bool _newBypass = false; int _bypassGPIOHigh = -1; int _bypassGPIOLow = -1; }; diff --git a/src/main.cpp b/src/main.cpp index 142450f..3be50bc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -546,7 +546,7 @@ void logCoreDump() file.printf("%s\r\n", NUKI_HUB_HW); file.printf("%s\r\n", NUKI_HUB_BUILD); } - + Serial.printf("%s\r\n", NUKI_HUB_HW); Serial.printf("%s\r\n", NUKI_HUB_BUILD); @@ -581,7 +581,7 @@ void logCoreDump() file.printf("%s", str_dst); } } - + Serial.println(""); if (file) { @@ -669,7 +669,7 @@ void setup() { preferences->putString(preference_updater_date, NUKI_HUB_DATE); } - + importExport = new ImportExport(preferences); network = new NukiNetwork(preferences); @@ -796,7 +796,7 @@ void setup() Log->print(gpioDesc.c_str()); const String mqttLockPath = preferences->getString(preference_mqtt_lock_path); - + importExport = new ImportExport(preferences); network = new NukiNetwork(preferences, gpio, mqttLockPath, CharBuffer::get(), buffer_size, importExport); From 4e48b61121e88918ea48e750b9f7cfea7cf05714 Mon Sep 17 00:00:00 2001 From: iranl Date: Fri, 14 Feb 2025 20:32:15 +0100 Subject: [PATCH 08/19] Add Admin key --- README.md | 2 + src/Config.h | 2 +- src/ImportExport.cpp | 4 ++ src/PreferencesKeys.h | 6 +- src/WebCfgServer.cpp | 134 +++++++++++++++++++++++++++++++++++++----- src/WebCfgServer.h | 2 +- 6 files changed, 131 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 3579053..35acdff 100644 --- a/README.md +++ b/README.md @@ -333,6 +333,8 @@ Note: All of the following requires the Nuki security code / PIN to be set, see - Duo secret key: Set to the Duo secret key - Duo user: Set to the Duo user that you want to receive the push notification - TOTP Secret Key: Set a TOTP secret key to enable TOTP MFA. Enter the TOTP secret key in an authenticator application (Password manager, Microsoft/Google Authenticator etc.) to generate TOTP codes. +- One-time MFA Bypass: Set a 32 character long alphanumeric string that can be used as a one-time MFA bypass when the ESP32 is unable to sync it's time and TOTP and Duo are unavailable as a result. +- Admin key: Set a 32 character long alphanumeric string that can be used in combination with a TOTP code to export and import settings without needing to log in (for use with automated systems). - Session validity (in seconds): Session validity to use with form authentication when the "Remember me" checkbox is disabled, default 3600 seconds. - Session validity remember (in hours): Session validity to use with form authentication when the "Remember me" checkbox is enabled, default 720 hours. - Duo Session validity (in seconds): Session validity to use with Duo authentication when the "Remember me" checkbox is disabled, default 3600 seconds. diff --git a/src/Config.h b/src/Config.h index cbc09ea..5f64dce 100644 --- a/src/Config.h +++ b/src/Config.h @@ -5,7 +5,7 @@ #define NUKI_HUB_VERSION "9.09" #define NUKI_HUB_VERSION_INT (uint32_t)909 #define NUKI_HUB_BUILD "unknownbuildnr" -#define NUKI_HUB_DATE "2025-02-13" +#define NUKI_HUB_DATE "2025-02-14" #define GITHUB_LATEST_RELEASE_URL (char*)"https://github.com/technyon/nuki_hub/releases/latest" #define GITHUB_OTA_MANIFEST_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/manifest.json" diff --git a/src/ImportExport.cpp b/src/ImportExport.cpp index 8d307c2..049be5c 100644 --- a/src/ImportExport.cpp +++ b/src/ImportExport.cpp @@ -477,6 +477,10 @@ void ImportExport::exportNukiHubJson(JsonDocument &json, bool redacted, bool pai { continue; } + if(strcmp(key, preference_admin_secret) == 0) + { + continue; + } if(!redacted) if(std::find(redactedPrefs.begin(), redactedPrefs.end(), key) != redactedPrefs.end()) { continue; diff --git a/src/PreferencesKeys.h b/src/PreferencesKeys.h index 6722fce..9cfcdef 100644 --- a/src/PreferencesKeys.h +++ b/src/PreferencesKeys.h @@ -97,6 +97,7 @@ #define preference_config_from_mqtt (char*)"nhCntrlEnabled" #define preference_totp_secret (char*)"totpsecret" #define preference_bypass_secret (char*)"bypassecret" +#define preference_admin_secret (char*)"adminsecret" // CHANGE DOES NOT REQUIRE REBOOT TO TAKE EFFECT #define preference_find_best_rssi (char*)"nwbestrssi" @@ -536,13 +537,14 @@ private: preference_cred_duo_host, preference_cred_duo_ikey, preference_cred_duo_skey, preference_cred_duo_user, preference_cred_duo_enabled, preference_https_fqdn, preference_bypass_proxy, preference_cred_session_lifetime, preference_cred_session_lifetime_remember, preference_cred_session_lifetime_duo, preference_cred_session_lifetime_duo_remember, preference_cred_duo_approval, preference_cred_bypass_boot_btn_enabled, preference_cred_bypass_gpio_high, preference_cred_bypass_gpio_low, preference_publish_config, - preference_config_from_mqtt, preference_totp_secret, preference_cred_session_lifetime_totp, preference_cred_session_lifetime_totp_remember, preference_bypass_secret + preference_config_from_mqtt, preference_totp_secret, preference_cred_session_lifetime_totp, preference_cred_session_lifetime_totp_remember, preference_bypass_secret, + preference_admin_secret }; std::vector _redact = { preference_mqtt_user, preference_mqtt_password, preference_cred_user, preference_cred_password, preference_nuki_id_lock, preference_nuki_id_opener, preference_wifi_pass, preference_lock_gemini_pin, preference_cred_duo_host, preference_cred_duo_ikey, preference_cred_duo_skey, preference_cred_duo_user, preference_bypass_proxy, - preference_totp_secret, preference_bypass_secret + preference_totp_secret, preference_bypass_secret, preference_admin_secret }; std::vector _boolPrefs = { diff --git a/src/WebCfgServer.cpp b/src/WebCfgServer.cpp index bb14218..77d492f 100644 --- a/src/WebCfgServer.cpp +++ b/src/WebCfgServer.cpp @@ -531,10 +531,38 @@ void WebCfgServer::initialize() value = p->value(); } } - int authReq = doAuthentication(request); - if (value != "status" && value != "login" && value != "duocheck" && value != "bypass") + bool adminKeyValid = false; + if(value == "export" && timeSynced && request->hasParam("adminkey") && request->hasParam("totpkey") && _importExport->getTOTPEnabled()) { + String value2 = ""; + if(request->hasParam("adminkey")) + { + const PsychicWebParameter* p = request->getParam("adminkey"); + if(p->value() != "") + { + value2 = p->value(); + } + } + String value3 = ""; + if(request->hasParam("totpkey")) + { + const PsychicWebParameter* p = request->getParam("totpkey"); + if(p->value() != "") + { + value3 = p->value(); + } + } + if (value2.length() > 0 && value2 == _preferences->getString(preference_admin_secret, "") && _importExport->checkTOTP(&value3)) + { + adminKeyValid = true; + } + } + + if (!adminKeyValid && value != "status" && value != "login" && value != "duocheck" && value != "bypass") + { + int authReq = doAuthentication(request); + switch (authReq) { case 0: @@ -569,14 +597,16 @@ void WebCfgServer::initialize() break; } } - else if (value == "status" && authReq != 4) + else if (value == "status") { - resp->setCode(200); - resp->setContentType("application/json"); - resp->setContent("{}"); - return resp->send(); + if (doAuthentication(request) != 4) + { + resp->setCode(200); + resp->setContentType("application/json"); + resp->setContent("{}"); + return resp->send(); + } } - if (value == "login") { return buildLoginHtml(request, resp); @@ -592,7 +622,7 @@ void WebCfgServer::initialize() else if (value == "newbypass" && _newBypass) { _newBypass = false; - return buildConfirmHtml(request, resp, "Logged in using Bypass. New bypass: " + _preferences->getString(preference_bypass_secret, ""), 3, false); + return buildConfirmHtml(request, resp, "Logged in using Bypass. New bypass: " + _preferences->getString(preference_bypass_secret, "") + "

Home page", 3, false); } else if (value == "logout") { @@ -659,6 +689,11 @@ void WebCfgServer::initialize() return sendSettings(request, resp); } + if(adminKeyValid) + { + return sendSettings(request, resp, true); + } + if(_importExport->_sessionsOpts[request->client()->localIP().toString() + "approve"]) { _importExport->_sessionsOpts[request->client()->localIP().toString() + "approve"] = false; @@ -851,7 +886,33 @@ void WebCfgServer::initialize() } } - if (value != "login" && value != "totp" && value != "bypass") + bool adminKeyValid = false; + if(value == "import" && timeSynced && request->hasParam("adminkey") && request->hasParam("totpkey") && _importExport->getTOTPEnabled()) + { + String value2 = ""; + if(request->hasParam("adminkey")) + { + const PsychicWebParameter* p = request->getParam("adminkey"); + if(p->value() != "") + { + value2 = p->value(); + } + } + String value3 = ""; + if(request->hasParam("totpkey")) + { + const PsychicWebParameter* p = request->getParam("totpkey"); + if(p->value() != "") + { + value3 = p->value(); + } + } + if (value2.length() > 0 && value2 == _preferences->getString(preference_admin_secret, "") && _importExport->checkTOTP(&value3)) + { + adminKeyValid = true; + } + } + if(!adminKeyValid && value != "login" && value != "totp" && value != "bypass") { int authReq = doAuthentication(request); @@ -1026,7 +1087,23 @@ void WebCfgServer::initialize() { String message = ""; bool restart = processImport(request, resp, message); - return buildConfirmHtml(request, resp, message, 3, true); + + if(adminKeyValid) + { + resp->setCode(200); + resp->setContentType("application/json"); + resp->setContent("{ \"result\": \"success\"}"); + esp_err_t res = resp->send(); + if(restart) + { + restartEsp(RestartReason::RequestedViaWebServer); + } + return res; + } + else + { + return buildConfirmHtml(request, resp, message, 3, true); + } } #endif else @@ -2303,7 +2380,7 @@ bool WebCfgServer::processTOTP(PsychicRequest *request, PsychicResponse* resp) } #ifndef NUKI_HUB_UPDATER -esp_err_t WebCfgServer::sendSettings(PsychicRequest *request, PsychicResponse* resp) +esp_err_t WebCfgServer::sendSettings(PsychicRequest *request, PsychicResponse* resp, bool adminKey) { JsonDocument json; String jsonPretty; @@ -2352,7 +2429,10 @@ esp_err_t WebCfgServer::sendSettings(PsychicRequest *request, PsychicResponse* r serializeJsonPretty(json, jsonPretty); char buf[26 + name.length()]; snprintf(buf, sizeof(buf), "attachment; filename=\"%s\"", name.c_str()); - resp->addHeader("Content-Disposition", buf); + if(!adminKey) + { + resp->addHeader("Content-Disposition", buf); + } resp->setCode(200); resp->setContentType("application/json"); resp->setContent(jsonPretty.c_str()); @@ -4293,6 +4373,19 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S } } } + else if(key == "CREDADMIN") + { + if(value != "*") + { + if(_preferences->getString(preference_admin_secret, "") != value) + { + _preferences->putString(preference_admin_secret, value); + Log->print("Setting changed: "); + Log->println(key); + configChanged = true; + } + } + } else if(key == "NUKIPIN" && _nuki != nullptr) { if(value == "#") @@ -4843,20 +4936,27 @@ esp_err_t WebCfgServer::buildHtml(PsychicRequest *request, PsychicResponse* resp esp_err_t WebCfgServer::buildCredHtml(PsychicRequest *request, PsychicResponse* resp) { + char chars[] = {'2', '3','4', '5', '6','7', 'A', 'B', 'C', 'D','E', 'F', 'G','H', 'I', 'J','K', 'L', 'M', 'N', 'O','P', 'Q','R', 'S', 'T','U', 'V', 'W','X', 'Y', 'Z'}; + char chars2[] = {'1', '2', '3','4', '5', '6','7', '8', '9', '0', 'A', 'B', 'C', 'D','E', 'F', 'G','H', 'I', 'J','K', 'L', 'M', 'N', 'O','P', 'Q','R', 'S', 'T','U', 'V', 'W','X', 'Y', 'Z'}; + char randomstr[17]; randomSeed(analogRead(0)); - char chars[] = {'2', '3','4', '5', '6','7', 'A', 'B', 'C', 'D','E', 'F', 'G','H', 'I', 'J','K', 'L', 'M', 'N', 'O','P', 'Q','R', 'S', 'T','U', 'V', 'W','X', 'Y', 'Z'}; for(int i = 0;i < 16; i++){ randomstr[i] = chars[random(32)]; } randomstr[16] = '\0'; char randomstr2[33]; randomSeed(analogRead(0)); - char chars2[] = {'1', '2', '3','4', '5', '6','7', '8', '9', '0', 'A', 'B', 'C', 'D','E', 'F', 'G','H', 'I', 'J','K', 'L', 'M', 'N', 'O','P', 'Q','R', 'S', 'T','U', 'V', 'W','X', 'Y', 'Z'}; for(int i = 0;i < 32; i++){ randomstr2[i] = chars2[random(36)]; } randomstr2[32] = '\0'; + char randomstr3[33]; + randomSeed(analogRead(0)); + for(int i = 0;i < 32; i++){ + randomstr3[i] = chars2[random(36)]; + } + randomstr3[32] = '\0'; PsychicStreamResponse response(resp, "text/html"); response.beginSend(); @@ -4891,6 +4991,10 @@ esp_err_t WebCfgServer::buildCredHtml(PsychicRequest *request, PsychicResponse* response.print(""); + printInputField(&response, "CREDADMIN", "Admin key", "*", 32, "", true, false); + response.print(""); printInputField(&response, "CREDLFTM", "Session validity (in seconds)", _preferences->getInt(preference_cred_session_lifetime, 3600), 12, ""); printInputField(&response, "CREDLFTMRMBR", "Session validity remember (in hours)", _preferences->getInt(preference_cred_session_lifetime_remember, 720), 12, ""); printInputField(&response, "CREDDUOLFTM", "Duo Session validity (in seconds)", _preferences->getInt(preference_cred_session_lifetime_duo, 3600), 12, ""); diff --git a/src/WebCfgServer.h b/src/WebCfgServer.h index fbc5bdb..04a7224 100644 --- a/src/WebCfgServer.h +++ b/src/WebCfgServer.h @@ -52,7 +52,7 @@ public: private: #ifndef NUKI_HUB_UPDATER - esp_err_t sendSettings(PsychicRequest *request, PsychicResponse* resp); + esp_err_t sendSettings(PsychicRequest *request, PsychicResponse* resp, bool adminKey = false); bool processArgs(PsychicRequest *request, PsychicResponse* resp, String& message); bool processImport(PsychicRequest *request, PsychicResponse* resp, String& message); void processGpioArgs(PsychicRequest *request, PsychicResponse* resp); From 8710a2b6c0d7d394124f4af441bf72fc79b0ceff Mon Sep 17 00:00:00 2001 From: iranl Date: Thu, 13 Feb 2025 20:28:17 +0100 Subject: [PATCH 09/19] Update pio and esp-nimble-cpp --- README.md | 2 +- platformio.ini | 2 +- src/idf_component.yml | 2 +- updater/platformio.ini | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 35acdff..23fcb54 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Feel free to join us on Discord: https://discord.gg/9nPq85bP4p ## Supported devices Supported ESP32 devices: -- Nuki Hub is compiled against all ESP32 models with Wi-Fi and Bluetooh Low Energy (BLE) which are supported by ESP-IDF 5.3.2 and Arduino Core 3.1.0. +- Nuki Hub is compiled against all ESP32 models with Wi-Fi and Bluetooh Low Energy (BLE) which are supported by ESP-IDF 5.3.2 and Arduino Core 3.1.2. - Tested stable builds are provided for the ESP32, ESP32-S3, ESP32-C3, ESP32-C6 and ESP32-H2. - Untested builds are provided for the ESP32-Solo1 (as the developers don't own one). diff --git a/platformio.ini b/platformio.ini index 74401d4..caf0cff 100644 --- a/platformio.ini +++ b/platformio.ini @@ -13,7 +13,7 @@ default_envs = esp32 boards_dir = boards [env] -platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.11/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.12/platform-espressif32.zip platform_packages = framework = arduino, espidf board_build.embed_txtfiles = diff --git a/src/idf_component.yml b/src/idf_component.yml index 5a9217d..74e26b3 100644 --- a/src/idf_component.yml +++ b/src/idf_component.yml @@ -4,7 +4,7 @@ dependencies: esp-nimble-cpp: git: https://github.com/h2zero/esp-nimble-cpp.git - version: fa468d360a56712f3f39a1fba74b97340ebca8a9 + version: 74b5c59887a381ca02c8193384bd89968c8409a6 espressif/libsodium: "^1.0.20~2" diff --git a/updater/platformio.ini b/updater/platformio.ini index 99e37d8..e1e2c71 100644 --- a/updater/platformio.ini +++ b/updater/platformio.ini @@ -13,7 +13,7 @@ default_envs = updater_esp32 boards_dir = ../boards [env] -platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.11/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.12/platform-espressif32.zip platform_packages = framework = arduino, espidf board_build.embed_txtfiles = From e1904235ba66354ec64a7697301926feb3ac4089 Mon Sep 17 00:00:00 2001 From: iranl Date: Fri, 14 Feb 2025 23:27:59 +0100 Subject: [PATCH 10/19] Update main.cpp --- src/main.cpp | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 3be50bc..4bfb9af 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -518,7 +518,7 @@ void logCoreDump() { coredumpPrinted = false; delay(500); - Serial.println("Printing coredump and saving to coredump.hex on SPIFFS"); + Log->println("Printing coredump and saving to coredump.hex on SPIFFS"); size_t size = 0; size_t address = 0; if (esp_core_dump_image_get(&address, &size) == ESP_OK) @@ -528,6 +528,7 @@ void logCoreDump() if (pt != NULL) { + File file; uint8_t bf[256]; char str_dst[640]; int16_t toRead; @@ -536,15 +537,17 @@ void logCoreDump() { Log->println("SPIFFS Mount Failed"); } - - File file = SPIFFS.open("/coredump.hex", FILE_WRITE); - if (!file) { - Log->println("Failed to open /coredump.hex for writing"); - } else { - file.printf("%s\r\n", NUKI_HUB_HW); - file.printf("%s\r\n", NUKI_HUB_BUILD); + file = SPIFFS.open("/coredump.hex", FILE_WRITE); + if (!file) { + Log->println("Failed to open /coredump.hex for writing"); + } + else + { + file.printf("%s\r\n", NUKI_HUB_HW); + file.printf("%s\r\n", NUKI_HUB_BUILD); + } } Serial.printf("%s\r\n", NUKI_HUB_HW); From 5b8184be3e668182e29cfa1594e260c3a86dbec2 Mon Sep 17 00:00:00 2001 From: iranl Date: Fri, 14 Feb 2025 23:29:32 +0100 Subject: [PATCH 11/19] Revert to 3.1.1 --- README.md | 2 +- platformio.ini | 2 +- updater/platformio.ini | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 23fcb54..f8524f5 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Feel free to join us on Discord: https://discord.gg/9nPq85bP4p ## Supported devices Supported ESP32 devices: -- Nuki Hub is compiled against all ESP32 models with Wi-Fi and Bluetooh Low Energy (BLE) which are supported by ESP-IDF 5.3.2 and Arduino Core 3.1.2. +- Nuki Hub is compiled against all ESP32 models with Wi-Fi and Bluetooh Low Energy (BLE) which are supported by ESP-IDF 5.3.2 and Arduino Core 3.1.1. - Tested stable builds are provided for the ESP32, ESP32-S3, ESP32-C3, ESP32-C6 and ESP32-H2. - Untested builds are provided for the ESP32-Solo1 (as the developers don't own one). diff --git a/platformio.ini b/platformio.ini index caf0cff..74401d4 100644 --- a/platformio.ini +++ b/platformio.ini @@ -13,7 +13,7 @@ default_envs = esp32 boards_dir = boards [env] -platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.12/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.11/platform-espressif32.zip platform_packages = framework = arduino, espidf board_build.embed_txtfiles = diff --git a/updater/platformio.ini b/updater/platformio.ini index e1e2c71..99e37d8 100644 --- a/updater/platformio.ini +++ b/updater/platformio.ini @@ -13,7 +13,7 @@ default_envs = updater_esp32 boards_dir = ../boards [env] -platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.12/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.11/platform-espressif32.zip platform_packages = framework = arduino, espidf board_build.embed_txtfiles = From e98877211a08e9adf65e793f7bbd794058a17533 Mon Sep 17 00:00:00 2001 From: iranl Date: Fri, 14 Feb 2025 23:31:34 +0100 Subject: [PATCH 12/19] Revert 3.1.1 --- platformio.ini | 2 +- updater/platformio.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/platformio.ini b/platformio.ini index caf0cff..74401d4 100644 --- a/platformio.ini +++ b/platformio.ini @@ -13,7 +13,7 @@ default_envs = esp32 boards_dir = boards [env] -platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.12/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.11/platform-espressif32.zip platform_packages = framework = arduino, espidf board_build.embed_txtfiles = diff --git a/updater/platformio.ini b/updater/platformio.ini index e1e2c71..99e37d8 100644 --- a/updater/platformio.ini +++ b/updater/platformio.ini @@ -13,7 +13,7 @@ default_envs = updater_esp32 boards_dir = ../boards [env] -platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.12/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.11/platform-espressif32.zip platform_packages = framework = arduino, espidf board_build.embed_txtfiles = From 38f5d4d6f69ddb72129f21bce199ab8610cb0e1a Mon Sep 17 00:00:00 2001 From: iranl Date: Sun, 16 Feb 2025 13:17:33 +0100 Subject: [PATCH 13/19] Improve HA friendly names --- src/HomeAssistantDiscovery.cpp | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/HomeAssistantDiscovery.cpp b/src/HomeAssistantDiscovery.cpp index 2c704e8..9cd6812 100644 --- a/src/HomeAssistantDiscovery.cpp +++ b/src/HomeAssistantDiscovery.cpp @@ -216,12 +216,12 @@ void HomeAssistantDiscovery::publishHASSNukiHubConfig() "", { { (char*)"en", (char*)"true" }}); - // Nuki Hub Webserver enabled + // Webserver enabled publishHassTopic("switch", "webserver", _nukiHubUidString, "_webserver", - "Nuki Hub webserver enabled", + "Webserver enabled", _hostname.c_str(), _baseTopic.c_str(), String("~") + mqtt_topic_webserver_state, @@ -317,12 +317,12 @@ void HomeAssistantDiscovery::publishHASSNukiHubConfig() {(char*)"ic", (char*)"mdi:counter"} }); - // Nuki Hub restart reason + // Restart reason publishHassTopic("sensor", "nuki_hub_restart_reason", _nukiHubUidString, "_nuki_hub_restart_reason", - "Nuki Hub restart reason", + "Restart reason", _hostname.c_str(), _baseTopic.c_str(), String("~") + mqtt_topic_restart_reason_fw, @@ -333,12 +333,12 @@ void HomeAssistantDiscovery::publishHASSNukiHubConfig() "", { { (char*)"en", (char*)"true" }}); - // Nuki Hub restart reason ESP + // Restart reason ESP publishHassTopic("sensor", "nuki_hub_restart_reason_esp", _nukiHubUidString, "_nuki_hub_restart_reason_esp", - "Nuki Hub restart reason ESP", + "Restart reason ESP", _hostname.c_str(), _baseTopic.c_str(), String("~") + mqtt_topic_restart_reason_esp, @@ -351,12 +351,12 @@ void HomeAssistantDiscovery::publishHASSNukiHubConfig() if(_checkUpdates) { - // NUKI Hub latest + // Nuki Hub latest publishHassTopic("sensor", "nuki_hub_latest", _nukiHubUidString, "_nuki_hub_latest", - "NUKI Hub latest", + "Nuki Hub latest", _hostname.c_str(), _baseTopic.c_str(), String("~") + mqtt_topic_info_nuki_hub_latest, @@ -370,7 +370,7 @@ void HomeAssistantDiscovery::publishHASSNukiHubConfig() {(char*)"ic", (char*)"mdi:counter"} }); - // NUKI Hub update + // Nuki Hub update char latest_version_topic[250]; _baseTopic.toCharArray(latest_version_topic,_baseTopic.length() + 1); strcat(latest_version_topic, mqtt_topic_info_nuki_hub_latest); @@ -381,7 +381,7 @@ void HomeAssistantDiscovery::publishHASSNukiHubConfig() "nuki_hub_update", _nukiHubUidString, "_nuki_hub_update", - "NUKI Hub firmware update", + "Nuki Hub firmware update", _hostname.c_str(), _baseTopic.c_str(), String("~") + mqtt_topic_info_nuki_hub_version, @@ -403,7 +403,7 @@ void HomeAssistantDiscovery::publishHASSNukiHubConfig() "nuki_hub_update", _nukiHubUidString, "_nuki_hub_update", - "NUKI Hub firmware update", + "Nuki Hub firmware update", _hostname.c_str(), _baseTopic.c_str(), String("~") + mqtt_topic_info_nuki_hub_version, @@ -427,12 +427,12 @@ void HomeAssistantDiscovery::publishHASSNukiHubConfig() removeHassTopic((char*)"update", (char*)"nuki_hub_update", _nukiHubUidString); } - // Nuki Hub IP Address + // IP Address publishHassTopic("sensor", "nuki_hub_ip", _nukiHubUidString, "_nuki_hub_ip", - "Nuki Hub IP", + "IP", _hostname.c_str(), _baseTopic.c_str(), String("~") + mqtt_topic_info_nuki_hub_ip, From f3ea8a8eac26cab7e86ff98ea51149d812cb6515 Mon Sep 17 00:00:00 2001 From: iranl Date: Sun, 16 Feb 2025 22:37:31 +0100 Subject: [PATCH 14/19] Generate self-signed certificate --- src/Config.h | 2 +- src/WebCfgServer.cpp | 80 +++++++++- src/WebCfgServer.h | 4 +- src/util/SSLCert.cpp | 343 +++++++++++++++++++++++++++++++++++++++++++ src/util/SSLCert.hpp | 171 +++++++++++++++++++++ 5 files changed, 597 insertions(+), 3 deletions(-) create mode 100644 src/util/SSLCert.cpp create mode 100644 src/util/SSLCert.hpp diff --git a/src/Config.h b/src/Config.h index 5f64dce..cb8f868 100644 --- a/src/Config.h +++ b/src/Config.h @@ -5,7 +5,7 @@ #define NUKI_HUB_VERSION "9.09" #define NUKI_HUB_VERSION_INT (uint32_t)909 #define NUKI_HUB_BUILD "unknownbuildnr" -#define NUKI_HUB_DATE "2025-02-14" +#define NUKI_HUB_DATE "2025-02-16" #define GITHUB_LATEST_RELEASE_URL (char*)"https://github.com/technyon/nuki_hub/releases/latest" #define GITHUB_OTA_MANIFEST_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/manifest.json" diff --git a/src/WebCfgServer.cpp b/src/WebCfgServer.cpp index 77d492f..70256f0 100644 --- a/src/WebCfgServer.cpp +++ b/src/WebCfgServer.cpp @@ -9,6 +9,7 @@ #include "esp_random.h" #ifdef CONFIG_SOC_SPIRAM_SUPPORTED #include "esp_psram.h" +#include "util/SSLCert.hpp" #endif #ifndef CONFIG_IDF_TARGET_ESP32H2 #include @@ -774,6 +775,10 @@ void WebCfgServer::initialize() { return buildHttpSSLConfigHtml(request, resp, 2); } + else if (value == "selfsignhttps") + { + return buildHttpSSLConfigHtml(request, resp, 3); + } else if (value == "nukicfg") { return buildNukiConfigHtml(request, resp); @@ -2727,6 +2732,13 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S } } } + else if(key == "HTTPGEN") + { + createSSLCertificate(); + Log->print("Setting changed: "); + Log->println(key); + configChanged = true; + } #endif else if(key == "UPTIME") { @@ -5097,6 +5109,7 @@ esp_err_t WebCfgServer::buildNetworkConfigHtml(PsychicRequest *request, PsychicR { response.print("Set HTTP SSL Certificate"); response.print("Set HTTP SSL Key"); + response.print("Generate self-signed HTTP SSL Certificate and key"); printInputField(&response, "HTTPSFQDN", "Nuki Hub FQDN for HTTP redirect", _preferences->getString(preference_https_fqdn, "").c_str(), 255, ""); } #endif @@ -5326,7 +5339,7 @@ esp_err_t WebCfgServer::buildHttpSSLConfigHtml(PsychicRequest *request, PsychicR printTextarea(&response, "HTTPCRT", "HTTP SSL Certificate (*, optional)", "", 4400, true, true); } } - else + else if (type == 2) { bool found = false; @@ -5360,6 +5373,11 @@ esp_err_t WebCfgServer::buildHttpSSLConfigHtml(PsychicRequest *request, PsychicR printTextarea(&response, "HTTPKEY", "HTTP SSL Key (*, optional)", "", 2200, true, true); } } + else + { + response.print(""); + response.print("Click save to generate a HTTPS SSL Certificate and key"); + } response.print(""); response.print("
"); response.print(""); @@ -7005,4 +7023,64 @@ const String WebCfgServer::getPreselectionForGpio(const uint8_t &pin) const return String((int8_t)PinRole::Disabled); } + +#ifdef CONFIG_SOC_SPIRAM_SUPPORTED +void WebCfgServer::createSSLCertificate() +{ + SSLCert* cert; + cert = new SSLCert(); + int createCertResult = createSelfSignedCert( + *cert, + KEYSIZE_2048, + "CN=nukihub.local,O=NukiHub,C=DE", + "20250101000000", + "20350101000000" + ); + bool crtSuccess = false; + bool keySuccess = false; + + if (createCertResult == 0) { + if (!SPIFFS.begin(true)) { + Log->println("SPIFFS Mount Failed"); + } + else + { + File file = SPIFFS.open("/http_ssl.crt", FILE_WRITE); + if (!file) { + Log->println("Failed to open /http_ssl.crt for writing"); + } + else + { + if (!file.write((byte *)cert->getCertData(), cert->getCertLength())) + { + Log->println("Failed to write /http_ssl.crt"); + } + else + { + crtSuccess = true; + } + file.close(); + } + + File file2 = SPIFFS.open("/http_ssl.key", FILE_WRITE); + if (!file2) { + Log->println("Failed to open /http_ssl.key for writing"); + } + else + { + if (!file2.write((byte *)cert->getPKData(), cert->getPKLength())) + { + Log->println("Failed to write /http_ssl.key"); + } + else + { + keySuccess = true; + } + file2.close(); + } + } + } +} +#endif + #endif diff --git a/src/WebCfgServer.h b/src/WebCfgServer.h index 04a7224..f32a06e 100644 --- a/src/WebCfgServer.h +++ b/src/WebCfgServer.h @@ -86,7 +86,9 @@ private: #if defined(CONFIG_IDF_TARGET_ESP32) const std::vector> getNetworkCustomCLKOptions() const; #endif - + #ifdef CONFIG_SOC_SPIRAM_SUPPORTED + void createSSLCertificate(); + #endif const String getPreselectionForGpio(const uint8_t& pin) const; const String pinStateToString(const NukiPinState& value) const; diff --git a/src/util/SSLCert.cpp b/src/util/SSLCert.cpp new file mode 100644 index 0000000..df99128 --- /dev/null +++ b/src/util/SSLCert.cpp @@ -0,0 +1,343 @@ +#include "SSLCert.hpp" + +SSLCert::SSLCert(unsigned char * certData, uint16_t certLength, unsigned char * pkData, uint16_t pkLength): + _certLength(certLength), + _certData(certData), + _pkLength(pkLength), + _pkData(pkData) { + +} + +SSLCert::~SSLCert() { + // TODO Auto-generated destructor stub +} + + +uint16_t SSLCert::getCertLength() { + return _certLength; +} + +uint16_t SSLCert::getPKLength() { + return _pkLength; +} + +unsigned char * SSLCert::getCertData() { + return _certData; +} + +unsigned char * SSLCert::getPKData() { + return _pkData; +} + +void SSLCert::setPK(unsigned char * pkData, uint16_t length) { + _pkData = pkData; + _pkLength = length; +} + +void SSLCert::setCert(unsigned char * certData, uint16_t length) { + _certData = certData; + _certLength = length; +} + +void SSLCert::clear() { + for(uint16_t i = 0; i < _certLength; i++) _certData[i]=0; + delete _certData; + _certLength = 0; + + for(uint16_t i = 0; i < _pkLength; i++) _pkData[i] = 0; + delete _pkData; + _pkLength = 0; +} + +/** + * Function to create the key for a self-signed certificate. + * + * Writes private key as DER in certCtx + * + * Based on programs/pkey/gen_key.c + */ +static int gen_key(SSLCert &certCtx, SSLKeySize keySize) { + + // Initialize the entropy source + mbedtls_entropy_context entropy; + mbedtls_entropy_init( &entropy ); + + // Initialize the RNG + mbedtls_ctr_drbg_context ctr_drbg; + mbedtls_ctr_drbg_init( &ctr_drbg ); + int rngRes = mbedtls_ctr_drbg_seed( + &ctr_drbg, mbedtls_entropy_func, &entropy, + NULL, 0 + ); + if (rngRes != 0) { + mbedtls_entropy_free( &entropy ); + return HTTPS_SERVER_ERROR_KEYGEN_RNG; + } + + // Initialize the private key + mbedtls_pk_context key; + mbedtls_pk_init( &key ); + int resPkSetup = mbedtls_pk_setup( &key, mbedtls_pk_info_from_type( MBEDTLS_PK_RSA ) ); + if ( resPkSetup != 0) { + mbedtls_ctr_drbg_free( &ctr_drbg ); + mbedtls_entropy_free( &entropy ); + return HTTPS_SERVER_ERROR_KEYGEN_SETUP_PK; + } + + // Actual key generation + int resPkGen = mbedtls_rsa_gen_key( + mbedtls_pk_rsa( key ), + mbedtls_ctr_drbg_random, + &ctr_drbg, + keySize, + 65537 + ); + if ( resPkGen != 0) { + mbedtls_pk_free( &key ); + mbedtls_ctr_drbg_free( &ctr_drbg ); + mbedtls_entropy_free( &entropy ); + return HTTPS_SERVER_ERROR_KEYGEN_GEN_PK; + } + + // Free the entropy source and the RNG as they are no longer needed + mbedtls_ctr_drbg_free( &ctr_drbg ); + mbedtls_entropy_free( &entropy ); + + // Allocate the space on the heap, as stack size is quite limited + unsigned char * output_buf = new unsigned char[4096]; + if (output_buf == NULL) { + mbedtls_pk_free( &key ); + return HTTPS_SERVER_ERROR_KEY_OUT_OF_MEM; + } + memset(output_buf, 0, 4096); + + // Write the key to the temporary buffer and determine its length + int resPkWrite = mbedtls_pk_write_key_der( &key, output_buf, 4096 ); + if (resPkWrite < 0) { + delete[] output_buf; + mbedtls_pk_free( &key ); + return HTTPS_SERVER_ERROR_KEY_WRITE_PK; + } + size_t pkLength = resPkWrite; + unsigned char *pkOffset = output_buf + sizeof(unsigned char) * 4096 - pkLength; + + // Copy the key into a new, fitting space on the heap + unsigned char * output_pk = new unsigned char[pkLength]; + if (output_pk == NULL) { + delete[] output_buf; + mbedtls_pk_free( &key ); + return HTTPS_SERVER_ERROR_KEY_WRITE_PK; + } + memcpy(output_pk, pkOffset, pkLength); + + // Clean up the temporary buffer and clear the key context + delete[] output_buf; + mbedtls_pk_free( &key ); + + // Set the private key in the context + certCtx.setPK(output_pk, pkLength); + + return 0; +} + +static int parse_serial_decimal_format(unsigned char *obuf, size_t obufmax, + const char *ibuf, size_t *len) +{ + unsigned long long int dec; + unsigned int remaining_bytes = sizeof(dec); + unsigned char *p = obuf; + unsigned char val; + char *end_ptr = NULL; + + errno = 0; + dec = strtoull(ibuf, &end_ptr, 10); + + if ((errno != 0) || (end_ptr == ibuf)) { + return -1; + } + + *len = 0; + + while (remaining_bytes > 0) { + if (obufmax < (*len + 1)) { + return -1; + } + + val = (dec >> ((remaining_bytes - 1) * 8)) & 0xFF; + + /* Skip leading zeros */ + if ((val != 0) || (*len != 0)) { + *p = val; + (*len)++; + p++; + } + + remaining_bytes--; + } + + return 0; +} + +/** + * Function to generate the X509 certificate data for a private key + * + * Writes certificate in certCtx + * + * Based on programs/x509/cert_write.c + */ + +static int cert_write(SSLCert &certCtx, std::string dn, std::string validityFrom, std::string validityTo) { + int funcRes = 0; + int stepRes = 0; + + mbedtls_entropy_context entropy; + mbedtls_ctr_drbg_context ctr_drbg; + mbedtls_pk_context key; + mbedtls_x509write_cert crt; + unsigned char * primary_buffer; + unsigned char *certOffset; + unsigned char * output_buffer; + size_t certLength; + const char *defaultSerial = "1"; + unsigned char serial[MBEDTLS_X509_RFC5280_MAX_SERIAL_LEN]; + size_t serial_len; + + // Make a C-friendly version of the distinguished name + char dn_cstr[dn.length()+1]; + strcpy(dn_cstr, dn.c_str()); + + // Initialize the entropy source + mbedtls_entropy_init( &entropy ); + + // Initialize the RNG + mbedtls_ctr_drbg_init( &ctr_drbg ); + stepRes = mbedtls_ctr_drbg_seed( &ctr_drbg, mbedtls_entropy_func, &entropy, NULL, 0 ); + if (stepRes != 0) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_RNG; + goto error_after_entropy; + } + + mbedtls_pk_init( &key ); + stepRes = mbedtls_pk_parse_key( &key, certCtx.getPKData(), certCtx.getPKLength(), NULL, 0, mbedtls_ctr_drbg_random, &ctr_drbg); + if (stepRes != 0) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_READKEY; + goto error_after_key; + } + + // Start configuring the certificate + mbedtls_x509write_crt_init( &crt ); + // Set version and hash algorithm + mbedtls_x509write_crt_set_version( &crt, MBEDTLS_X509_CRT_VERSION_3 ); + mbedtls_x509write_crt_set_md_alg( &crt, MBEDTLS_MD_SHA256 ); + + // Set the keys (same key as we self-sign) + mbedtls_x509write_crt_set_subject_key( &crt, &key ); + mbedtls_x509write_crt_set_issuer_key( &crt, &key ); + + // Set issuer and subject (same, as we self-sign) + stepRes = mbedtls_x509write_crt_set_subject_name( &crt, dn_cstr ); + if (stepRes != 0) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_NAME; + goto error_after_cert; + } + stepRes = mbedtls_x509write_crt_set_issuer_name( &crt, dn_cstr ); + if (stepRes != 0) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_NAME; + goto error_after_cert; + } + + // Set the validity of the certificate. At the moment, it's fixed from 2019 to end of 2029. + stepRes = mbedtls_x509write_crt_set_validity( &crt, validityFrom.c_str(), validityTo.c_str()); + if (stepRes != 0) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_VALIDITY; + goto error_after_cert; + } + + // Make this a CA certificate + stepRes = mbedtls_x509write_crt_set_basic_constraints( &crt, 1, 0 ); + if (stepRes != 0) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_VALIDITY; + goto error_after_cert; + } + + // Initialize the serial number + stepRes = parse_serial_decimal_format(serial, sizeof(serial), defaultSerial, &serial_len); + + if (stepRes != 0) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_SERIAL; + goto error_after_cert_serial; + } + + stepRes = mbedtls_x509write_crt_set_serial_raw( &crt, serial, sizeof(serial) ); + if (stepRes != 0) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_SERIAL; + goto error_after_cert_serial; + } + + // Create buffer to write the certificate + primary_buffer = new unsigned char[4096]; + if (primary_buffer == NULL) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_OUT_OF_MEM; + goto error_after_cert_serial; + } + + // Write the actual certificate + stepRes = mbedtls_x509write_crt_der(&crt, primary_buffer, 4096, mbedtls_ctr_drbg_random, &ctr_drbg ); + if (stepRes < 0) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_WRITE; + goto error_after_primary_buffer; + } + + // Create a matching buffer + certLength = stepRes; + certOffset = primary_buffer + sizeof(unsigned char) * 4096 - certLength; + + // Copy the cert into a new, fitting space on the heap + output_buffer = new unsigned char[certLength]; + if (output_buffer == NULL) { + funcRes = HTTPS_SERVER_ERROR_CERTGEN_OUT_OF_MEM; + goto error_after_primary_buffer; + } + memcpy(output_buffer, certOffset, certLength); + + // Configure the cert in the context + certCtx.setCert(output_buffer, certLength); + + // Run through the cleanup process +error_after_primary_buffer: + delete[] primary_buffer; + +error_after_cert_serial: + +error_after_cert: + mbedtls_x509write_crt_free( &crt ); + +error_after_key: + mbedtls_pk_free(&key); + +error_after_entropy: + mbedtls_ctr_drbg_free( &ctr_drbg ); + mbedtls_entropy_free( &entropy ); + return funcRes; +} + +int createSelfSignedCert(SSLCert &certCtx, SSLKeySize keySize, std::string dn, std::string validFrom, std::string validUntil) { + + // Add the private key + int keyRes = gen_key(certCtx, keySize); + if (keyRes != 0) { + // Key-generation failed, return the failure code + return keyRes; + } + + // Add the self-signed certificate + int certRes = cert_write(certCtx, dn, validFrom, validUntil); + if (certRes != 0) { + // Cert writing failed, reset the pk and return failure code + certCtx.setPK(NULL, 0); + return certRes; + } + + // If all went well, return 0 + return 0; +} \ No newline at end of file diff --git a/src/util/SSLCert.hpp b/src/util/SSLCert.hpp new file mode 100644 index 0000000..8860d56 --- /dev/null +++ b/src/util/SSLCert.hpp @@ -0,0 +1,171 @@ +#ifndef SRC_SSLCERT_HPP_ +#define SRC_SSLCERT_HPP_ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#define HTTPS_SERVER_ERROR_KEYGEN 0x0F +#define HTTPS_SERVER_ERROR_KEYGEN_RNG 0x02 +#define HTTPS_SERVER_ERROR_KEYGEN_SETUP_PK 0x03 +#define HTTPS_SERVER_ERROR_KEYGEN_GEN_PK 0x04 +#define HTTPS_SERVER_ERROR_KEY_WRITE_PK 0x05 +#define HTTPS_SERVER_ERROR_KEY_OUT_OF_MEM 0x06 +#define HTTPS_SERVER_ERROR_CERTGEN 0x1F +#define HTTPS_SERVER_ERROR_CERTGEN_RNG 0x12 +#define HTTPS_SERVER_ERROR_CERTGEN_READKEY 0x13 +#define HTTPS_SERVER_ERROR_CERTGEN_WRITE 0x15 +#define HTTPS_SERVER_ERROR_CERTGEN_OUT_OF_MEM 0x16 +#define HTTPS_SERVER_ERROR_CERTGEN_NAME 0x17 +#define HTTPS_SERVER_ERROR_CERTGEN_SERIAL 0x18 +#define HTTPS_SERVER_ERROR_CERTGEN_VALIDITY 0x19 + +/** + * \brief Certificate and private key that can be passed to the HTTPSServer. + * + * **Converting PEM to DER Files** + * + * Certificate: + * ```bash + * openssl x509 -inform PEM -outform DER -in myCert.crt -out cert.der + * ``` + * + * Private Key: + * ```bash + * openssl rsa -inform PEM -outform DER -in myCert.key -out key.der + * ``` + * + * **Converting DER File to C Header** + * + * ```bash + * echo "#ifndef KEY_H_" > ./key.h + * echo "#define KEY_H_" >> ./key.h + * xxd -i key.der >> ./key.h + * echo "#endif" >> ./key.h + * ``` + */ +class SSLCert { +public: + /** + * \brief Creates a new SSLCert. + * + * The certificate and key data may be NULL (default values) if the certificate is meant + * to be passed to createSelfSignedCert(). + * + * Otherwise, the data must reside in a memory location that is not deleted until the server + * using the certificate is stopped. + * + * \param[in] certData The certificate data to use (DER format) + * \param[in] certLength The length of the certificate data + * \param[in] pkData The private key data to use (DER format) + * \param[in] pkLength The length of the private key + */ + SSLCert( + unsigned char * certData = NULL, + uint16_t certLength = 0, + unsigned char * pkData = NULL, + uint16_t pkLength = 0 + ); + virtual ~SSLCert(); + + /** + * \brief Returns the length of the certificate in byte + */ + uint16_t getCertLength(); + + /** + * \brief Returns the length of the private key in byte + */ + uint16_t getPKLength(); + + /** + * \brief Returns the certificate data + */ + unsigned char * getCertData(); + + /** + * \brief Returns the private key data + */ + unsigned char * getPKData(); + + /** + * \brief Sets the private key in DER format + * + * The data has to reside in a place in memory that is not deleted as long as the + * server is running. + * + * See SSLCert() for some information on how to generate DER data. + * + * \param[in] _pkData The data of the private key + * \param[in] length The length of the private key + */ + void setPK(unsigned char * _pkData, uint16_t length); + + /** + * \brief Sets the certificate data in DER format + * + * The data has to reside in a place in memory that is not deleted as long as the + * server is running. + * + * See SSLCert for some information on how to generate DER data. + * + * \param[in] _certData The data of the certificate + * \param[in] length The length of the certificate + */ + void setCert(unsigned char * _certData, uint16_t length); + + /** + * \brief Clears the key buffers and deletes them. + */ + void clear(); + +private: + uint16_t _certLength; + unsigned char * _certData; + uint16_t _pkLength; + unsigned char * _pkData; + +}; + +/** + * \brief Defines the key size for key generation + * + * Not available if the `HTTPS_DISABLE_SELFSIGNING` compiler flag is set + */ +enum SSLKeySize { + /** \brief RSA key with 1024 bit */ + KEYSIZE_1024 = 1024, + /** \brief RSA key with 2048 bit */ + KEYSIZE_2048 = 2048, + /** \brief RSA key with 4096 bit */ + KEYSIZE_4096 = 4096 +}; + +/** + * \brief Creates a self-signed certificate on the ESP32 + * + * This function creates a new self-signed certificate for the given hostname on the heap. + * Make sure to clear() it before you delete it. + * + * The distinguished name (dn) parameter has to follow the x509 specifications. An example + * would be: + * CN=myesp.local,O=acme,C=US + * + * The strings validFrom and validUntil have to be formatted like this: + * "20190101000000", "20300101000000" + * + * This will take some time, so you should probably write the certificate data to non-volatile + * storage when you are done. + * + * Setting the `HTTPS_DISABLE_SELFSIGNING` compiler flag will remove this function from the library + */ +int createSelfSignedCert(SSLCert &certCtx, SSLKeySize keySize, std::string dn, std::string validFrom = "20190101000000", std::string validUntil = "20300101000000"); + +#endif /* SRC_SSLCERT_HPP_ */ From c132dd9f525e0859121bacc77ab267c8539e13cd Mon Sep 17 00:00:00 2001 From: iranl Date: Sun, 16 Feb 2025 23:01:29 +0100 Subject: [PATCH 15/19] Add separate GL-S10 build --- .github/workflows/beta.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/nightly.yml | 2 +- .github/workflows/release.yml | 2 +- boards/nuki-esp32gls10.json | 37 ++++++++++++++++++++++++++++++++ pio_package_post.py | 2 ++ platformio.ini | 19 ++++++++++++++++ resources/how-to-flash.txt | 11 ++++++++++ sdkconfig.gls10.defaults | 1 + src/Config.h | 15 +++++++++++++ updater/pio_package_post.py | 2 ++ updater/platformio.ini | 7 ++++++ updater/sdkconfig.gls10.defaults | 1 + 13 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 boards/nuki-esp32gls10.json create mode 100644 sdkconfig.gls10.defaults create mode 100644 updater/sdkconfig.gls10.defaults diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index 08c5df7..f5cb18d 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false matrix: - board: [esp32, esp32-s3, esp32-s3-oct, esp32-c3, esp32-c6, esp32-h2, esp32-solo1] + board: [esp32, esp32-s3, esp32-s3-oct, esp32-c3, esp32-c6, esp32-h2, esp32-solo1, esp32-gl-s10] build: [release] env: BOARD: ${{ matrix.board }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6c9b069..a43601c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - board: [esp32, esp32-s3, esp32-s3-oct, esp32-c3, esp32-c6, esp32-h2, esp32-solo1] + board: [esp32, esp32-s3, esp32-s3-oct, esp32-c3, esp32-c6, esp32-h2, esp32-solo1, esp32-gl-s10] build: [release] env: BOARD: ${{ matrix.board }} diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index f8cc550..28be9a7 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -34,7 +34,7 @@ jobs: strategy: fail-fast: false matrix: - board: [esp32, esp32-s3, esp32-s3-oct, esp32-c3, esp32-c6, esp32-h2, esp32-solo1] + board: [esp32, esp32-s3, esp32-s3-oct, esp32-c3, esp32-c6, esp32-h2, esp32-solo1, esp32-gl-s10] build: [release] env: BOARD: ${{ matrix.board }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 01c22d9..ed34bd1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false matrix: - board: [esp32, esp32-s3, esp32-s3-oct, esp32-c3, esp32-c6, esp32-h2, esp32-solo1] + board: [esp32, esp32-s3, esp32-s3-oct, esp32-c3, esp32-c6, esp32-h2, esp32-solo1, esp32-gl-s10] build: [release] env: BOARD: ${{ matrix.board }} diff --git a/boards/nuki-esp32gls10.json b/boards/nuki-esp32gls10.json new file mode 100644 index 0000000..6cf3af6 --- /dev/null +++ b/boards/nuki-esp32gls10.json @@ -0,0 +1,37 @@ +{ + "build": { + "arduino":{ + "ldscript": "esp32_out.ld" + }, + "core": "esp32", + "extra_flags": "-DARDUINO_ESP32_DEV -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue", + "f_cpu": "240000000L", + "f_flash": "40000000L", + "flash_mode": "dio", + "mcu": "esp32", + "variant": "doitESP32devkitV1" + }, + "connectivity": [ + "wifi", + "bluetooth", + "ethernet", + "can" + ], + "debug": { + "openocd_board": "esp-wroom-32.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "DOIT ESP32 DEVKIT V1", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 327680, + "maximum_size": 4194304, + "require_upload_port": true, + "speed": 460800 + }, + "url": "http://www.doit.am/", + "vendor": "DOIT" +} \ No newline at end of file diff --git a/pio_package_post.py b/pio_package_post.py index 1154d53..60fc392 100644 --- a/pio_package_post.py +++ b/pio_package_post.py @@ -9,6 +9,8 @@ def get_board_name(env): if env.get('BOARD') == 'nuki-esp32solo1': board = 'esp32solo1' + elif env.get('BOARD') == 'nuki-esp32gls10': + board = 'esp32gls10' elif env.get('BOARD') == 'nuki-esp32-s3-oct': board = 'esp32s3oct' return board diff --git a/platformio.ini b/platformio.ini index 74401d4..6a14791 100644 --- a/platformio.ini +++ b/platformio.ini @@ -78,6 +78,13 @@ build_flags = -DCONFIG_NIMBLE_CPP_LOG_LEVEL=0 -DCONFIG_BT_NIMBLE_LOG_LEVEL=0 +[env:esp32-gl-s10] +extends = env:esp32 +board = nuki-esp32gls10 +board_build.cmake_extra_args = + -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.release.defaults;sdkconfig.defaults.esp32;sdkconfig.gls10.defaults" + -DNUKI_TARGET_GL_S10=y + [env:esp32-c3] extends = env:esp32 board = esp32-c3-devkitc-02 @@ -136,6 +143,18 @@ build_flags = -DCONFIG_BT_NIMBLE_LOG_LEVEL=0 -DDEBUG_NUKIHUB +[env:esp32-gl-s10_dbg] +extends = env:esp32-gl-s10 +custom_build = debug +board_build.cmake_extra_args = + -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.debug.defaults;sdkconfig.defaults.esp32;sdkconfig.gls10.defaults" +build_flags = + ${env.build_flags} + -DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG + -DCONFIG_NIMBLE_CPP_LOG_LEVEL=0 + -DCONFIG_BT_NIMBLE_LOG_LEVEL=0 + -DDEBUG_NUKIHUB + [env:esp32-c3_dbg] extends = env:esp32-c3 custom_build = debug diff --git a/resources/how-to-flash.txt b/resources/how-to-flash.txt index fc2737d..99c1956 100644 --- a/resources/how-to-flash.txt +++ b/resources/how-to-flash.txt @@ -29,6 +29,13 @@ e000 boot_app0.bin 10000 nuki_hub_esp32.bin 280000 nuki_hub_updater_esp32.bin +ESP32-GL-S10 +e000 boot_app0.bin +1000 bootloader.bin +8000 nuki_hub.partitions.bin +10000 nuki_hub_esp32.bin +280000 nuki_hub_updater_esp32.bin + ESP32-S3 e000 boot_app0.bin 0 bootloader.bin @@ -87,6 +94,10 @@ As an alternative to the Download Tools, you can also use the esptool from the E esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq keep --flash_size detect 0xe000 boot_app0.bin 0x1000 bootloader.bin 0x10000 nuki_hub_esp32.bin 0x280000 nuki_hub_updater_esp32.bin 0x8000 nuki_hub.partitions.bin +## ESP32-GL-S10 + +esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq keep --flash_size detect 0xe000 boot_app0.bin 0x1000 bootloader.bin 0x10000 nuki_hub_esp32.bin 0x280000 nuki_hub_updater_esp32.bin 0x8000 nuki_hub.partitions.bin + ## ESP32-S3 esptool.py --chip esp32s3 --port /dev/ttyUSB0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq keep --flash_size detect 0xe000 boot_app0.bin 0x0 bootloader.bin 0x10000 nuki_hub_esp32s3.bin 0x280000 nuki_hub_updater_esp32s3.bin 0x8000 nuki_hub.partitions.bin diff --git a/sdkconfig.gls10.defaults b/sdkconfig.gls10.defaults new file mode 100644 index 0000000..71c8610 --- /dev/null +++ b/sdkconfig.gls10.defaults @@ -0,0 +1 @@ +CONFIG_D0WD_PSRAM_CLK_IO=6 \ No newline at end of file diff --git a/src/Config.h b/src/Config.h index cb8f868..1f7057d 100644 --- a/src/Config.h +++ b/src/Config.h @@ -107,6 +107,21 @@ #define GITHUB_MASTER_UPDATER_BINARY_URL_DBG (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/debug/master/nuki_hub_updater_esp32-solo1.bin" #define NUKI_HUB_HW (char*)"ESP32-SOLO1" #define BOOT_BUTTON_GPIO (gpio_num_t)0 +#elif defined(NUKI_TARGET_GL_S10) +#define GITHUB_LATEST_RELEASE_BINARY_URL "https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/nuki_hub_esp32-gl-s10.bin" +#define GITHUB_LATEST_UPDATER_BINARY_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/nuki_hub_updater_esp32.bin" +#define GITHUB_BETA_RELEASE_BINARY_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/beta/nuki_hub_esp32-gl-s10.bin" +#define GITHUB_BETA_UPDATER_BINARY_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/beta/nuki_hub_updater_esp32.bin" +#define GITHUB_MASTER_RELEASE_BINARY_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/master/nuki_hub_esp32-gl-s10.bin" +#define GITHUB_MASTER_UPDATER_BINARY_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/master/nuki_hub_updater_esp32.bin" +#define GITHUB_LATEST_RELEASE_BINARY_URL_DBG (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/debug/nuki_hub_esp32-gl-s10.bin" +#define GITHUB_LATEST_UPDATER_BINARY_URL_DBG (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/debug/nuki_hub_updater_esp32.bin" +#define GITHUB_BETA_RELEASE_BINARY_URL_DBG (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/debug/beta/nuki_hub_esp32-gl-s10.bin" +#define GITHUB_BETA_UPDATER_BINARY_URL_DBG (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/debug/beta/nuki_hub_updater_esp32.bin" +#define GITHUB_MASTER_RELEASE_BINARY_URL_DBG (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/debug/master/nuki_hub_esp32-gl-s10.bin" +#define GITHUB_MASTER_UPDATER_BINARY_URL_DBG (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/debug/master/nuki_hub_updater_esp32.bin" +#define NUKI_HUB_HW (char*)"ESP32-GL-S10" +#define BOOT_BUTTON_GPIO (gpio_num_t)0 #else #define GITHUB_LATEST_RELEASE_BINARY_URL "https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/nuki_hub_esp32.bin" #define GITHUB_LATEST_UPDATER_BINARY_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/nuki_hub_updater_esp32.bin" diff --git a/updater/pio_package_post.py b/updater/pio_package_post.py index 1c6f405..2bb6416 100644 --- a/updater/pio_package_post.py +++ b/updater/pio_package_post.py @@ -8,6 +8,8 @@ def get_board_name(env): board = env.get('BOARD_MCU') if env.get('BOARD') == 'nuki-esp32solo1': board = 'esp32solo1' + elif env.get('BOARD') == 'nuki-esp32gls10': + board = 'esp32gls10' elif env.get('BOARD') == 'nuki-esp32-s3-oct': board = 'esp32s3oct' return board diff --git a/updater/platformio.ini b/updater/platformio.ini index 99e37d8..254af6a 100644 --- a/updater/platformio.ini +++ b/updater/platformio.ini @@ -67,6 +67,13 @@ extra_scripts = board_build.cmake_extra_args = -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32" +[env:updater_esp32-gl-s10] +extends = env:updater_esp32 +board = nuki-esp32gls10 +board_build.cmake_extra_args = + -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32;sdkconfig.gls10.defaults" + -DNUKI_TARGET_GL_S10=y + [env:updater_esp32-c3] extends = env:updater_esp32 board = esp32-c3-devkitc-02 diff --git a/updater/sdkconfig.gls10.defaults b/updater/sdkconfig.gls10.defaults new file mode 100644 index 0000000..71c8610 --- /dev/null +++ b/updater/sdkconfig.gls10.defaults @@ -0,0 +1 @@ +CONFIG_D0WD_PSRAM_CLK_IO=6 \ No newline at end of file From 1a31b809186bcd58e0343bfd15ca2413c4998c2b Mon Sep 17 00:00:00 2001 From: iranl Date: Sun, 16 Feb 2025 23:28:56 +0100 Subject: [PATCH 16/19] DuoAuth license --- lib/DuoAuthLibrary/AUTHORS | 1 + lib/DuoAuthLibrary/CHANGELOG.md | 5 +++++ lib/DuoAuthLibrary/library.properties | 8 ++++---- lib/DuoAuthLibrary/src/DuoAuthLib.cpp | 6 ++++++ lib/DuoAuthLibrary/src/DuoAuthLib.h | 6 ++++++ 5 files changed, 22 insertions(+), 4 deletions(-) diff --git a/lib/DuoAuthLibrary/AUTHORS b/lib/DuoAuthLibrary/AUTHORS index dee7f70..295b394 100644 --- a/lib/DuoAuthLibrary/AUTHORS +++ b/lib/DuoAuthLibrary/AUTHORS @@ -2,3 +2,4 @@ # List of Authors Gary Oppel +iranl <25727444+iranl@users.noreply.github.com> \ No newline at end of file diff --git a/lib/DuoAuthLibrary/CHANGELOG.md b/lib/DuoAuthLibrary/CHANGELOG.md index b397015..4dd6880 100644 --- a/lib/DuoAuthLibrary/CHANGELOG.md +++ b/lib/DuoAuthLibrary/CHANGELOG.md @@ -5,3 +5,8 @@ v1.0.0 (08/26/2020) ------ * Initial Library Release + +v1.1.0 (01/20/2025) +------ + +* Enable using ESP32 CA Certificate bundle diff --git a/lib/DuoAuthLibrary/library.properties b/lib/DuoAuthLibrary/library.properties index 5ea5068..4fc35d3 100644 --- a/lib/DuoAuthLibrary/library.properties +++ b/lib/DuoAuthLibrary/library.properties @@ -1,9 +1,9 @@ name=Duo Auth Library -version=1.0.0 -author=Gary Oppel -maintainer=Gary Oppel +version=1.1.0 +author=iranl +maintainer=iranl sentence=Enables Duo Authentication within your ESP32 Wi-Fi Projects paragraph=Extends Duo Authentication API's for Push, Passcode, and Asynchronous Push Authentication requests. category=Other -url=https://github.com/CiscoDevNet/Arduino-DuoAuthLibrary-ESP32 +url=https://github.com/technyon/nuki_hub/lib/DuoAuthLibrary architectures=esp32 diff --git a/lib/DuoAuthLibrary/src/DuoAuthLib.cpp b/lib/DuoAuthLibrary/src/DuoAuthLib.cpp index 4f3a342..2667c37 100644 --- a/lib/DuoAuthLibrary/src/DuoAuthLib.cpp +++ b/lib/DuoAuthLibrary/src/DuoAuthLib.cpp @@ -2,6 +2,7 @@ *@license * *Copyright 2020 Cisco Systems, Inc. or its affiliates + *Modifications copyright (C) 2025 iranl / Nuki Hub * *Licensed under the Apache License, Version 2.0 (the "License"); *you may not use this file except in compliance with the License. @@ -24,6 +25,11 @@ * @url https://github.com/CiscoDevNet/Arduino-DuoAuthLibrary-ESP32 * @version 1.0.0 * @author Gary Oppel + + * @url https://github.com/technyon/nuki_hub/lib/DuoAuthLibrary + * @version 1.1.0 + * @author iranl <25727444+iranl@users.noreply.github.com> + * Modified to enable using ESP32 CA Certificate bundle */ //Include DuoAuthLib Library Header diff --git a/lib/DuoAuthLibrary/src/DuoAuthLib.h b/lib/DuoAuthLibrary/src/DuoAuthLib.h index 636d630..5c60301 100644 --- a/lib/DuoAuthLibrary/src/DuoAuthLib.h +++ b/lib/DuoAuthLibrary/src/DuoAuthLib.h @@ -2,6 +2,7 @@ *@license * *Copyright 2020 Cisco Systems, Inc. or its affiliates + *Modifications copyright (C) 2025 iranl / Nuki Hub * *Licensed under the Apache License, Version 2.0 (the "License"); *you may not use this file except in compliance with the License. @@ -24,6 +25,11 @@ * @url https://github.com/CiscoDevNet/Arduino-DuoAuthLibrary-ESP32 * @version 1.0.0 * @author Gary Oppel + + * @url https://github.com/technyon/nuki_hub/lib/DuoAuthLibrary + * @version 1.1.0 + * @author iranl <25727444+iranl@users.noreply.github.com> + * Modified to enable using ESP32 CA Certificate bundle */ //Verify that the Duo Auth Library descriptor is only included once From 59a5f02ed655514dd9d4660a22a2313dbfebc44f Mon Sep 17 00:00:00 2001 From: iranl Date: Sun, 16 Feb 2025 23:34:27 +0100 Subject: [PATCH 17/19] Rename root CA bundle --- resources/{github_root_ca.pem => root_ca.pem} | 0 sdkconfig.defaults | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename resources/{github_root_ca.pem => root_ca.pem} (100%) diff --git a/resources/github_root_ca.pem b/resources/root_ca.pem similarity index 100% rename from resources/github_root_ca.pem rename to resources/root_ca.pem diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 81cd6fc..323e589 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -39,7 +39,7 @@ CONFIG_MBEDTLS_KEY_EXCHANGE_PSK=y CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_NONE=y CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE=y -CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE_PATH="resources/github_root_ca.pem" +CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE_PATH="resources/root_ca.pem" CONFIG_MBEDTLS_DYNAMIC_BUFFER=y # RTC WDT From 21d5491e50bf9b6df63f039898be0314ea9185c7 Mon Sep 17 00:00:00 2001 From: iranl Date: Mon, 17 Feb 2025 19:27:28 +0100 Subject: [PATCH 18/19] Pioarduino 3.1.3 --- platformio.ini | 2 +- updater/platformio.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/platformio.ini b/platformio.ini index 6a14791..17a2196 100644 --- a/platformio.ini +++ b/platformio.ini @@ -13,7 +13,7 @@ default_envs = esp32 boards_dir = boards [env] -platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.11/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.13/platform-espressif32.zip platform_packages = framework = arduino, espidf board_build.embed_txtfiles = diff --git a/updater/platformio.ini b/updater/platformio.ini index 254af6a..18fa09c 100644 --- a/updater/platformio.ini +++ b/updater/platformio.ini @@ -13,7 +13,7 @@ default_envs = updater_esp32 boards_dir = ../boards [env] -platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.11/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.13/platform-espressif32.zip platform_packages = framework = arduino, espidf board_build.embed_txtfiles = From 3fa605b2b0ced567f3ccb494988feb23d2d4b367 Mon Sep 17 00:00:00 2001 From: iranl Date: Tue, 18 Feb 2025 15:27:03 +0100 Subject: [PATCH 19/19] SPIFFS and self-sign fixes --- partitions.csv | 2 +- sdkconfig.defaults | 1 + src/Config.h | 2 +- src/WebCfgServer.cpp | 24 +++--- src/main.cpp | 42 ++++++++++ src/util/SSLCert.cpp | 180 ++++++++++++++++++++++++++--------------- src/util/SSLCert.hpp | 154 +++++++++-------------------------- updater/partitions.csv | 2 +- 8 files changed, 211 insertions(+), 196 deletions(-) diff --git a/partitions.csv b/partitions.csv index 6f27eb7..1b64460 100644 --- a/partitions.csv +++ b/partitions.csv @@ -1 +1 @@ -# Espressif ESP32 Partition Table # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, app0, app, ota_0, 0x10000, 0x270000, app1, app, ota_1, 0x280000, 0x150000, spiffs, data, spiffs, 0x3D0000, 0x20000, coredump, data, coredump,0x3F0000, 0x10000, \ No newline at end of file +# Espressif ESP32 Partition Table # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, app0, app, ota_0, 0x10000, 0x270000, app1, app, ota_1, 0x280000, 0x130000, spiffs, data, spiffs, 0x3B0000, 0x40000, coredump, data, coredump,0x3F0000, 0x10000, \ No newline at end of file diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 323e589..6e3503f 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -3,6 +3,7 @@ CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_SIZE=y CONFIG_COMPILER_OPTIMIZATION_SIZE=y CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y CONFIG_ESPTOOLPY_HEADER_FLASHSIZE_UPDATE=y +CONFIG_SPIFFS_GC_MAX_RUNS=512 # ARDUINO CONFIG_AUTOSTART_ARDUINO=y diff --git a/src/Config.h b/src/Config.h index 1f7057d..edbc838 100644 --- a/src/Config.h +++ b/src/Config.h @@ -5,7 +5,7 @@ #define NUKI_HUB_VERSION "9.09" #define NUKI_HUB_VERSION_INT (uint32_t)909 #define NUKI_HUB_BUILD "unknownbuildnr" -#define NUKI_HUB_DATE "2025-02-16" +#define NUKI_HUB_DATE "2025-02-18" #define GITHUB_LATEST_RELEASE_URL (char*)"https://github.com/technyon/nuki_hub/releases/latest" #define GITHUB_OTA_MANIFEST_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/manifest.json" diff --git a/src/WebCfgServer.cpp b/src/WebCfgServer.cpp index 70256f0..ac05eb0 100644 --- a/src/WebCfgServer.cpp +++ b/src/WebCfgServer.cpp @@ -5948,6 +5948,11 @@ esp_err_t WebCfgServer::buildInfoHtml(PsychicRequest *request, PsychicResponse* response.print(uxTaskGetStackHighWaterMark(networkTaskHandle)); response.print("\nNuki task stack high watermark: "); response.print(uxTaskGetStackHighWaterMark(nukiTaskHandle)); + SPIFFS.begin(true); + response.print("\n\n------------ SPIFFS ------------"); + response.printf("\nSPIFFS Total Bytes: %u", SPIFFS.totalBytes()); + response.printf("\nSPIFFS Used Bytes: %u", SPIFFS.usedBytes()); + response.printf("\nSPIFFS Free Bytes: %u", SPIFFS.totalBytes() - SPIFFS.usedBytes()); response.print("\n\n------------ GENERAL SETTINGS ------------"); response.print("\nNetwork task stack size: "); response.print(_preferences->getInt(preference_task_size_network, NETWORK_TASK_SIZE)); @@ -7036,8 +7041,6 @@ void WebCfgServer::createSSLCertificate() "20250101000000", "20350101000000" ); - bool crtSuccess = false; - bool keySuccess = false; if (createCertResult == 0) { if (!SPIFFS.begin(true)) { @@ -7051,14 +7054,10 @@ void WebCfgServer::createSSLCertificate() } else { - if (!file.write((byte *)cert->getCertData(), cert->getCertLength())) + if (!file.print(cert->getCertPEM())) { Log->println("Failed to write /http_ssl.crt"); } - else - { - crtSuccess = true; - } file.close(); } @@ -7068,18 +7067,19 @@ void WebCfgServer::createSSLCertificate() } else { - if (!file2.write((byte *)cert->getPKData(), cert->getPKLength())) + if (!file2.print(cert->getKeyPEM())) { Log->println("Failed to write /http_ssl.key"); } - else - { - keySuccess = true; - } file2.close(); } } } + else + { + Log->print("SSL Self sign failed: "); + Log->println(createCertResult); + } } #endif diff --git a/src/main.cpp b/src/main.cpp index 4bfb9af..0c01222 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -185,6 +185,43 @@ uint8_t checkPartition() } } +void listDir(fs::FS &fs, const char *dirname, uint8_t levels) { + Serial.printf("Listing directory: %s\r\n", dirname); + + File root = fs.open(dirname); + if (!root) { + Serial.println("- failed to open directory"); + return; + } + if (!root.isDirectory()) { + Serial.println(" - not a directory"); + return; + } + + File file = root.openNextFile(); + while (file) { + if (file.isDirectory()) { + Serial.print(" DIR : "); + Serial.println(file.name()); + if (levels) { + listDir(fs, file.path(), levels - 1); + } + } else { + Serial.print(" FILE: "); + Serial.print(file.name()); + Serial.print("\tSIZE: "); + Serial.println(file.size()); + } + + if (file.size() > (int)(SPIFFS.totalBytes() * 0.4)) + { + SPIFFS.remove((String)"/" + file.name()); + } + + file = root.openNextFile(); + } +} + void cbSyncTime(struct timeval *tv) { Log->println("NTP time synced"); timeSynced = true; @@ -635,6 +672,11 @@ void setup() { logCoreDump(); } + + if (SPIFFS.begin(true)) + { + listDir(SPIFFS, "/", 1); + } uint8_t partitionType = checkPartition(); diff --git a/src/util/SSLCert.cpp b/src/util/SSLCert.cpp index df99128..f7d1f49 100644 --- a/src/util/SSLCert.cpp +++ b/src/util/SSLCert.cpp @@ -1,10 +1,34 @@ +/* +MIT License + +Copyright (c) 2017 Frank Hessel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + #include "SSLCert.hpp" -SSLCert::SSLCert(unsigned char * certData, uint16_t certLength, unsigned char * pkData, uint16_t pkLength): +SSLCert::SSLCert(uint16_t certLength, uint16_t pkLength, String keyPEM, String certPEM): _certLength(certLength), - _certData(certData), _pkLength(pkLength), - _pkData(pkData) { + _keyPEM(keyPEM), + _certPEM(certPEM) { } @@ -12,7 +36,6 @@ SSLCert::~SSLCert() { // TODO Auto-generated destructor stub } - uint16_t SSLCert::getCertLength() { return _certLength; } @@ -21,39 +44,88 @@ uint16_t SSLCert::getPKLength() { return _pkLength; } -unsigned char * SSLCert::getCertData() { - return _certData; +String SSLCert::getKeyPEM() { + return _keyPEM; } -unsigned char * SSLCert::getPKData() { - return _pkData; +String SSLCert::getCertPEM() { + return _certPEM; } -void SSLCert::setPK(unsigned char * pkData, uint16_t length) { - _pkData = pkData; - _pkLength = length; +void SSLCert::setPK(String keyPEM) { + _keyPEM = keyPEM; + _pkLength = keyPEM.length(); } -void SSLCert::setCert(unsigned char * certData, uint16_t length) { - _certData = certData; - _certLength = length; + +void SSLCert::setCert(String certPEM) { + _certPEM = certPEM; + _certLength = certPEM.length(); } void SSLCert::clear() { - for(uint16_t i = 0; i < _certLength; i++) _certData[i]=0; - delete _certData; _certLength = 0; - - for(uint16_t i = 0; i < _pkLength; i++) _pkData[i] = 0; - delete _pkData; _pkLength = 0; + + _keyPEM = ""; + _certPEM = ""; +} + +/** + * Returns the CN value from a DN, or "" if it cannot be found + */ +static std::string get_cn(std::string dn) { + size_t cnStart = dn.find("CN="); + if (cnStart == std::string::npos) { + return ""; + } + cnStart += 3; + size_t cnStop = dn.find(",", cnStart); + if (cnStop == std::string::npos) { + cnStop = dn.length(); + } + return dn.substr(cnStart, cnStop - cnStart); +} + +/** + * Sets the DN as subjectAltName extension in the certificate + */ +static int add_subject_alt_name(mbedtls_x509write_cert *crt, std::string &cn) { + size_t bufsize = cn.length() + 8; // some additional space for tags and length fields + uint8_t buf[bufsize]; + uint8_t *p = &buf[bufsize - 1]; + uint8_t *start = buf; + int length = 0; + int ret; // used by MBEDTLS macro + + // The ASN structure that we will construct as parameter for write_crt_set_extension is as follows: + // | 0x30 = Sequence | length | 0x82 = dNSName, context-specific | length | cn0 | cn1 | cn2 | cn3 | .. | cnn | + // ↑ : ↑ `-------------v------------------´: + // | : `-------------------´ : + // | `----------v------------------------------------------------------------------´ + // `---------------´ + // Let's encrypt has useful infos: https://letsencrypt.org/docs/a-warm-welcome-to-asn1-and-der/#choice-and-any-encoding + MBEDTLS_ASN1_CHK_ADD(length, + mbedtls_asn1_write_raw_buffer(&p, start, (uint8_t*)cn.c_str(), cn.length())); + MBEDTLS_ASN1_CHK_ADD(length, + mbedtls_asn1_write_len(&p, start, length)); + MBEDTLS_ASN1_CHK_ADD(length, + mbedtls_asn1_write_tag(&p, start, MBEDTLS_ASN1_CONTEXT_SPECIFIC | 0x02)); // 0x02 = dNSName + MBEDTLS_ASN1_CHK_ADD(length, + mbedtls_asn1_write_len(&p, start, length)); + MBEDTLS_ASN1_CHK_ADD(length, + mbedtls_asn1_write_tag(&p, start, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE )); + return mbedtls_x509write_crt_set_extension( crt, + MBEDTLS_OID_SUBJECT_ALT_NAME, MBEDTLS_OID_SIZE(MBEDTLS_OID_SUBJECT_ALT_NAME), + 0, // not critical + p, length); } /** * Function to create the key for a self-signed certificate. - * + * * Writes private key as DER in certCtx - * + * * Based on programs/pkey/gen_key.c */ static int gen_key(SSLCert &certCtx, SSLKeySize keySize) { @@ -84,7 +156,7 @@ static int gen_key(SSLCert &certCtx, SSLKeySize keySize) { return HTTPS_SERVER_ERROR_KEYGEN_SETUP_PK; } - // Actual key generation + // Actual key generation int resPkGen = mbedtls_rsa_gen_key( mbedtls_pk_rsa( key ), mbedtls_ctr_drbg_random, @@ -112,30 +184,20 @@ static int gen_key(SSLCert &certCtx, SSLKeySize keySize) { memset(output_buf, 0, 4096); // Write the key to the temporary buffer and determine its length - int resPkWrite = mbedtls_pk_write_key_der( &key, output_buf, 4096 ); + int resPkWrite = mbedtls_pk_write_key_pem( &key, output_buf, 4096 ); if (resPkWrite < 0) { delete[] output_buf; mbedtls_pk_free( &key ); return HTTPS_SERVER_ERROR_KEY_WRITE_PK; } - size_t pkLength = resPkWrite; - unsigned char *pkOffset = output_buf + sizeof(unsigned char) * 4096 - pkLength; - - // Copy the key into a new, fitting space on the heap - unsigned char * output_pk = new unsigned char[pkLength]; - if (output_pk == NULL) { - delete[] output_buf; - mbedtls_pk_free( &key ); - return HTTPS_SERVER_ERROR_KEY_WRITE_PK; - } - memcpy(output_pk, pkOffset, pkLength); // Clean up the temporary buffer and clear the key context - delete[] output_buf; mbedtls_pk_free( &key ); // Set the private key in the context - certCtx.setPK(output_pk, pkLength); + certCtx.setPK((char*)output_buf); + + delete[] output_buf; return 0; } @@ -180,12 +242,12 @@ static int parse_serial_decimal_format(unsigned char *obuf, size_t obufmax, /** * Function to generate the X509 certificate data for a private key - * + * * Writes certificate in certCtx * * Based on programs/x509/cert_write.c */ - + static int cert_write(SSLCert &certCtx, std::string dn, std::string validityFrom, std::string validityTo) { int funcRes = 0; int stepRes = 0; @@ -198,14 +260,18 @@ static int cert_write(SSLCert &certCtx, std::string dn, std::string validityFrom unsigned char *certOffset; unsigned char * output_buffer; size_t certLength; - const char *defaultSerial = "1"; - unsigned char serial[MBEDTLS_X509_RFC5280_MAX_SERIAL_LEN]; + const char *serial = "peer"; size_t serial_len; // Make a C-friendly version of the distinguished name char dn_cstr[dn.length()+1]; strcpy(dn_cstr, dn.c_str()); + std::string cn = get_cn(dn); + if (cn == "") { + return HTTPS_SERVER_ERROR_CERTGEN_CN; + } + // Initialize the entropy source mbedtls_entropy_init( &entropy ); @@ -218,7 +284,8 @@ static int cert_write(SSLCert &certCtx, std::string dn, std::string validityFrom } mbedtls_pk_init( &key ); - stepRes = mbedtls_pk_parse_key( &key, certCtx.getPKData(), certCtx.getPKLength(), NULL, 0, mbedtls_ctr_drbg_random, &ctr_drbg); + + stepRes = mbedtls_pk_parse_key( &key, (const unsigned char *)certCtx.getKeyPEM().c_str(), certCtx.getPKLength() + 1, NULL, 0, mbedtls_ctr_drbg_random, &ctr_drbg); if (stepRes != 0) { funcRes = HTTPS_SERVER_ERROR_CERTGEN_READKEY; goto error_after_key; @@ -260,15 +327,14 @@ static int cert_write(SSLCert &certCtx, std::string dn, std::string validityFrom goto error_after_cert; } - // Initialize the serial number - stepRes = parse_serial_decimal_format(serial, sizeof(serial), defaultSerial, &serial_len); - + stepRes = add_subject_alt_name( &crt, cn ); if (stepRes != 0) { - funcRes = HTTPS_SERVER_ERROR_CERTGEN_SERIAL; - goto error_after_cert_serial; + funcRes = HTTPS_SERVER_ERROR_CERTGEN_NAME; + goto error_after_cert; } - - stepRes = mbedtls_x509write_crt_set_serial_raw( &crt, serial, sizeof(serial) ); + + // Initialize the serial number + stepRes = mbedtls_x509write_crt_set_serial_raw( &crt, (unsigned char *)serial, strlen(serial) ); if (stepRes != 0) { funcRes = HTTPS_SERVER_ERROR_CERTGEN_SERIAL; goto error_after_cert_serial; @@ -282,26 +348,14 @@ static int cert_write(SSLCert &certCtx, std::string dn, std::string validityFrom } // Write the actual certificate - stepRes = mbedtls_x509write_crt_der(&crt, primary_buffer, 4096, mbedtls_ctr_drbg_random, &ctr_drbg ); + stepRes = mbedtls_x509write_crt_pem(&crt, primary_buffer, 4096, mbedtls_ctr_drbg_random, &ctr_drbg ); if (stepRes < 0) { funcRes = HTTPS_SERVER_ERROR_CERTGEN_WRITE; goto error_after_primary_buffer; } - // Create a matching buffer - certLength = stepRes; - certOffset = primary_buffer + sizeof(unsigned char) * 4096 - certLength; - - // Copy the cert into a new, fitting space on the heap - output_buffer = new unsigned char[certLength]; - if (output_buffer == NULL) { - funcRes = HTTPS_SERVER_ERROR_CERTGEN_OUT_OF_MEM; - goto error_after_primary_buffer; - } - memcpy(output_buffer, certOffset, certLength); - // Configure the cert in the context - certCtx.setCert(output_buffer, certLength); + certCtx.setCert((char*)primary_buffer); // Run through the cleanup process error_after_primary_buffer: @@ -334,7 +388,7 @@ int createSelfSignedCert(SSLCert &certCtx, SSLKeySize keySize, std::string dn, s int certRes = cert_write(certCtx, dn, validFrom, validUntil); if (certRes != 0) { // Cert writing failed, reset the pk and return failure code - certCtx.setPK(NULL, 0); + certCtx.setPK(""); return certRes; } diff --git a/src/util/SSLCert.hpp b/src/util/SSLCert.hpp index 8860d56..954cef8 100644 --- a/src/util/SSLCert.hpp +++ b/src/util/SSLCert.hpp @@ -1,3 +1,27 @@ +/* +MIT License + +Copyright (c) 2017 Frank Hessel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + #ifndef SRC_SSLCERT_HPP_ #define SRC_SSLCERT_HPP_ @@ -11,6 +35,8 @@ #include #include #include +#include +#include #define HTTPS_SERVER_ERROR_KEYGEN 0x0F #define HTTPS_SERVER_ERROR_KEYGEN_RNG 0x02 @@ -26,146 +52,38 @@ #define HTTPS_SERVER_ERROR_CERTGEN_NAME 0x17 #define HTTPS_SERVER_ERROR_CERTGEN_SERIAL 0x18 #define HTTPS_SERVER_ERROR_CERTGEN_VALIDITY 0x19 +#define HTTPS_SERVER_ERROR_CERTGEN_CN 0x1a -/** - * \brief Certificate and private key that can be passed to the HTTPSServer. - * - * **Converting PEM to DER Files** - * - * Certificate: - * ```bash - * openssl x509 -inform PEM -outform DER -in myCert.crt -out cert.der - * ``` - * - * Private Key: - * ```bash - * openssl rsa -inform PEM -outform DER -in myCert.key -out key.der - * ``` - * - * **Converting DER File to C Header** - * - * ```bash - * echo "#ifndef KEY_H_" > ./key.h - * echo "#define KEY_H_" >> ./key.h - * xxd -i key.der >> ./key.h - * echo "#endif" >> ./key.h - * ``` - */ class SSLCert { public: - /** - * \brief Creates a new SSLCert. - * - * The certificate and key data may be NULL (default values) if the certificate is meant - * to be passed to createSelfSignedCert(). - * - * Otherwise, the data must reside in a memory location that is not deleted until the server - * using the certificate is stopped. - * - * \param[in] certData The certificate data to use (DER format) - * \param[in] certLength The length of the certificate data - * \param[in] pkData The private key data to use (DER format) - * \param[in] pkLength The length of the private key - */ SSLCert( - unsigned char * certData = NULL, uint16_t certLength = 0, - unsigned char * pkData = NULL, - uint16_t pkLength = 0 + uint16_t pkLength = 0, + String keyPEM = "", + String certPEM = "" ); virtual ~SSLCert(); - - /** - * \brief Returns the length of the certificate in byte - */ uint16_t getCertLength(); - - /** - * \brief Returns the length of the private key in byte - */ uint16_t getPKLength(); - - /** - * \brief Returns the certificate data - */ - unsigned char * getCertData(); - - /** - * \brief Returns the private key data - */ - unsigned char * getPKData(); - - /** - * \brief Sets the private key in DER format - * - * The data has to reside in a place in memory that is not deleted as long as the - * server is running. - * - * See SSLCert() for some information on how to generate DER data. - * - * \param[in] _pkData The data of the private key - * \param[in] length The length of the private key - */ - void setPK(unsigned char * _pkData, uint16_t length); - - /** - * \brief Sets the certificate data in DER format - * - * The data has to reside in a place in memory that is not deleted as long as the - * server is running. - * - * See SSLCert for some information on how to generate DER data. - * - * \param[in] _certData The data of the certificate - * \param[in] length The length of the certificate - */ - void setCert(unsigned char * _certData, uint16_t length); - - /** - * \brief Clears the key buffers and deletes them. - */ + String getCertPEM(); + String getKeyPEM(); + void setPK(String _keyPEM); + void setCert(String _certPEM); void clear(); private: uint16_t _certLength; - unsigned char * _certData; uint16_t _pkLength; - unsigned char * _pkData; - + String _keyPEM; + String _certPEM; }; -/** - * \brief Defines the key size for key generation - * - * Not available if the `HTTPS_DISABLE_SELFSIGNING` compiler flag is set - */ enum SSLKeySize { - /** \brief RSA key with 1024 bit */ KEYSIZE_1024 = 1024, - /** \brief RSA key with 2048 bit */ KEYSIZE_2048 = 2048, - /** \brief RSA key with 4096 bit */ KEYSIZE_4096 = 4096 }; -/** - * \brief Creates a self-signed certificate on the ESP32 - * - * This function creates a new self-signed certificate for the given hostname on the heap. - * Make sure to clear() it before you delete it. - * - * The distinguished name (dn) parameter has to follow the x509 specifications. An example - * would be: - * CN=myesp.local,O=acme,C=US - * - * The strings validFrom and validUntil have to be formatted like this: - * "20190101000000", "20300101000000" - * - * This will take some time, so you should probably write the certificate data to non-volatile - * storage when you are done. - * - * Setting the `HTTPS_DISABLE_SELFSIGNING` compiler flag will remove this function from the library - */ int createSelfSignedCert(SSLCert &certCtx, SSLKeySize keySize, std::string dn, std::string validFrom = "20190101000000", std::string validUntil = "20300101000000"); #endif /* SRC_SSLCERT_HPP_ */ diff --git a/updater/partitions.csv b/updater/partitions.csv index 0f6f14e..b024b85 100644 --- a/updater/partitions.csv +++ b/updater/partitions.csv @@ -1 +1 @@ -# Espressif ESP32 Partition Table # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, app0, app, ota_0, 0x10000, 0x150000, \ No newline at end of file +# Espressif ESP32 Partition Table # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, app0, app, ota_0, 0x10000, 0x130000, \ No newline at end of file