Bypass MFA
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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<int>() > 0)
|
||||
{
|
||||
_overwriteNukiHubConfigTS = espMillis() + (doc["exportHTTPS"].as<int>() * 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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<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_totp_secret, preference_bypass_secret
|
||||
};
|
||||
std::vector<char*> _boolPrefs =
|
||||
{
|
||||
|
||||
@@ -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<JsonVariant>()) || (type == 1 && _importExport->_duoSessions[cookie].is<JsonVariant>()) || (type == 2 && _importExport->_totpSessions[cookie].is<JsonVariant>()))
|
||||
if ((type == 0 && _httpSessions[cookie].is<JsonVariant>()) || (type == 1 && _importExport->_duoSessions[cookie].is<JsonVariant>()) || (type == 2 && _importExport->_totpSessions[cookie].is<JsonVariant>()) || (type == 3 && _importExport->_bypassSessions[cookie].is<JsonVariant>()))
|
||||
{
|
||||
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<signed long long>() > time_us) || (type == 1 && _importExport->_duoSessions[cookie].as<signed long long>() > time_us) || (type == 2 && _importExport->_totpSessions[cookie].as<signed long long>() > time_us))
|
||||
if ((type == 0 && _httpSessions[cookie].as<signed long long>() > time_us) || (type == 1 && _importExport->_duoSessions[cookie].as<signed long long>() > time_us) || (type == 2 && _importExport->_totpSessions[cookie].as<signed long long>() > time_us) || (type == 3 && _importExport->_bypassSessions[cookie].as<signed long long>() > 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 <a href=\"/get?page=bypass\">one-time bypass</a>", 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 <a href=\"/get?page=bypass\">one-time bypass</a>", 3, true);
|
||||
}
|
||||
|
||||
|
||||
PsychicStreamResponse response(resp, "text/html");
|
||||
response.beginSend();
|
||||
response.print("<html><head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
|
||||
@@ -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</a>", 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("<html><head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
|
||||
response.print("<style>form{border:3px solid #f1f1f1; max-width: 400px;}input[type=password],input[type=text]{width:100%;padding:12px 20px;margin:8px 0;display:inline-block;border:1px solid #ccc;box-sizing:border-box}button{background-color:#04aa6d;color:#fff;padding:14px 20px;margin:8px 0;border:none;cursor:pointer;width:100%}button:hover{opacity:.8}.container{padding:16px}span.password{float:right;padding-top:16px}@media screen and (max-width:300px){span.psw{display:block;float:none}}</style>");
|
||||
response.print("</head><body><center><h2>Nuki Hub One-time Bypass</h2>");
|
||||
response.print("<form action=\"/post?page=bypass\" method=\"post\">");
|
||||
response.print("<div class=\"container\">");
|
||||
response.print("<label for=\"bypass\"><b>Bypass code</b></label><input type=\"text\" placeholder=\"Enter bypass code\" name=\"bypass\">");
|
||||
response.print("<button type=\"submit\" ");
|
||||
response.print(">Login</button></div>");
|
||||
response.print("</form></center></body></html>");
|
||||
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 <a href=\"/get?page=bypass\">one-time bypass</a>", 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("<tr id=\"totpgentr\" ><td><input type=\"button\" id=\"totpgen\" onclick=\"document.getElementsByName('CREDTOTP')[0].type='text'; document.getElementsByName('CREDTOTP')[0].value='");
|
||||
response.print(randomstr);
|
||||
response.print("'; document.getElementById('totpgentr').style.display='none';\" value=\"Generate new TOTP key\"></td></tr>");
|
||||
printInputField(&response, "CREDBYPASS", "One-time MFA Bypass", "*", 32, "", true, false);
|
||||
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, "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, "");
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user