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);