Add Admin key
This commit is contained in:
@@ -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 secret key: Set to the Duo secret key
|
||||||
- Duo user: Set to the Duo user that you want to receive the push notification
|
- 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.
|
- 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 (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.
|
- 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.
|
- Duo Session validity (in seconds): Session validity to use with Duo authentication when the "Remember me" checkbox is disabled, default 3600 seconds.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
#define NUKI_HUB_VERSION "9.09"
|
#define NUKI_HUB_VERSION "9.09"
|
||||||
#define NUKI_HUB_VERSION_INT (uint32_t)909
|
#define NUKI_HUB_VERSION_INT (uint32_t)909
|
||||||
#define NUKI_HUB_BUILD "unknownbuildnr"
|
#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_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"
|
#define GITHUB_OTA_MANIFEST_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/manifest.json"
|
||||||
|
|||||||
@@ -477,6 +477,10 @@ void ImportExport::exportNukiHubJson(JsonDocument &json, bool redacted, bool pai
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if(strcmp(key, preference_admin_secret) == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if(!redacted) if(std::find(redactedPrefs.begin(), redactedPrefs.end(), key) != redactedPrefs.end())
|
if(!redacted) if(std::find(redactedPrefs.begin(), redactedPrefs.end(), key) != redactedPrefs.end())
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -97,6 +97,7 @@
|
|||||||
#define preference_config_from_mqtt (char*)"nhCntrlEnabled"
|
#define preference_config_from_mqtt (char*)"nhCntrlEnabled"
|
||||||
#define preference_totp_secret (char*)"totpsecret"
|
#define preference_totp_secret (char*)"totpsecret"
|
||||||
#define preference_bypass_secret (char*)"bypassecret"
|
#define preference_bypass_secret (char*)"bypassecret"
|
||||||
|
#define preference_admin_secret (char*)"adminsecret"
|
||||||
|
|
||||||
// CHANGE DOES NOT REQUIRE REBOOT TO TAKE EFFECT
|
// CHANGE DOES NOT REQUIRE REBOOT TO TAKE EFFECT
|
||||||
#define preference_find_best_rssi (char*)"nwbestrssi"
|
#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_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_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_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<char*> _redact =
|
std::vector<char*> _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_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_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<char*> _boolPrefs =
|
std::vector<char*> _boolPrefs =
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -531,10 +531,38 @@ void WebCfgServer::initialize()
|
|||||||
value = p->value();
|
value = p->value();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
int authReq = doAuthentication(request);
|
||||||
|
|
||||||
if (value != "status" && value != "login" && value != "duocheck" && value != "bypass")
|
|
||||||
{
|
|
||||||
switch (authReq)
|
switch (authReq)
|
||||||
{
|
{
|
||||||
case 0:
|
case 0:
|
||||||
@@ -569,14 +597,16 @@ void WebCfgServer::initialize()
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (value == "status" && authReq != 4)
|
else if (value == "status")
|
||||||
|
{
|
||||||
|
if (doAuthentication(request) != 4)
|
||||||
{
|
{
|
||||||
resp->setCode(200);
|
resp->setCode(200);
|
||||||
resp->setContentType("application/json");
|
resp->setContentType("application/json");
|
||||||
resp->setContent("{}");
|
resp->setContent("{}");
|
||||||
return resp->send();
|
return resp->send();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (value == "login")
|
if (value == "login")
|
||||||
{
|
{
|
||||||
return buildLoginHtml(request, resp);
|
return buildLoginHtml(request, resp);
|
||||||
@@ -592,7 +622,7 @@ void WebCfgServer::initialize()
|
|||||||
else if (value == "newbypass" && _newBypass)
|
else if (value == "newbypass" && _newBypass)
|
||||||
{
|
{
|
||||||
_newBypass = false;
|
_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, "") + " <br/><br/><a href=\"/\">Home page</a>", 3, false);
|
||||||
}
|
}
|
||||||
else if (value == "logout")
|
else if (value == "logout")
|
||||||
{
|
{
|
||||||
@@ -659,6 +689,11 @@ void WebCfgServer::initialize()
|
|||||||
return sendSettings(request, resp);
|
return sendSettings(request, resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(adminKeyValid)
|
||||||
|
{
|
||||||
|
return sendSettings(request, resp, true);
|
||||||
|
}
|
||||||
|
|
||||||
if(_importExport->_sessionsOpts[request->client()->localIP().toString() + "approve"])
|
if(_importExport->_sessionsOpts[request->client()->localIP().toString() + "approve"])
|
||||||
{
|
{
|
||||||
_importExport->_sessionsOpts[request->client()->localIP().toString() + "approve"] = false;
|
_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);
|
int authReq = doAuthentication(request);
|
||||||
|
|
||||||
@@ -1026,8 +1087,24 @@ void WebCfgServer::initialize()
|
|||||||
{
|
{
|
||||||
String message = "";
|
String message = "";
|
||||||
bool restart = processImport(request, resp, message);
|
bool restart = processImport(request, resp, message);
|
||||||
|
|
||||||
|
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);
|
return buildConfirmHtml(request, resp, message, 3, true);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -2303,7 +2380,7 @@ bool WebCfgServer::processTOTP(PsychicRequest *request, PsychicResponse* resp)
|
|||||||
}
|
}
|
||||||
|
|
||||||
#ifndef NUKI_HUB_UPDATER
|
#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;
|
JsonDocument json;
|
||||||
String jsonPretty;
|
String jsonPretty;
|
||||||
@@ -2352,7 +2429,10 @@ esp_err_t WebCfgServer::sendSettings(PsychicRequest *request, PsychicResponse* r
|
|||||||
serializeJsonPretty(json, jsonPretty);
|
serializeJsonPretty(json, jsonPretty);
|
||||||
char buf[26 + name.length()];
|
char buf[26 + name.length()];
|
||||||
snprintf(buf, sizeof(buf), "attachment; filename=\"%s\"", name.c_str());
|
snprintf(buf, sizeof(buf), "attachment; filename=\"%s\"", name.c_str());
|
||||||
|
if(!adminKey)
|
||||||
|
{
|
||||||
resp->addHeader("Content-Disposition", buf);
|
resp->addHeader("Content-Disposition", buf);
|
||||||
|
}
|
||||||
resp->setCode(200);
|
resp->setCode(200);
|
||||||
resp->setContentType("application/json");
|
resp->setContentType("application/json");
|
||||||
resp->setContent(jsonPretty.c_str());
|
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)
|
else if(key == "NUKIPIN" && _nuki != nullptr)
|
||||||
{
|
{
|
||||||
if(value == "#")
|
if(value == "#")
|
||||||
@@ -4843,20 +4936,27 @@ esp_err_t WebCfgServer::buildHtml(PsychicRequest *request, PsychicResponse* resp
|
|||||||
|
|
||||||
esp_err_t WebCfgServer::buildCredHtml(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];
|
char randomstr[17];
|
||||||
randomSeed(analogRead(0));
|
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++){
|
for(int i = 0;i < 16; i++){
|
||||||
randomstr[i] = chars[random(32)];
|
randomstr[i] = chars[random(32)];
|
||||||
}
|
}
|
||||||
randomstr[16] = '\0';
|
randomstr[16] = '\0';
|
||||||
char randomstr2[33];
|
char randomstr2[33];
|
||||||
randomSeed(analogRead(0));
|
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++){
|
for(int i = 0;i < 32; i++){
|
||||||
randomstr2[i] = chars2[random(36)];
|
randomstr2[i] = chars2[random(36)];
|
||||||
}
|
}
|
||||||
randomstr2[32] = '\0';
|
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");
|
PsychicStreamResponse response(resp, "text/html");
|
||||||
response.beginSend();
|
response.beginSend();
|
||||||
@@ -4891,6 +4991,10 @@ esp_err_t WebCfgServer::buildCredHtml(PsychicRequest *request, PsychicResponse*
|
|||||||
response.print("<tr id=\"bypassgentr\" ><td><input type=\"button\" id=\"bypassgen\" onclick=\"document.getElementsByName('CREDBYPASS')[0].type='text'; document.getElementsByName('CREDBYPASS')[0].value='");
|
response.print("<tr id=\"bypassgentr\" ><td><input type=\"button\" id=\"bypassgen\" onclick=\"document.getElementsByName('CREDBYPASS')[0].type='text'; document.getElementsByName('CREDBYPASS')[0].value='");
|
||||||
response.print(randomstr2);
|
response.print(randomstr2);
|
||||||
response.print("'; document.getElementById('bypassgentr').style.display='none';\" value=\"Generate new Bypass\"></td></tr>");
|
response.print("'; document.getElementById('bypassgentr').style.display='none';\" value=\"Generate new Bypass\"></td></tr>");
|
||||||
|
printInputField(&response, "CREDADMIN", "Admin key", "*", 32, "", true, false);
|
||||||
|
response.print("<tr id=\"admingentr\" ><td><input type=\"button\" id=\"admingen\" onclick=\"document.getElementsByName('CREDADMIN')[0].type='text'; document.getElementsByName('CREDADMIN')[0].value='");
|
||||||
|
response.print(randomstr3);
|
||||||
|
response.print("'; document.getElementById('admingentr').style.display='none';\" value=\"Generate new Admin key\"></td></tr>");
|
||||||
printInputField(&response, "CREDLFTM", "Session validity (in seconds)", _preferences->getInt(preference_cred_session_lifetime, 3600), 12, "");
|
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, "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, "");
|
printInputField(&response, "CREDDUOLFTM", "Duo Session validity (in seconds)", _preferences->getInt(preference_cred_session_lifetime_duo, 3600), 12, "");
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
#ifndef NUKI_HUB_UPDATER
|
#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 processArgs(PsychicRequest *request, PsychicResponse* resp, String& message);
|
||||||
bool processImport(PsychicRequest *request, PsychicResponse* resp, String& message);
|
bool processImport(PsychicRequest *request, PsychicResponse* resp, String& message);
|
||||||
void processGpioArgs(PsychicRequest *request, PsychicResponse* resp);
|
void processGpioArgs(PsychicRequest *request, PsychicResponse* resp);
|
||||||
|
|||||||
Reference in New Issue
Block a user