Add Admin key

This commit is contained in:
iranl
2025-02-14 20:32:15 +01:00
parent b2fd9d9349
commit 92a6ca5b35
6 changed files with 131 additions and 19 deletions

View File

@@ -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.

View File

@@ -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"

View File

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

View File

@@ -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<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_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 =
{

View File

@@ -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, "") + " <br/><br/><a href=\"/\">Home page</a>", 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("<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("'; 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, "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, "");

View File

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