diff --git a/README.md b/README.md index 66798f5..020dcb6 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,10 @@ Feel free to join us on Discord: https://discord.gg/9nPq85bP4p - Nuki Smart Lock 1.0 - Nuki Smart Lock 2.0 - Nuki Smart Lock 3.0 -- Nuki Smart Lock 3.0 Pro (read FAQ below) -- Nuki Smart Lock 4.0 (read FAQ below) -- Nuki Smart Lock 4.0 Pro (read FAQ below) -- Nuki Smart Lock Ultra (read FAQ below) +- Nuki Smart Lock 3.0 Pro +- Nuki Smart Lock 4.0 +- Nuki Smart Lock 4.0 Pro +- Nuki Smart Lock Ultra - Nuki Opener - Nuki Keypad 1.0 - Nuki Keypad 2.0 @@ -78,7 +78,7 @@ Devices ranked best-to-worst: - ESP32 without PSRAM - ......
(Devices below will not support more Nuki Hub functions) -- ...... +- ...... - ESP32-C6 - ESP32-solo1 - ESP32-C3 @@ -147,7 +147,7 @@ To configure the connection to the MQTT broker, first connect your client device In a browser navigate to the IP address assigned to the ESP32 via DHCP (often found in the web interface of your internet router) or static IP.

Next click on "MQTT Configuration" and enter the address and port (usually 1883) of your MQTT broker and a username and password if required by your MQTT broker.

-The firmware supports SSL encryption for MQTT, however most people don't use this.
+The firmware supports SSL encryption for MQTT.
See the "[MQTT Encryption](#mqtt-encryption-optional)" section of this README. ## Pairing with a Nuki Lock (1.0-4.0) or Opener @@ -292,7 +292,9 @@ In a browser navigate to the IP address assigned to the ESP32. ### Access Level Configuration #### Nuki General Access Control -- Publish Nuki configuration information: Enable to publish information about the configuration of the connected Nuki device(s) through MQTT. +- Publish Nuki Hub configuration information: Publish Nuki Hub settings over MQTT, see "[Import and Export Nuki Hub settings over MQTT](#import-and-export-nuki-hub-settings-over-mqtt)" +- Modify Nuki Hub configuration over MQTT: Allow changing Nuki Hub settings using MQTT, see "[Import and Export Nuki Hub settings over MQTT](#import-and-export-nuki-hub-settings-over-mqtt)" +- Publish Nuki configuration information: Enable to publish information about the configuration of the connected Nuki device(s) through MQTT. Note: All of the following requires the Nuki security code / PIN to be set, see "[Nuki Lock PIN / Nuki Opener PIN](#nuki-lock-pin--nuki-opener-pin)" @@ -322,18 +324,21 @@ Note: All of the following requires the Nuki security code / PIN to be set, see - HTTP Authentication type: Select from Basic, Digest or Form based authentication. Digest authentication is more secure than Basic or Form based authentication, especially over unencrypted (HTTP) connections. Form based authentication works best with password managers. Note: Firefox seems to have issues with basic authentication. - Bypass authentication for reverse proxy with IP: IP for which authentication is bypassed. Use in conjunction with a reverse proxy server with separate authentication. - Duo Push authentication enabled: Enable to use Duo Push Multi Factor Authentication (MFA). See [Duo Push authentication](/DUOAUTH.md) for instructions on how to setup Duo Push authentication. -- Require Duo Push authentication for all sensitive Nuki Hub operations (changing/exporting settings): Enable to require Duo Push approval on all sensitive operations. -- Bypass Duo Push authentication by pressing the BOOT button during login: Enable to allow bypassing Duo Push authentication by pressing the BOOT button on the ESP during login. Note that this does not work on all ESP's (nor do all ESP's have a boot button to begin with). Test before relying on this function. -- Bypass Duo Push authentication by pulling GPIO High: Set to a GPIO pin to allow bypassing Duo Push authentication by pulling the GPIO high during login. -- Bypass Duo Push authentication by pulling GPIO Low: Set to a GPIO pin to allow bypassing Duo Push authentication by pulling the GPIO low during login. +- Require MFA (Duo/TOTP) authentication for all sensitive Nuki Hub operations (changing/exporting settings): Enable to require MFA approval on all sensitive operations. +- Bypass MFA (Duo/TOTP) authentication by pressing the BOOT button during login: Enable to allow bypassing MFA authentication by pressing the BOOT button on the ESP during login. Note that this does not work on all ESP's (nor do all ESP's have a boot button to begin with). Test before relying on this function. +- Bypass MFA (Duo/TOTP) authentication by pulling GPIO High: Set to a GPIO pin to allow bypassing MFA authentication by pulling the GPIO high during login. +- Bypass MFA (Duo/TOTP) authentication by pulling GPIO Low: Set to a GPIO pin to allow bypassing MFA authentication by pulling the GPIO low during login. - Duo API hostname: Set to the Duo API hostname - Duo integration key: Set to the Duo integration 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 +- 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. - 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. - Duo Session validity remember (in hours): Session validity to use with Duo authentication when the "Remember me" checkbox is enabled, default 720 hours. +- TOTP Session validity (in seconds): Session validity to use with TOTP authentication when the "Remember me" checkbox is disabled, default 3600 seconds. +- TOTP Session validity remember (in hours): Session validity to use with TOTP authentication when the "Remember me" checkbox is enabled, default 720 hours. #### Nuki Lock PIN / Nuki Opener PIN @@ -461,8 +466,11 @@ Note that the following options can break Nuki Hub and cause bootloops that will - [opener/]configuration/soundLevel: Set to the volume for sounds the Nuki Opener plays (0 = min; 255 = max) (Opener only). - [lock/opener/]configuration/action: Allows changing configuration settings of the Nuki Lock/Opener using a JSON formatted value. After receiving the action, the value is set to "--". See the "[Changing Nuki Lock/Opener Configuration](#changing-nuki-lockopener-configuration)" section of this README for possible actions/values - [lock/opener/]configuration/commandResult: Result of the last configuration change action as JSON data. See the "[Changing Nuki Lock/Opener Configuration](#changing-nuki-lockopener-configuration)" section of this README for possible values -- [lock/opener/]configuration/basicJson: The current basic configuration of the Nuki Lock/Opener as JSON data. See [Nuki Smart Lock API](https://developer.nuki.io/page/nuki-smart-lock-api-2/2/#heading--set-config) and [Nuki Opener API](https://developer.nuki.io/page/nuki-opener-api-1/7/#heading--set-config) for available settings. Please note: Longitude and Latitude of the Lock/Opener are not published to MQTT by design. These values can still be changed though. -- [lock/opener/]configuration/advancedJson: The current advanced configuration of the Nuki Lock/Opener as JSON data. See [Nuki Smart Lock API](https://developer.nuki.io/page/nuki-smart-lock-api-2/2/#heading--advanced-config) and [Nuki Opener API](https://developer.nuki.io/page/nuki-opener-api-1/7/#heading--advanced-config) for available settings. +- [lock/opener/]configuration/basicJson: The current basic configuration of the Nuki Lock/Opener as JSON data. See [Nuki Bluetooth API](https://developer.nuki.io/t/bluetooth-api/27) for available settings. Please note: Longitude and Latitude of the Lock/Opener are not published to MQTT by design. These values can still be changed though. +- [lock/opener/]configuration/advancedJson: The current advanced configuration of the Nuki Lock/Opener as JSON data. See [Nuki Bluetooth API](https://developer.nuki.io/t/bluetooth-api/27) for available settings. +- configuration/action: Allows importing and exporting configuration settings of Nuki Hub using a JSON formatted value. After receiving the action, the value is set to "--", see "[Import and Export Nuki Hub settings over MQTT](#import-and-export-nuki-hub-settings-over-mqtt)" +- configuration/commandResult: Result of the last Nuki Hub configuration import action as JSON data, see "[Import and Export Nuki Hub settings over MQTT](#import-and-export-nuki-hub-settings-over-mqtt)" +- configuration/json: Topic where you can export Nuki Hub configuration as JSON data to, see "[Import and Export Nuki Hub settings over MQTT](#import-and-export-nuki-hub-settings-over-mqtt)" ### Query @@ -512,12 +520,55 @@ Note that the following options can break Nuki Hub and cause bootloops that will - maintenance/wifiRssi: The Wi-Fi signal strength of the Wi-Fi Access Point as measured by the ESP32 and expressed by the RSSI Value in dBm. - maintenance/log: If "Enable MQTT logging" is enabled in the web interface, this topic will be filled with debug log information. - maintenance/freeHeap: Only available when debug mode is enabled. Set to the current size of free heap memory in bytes. -- maintenance/restartReasonNukiHub: Set to the last reason Nuki Hub was restarted. See [RestartReason.h](/RestartReason.h) for possible values -- maintenance/restartReasonNukiEsp: Set to the last reason the ESP was restarted. See [RestartReason.h](/RestartReason.h) for possible values +- maintenance/restartReasonNukiHub: Set to the last reason Nuki Hub was restarted. See [RestartReason.h](/src/RestartReason.h) for possible values +- maintenance/restartReasonNukiEsp: Set to the last reason the ESP was restarted. See [RestartReason.h](/src/RestartReason.h) for possible values + +## Import and Export Nuki Hub settings over MQTT (BETA) + +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. +- 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 + +To allow Nuki Hub to export configuration over MQTT first enable "Publish Nuki Hub configuration information" in "Access Level Configuration" and save the configuration. +You can export Nuki Hub settings in JSON format by sending the following JSON values to the `nukihub/configuration/action` topic: +- Export Nuki Hub settings without redacted settings and without pairing settings: `{"exportNH": 0}` + +NOTE: The following settings can only be exported if you have setup a secure MQTT connection (MQTT over SSL) + +- Export Nuki Hub settings with redacted settings and without pairing settings: `{"exportNH": 0, "redacted": 1}` +- Export Nuki Hub settings without redacted settings and with pairing settings: `{"exportNH": 0, "pairing": 1}` +- Export Nuki Hub settings with redacted settings and pairing settings: `{"exportNH": 0, "redacted": 1, "pairing": 1}` +- Export Nuki Hub MQTTS certificates and key: `{"exportMQTTS": 0}` +- Export Nuki Hub HTTPS certificate and key: `{"exportHTTPS": 0}` + +The exported values will be available in the `nukihub/configuration/json` topic in JSON format. +A general explanation of the exported values can be found in the [PreferencesKeys.h](/src/PreferencesKeys.h) file + +If you set the value of `exportNH`/`exportMQTTS`/`exportHTTPS` to an integer value > 0 the `nukihub/configuration/json` will be cleared after the given amount of seconds (e.g. `{"exportMQTTS": 30}` will clear the JSON topic after 30 seconds) + +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 exported. + +### Import/Change Nuki Hub settings over MQTT + +To allow Nuki Hub to import/change configuration over MQTT first enable "Modify Nuki Hub configuration over MQTT" in "Access Level Configuration" and save the configuration. +You can import Nuki Hub settings in JSON format by sending the desired JSON values to be changed to the `nukihub/configuration/action` topic. +The expected values and format is the same as the JSON files/values that can be exported over MQTT or through the Web Configurator. + +The result of the import will be available in the `nukihub/configuration/commandResult` topic in JSON format. +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. +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 ## Changing Nuki Lock/Opener Configuration -To change Nuki Lock/Opener settings set the `configuration/action` topic to a JSON formatted value with any of the following settings. Multiple settings can be changed at once. See [Nuki Smart Lock API Basic Config](https://developer.nuki.io/page/nuki-smart-lock-api-2/2/#heading--set-config), [Nuki Smart Lock API Advanced Config](https://developer.nuki.io/page/nuki-smart-lock-api-2/2/#heading--advanced-config), [Nuki Opener API Basic Config](https://developer.nuki.io/page/nuki-opener-api-1/7/#heading--set-config) and [Nuki Opener API Advanced Config](https://developer.nuki.io/page/nuki-opener-api-1/7/#heading--advanced-config) for more information on the available settings.
+To change Nuki Lock/Opener settings set the `configuration/action` topic to a JSON formatted value with any of the following settings. Multiple settings can be changed at once. See [Nuki Bluetooh API](https://developer.nuki.io/t/bluetooth-api/27) for more information on the available settings.
Changing settings has to enabled first in the configuration portal. Check the settings you want to be able to change under "Nuki Lock/Opener Config Control" in "Access Level Configuration" and save the configuration. ### Nuki Lock Configuration @@ -539,7 +590,7 @@ Changing settings has to enabled first in the configuration portal. Check the se | fobAction3 | The desired action, if a Nuki Fob is pressed three times. | "No Action", "Unlock", "Lock", "Lock n Go", "Intelligent" |`{ "fobAction3": "Unlock" }` | | singleLock | Whether only a single lock or double lock should be performed | 0 = double lock, 1 = single lock |`{ "singleLock": "0" }` | | advertisingMode | The desired advertising mode. | "Automatic", "Normal", "Slow", "Slowest" |`{ "advertisingMode": "Normal" }` | -| timeZone | The current timezone or "None" if timezones are not supported | "None" or one of the timezones from [Nuki Timezones](https://developer.nuki.io/page/nuki-smart-lock-api-2/2/#heading--list-of-timezone-ids) |`{ "timeZone": "Europe/Berlin" }` | +| timeZone | The current timezone or "None" if timezones are not supported | "None" or one of the timezones from [Nuki Bluetooh API](https://developer.nuki.io/t/bluetooth-api/27) |`{ "timeZone": "Europe/Berlin" }` | | unlockedPositionOffsetDegrees | Offset that alters the unlocked position in degrees. | Integer between -90 and 180 |`{ "unlockedPositionOffsetDegrees": "-90" }` | | lockedPositionOffsetDegrees | Offset that alters the locked position in degrees. | Integer between -180 and 90 |`{ "lockedPositionOffsetDegrees": "80" }` | | singleLockedPositionOffsetDegrees | Offset that alters the single locked position in degrees. | Integer between -180 and 180 |`{ "singleLockedPositionOffsetDegrees": "120" }` | @@ -583,7 +634,7 @@ Changing settings has to enabled first in the configuration portal. Check the se | fobAction3 | The desired action, if a Nuki Fob is pressed three times. | "No Action", "Toggle RTO", "Activate RTO", "Deactivate RTO", "Open", "Ring" |`{ "fobAction3": "Ring" }` | | operatingMode | The desired operating mode | "Generic door opener", "Analogue intercom", "Digital intercom", "Siedle", "TCS", "Bticino", "Siedle HTS", "STR", "Ritto", "Fermax", "Comelit", "Urmet BiBus", "Urmet 2Voice", "Golmar", "SKS", "Spare" |`{ "operatingMode": "TCS" }` | | advertisingMode | The desired advertising mode. | "Automatic", "Normal", "Slow", "Slowest" |`{ "advertisingMode": "Normal" }` | -| timeZone | The current timezone or "None" if timezones are not supported | "None" or one of the timezones from [Nuki Timezones](https://developer.nuki.io/page/nuki-smart-lock-api-2/2/#heading--list-of-timezone-ids) |`{ "timeZone": "Europe/Berlin" }` | +| timeZone | The current timezone or "None" if timezones are not supported | "None" or one of the timezones from [Nuki Bluetooh API](https://developer.nuki.io/t/bluetooth-api/27) |`{ "timeZone": "Europe/Berlin" }` | | intercomID | Database ID of the connected intercom. | Integer |`{ "intercomID": "1" }` | | busModeSwitch | Method to switch between data and analogue mode | 0 = none, 1 =vshort circuit |`{ "busModeSwitch": "0" }` | | shortCircuitDuration | Duration of the short circuit for BUS mode switching in ms. | Integer |`{ "shortCircuitDuration": "250" }` | @@ -913,7 +964,7 @@ This unfortunately means that older versions of Home Assistant are not supported ### Nuki Hub in bridge mode doesn't work when Thread or Wi-Fi on a Nuki Smartlock (3.0 Pro / 4.0 / 4.0 Pro) is turned on. -According to Nuki this is by design and part of the specification of the Pro lock.
+According to Nuki this is by design and part of the specification of Wi-Fi/Thread enabled locks.
You can use either the built-in Wi-Fi/Thread or a Bridge (which Nuki Hub registers as), using both at the same time is not supported.
Or you can use Nuki Hub in Hybrid mode using Wi-Fi or Thread, see [hybrid mode](/HYBRID.md)
diff --git a/clion/CMakeLists.txt b/clion/CMakeLists.txt index d7905d6..60ad55d 100644 --- a/clion/CMakeLists.txt +++ b/clion/CMakeLists.txt @@ -52,6 +52,7 @@ set(SRCFILES ../src/util/NetworkDeviceInstantiator.cpp ../src/HomeAssistantDiscovery.cpp ../src/NukiOfficial.cpp + ../src/ImportExport.cpp ../src/NukiPublisher.cpp ../src/EspMillis.h ../src/enums/NukiPinState.h @@ -70,7 +71,12 @@ file(GLOB_RECURSE SRCFILESREC lib/espMqttClient/src/Packets/*.cpp lib/espMqttClient/src/Packets/*.h lib/espMqttClient/src/Transport/*.cpp - lib/espMqttClient/src/Transport/*.h + lib/espMqttClient/src/Transport/*.h + lib/Arduino-TOTP-RFC6238-generator/src/*.hpp + lib/Arduino-Base32-Decode/src/*.cpp + lib/Arduino-Base32-Decode/src/*.h + lib/DuoAuthLibrary/src/*.cpp + lib/DuoAuthLibrary/src/*.h ) include_directories( @@ -80,6 +86,9 @@ include_directories( ../lib/MqttLogger/src ../lib/nuki_ble/src ../lib/PsychicHttp/src + ../lib/Arduino-TOTP-RFC6238-generator/src + ../lib/Arduino-Base32-Decode/src + ../lib/DuoAuthLibrary/src ../src ) diff --git a/lib/Arduino-Base32-Decode/LICENSE.txt b/lib/Arduino-Base32-Decode/LICENSE.txt new file mode 100644 index 0000000..bbfaf21 --- /dev/null +++ b/lib/Arduino-Base32-Decode/LICENSE.txt @@ -0,0 +1,21 @@ +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. + +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +This cose is based on MacOSX code from taken from https://github.com/ekscrypto/Base32/ +which itself is under the Unlicense Mhttps://unlicense.org>. + +This code was based on code by Dave Poirier date 12-06-14 who released this +as "Public Domain" + +The test vectors where taken from the https://www.rfc-editor.org/rfc/rfc4648 +and are Copyright (C) The Internet Society (2006). diff --git a/lib/Arduino-Base32-Decode/README.md b/lib/Arduino-Base32-Decode/README.md new file mode 100644 index 0000000..fd1b431 --- /dev/null +++ b/lib/Arduino-Base32-Decode/README.md @@ -0,0 +1,60 @@ +# Arduino libary for Base32 (rfc4648) decoding. + + +## Traditional C interface + +### base32decode : decode a \0 terminated base32 encoded string. + + int base32decode( + const char * encoded, + unsigned char * decodedBytes, + size_t maxbuf + ); + +#### inputs: + + encoded \0 terminated char buffer with encoded string + decodedBytes outputbuffer (or NULL) + maxbuff size of the output buffer + +#### outputs: + + returns the decoded byte array in decodedBytes and the length. Or if + decodedBytes==NULL, will just return the length needed; regardless of + the value of maxbuff. + + If the size of maxbuff allows it - a terminating \0 is added (but not + including in the length returned) - as very often the decoded data + itself is actually again a string. + + +## C++/INO/Arduino# tring interface + +### base32decodeToString - decode a String into a decoded String + + int base32decodeToString(String encoded, String &decoded); + +#### inputs: + encoded String with the encoded base32 value + &decoded returned string (if any) + +#### outputs: +Will return the length of the decoded string or a negative + value on error. + + +# Example + +Typical use: + + String in = "IFZGI5LJNZXSAUTVNRSXU==="; + String out; + + int r = base32decodeToString(in, out); + if (r < 0) { + Serial.println("Could not decode the string"); + return; + }; + + Serial.print("Decoded: "); + Serial.println(out); diff --git a/lib/Arduino-Base32-Decode/examples/Base32-decode/Base32-decode.ino b/lib/Arduino-Base32-Decode/examples/Base32-decode/Base32-decode.ino new file mode 100644 index 0000000..895f76a --- /dev/null +++ b/lib/Arduino-Base32-Decode/examples/Base32-decode/Base32-decode.ino @@ -0,0 +1,127 @@ + +#include +#include +#include +#include + +#include + +void example1() { + String in, out; + in = "IFZGI5LJNZXSAUTVNRSXU==="; + + int r = base32decodeToString(in, out); + if (r < 0) { + Serial.println("Could not decode string"); + return; + } + Serial.print("Decoded: "); + Serial.println(out); +} + + +void example2() { + const char * in = "IFZGI5LJNZXSAUTVNRSXU==="; + size_t maxout = strlen(in); // we know that the encoded string is as long, or shorter than the decoded string. + char out[maxout]; + + int r = base32decode(in, (unsigned char*) out, maxout); + if (r < 0) { + Serial.println("Could not decode string"); + return; + } + Serial.print("Decoded: "); + Serial.println(out); +} + + +void example3() { + const char * in = "IFZGI5LJNZXSAUTVNRSXU==="; + + // figure out the lenght we're going to get + // + int maxout = base32decode(in, NULL, 0); + + // keep room for an terminating \0 + maxout += 1; + + // declare just enough memory + char out[maxout]; + + int r = base32decode(in, (unsigned char*) out, maxout); + if (r < 0) { + Serial.println("Could not decode string"); + return; + } + Serial.print("Decoded: "); + Serial.println(out); +} + + +// RFC 4648 test vectors - https://www.rfc-editor.org/rfc/rfc4648 section 10 + +void runalltests() { + typedef struct testvector_t { + char *out; + char *in; + } testvector_t; + testvector_t testvectors[] = { + // normal with padding + { (char *) "", (char *)""}, + { (char *) "f", (char *)"MY======"}, + { (char *) "fo", (char *)"MZXQ===="}, + { (char *) "foo", (char *)"MZXW6==="}, + { (char *) "foob", (char *)"MZXW6YQ="}, + { (char *) "fooba", (char *)"MZXW6YTB"}, + { (char *) "foobar", (char *)"MZXW6YTBOI======"}, + // careless without the normal padding (but happens a lot) + { (char *) "f", (char *)"MY"}, + { (char *) "fo", (char *) "MZXQ"}, + { (char *) "foo", (char *)"MZXW6"}, + { (char *) "foob", (char *)"MZXW6YQ"}, + { (char *) "fooba", (char *)"MZXW6YTB"}, + { (char *)"foobar", (char *)"MZXW6YTBOI"}, + // wrong case. + { (char *) "f", (char *)"my"}, + { (char *) "fo", (char *)"mzxq"}, + { (char *) "foo", (char *)"mzxw6"}, + { (char *) "foob", (char *)"mzxw6yq"}, + { (char *) "fooba", (char *)"mzxw6ytb"}, + { (char *)"foobar", (char *)"mzxw6ytboi"}, + // acidental crufft (not in the RFC) + { (char *)"", (char *)" "}, + { (char *)"", (char *)" "}, + { (char *)"foobar", (char *)" mzx w6 yt b o i"}, + { (char *)"foobar", (char *)" m zx w6 yt b o i"}, + { (char *)"foobar", (char *)"mzx\tw6ytboi"}, + { (char *)"foobar", (char *)"mzxw6\nytboi"}, + { (char *)"foobar", (char *)"mzxw6 ytb oi "} + }; + for (int i = 0; i < sizeof(testvectors) / sizeof(testvector_t); i++) { + unsigned char buff[1024]; + int ret = base32decode(testvectors[i].in, buff, sizeof(buff)); + Serial.printf("test %d: %s -> '%s' == '%s' (size %d)\n", i + 1, testvectors[i].in, buff, testvectors[i].out, ret); + + assert(ret == strlen(testvectors[i].out)); + assert(strcmp((char *)buff, testvectors[i].out) == 0); + + printf("test: %d ok\n\n", i + 1); + } + Serial.println("==\nAll test passed\n\n"); +} + +void setup() { + Serial.begin(119200); + while (!Serial) delay(10); + Serial.println("\n\n" __FILE__ " started"); + + // runalltests(); + example1(); + example2(); + example3(); +} + +void loop() { + delay(10000); +} + diff --git a/lib/Arduino-Base32-Decode/library.properties b/lib/Arduino-Base32-Decode/library.properties new file mode 100644 index 0000000..e5d3b94 --- /dev/null +++ b/lib/Arduino-Base32-Decode/library.properties @@ -0,0 +1,12 @@ +name=Base32-Decode +version=1.0.1 +author=Dirk-Willem van Gulik et.al. +license=ASLv2, Unlicense, Public-Domain +maintainer=Dirk-Willem van Gulik +sentence=Base32 decoder; able to handle both binary and string encoded data. +paragraph=RFC4648 Base32 decoder; handles both binary and string encoded data. With a char/unsigned-char interface as well as a String interface. +category=Communication +url=https://github.com/dirkx/Arduino-Base32-Decode +architectures=* +depends= +includes=Base32-Decode.h diff --git a/lib/Arduino-Base32-Decode/src/Base32-Decode.cpp b/lib/Arduino-Base32-Decode/src/Base32-Decode.cpp new file mode 100644 index 0000000..ef64219 --- /dev/null +++ b/lib/Arduino-Base32-Decode/src/Base32-Decode.cpp @@ -0,0 +1,174 @@ +#include +#include +#include + +#include "Base32-Decode.h" + +// Code and table taken from https://github.com/ekscrypto/Base32/ +// under the Unlincense https://unlicense.org> and also by +// Dave Poirier on 12-06-14 who released this as "Public Domain" +// + +int base32decode(const char * encoded, unsigned char * decoded, size_t maxbuf) { +#define __ 255 + static char decodingTable[256] = { + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 0x00 - 0x0F + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 0x10 - 0x1F + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 0x20 - 0x2F + __, __, 26, 27, 28, 29, 30, 31, __, __, __, __, __, 0, __, __, // 0x30 - 0x3F + __, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 0x40 - 0x4F + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, __, __, __, __, __, // 0x50 - 0x5F + __, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 0x60 - 0x6F + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, __, __, __, __, __, // 0x70 - 0x7F + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 0x80 - 0x8F + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 0x90 - 0x9F + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 0xA0 - 0xAF + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 0xB0 - 0xBF + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 0xC0 - 0xCF + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 0xD0 - 0xDF + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 0xE0 - 0xEF + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 0xF0 - 0xFF + }; + + size_t encodedLength = strlen(encoded); + + // strip any trailing padding. + while (encodedLength && encoded[encodedLength - 1] == '=') encodedLength--; + + int blocks = (encodedLength + 7) >> 3; + int expectedDataLength = blocks * 5; + + if (decoded == NULL) + return expectedDataLength + 1; // for terminating 0 + + if (maxbuf <= expectedDataLength) + return -1; + + unsigned char encodedByte1, encodedByte2, encodedByte3, encodedByte4; + unsigned char encodedByte5, encodedByte6, encodedByte7, encodedByte8; + + unsigned int encodedToProcess = encodedLength; + unsigned int encodedBaseIndex = 0; + unsigned int decodedBaseIndex = 0; + + unsigned char block[8] = {0, 0, 0, 0, 0, 0, 0, 0}; + unsigned int blockIndex = 0; + unsigned char c; + + while ( encodedToProcess-- >= 1 ) { + c = encoded[encodedBaseIndex++]; + if ( c == '=' ) break; // padding... + + c = decodingTable[c]; + if ( c == __ ) continue; // skip anything we do not know. + + block[blockIndex++] = c; + if ( blockIndex == 8 ) { + encodedByte1 = block[0]; + encodedByte2 = block[1]; + encodedByte3 = block[2]; + encodedByte4 = block[3]; + encodedByte5 = block[4]; + encodedByte6 = block[5]; + encodedByte7 = block[6]; + encodedByte8 = block[7]; + decoded[decodedBaseIndex + 0] = ((encodedByte1 << 3) & 0xF8) | ((encodedByte2 >> 2) & 0x07); + decoded[decodedBaseIndex + 1] = ((encodedByte2 << 6) & 0xC0) | ((encodedByte3 << 1) & 0x3E) | ((encodedByte4 >> 4) & 0x01); + decoded[decodedBaseIndex + 2] = ((encodedByte4 << 4) & 0xF0) | ((encodedByte5 >> 1) & 0x0F); + decoded[decodedBaseIndex + 3] = ((encodedByte5 << 7) & 0x80) | ((encodedByte6 << 2) & 0x7C) | ((encodedByte7 >> 3) & 0x03); + decoded[decodedBaseIndex + 4] = ((encodedByte7 << 5) & 0xE0) | (encodedByte8 & 0x1F); + decodedBaseIndex += 5; + blockIndex = 0; + } + } + encodedByte7 = 0; + encodedByte6 = 0; + encodedByte5 = 0; + encodedByte4 = 0; + encodedByte3 = 0; + encodedByte2 = 0; + + if (blockIndex > 6) + encodedByte7 = block[6]; + if (blockIndex > 5) + encodedByte6 = block[5]; + if (blockIndex > 4) + encodedByte5 = block[4]; + if (blockIndex > 3) + encodedByte4 = block[3]; + if (blockIndex > 2) + encodedByte3 = block[2]; + if (blockIndex > 1) + encodedByte2 = block[1]; + if (blockIndex > 0) { + encodedByte1 = block[0]; + decoded[decodedBaseIndex + 0] = ((encodedByte1 << 3) & 0xF8) | ((encodedByte2 >> 2) & 0x07); + decoded[decodedBaseIndex + 1] = ((encodedByte2 << 6) & 0xC0) | ((encodedByte3 << 1) & 0x3E) | ((encodedByte4 >> 4) & 0x01); + decoded[decodedBaseIndex + 2] = ((encodedByte4 << 4) & 0xF0) | ((encodedByte5 >> 1) & 0x0F); + decoded[decodedBaseIndex + 3] = ((encodedByte5 << 7) & 0x80) | ((encodedByte6 << 2) & 0x7C) | ((encodedByte7 >> 3) & 0x03); + decoded[decodedBaseIndex + 4] = ((encodedByte7 << 5) & 0xE0); + }; + + static int paddingAdjustment[8] = {0, 1, 1, 1, 2, 3, 3, 4}; + decodedBaseIndex += paddingAdjustment[blockIndex]; + + // ensure null terminated if there is space. + if (decodedBaseIndex < maxbuf) + decoded[decodedBaseIndex] = 0; + return decodedBaseIndex; +} + +int base32decodeToString(String encoded, String &decoded) { + size_t maxlen = encoded.length() * 5 / 8 + 1; + char * buff = new char[maxlen]; + int ret = base32decode(encoded.c_str(), (unsigned char*) buff, maxlen); + if (ret >= 0) + decoded = String(buff); + return ret; +} + +#ifdef TEST_BASE32 +#include +#include + +int main(int a, char **b) { + typedef struct testvector_t { + char *out; + char *in; + } testvector_t; + // RFC 4648 test vectors - https://www.rfc-editor.org/rfc/rfc4648 section 10 + testvector_t testvectors[] = { + // normal with padding + {"", ""}, + {"f", "MY======"}, + { "fo", "MZXQ===="}, + {"foo", "MZXW6==="}, + {"foob", "MZXW6YQ="}, + {"fooba", "MZXW6YTB"}, + {"foobar", "MZXW6YTBOI======"}, + // careless without + {"f", "MY"}, + {"fo", "MZXQ"}, + {"foo", "MZXW6"}, + {"foob", "MZXW6YQ"}, + {"fooba", "MZXW6YTB"}, + { "foobar", "MZXW6YTBOI"}, + // wrong case. + {"f", "my"}, + {"fo", "mzxq"}, + {"foo", "mzxw6"}, + {"foob", "mzxw6yq"}, + {"fooba", "mzxw6ytb"}, + { "foobar", "mzxw6ytboi"} + }; + for (int i = 0; i < sizeof(testvectors) / sizeof(testvector_t); i++) { + char buff[1024]; + int ret = base32decode(testvectors[i].in, buff, sizeof(buff)); + printf("test %d: %s -> '%s' == '%s' (size %d)\n", i + 1, testvectors[i].in, buff, testvectors[i].out, ret); + assert(ret == strlen(testvectors[i].out)); + assert(strcmp(buff, testvectors[i].out) == 0); + printf("test %d ok\n", i + 1); + } + return 0; +} +#endif diff --git a/lib/Arduino-Base32-Decode/src/Base32-Decode.h b/lib/Arduino-Base32-Decode/src/Base32-Decode.h new file mode 100644 index 0000000..bcd8cb0 --- /dev/null +++ b/lib/Arduino-Base32-Decode/src/Base32-Decode.h @@ -0,0 +1,30 @@ +#ifndef BASE32_DECODE_H +#define BASE32_DECODE_H + +/* base32decode - decode a \0 terminated base32 encoded string. + * + * encoded \0 terminated char buffer with encoded string + * decodedBytes outputbuffer (or NULL) + * maxbuff size of the output buffer + * + * returns the decoded byte array in decodedBytes and the length. Or if + * decodedBytes==NULL, will just return the length needed; regardless of + * the value of maxbuff. + * + * If the size of maxbuff allows it - a terminating \0 is added (but not + * including in the length returned) - as very often the decoded data + * itself is actually again a string. + */ +extern int base32decode(const char * encoded, unsigned char * decodedBytes, size_t maxbuf); + +/* base32decodeToString - decode a String into a decoded String + * + * encoded String with the encoded base32 value + * &decoded returned string (if any) + * + * Will return the length of the decoded string or a negative + * value on error. + */ + +extern int base32decodeToString(String encoded, String &decoded); +#endif diff --git a/lib/Arduino-TOTP-RFC6238-generator/LICENSE.txt b/lib/Arduino-TOTP-RFC6238-generator/LICENSE.txt new file mode 100644 index 0000000..a2da5d8 --- /dev/null +++ b/lib/Arduino-TOTP-RFC6238-generator/LICENSE.txt @@ -0,0 +1,12 @@ +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. + +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/lib/Arduino-TOTP-RFC6238-generator/README.md b/lib/Arduino-TOTP-RFC6238-generator/README.md new file mode 100644 index 0000000..b073cfb --- /dev/null +++ b/lib/Arduino-TOTP-RFC6238-generator/README.md @@ -0,0 +1,24 @@ +# Arduino libary for TOTP generation + +Example use: + + // Seed value - as per the QR code; which is in fact a base32 encoded + // byte array (i.e. it is binary). + // + const char * seed = "ORUGKU3FMNZGK5CTMVSWI==="; + + // Example of the same thing - but as usually formatted when shown + // as the 'alternative text to enter' + // + // const char * seed = "ORU GKU 3FM NZG K5C TMV SWI"; + + String * otp = TOTP::currentOTP(seed); + + Serial.print(ctime(&t)); + Serial.print(" TOTP at this time is: "); + Serial.println(*otp); + Serial.println(); + +This assumes a normal RFC compliant TOTP. It is possible that the Qr code provides +different values for the interval (default is 30 seconds), epoch or the hash (sha1). +These can be passwd as optional arguments. diff --git a/lib/Arduino-TOTP-RFC6238-generator/examples/RFC6238-generator/RFC6238-generator.ino b/lib/Arduino-TOTP-RFC6238-generator/examples/RFC6238-generator/RFC6238-generator.ino new file mode 100644 index 0000000..b5191b1 --- /dev/null +++ b/lib/Arduino-TOTP-RFC6238-generator/examples/RFC6238-generator/RFC6238-generator.ino @@ -0,0 +1,100 @@ +/* Copyright (c) Dirk-Willem van Gulik, All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include + +#include + +#ifndef WIFI_NETWORK +#define WIFI_NETWORK "mySecretWiFiPassword" +#warning "You propably want to change this line!" +#endif + +#ifndef WIFI_PASSWD +#define WIFI_PASSWD "mySecretWiFiPassword" +#warning "You propably want to change this line!" +#endif + +#ifndef NTP_SERVER +#define NTP_SERVER "nl.pool.ntp.org" +#warning "You MUST set an appropriate ntp pool - see http://ntp.org" +#endif + +#ifndef NTP_DEFAULT_TZ +#define NTP_DEFAULT_TZ "CET-1CEST,M3.5.0,M10.5.0/3" +#endif + +const char* ssid = WIFI_NETWORK; +const char* password = WIFI_PASSWD; + +void setup() { + Serial.begin(115200); + while (!Serial) delay(10); + + Serial.println("\n\n" __FILE__ "Started"); + + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + if (WiFi.waitForConnectResult() != WL_CONNECTED) { + Serial.println("WiFi Connect Failed! Rebooting..."); + delay(1000); + ESP.restart(); + } + + // we need a reasonable accurate time for TOTP to work. + // + configTzTime(NTP_DEFAULT_TZ, NTP_SERVER); +} + + +void loop() { + // Print the one time passcode every seconds; + // + static unsigned long lst = millis(); + if (millis() - lst < 1000) + return; + lst = millis(); + + time_t t = time(NULL); + if (t < 1000000) { + Serial.println("Not having a stable time yet.. TOTP is not going to work."); + return; + }; + + // Seed value - as per the QR code; which is in fact a base32 encoded + // byte array (i.e. it is binary). + // + const char * seed = "ORUGKU3FMNZGK5CTMVSWI==="; + + // Example of the same thing - but as usually formatted when shown + // as the 'alternative text to enter' + // + // const char * seed = "ORU GKU 3FM NZG K5C TMV SWI"; + + String * otp = TOTP::currentOTP(seed); + + Serial.print(ctime(&t)); + Serial.print(" TOTP at this time is: "); + Serial.println(*otp); + Serial.println(); + + delete otp; +} + diff --git a/lib/Arduino-TOTP-RFC6238-generator/library.properties b/lib/Arduino-TOTP-RFC6238-generator/library.properties new file mode 100644 index 0000000..b846ae1 --- /dev/null +++ b/lib/Arduino-TOTP-RFC6238-generator/library.properties @@ -0,0 +1,12 @@ +name=TOTP-generator +version=1.0.1 +author=Dirk-Willem van Gulik +license=ASLv2 +maintainer=Dirk-Willem van Gulik +sentence=Time based one time password generator; complies with RFC 6238 +paragraph=RFC 6238 time based one time password generator. It will accept the base32 encoded seeds (and all the other parameters typically found in the Qr codes). +category=Communication +url=https://github.com/dirkx/Arduino-TOTP-RFC6238-generator +architectures=* +depends=Base32-Decode +includes=TOTP-generator.hpp diff --git a/lib/Arduino-TOTP-RFC6238-generator/src/TOTP-RC6236-generator.hpp b/lib/Arduino-TOTP-RFC6238-generator/src/TOTP-RC6236-generator.hpp new file mode 100644 index 0000000..f652fa1 --- /dev/null +++ b/lib/Arduino-TOTP-RFC6238-generator/src/TOTP-RC6236-generator.hpp @@ -0,0 +1,121 @@ +/* Copyright (c) Dirk-Willem van Gulik, All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#ifndef _TOTP_RFC6238_H +#define _TOTP_RFC6238_H + +// Needed for the SHA1 +// +#include + +// Needed for base32 decode - origin +// https://github.com/dirkx/Arduino-Base32-Decode/releases +// +#include + +class TOTP { + public: + + // Defaults from RFC 6238 + // Seed assumed in base64 format; and to be a multiple of 8 bits. + // once decoded. + static const time_t RFC6238_DEFAULT_interval = 30; // seconds (default) + static const time_t RFC6238_DEFAULT_epoch = 0; // epoch relative to the unix epoch (jan 1970 is the default) + static const int RFC6238_DEFAULT_digits = 6; // length (default is 6) + + static String * currentOTP(String seed, + time_t interval = RFC6238_DEFAULT_interval, + int digits = RFC6238_DEFAULT_digits, + time_t epoch = RFC6238_DEFAULT_epoch + ) + { + return currentOTP(time(NULL), seed, interval, digits, epoch); + } + + static String * currentOTP(time_t t, + String seed, + time_t interval = RFC6238_DEFAULT_interval, + int digits = RFC6238_DEFAULT_digits, + time_t epoch = RFC6238_DEFAULT_epoch + ) + { + uint64_t v = t; + v = (v - epoch) / interval; + + // HMAC is calculated in big-endian (network) order. + // v = htonll(v); + + // Unfortunately htonll is not exposed + uint32_t endianness = 0xdeadbeef; + if ((*(const uint8_t *)&endianness) == 0xef) { + v = ((v & 0x00000000ffffffff) << 32) | ((v & 0xffffffff00000000) >> 32); + v = ((v & 0x0000ffff0000ffff) << 16) | ((v & 0xffff0000ffff0000) >> 16); + v = ((v & 0x00ff00ff00ff00ff) << 8) | ((v & 0xff00ff00ff00ff00) >> 8); + }; + + unsigned char buff[ seed.length() ]; + bzero(buff, sizeof(buff)); + int n = base32decode(seed.c_str(), buff, sizeof(buff)); + if (n < 0) { + Serial.println("Could not decode base32 seed"); + return NULL; + } + +#ifdef RFC6238_DEBUG + Serial.print("Key: "); + Serial.print(seed); + Serial.print(" --> "); + for (int i = 0; i < n; i++) { + Serial.printf("%02x", buff[i]); + } + Serial.printf(" -- bits=%d -- check this against https://cryptotools.net/otp\n",n * 8); +#endif + + unsigned char digest[20]; + if (mbedtls_md_hmac(mbedtls_md_info_from_type(MBEDTLS_MD_SHA1), + buff, n, // key + (unsigned char*) &v, sizeof(v), // input + digest)) return NULL; + + uint8_t offst = digest[19] & 0x0f; + uint32_t bin_code = (digest[offst + 0] & 0x7f) << 24 + | (digest[offst + 1] & 0xff) << 16 + | (digest[offst + 2] & 0xff) << 8 + | (digest[offst + 3] & 0xff); + int power = pow(10, digits); + +#if RFC6238_DEBUG + // To check against https://cryptotools.net/otp + // + for (int i = 0; i < 20; i++) { + if (offst == i) Serial.print("|"); + Serial.printf("%02x", digest[i]); + if (offst == i) Serial.print("|"); + } + Serial.println(); +#endif + + // prefix with zero's - as needed & cut off to right number of digits. + // + char outbuff[32]; + snprintf(outbuff, sizeof(outbuff), "%06u", bin_code % power); + String * otp = new String(outbuff); + + return (otp); + } +}; +#endif diff --git a/lib/Arduino-TOTP-RFC6238-generator/src/TOTP-generator.hpp b/lib/Arduino-TOTP-RFC6238-generator/src/TOTP-generator.hpp new file mode 100644 index 0000000..927d3db --- /dev/null +++ b/lib/Arduino-TOTP-RFC6238-generator/src/TOTP-generator.hpp @@ -0,0 +1,121 @@ +/* Copyright (c) Dirk-Willem van Gulik, All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#ifndef _TOTP_RFC6238_H +#define _TOTP_RFC6238_H + +// Needed for the SHA1 +// +#include + +// Needed for base32 decode - origin +// https://github.com/dirkx/Arduino-Base32-Decode/releases +// +#include + +class TOTP { + public: + + // Defaults from RFC 6238 + // Seed assumed in base64 format; and to be a multiple of 8 bits. + // once decoded. + static const time_t RFC6238_DEFAULT_interval = 30; // seconds (default) + static const time_t RFC6238_DEFAULT_epoch = 0; // epoch relative to the unix epoch (jan 1970 is the default) + static const int RFC6238_DEFAULT_digits = 6; // length (default is 6) + + static String * currentOTP(String seed, + time_t interval = RFC6238_DEFAULT_interval, + int digits = RFC6238_DEFAULT_digits, + time_t epoch = RFC6238_DEFAULT_epoch + ) + { + return currentOTP(seed, time(NULL), interval, digits, epoch); + } + + static String * currentOTP(String seed, + time_t t, + time_t interval = RFC6238_DEFAULT_interval, + int digits = RFC6238_DEFAULT_digits, + time_t epoch = RFC6238_DEFAULT_epoch + ) + { + uint64_t v = t; + v = (v - epoch) / interval; + + // HMAC is calculated in big-endian (network) order. + // v = htonll(v); + + // Unfortunately htonll is not exposed + uint32_t endianness = 0xdeadbeef; + if ((*(const uint8_t *)&endianness) == 0xef) { + v = ((v & 0x00000000ffffffff) << 32) | ((v & 0xffffffff00000000) >> 32); + v = ((v & 0x0000ffff0000ffff) << 16) | ((v & 0xffff0000ffff0000) >> 16); + v = ((v & 0x00ff00ff00ff00ff) << 8) | ((v & 0xff00ff00ff00ff00) >> 8); + }; + + unsigned char buff[ seed.length() ]; + bzero(buff, sizeof(buff)); + int n = base32decode(seed.c_str(), buff, sizeof(buff)); + if (n < 0) { + Serial.println("Could not decode base32 seed"); + return NULL; + } + +#ifdef RFC6238_DEBUG + Serial.print("Key: "); + Serial.print(seed); + Serial.print(" --> "); + for (int i = 0; i < n; i++) { + Serial.printf("%02x", buff[i]); + } + Serial.printf(" -- bits=%d -- check this against https://cryptotools.net/otp\n",n * 8); +#endif + + unsigned char digest[20]; + if (mbedtls_md_hmac(mbedtls_md_info_from_type(MBEDTLS_MD_SHA1), + buff, n, // key + (unsigned char*) &v, sizeof(v), // input + digest)) return NULL; + + uint8_t offst = digest[19] & 0x0f; + uint32_t bin_code = (digest[offst + 0] & 0x7f) << 24 + | (digest[offst + 1] & 0xff) << 16 + | (digest[offst + 2] & 0xff) << 8 + | (digest[offst + 3] & 0xff); + int power = pow(10, digits); + +#if RFC6238_DEBUG + // To check against https://cryptotools.net/otp + // + for (int i = 0; i < 20; i++) { + if (offst == i) Serial.print("|"); + Serial.printf("%02x", digest[i]); + if (offst == i) Serial.print("|"); + } + Serial.println(); +#endif + + // prefix with zero's - as needed & cut off to right number of digits. + // + char outbuff[32]; + snprintf(outbuff, sizeof(outbuff), "%06u", bin_code % power); + String * otp = new String(outbuff); + + return (otp); + } +}; +#endif diff --git a/sdkconfig.defaults b/sdkconfig.defaults index e01a43e..81cd6fc 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -1,11 +1,56 @@ -CONFIG_AUTOSTART_ARDUINO=y -CONFIG_FREERTOS_HZ=1000 -CONFIG_MBEDTLS_PSK_MODES=y -CONFIG_MBEDTLS_KEY_EXCHANGE_PSK=y +# ESP-IDF CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_SIZE=y CONFIG_COMPILER_OPTIMIZATION_SIZE=y CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y CONFIG_ESPTOOLPY_HEADER_FLASHSIZE_UPDATE=y + +# ARDUINO +CONFIG_AUTOSTART_ARDUINO=y +CONFIG_FREERTOS_HZ=1000 +CONFIG_ARDUINO_SELECTIVE_COMPILATION=y +CONFIG_ARDUINO_SELECTIVE_WebServer=n +CONFIG_ARDUINO_SELECTIVE_WiFiProv=n +CONFIG_ARDUINO_SELECTIVE_BLE=n +CONFIG_ARDUINO_SELECTIVE_BluetoothSerial=n +CONFIG_ARDUINO_SELECTIVE_SimpleBLE=n +CONFIG_ARDUINO_SELECTIVE_ESP_SR=n +CONFIG_ARDUINO_SELECTIVE_Zigbee=n +CONFIG_ARDUINO_SELECTIVE_SD=n +CONFIG_ARDUINO_SELECTIVE_SD_MMC=n +CONFIG_ARDUINO_SELECTIVE_FFat=n +CONFIG_ARDUINO_SELECTIVE_LittleFS=n +CONFIG_ARDUINO_SELECTIVE_PPP=n +CONFIG_ARDUINO_SELECTIVE_Matter=n +CONFIG_ARDUINO_SELECTIVE_RainMaker=n +CONFIG_ARDUINO_SELECTIVE_OpenThread=n +CONFIG_ARDUINO_SELECTIVE_Insights=n +CONFIG_ARDUINO_LOOP_STACK_SIZE=12288 + +# LOGS +CONFIG_HEAP_TASK_TRACKING=n +CONFIG_LOG_COLORS=n +CONFIG_LOG_MAXIMUM_EQUALS_DEFAULT=n +CONFIG_LOG_MAXIMUM_LEVEL=4 +CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH=y + +# MBEDTLS +CONFIG_MBEDTLS_PSK_MODES=y +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_DYNAMIC_BUFFER=y + +# RTC WDT +CONFIG_BOOTLOADER_WDT_DISABLE_IN_USER_CODE=y +CONFIG_BOOTLOADER_WDT_TIME_MS=120000 + +# PSRAM +CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=n +CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=1024 + +# BLUETOOTH CONFIG_BT_ENABLED=y CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=n @@ -65,52 +110,21 @@ CONFIG_BTDM_BLE_ADV_REPORT_FLOW_CTRL_NUM=100 CONFIG_BTDM_BLE_ADV_REPORT_DISCARD_THRSHOLD=20 CONFIG_BT_NIMBLE_MSYS_BUF_FROM_HEAP=n CONFIG_BT_NIMBLE_50_FEATURE_SUPPORT=n -CONFIG_IEEE802154_ENABLED=n -CONFIG_ARDUINO_SELECTIVE_COMPILATION=y -CONFIG_ARDUINO_SELECTIVE_WebServer=n -CONFIG_ARDUINO_SELECTIVE_WiFiProv=n -CONFIG_ARDUINO_SELECTIVE_BLE=n -CONFIG_ARDUINO_SELECTIVE_BluetoothSerial=n -CONFIG_ARDUINO_SELECTIVE_SimpleBLE=n -CONFIG_ARDUINO_SELECTIVE_ESP_SR=n -CONFIG_ARDUINO_SELECTIVE_Zigbee=n -CONFIG_ARDUINO_SELECTIVE_SD=n -CONFIG_ARDUINO_SELECTIVE_SD_MMC=n -CONFIG_ARDUINO_SELECTIVE_FFat=n -CONFIG_ARDUINO_SELECTIVE_LittleFS=n -CONFIG_ARDUINO_SELECTIVE_PPP=n -CONFIG_ARDUINO_SELECTIVE_Matter=n -CONFIG_ARDUINO_SELECTIVE_RainMaker=n -CONFIG_ARDUINO_SELECTIVE_OpenThread=n -CONFIG_ARDUINO_SELECTIVE_Insights=n -CONFIG_HEAP_TASK_TRACKING=n -CONFIG_LOG_COLORS=n -CONFIG_LOG_MAXIMUM_EQUALS_DEFAULT=n -CONFIG_LOG_MAXIMUM_LEVEL=4 + +# LWIP +CONFIG_LWIP_MAX_SOCKETS=24 +CONFIG_LWIP_DHCP_GET_NTP_SRV=y +CONFIG_LWIP_SNTP_UPDATE_DELAY=43200000 +CONFIG_LWIP_SNTP_MAX_SERVERS=3 + +# ETHERNET CONFIG_ETH_ENABLED=y CONFIG_ETH_USE_SPI_ETHERNET=y CONFIG_ETH_SPI_ETHERNET_W5500=y CONFIG_ETH_SPI_ETHERNET_DM9051=y CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL=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_ESP_HTTP_CLIENT_ENABLE_HTTPS=y -CONFIG_ESP_HTTP_CLIENT_ENABLE_BASIC_AUTH=y -CONFIG_ESP_HTTP_CLIENT_ENABLE_DIGEST_AUTH=y -CONFIG_HTTPD_MAX_REQ_HDR_LEN=2048 -CONFIG_HTTPD_MAX_URI_LEN=512 -CONFIG_HTTPD_ERR_RESP_NO_DELAY=y -CONFIG_HTTPD_PURGE_BUF_LEN=32 -CONFIG_HTTPD_WS_SUPPORT=y -CONFIG_ESP_HTTPS_SERVER_ENABLE=y -CONFIG_BOOTLOADER_WDT_DISABLE_IN_USER_CODE=y -CONFIG_BOOTLOADER_WDT_TIME_MS=120000 -CONFIG_LWIP_MAX_SOCKETS=24 -CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y -CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=1024 -CONFIG_ARDUINO_LOOP_STACK_SIZE=12288 + +# WIFI CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=4 CONFIG_ESP_WIFI_STATIC_TX_BUFFER_NUM=4 CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM=8 @@ -118,8 +132,19 @@ CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=8 CONFIG_ESP_WIFI_RX_BA_WIN=6 CONFIG_ESP_WIFI_IRAM_OPT=n CONFIG_ESP_WIFI_RX_IRAM_OPT=n -CONFIG_MBEDTLS_DYNAMIC_BUFFER=y -CONFIG_LWIP_DHCP_GET_NTP_SRV=y -CONFIG_LWIP_SNTP_UPDATE_DELAY=43200000 -CONFIG_LWIP_SNTP_MAX_SERVERS=3 -CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH=y \ No newline at end of file + +# ZIGBEE +CONFIG_IEEE802154_ENABLED=n + +# HTTP(S) CLIENT +CONFIG_ESP_HTTP_CLIENT_ENABLE_HTTPS=y +CONFIG_ESP_HTTP_CLIENT_ENABLE_BASIC_AUTH=y +CONFIG_ESP_HTTP_CLIENT_ENABLE_DIGEST_AUTH=y + +# HTTP(S) SERVER +CONFIG_ESP_HTTPS_SERVER_ENABLE=y +CONFIG_HTTPD_MAX_REQ_HDR_LEN=2048 +CONFIG_HTTPD_MAX_URI_LEN=512 +CONFIG_HTTPD_ERR_RESP_NO_DELAY=y +CONFIG_HTTPD_PURGE_BUF_LEN=32 +CONFIG_HTTPD_WS_SUPPORT=y \ No newline at end of file diff --git a/src/Config.h b/src/Config.h index fd4d2f7..3106db9 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-03" +#define NUKI_HUB_DATE "2025-02-10" #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/Gpio.cpp b/src/Gpio.cpp index 9490ca0..b3cd5f1 100644 --- a/src/Gpio.cpp +++ b/src/Gpio.cpp @@ -223,7 +223,7 @@ void Gpio::loadPinConfiguration() { PinEntry entry; entry.pin = serialized[i * 2]; - Log->print(("Pin ")); + Log->print("Pin "); Log->println(entry.pin); if(std::find(disabledPins.begin(), disabledPins.end(), entry.pin) == disabledPins.end()) @@ -234,14 +234,14 @@ void Gpio::loadPinConfiguration() } entry.role = (PinRole) serialized[(i * 2 + 1)]; Log->println("Not found in Ethernet disabled pins"); - Log->print(("Role: ")); + Log->print("Role: "); Log->println(getRoleDescription(entry.role)); } else { entry.role = PinRole::Ethernet; Log->println("Found in Ethernet disabled pins"); - Log->print(("Role: ")); + Log->print("Role: "); Log->println(getRoleDescription(entry.role)); } if(entry.role != PinRole::Disabled) @@ -359,7 +359,7 @@ const std::vector Gpio::getDisabledPins() const break; } - Log->print(("GPIO Boot button and Ethernet disabled pins:")); + Log->print("GPIO Boot button and Ethernet disabled pins:"); for_each_n(disabledPins.begin(), disabledPins.size(), [](int x) { @@ -382,7 +382,7 @@ void Gpio::savePinConfiguration(const std::vector &pinConfiguration) for(int i=0; i < len; i++) { const auto& entry = pinConfiguration[i]; - Log->print(("Pin ")); + Log->print("Pin "); Log->println(entry.pin); if(std::find(disabledPins.begin(), disabledPins.end(), entry.pin) != disabledPins.end()) @@ -390,7 +390,7 @@ void Gpio::savePinConfiguration(const std::vector &pinConfiguration) serialized[i * 2] = entry.pin; serialized[i * 2 + 1] = (int8_t)PinRole::Ethernet; Log->println("Found in Ethernet disabled pins"); - Log->print(("Role: ")); + Log->print("Role: "); Log->println(getRoleDescription(PinRole::Ethernet)); } @@ -401,7 +401,7 @@ void Gpio::savePinConfiguration(const std::vector &pinConfiguration) serialized[i * 2] = entry.pin; serialized[i * 2 + 1] = (int8_t) entry.role; Log->println("Not found in Ethernet disabled pins"); - Log->print(("Role: ")); + Log->print("Role: "); Log->println(getRoleDescription(entry.role)); } } diff --git a/src/ImportExport.cpp b/src/ImportExport.cpp new file mode 100644 index 0000000..a38e877 --- /dev/null +++ b/src/ImportExport.cpp @@ -0,0 +1,1066 @@ +#include "ImportExport.h" +#include "SPIFFS.h" +#include "Logger.h" +#include "PreferencesKeys.h" +#include +#include + +ImportExport::ImportExport(Preferences *preferences) + : _preferences(preferences) +{ + readSettings(); +} + +void ImportExport::readSettings() +{ + if (_preferences->getBool(preference_cred_duo_enabled, false)) + { + _duoEnabled = true; + _duoHost = _preferences->getString(preference_cred_duo_host, ""); + _duoIkey = _preferences->getString(preference_cred_duo_ikey, ""); + _duoSkey = _preferences->getString(preference_cred_duo_skey, ""); + _duoUser = _preferences->getString(preference_cred_duo_user, ""); + + if (_duoHost == "" || _duoIkey == "" || _duoSkey == "" || _duoUser == "" || !_preferences->getBool(preference_update_time, false)) + { + _duoEnabled = false; + } + else if (_preferences->getBool(preference_cred_bypass_boot_btn_enabled, false) || _preferences->getInt(preference_cred_bypass_gpio_high, -1) > -1 || _preferences->getInt(preference_cred_bypass_gpio_low, -1) > -1) + { + if (_preferences->getBool(preference_cred_bypass_boot_btn_enabled, false)) + { + _bypassGPIO = true; + } + _bypassGPIOHigh = _preferences->getInt(preference_cred_bypass_gpio_high, -1); + _bypassGPIOLow = _preferences->getInt(preference_cred_bypass_gpio_low, -1); + } + } + + _totpKey = _preferences->getString(preference_totp_secret, ""); + _totpEnabled = _totpKey.length() > 0; +} + +bool ImportExport::getDuoEnabled() +{ + return _duoEnabled; +} + +bool ImportExport::getTOTPEnabled() +{ + return _totpEnabled; +} + +bool ImportExport::getBypassGPIOEnabled() +{ + return _bypassGPIO; +} + +int ImportExport::getBypassGPIOHigh() +{ + return _bypassGPIOHigh; +} +int ImportExport::getBypassGPIOLow() +{ + return _bypassGPIOLow; +} + +bool ImportExport::startDuoAuth(char* pushType) +{ + int64_t timeout = esp_timer_get_time() - (30 * 1000 * 1000L); + if(!_duoActiveRequest || timeout > _duoRequestTS) + { + const char* duo_host = _duoHost.c_str(); + const char* duo_ikey = _duoIkey.c_str(); + const char* duo_skey = _duoSkey.c_str(); + const char* duo_user = _duoUser.c_str(); + + DuoAuthLib duoAuth; + bool duoRequestResult; + duoAuth.begin(duo_host, duo_ikey, duo_skey, &timeinfo); + duoAuth.setPushType(pushType); + duoRequestResult = duoAuth.pushAuth((char*)duo_user, true); + + if(duoRequestResult == true) + { + _duoTransactionId = duoAuth.getAuthTxId(); + _duoActiveRequest = true; + _duoRequestTS = esp_timer_get_time(); + Log->println("Duo MFA Auth sent"); + return true; + } + else + { + Log->println("Failed Duo MFA Auth"); + return false; + } + } + return true; +} + +void ImportExport::setDuoCheckIP(String duoCheckIP) +{ + _duoCheckIP = duoCheckIP; +} + +void ImportExport::setDuoCheckId(String duoCheckId) +{ + _duoCheckId = duoCheckId; +} + +void ImportExport::saveSessions() +{ + if(_preferences->getBool(preference_update_time, false)) + { + if (!SPIFFS.begin(true)) + { + Log->println("SPIFFS Mount Failed"); + } + else + { + File file; + file = SPIFFS.open("/duosessions.json", "w"); + serializeJson(_duoSessions, file); + file.close(); + } + } +} + +int ImportExport::checkDuoAuth(PsychicRequest *request) +{ + const char* duo_host = _duoHost.c_str(); + 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")) + { + const PsychicWebParameter* p = request->getParam("type"); + if(p->value() != "") + { + type = p->value().toInt(); + } + } + + if (request->hasParam("id")) { + const PsychicWebParameter* p = request->getParam("id"); + String id = p->value(); + DuoAuthLib duoAuth; + if(_duoActiveRequest && _duoCheckIP == request->client()->localIP().toString() && id == _duoCheckId) + { + duoAuth.begin(duo_host, duo_ikey, duo_skey, &timeinfo); + + Log->println("Checking Duo Push Status..."); + duoAuth.authStatus(_duoTransactionId); + + if(duoAuth.pushWaiting()) + { + Log->println("Duo Push Waiting..."); + return 2; + } + else + { + if (duoAuth.authSuccessful()) + { + Log->println("Successful Duo MFA Auth"); + _duoActiveRequest = false; + _duoTransactionId = ""; + _duoCheckIP = ""; + _duoCheckId = ""; + + if(type==0) + { + int64_t durationLength = 60*60*_preferences->getInt(preference_cred_session_lifetime_duo_remember, 720); + + if (!_sessionsOpts[request->client()->localIP().toString()]) + { + durationLength = _preferences->getInt(preference_cred_session_lifetime_duo, 3600); + } + struct timeval time; + gettimeofday(&time, NULL); + int64_t time_us = (int64_t)time.tv_sec * 1000000L + (int64_t)time.tv_usec; + _duoSessions[id] = time_us + (durationLength*1000000L); + saveSessions(); + if (_preferences->getBool(preference_mfa_reconfigure, false)) + { + _preferences->putBool(preference_mfa_reconfigure, false); + } + } + else + { + _sessionsOpts[request->client()->localIP().toString() + "approve"] = true; + } + return 1; + } + else + { + Log->println("Failed Duo MFA Auth"); + _duoActiveRequest = false; + _duoTransactionId = ""; + _duoCheckIP = ""; + _duoCheckId = ""; + + if(type==0) + { + if (_preferences->getBool(preference_mfa_reconfigure, false)) + { + _preferences->putBool(preference_cred_duo_enabled, false); + _duoEnabled = false; + _preferences->putBool(preference_mfa_reconfigure, false); + } + } + else + { + _sessionsOpts[request->client()->localIP().toString() + "approve"] = false; + } + return 0; + } + } + } + } + return 0; +} + +int ImportExport::checkDuoApprove() +{ + const char* duo_host = _duoHost.c_str(); + const char* duo_ikey = _duoIkey.c_str(); + const char* duo_skey = _duoSkey.c_str(); + const char* duo_user = _duoUser.c_str(); + + DuoAuthLib duoAuth; + if(_duoActiveRequest) + { + duoAuth.begin(duo_host, duo_ikey, duo_skey, &timeinfo); + + Log->println("Checking Duo Push Status..."); + duoAuth.authStatus(_duoTransactionId); + + if(duoAuth.pushWaiting()) + { + Log->println("Duo Push Waiting..."); + return 2; + } + else + { + if (duoAuth.authSuccessful()) + { + Log->println("Successful Duo MFA Auth"); + _duoActiveRequest = false; + _duoTransactionId = ""; + _duoCheckIP = ""; + _duoCheckId = ""; + return 1; + } + else + { + Log->println("Failed Duo MFA Auth"); + _duoActiveRequest = false; + _duoTransactionId = ""; + _duoCheckIP = ""; + _duoCheckId = ""; + return 0; + } + } + } + return 0; +} + +bool ImportExport::checkTOTP(String* totpKey) +{ + String key(totpKey->c_str()); + + if(_totpEnabled) + { + time_t now; + time(&now); + int totpTime = -60; + + while (totpTime <= 60) + { + String key2(TOTP::currentOTP(now, _totpKey, 30, 6, totpTime)->c_str()); + + if(key.toInt() == key2.toInt()) + { + Log->println("Successful TOTP MFA Auth"); + return true; + } + totpTime += 30; + } + Log->println("Failed TOTP MFA Auth"); + } + return false; +} + +void ImportExport::exportHttpsJson(JsonDocument &json) +{ + if (!SPIFFS.begin(true)) { + Log->println("SPIFFS Mount Failed"); + } + else + { + File file = SPIFFS.open("/http_ssl.crt"); + if (!file || file.isDirectory()) { + Log->println("http_ssl.crt not found"); + } + else + { + Log->println("Reading http_ssl.crt"); + size_t filesize = file.size(); + char cert[filesize + 1]; + + file.read((uint8_t *)cert, sizeof(cert)); + file.close(); + cert[filesize] = '\0'; + json["http_ssl.crt"] = cert; + } + } + + if (!SPIFFS.begin(true)) { + Log->println("SPIFFS Mount Failed"); + } + else + { + File file = SPIFFS.open("/http_ssl.key"); + if (!file || file.isDirectory()) { + Log->println("http_ssl.key not found"); + } + else + { + Log->println("Reading http_ssl.key"); + size_t filesize = file.size(); + char key[filesize + 1]; + + file.read((uint8_t *)key, sizeof(key)); + file.close(); + key[filesize] = '\0'; + json["http_ssl.key"] = key; + } + } +} + +void ImportExport::exportMqttsJson(JsonDocument &json) +{ + if (!SPIFFS.begin(true)) { + Log->println("SPIFFS Mount Failed"); + } + else + { + File file = SPIFFS.open("/mqtt_ssl.ca"); + if (!file || file.isDirectory()) { + Log->println("mqtt_ssl.ca not found"); + } + else + { + Log->println("Reading mqtt_ssl.ca"); + size_t filesize = file.size(); + char ca[filesize + 1]; + + file.read((uint8_t *)ca, sizeof(ca)); + file.close(); + ca[filesize] = '\0'; + json["mqtt_ssl.ca"] = ca; + } + } + + if (!SPIFFS.begin(true)) { + Log->println("SPIFFS Mount Failed"); + } + else + { + File file = SPIFFS.open("/mqtt_ssl.crt"); + if (!file || file.isDirectory()) { + Log->println("mqtt_ssl.crt not found"); + } + else + { + Log->println("Reading mqtt_ssl.crt"); + size_t filesize = file.size(); + char cert[filesize + 1]; + + file.read((uint8_t *)cert, sizeof(cert)); + file.close(); + cert[filesize] = '\0'; + json["mqtt_ssl.crt"] = cert; + } + } + + if (!SPIFFS.begin(true)) { + Log->println("SPIFFS Mount Failed"); + } + else + { + File file = SPIFFS.open("/mqtt_ssl.key"); + if (!file || file.isDirectory()) { + Log->println("mqtt_ssl.key not found"); + } + else + { + Log->println("Reading mqtt_ssl.key"); + size_t filesize = file.size(); + char key[filesize + 1]; + + file.read((uint8_t *)key, sizeof(key)); + file.close(); + key[filesize] = '\0'; + json["mqtt_ssl.key"] = key; + } + } +} + +void ImportExport::exportNukiHubJson(JsonDocument &json, bool redacted, bool pairing, bool nuki, bool nukiOpener) +{ + DebugPreferences debugPreferences; + + const std::vector keysPrefs = debugPreferences.getPreferencesKeys(); + const std::vector boolPrefs = debugPreferences.getPreferencesBoolKeys(); + const std::vector redactedPrefs = debugPreferences.getPreferencesRedactedKeys(); + const std::vector bytePrefs = debugPreferences.getPreferencesByteKeys(); + + for(const auto& key : keysPrefs) + { + if(strcmp(key, preference_show_secrets) == 0) + { + continue; + } + if(strcmp(key, preference_latest_version) == 0) + { + continue; + } + if(!redacted) if(std::find(redactedPrefs.begin(), redactedPrefs.end(), key) != redactedPrefs.end()) + { + continue; + } + if(!_preferences->isKey(key)) + { + json[key] = ""; + } + else if(std::find(boolPrefs.begin(), boolPrefs.end(), key) != boolPrefs.end()) + { + json[key] = _preferences->getBool(key) ? "1" : "0"; + } + else + { + switch(_preferences->getType(key)) + { + case PT_I8: + json[key] = String(_preferences->getChar(key)); + break; + case PT_I16: + json[key] = String(_preferences->getShort(key)); + break; + case PT_I32: + json[key] = String(_preferences->getInt(key)); + break; + case PT_I64: + json[key] = String(_preferences->getLong64(key)); + break; + case PT_U8: + json[key] = String(_preferences->getUChar(key)); + break; + case PT_U16: + json[key] = String(_preferences->getUShort(key)); + break; + case PT_U32: + json[key] = String(_preferences->getUInt(key)); + break; + case PT_U64: + json[key] = String(_preferences->getULong64(key)); + break; + case PT_STR: + json[key] = _preferences->getString(key); + break; + default: + json[key] = _preferences->getString(key); + break; + } + } + } + + if(pairing) + { + if(nuki) + { + unsigned char currentBleAddress[6]; + unsigned char authorizationId[4] = {0x00}; + unsigned char secretKeyK[32] = {0x00}; + uint16_t storedPincode = 0000; + uint32_t storedUltraPincode = 000000; + bool isUltra = false; + Preferences nukiBlePref; + nukiBlePref.begin("NukiHub", false); + nukiBlePref.getBytes("bleAddress", currentBleAddress, 6); + nukiBlePref.getBytes("secretKeyK", secretKeyK, 32); + nukiBlePref.getBytes("authorizationId", authorizationId, 4); + nukiBlePref.getBytes("securityPinCode", &storedPincode, 2); + nukiBlePref.getBytes("ultraPinCode", &storedUltraPincode, 4); + isUltra = nukiBlePref.getBool("isUltra", false); + nukiBlePref.end(); + char text[255]; + text[0] = '\0'; + for(int i = 0 ; i < 6 ; i++) + { + size_t offset = strlen(text); + sprintf(&(text[offset]), "%02x", currentBleAddress[i]); + } + json["bleAddressLock"] = text; + memset(text, 0, sizeof(text)); + text[0] = '\0'; + for(int i = 0 ; i < 32 ; i++) + { + size_t offset = strlen(text); + sprintf(&(text[offset]), "%02x", secretKeyK[i]); + } + json["secretKeyKLock"] = text; + memset(text, 0, sizeof(text)); + text[0] = '\0'; + for(int i = 0 ; i < 4 ; i++) + { + size_t offset = strlen(text); + sprintf(&(text[offset]), "%02x", authorizationId[i]); + } + json["authorizationIdLock"] = text; + memset(text, 0, sizeof(text)); + json["securityPinCodeLock"] = storedPincode; + json["ultraPinCodeLock"] = storedUltraPincode; + json["isUltra"] = isUltra ? "1" : "0"; + } + if(nukiOpener) + { + unsigned char currentBleAddressOpn[6]; + unsigned char authorizationIdOpn[4] = {0x00}; + unsigned char secretKeyKOpn[32] = {0x00}; + uint16_t storedPincodeOpn = 0000; + Preferences nukiBlePref; + nukiBlePref.begin("NukiHubopener", false); + nukiBlePref.getBytes("bleAddress", currentBleAddressOpn, 6); + nukiBlePref.getBytes("secretKeyK", secretKeyKOpn, 32); + nukiBlePref.getBytes("authorizationId", authorizationIdOpn, 4); + nukiBlePref.getBytes("securityPinCode", &storedPincodeOpn, 2); + nukiBlePref.end(); + char text[255]; + text[0] = '\0'; + for(int i = 0 ; i < 6 ; i++) + { + size_t offset = strlen(text); + sprintf(&(text[offset]), "%02x", currentBleAddressOpn[i]); + } + json["bleAddressOpener"] = text; + memset(text, 0, sizeof(text)); + text[0] = '\0'; + for(int i = 0 ; i < 32 ; i++) + { + size_t offset = strlen(text); + sprintf(&(text[offset]), "%02x", secretKeyKOpn[i]); + } + json["secretKeyKOpener"] = text; + memset(text, 0, sizeof(text)); + text[0] = '\0'; + for(int i = 0 ; i < 4 ; i++) + { + size_t offset = strlen(text); + sprintf(&(text[offset]), "%02x", authorizationIdOpn[i]); + } + json["authorizationIdOpener"] = text; + memset(text, 0, sizeof(text)); + json["securityPinCodeOpener"] = storedPincodeOpn; + } + } + + for(const auto& key : bytePrefs) + { + size_t storedLength = _preferences->getBytesLength(key); + if(storedLength == 0) + { + continue; + } + uint8_t serialized[storedLength]; + memset(serialized, 0, sizeof(serialized)); + size_t size = _preferences->getBytes(key, serialized, sizeof(serialized)); + if(size == 0) + { + continue; + } + char text[255]; + text[0] = '\0'; + for(int i = 0 ; i < size ; i++) + { + size_t offset = strlen(text); + sprintf(&(text[offset]), "%02x", serialized[i]); + } + json[key] = text; + memset(text, 0, sizeof(text)); + } +} + +JsonDocument ImportExport::importJson(JsonDocument &doc) +{ + JsonDocument json; + unsigned char currentBleAddress[6]; + unsigned char authorizationId[4] = {0x00}; + unsigned char secretKeyK[32] = {0x00}; + unsigned char currentBleAddressOpn[6]; + unsigned char authorizationIdOpn[4] = {0x00}; + unsigned char secretKeyKOpn[32] = {0x00}; + + DebugPreferences debugPreferences; + + const std::vector keysPrefs = debugPreferences.getPreferencesKeys(); + const std::vector boolPrefs = debugPreferences.getPreferencesBoolKeys(); + const std::vector bytePrefs = debugPreferences.getPreferencesByteKeys(); + const std::vector intPrefs = debugPreferences.getPreferencesIntKeys(); + const std::vector uintPrefs = debugPreferences.getPreferencesUIntKeys(); + const std::vector uint64Prefs = debugPreferences.getPreferencesUInt64Keys(); + + for(const auto& key : keysPrefs) + { + if(doc[key].isNull()) + { + continue; + } + if(strcmp(key, preference_show_secrets) == 0) + { + continue; + } + if(strcmp(key, preference_latest_version) == 0) + { + continue; + } + if(std::find(boolPrefs.begin(), boolPrefs.end(), key) != boolPrefs.end()) + { + if (doc[key].as().length() > 0) + { + _preferences->putBool(key, (doc[key].as() == "1" ? true : false)); + json[key] = "changed"; + } + else + { + json[key] = "removed"; + _preferences->remove(key); + } + continue; + } + if(std::find(intPrefs.begin(), intPrefs.end(), key) != intPrefs.end()) + { + if (doc[key].as().length() > 0) + { + json[key] = "changed"; + _preferences->putInt(key, doc[key].as()); + } + else + { + json[key] = "removed"; + _preferences->remove(key); + } + continue; + } + if(std::find(uintPrefs.begin(), uintPrefs.end(), key) != uintPrefs.end()) + { + if (doc[key].as().length() > 0) + { + json[key] = "changed"; + _preferences->putUInt(key, doc[key].as()); + } + else + { + json[key] = "removed"; + _preferences->remove(key); + } + continue; + } + if(std::find(uint64Prefs.begin(), uint64Prefs.end(), key) != uint64Prefs.end()) + { + if (doc[key].as().length() > 0) + { + json[key] = "changed"; + _preferences->putULong64(key, doc[key].as()); + } + else + { + json[key] = "removed"; + _preferences->remove(key); + } + continue; + } + if (doc[key].as().length() > 0) + { + json[key] = "changed"; + _preferences->putString(key, doc[key].as()); + } + else + { + json[key] = "removed"; + _preferences->remove(key); + } + } + + for(const auto& key : bytePrefs) + { + if(!doc[key].isNull() && doc[key].is()) + { + String value = doc[key].as(); + unsigned char tmpchar[32]; + for(int i=0; iputBytes(key, (byte*)(&tmpchar), (value.length() / 2)); + memset(tmpchar, 0, sizeof(tmpchar)); + } + } + + Preferences nukiBlePref; + nukiBlePref.begin("NukiHub", false); + + if(!doc["bleAddressLock"].isNull()) + { + if (doc["bleAddressLock"].as().length() == 12) + { + String value = doc["bleAddressLock"].as(); + for(int i=0; i().length() == 64) + { + String value = doc["secretKeyKLock"].as(); + for(int i=0; i().length() == 8) + { + String value = doc["authorizationIdLock"].as(); + for(int i=0; i().length() >0) + { + json["isUltra"] = "changed"; + nukiBlePref.putBool("isUltra", (doc["isUltra"].as() == "1" ? true : false)); + } + } + if(!doc["securityPinCodeLock"].isNull()) + { + if(doc["securityPinCodeLock"].as().length() > 0) + { + json["securityPinCodeLock"] = "changed"; + nukiBlePref.putBytes("securityPinCode", (byte*)(doc["securityPinCodeLock"].as()), 2); + //_nuki->setPin(doc["securityPinCodeLock"].as()); + } + else + { + json["securityPinCodeLock"] = "removed"; + unsigned char pincode[2] = {0x00}; + nukiBlePref.putBytes("securityPinCode", pincode, 2); + //_nuki->setPin(0xffff); + } + } + if(!doc["ultraPinCodeLock"].isNull()) + { + if(doc["ultraPinCodeLock"].as().length() > 0) + { + json["ultraPinCodeLock"] = "changed"; + nukiBlePref.putBytes("ultraPinCode", (byte*)(doc["ultraPinCodeLock"].as()), 4); + //_nuki->setUltraPin(doc["ultraPinCodeLock"].as()); + _preferences->putInt(preference_lock_gemini_pin, doc["ultraPinCodeLock"].as()); + } + else + { + json["ultraPinCodeLock"] = "removed"; + unsigned char ultraPincode[4] = {0x00}; + nukiBlePref.putBytes("ultraPinCode", ultraPincode, 4); + _preferences->putInt(preference_lock_gemini_pin, 0); + } + } + nukiBlePref.end(); + nukiBlePref.begin("NukiHubopener", false); + if(!doc["bleAddressOpener"].isNull()) + { + if (doc["bleAddressOpener"].as().length() == 12) + { + String value = doc["bleAddressOpener"].as(); + for(int i=0; i().length() == 64) + { + String value = doc["secretKeyKOpener"].as(); + for(int i=0; i().length() == 8) + { + String value = doc["authorizationIdOpener"].as(); + for(int i=0; i().length() > 0) + { + json["securityPinCodeOpener"] = "changed"; + nukiBlePref.putBytes("securityPinCode", (byte*)(doc["securityPinCodeOpener"].as()), 2); + //_nukiOpener->setPin(doc["securityPinCodeOpener"].as()); + } + else + { + json["securityPinCodeOpener"] = "removed"; + unsigned char pincode[2] = {0x00}; + nukiBlePref.putBytes("securityPinCode", pincode, 2); + //_nukiOpener->setPin(0xffff); + } + } + nukiBlePref.end(); + if(!doc["mqtt_ssl.ca"].isNull()) + { + if (!SPIFFS.begin(true)) { + Log->println("SPIFFS Mount Failed"); + json["mqtt_ssl.ca"] = "error"; + } + else + { + if(doc["mqtt_ssl.ca"].as().length() > 0) + { + File file = SPIFFS.open("/mqtt_ssl.ca", FILE_WRITE); + if (!file) { + Log->println("Failed to open /mqtt_ssl.ca for writing"); + json["mqtt_ssl.ca"] = "error"; + } + else + { + if (!file.print(doc["mqtt_ssl.ca"].as())) + { + Log->println("Failed to write /mqtt_ssl.ca"); + json["mqtt_ssl.crt"] = "error"; + } + else + { + json["mqtt_ssl.ca"] = "changed"; + } + file.close(); + } + } + else + { + if (!SPIFFS.remove("/mqtt_ssl.ca")) { + Log->println("Failed to delete /mqtt_ssl.ca"); + json["mqtt_ssl.crt"] = "error"; + } + else + { + json["mqtt_ssl.ca"] = "removed"; + } + } + } + } + if(!doc["mqtt_ssl.crt"].isNull()) + { + if (!SPIFFS.begin(true)) { + Log->println("SPIFFS Mount Failed"); + json["mqtt_ssl.crt"] = "error"; + } + else + { + if(doc["mqtt_ssl.crt"].as().length() > 0) + { + File file = SPIFFS.open("/mqtt_ssl.crt", FILE_WRITE); + if (!file) { + Log->println("Failed to open /mqtt_ssl.crt for writing"); + json["mqtt_ssl.crt"] = "error"; + } + else + { + if (!file.print(doc["mqtt_ssl.crt"].as())) + { + Log->println("Failed to write /mqtt_ssl.crt"); + json["mqtt_ssl.crt"] = "error"; + } + else + { + json["mqtt_ssl.crt"] = "changed"; + } + file.close(); + } + } + else + { + if (!SPIFFS.remove("/mqtt_ssl.crt")) { + Log->println("Failed to delete /mqtt_ssl.crt"); + json["mqtt_ssl.crt"] = "error"; + } + else + { + json["mqtt_ssl.crt"] = "removed"; + } + } + } + } + if(!doc["mqtt_ssl.key"].isNull()) + { + if (!SPIFFS.begin(true)) { + Log->println("SPIFFS Mount Failed"); + json["mqtt_ssl.key"] = "error"; + } + else + { + if(doc["mqtt_ssl.key"].as().length() > 0) + { + File file = SPIFFS.open("/mqtt_ssl.key", FILE_WRITE); + if (!file) { + Log->println("Failed to open /mqtt_ssl.key for writing"); + json["mqtt_ssl.key"] = "error"; + } + else + { + if (!file.print(doc["mqtt_ssl.key"].as())) + { + Log->println("Failed to write /mqtt_ssl.key"); + json["mqtt_ssl.key"] = "error"; + } + else + { + json["mqtt_ssl.key"] = "changed"; + } + file.close(); + } + } + else + { + if (!SPIFFS.remove("/mqtt_ssl.key")) { + Log->println("Failed to delete /mqtt_ssl.key"); + } + else + { + json["mqtt_ssl.key"] = "removed"; + } + } + } + } + if(!doc["http_ssl.crt"].isNull()) + { + if (!SPIFFS.begin(true)) { + Log->println("SPIFFS Mount Failed"); + json["http_ssl.crt"] = "error"; + } + else + { + if(doc["http_ssl.crt"].as().length() > 0) + { + File file = SPIFFS.open("/http_ssl.crt", FILE_WRITE); + if (!file) { + Log->println("Failed to open /http_ssl.crt for writing"); + json["http_ssl.crt"] = "error"; + } + else + { + if (!file.print(doc["http_ssl.crt"].as())) + { + Log->println("Failed to write /http_ssl.crt"); + json["http_ssl.crt"] = "error"; + } + else + { + json["http_ssl.crt"] = "changed"; + } + file.close(); + } + } + else + { + if (!SPIFFS.remove("/http_ssl.crt")) { + Log->println("Failed to delete /http_ssl.crt"); + json["http_ssl.crt"] = "error"; + } + else + { + json["http_ssl.crt"] = "removed"; + } + } + } + } + if(!doc["http_ssl.key"].isNull()) + { + if (!SPIFFS.begin(true)) { + Log->println("SPIFFS Mount Failed"); + json["http_ssl.key"] = "error"; + } + else + { + if(doc["http_ssl.key"].as().length() > 0) + { + File file = SPIFFS.open("/http_ssl.key", FILE_WRITE); + if (!file) { + Log->println("Failed to open /http_ssl.key for writing"); + json["http_ssl.key"] = "error"; + } + else + { + if (!file.print(doc["http_ssl.key"].as())) + { + Log->println("Failed to write /http_ssl.key"); + json["http_ssl.key"] = "error"; + } + else + { + json["http_ssl.key"] = "changed"; + } + file.close(); + } + } + else + { + if (!SPIFFS.remove("/http_ssl.key")) { + Log->println("Failed to delete /http_ssl.key"); + json["http_ssl.key"] = "error"; + } + else + { + json["http_ssl.key"] = "removed"; + } + } + } + } + + return json; +} \ No newline at end of file diff --git a/src/ImportExport.h b/src/ImportExport.h new file mode 100644 index 0000000..341d4dc --- /dev/null +++ b/src/ImportExport.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include "ArduinoJson.h" +#include + +class ImportExport +{ +public: + explicit ImportExport(Preferences* preferences); + void exportHttpsJson(JsonDocument &json); + void exportMqttsJson(JsonDocument &json); + void exportNukiHubJson(JsonDocument &json, bool redacted = false, bool pairing = false, bool nuki = false, bool nukiOpener = false); + JsonDocument importJson(JsonDocument &doc); + int checkDuoAuth(PsychicRequest *request); + int checkDuoApprove(); + bool startDuoAuth(char* pushType = (char*)""); + bool getTOTPEnabled(); + bool checkTOTP(String* totpKey); + bool getDuoEnabled(); + bool getBypassGPIOEnabled(); + int getBypassGPIOHigh(); + int getBypassGPIOLow(); + void readSettings(); + void setDuoCheckIP(String duoCheckIP); + void setDuoCheckId(String duoCheckId); + JsonDocument _duoSessions; + JsonDocument _totpSessions; + JsonDocument _sessionsOpts; +private: + void saveSessions(); + Preferences* _preferences; + struct tm timeinfo; + bool _totpEnabled = false; + bool _duoActiveRequest; + bool _duoEnabled = false; + bool _bypassGPIO = false; + int _bypassGPIOHigh = -1; + int _bypassGPIOLow = -1; + int64_t _duoRequestTS = 0; + String _duoTransactionId; + String _duoHost; + String _duoSkey; + String _duoIkey; + String _duoUser; + String _duoCheckId; + String _duoCheckIP; + String _totpKey; +}; + diff --git a/src/MqttTopics.h b/src/MqttTopics.h index 8b84d18..1866839 100644 --- a/src/MqttTopics.h +++ b/src/MqttTopics.h @@ -113,6 +113,11 @@ #define mqtt_topic_restart_reason_esp (char*)"/maintenance/restartReasonNukiEsp" #define mqtt_topic_mqtt_connection_state (char*)"/maintenance/mqttConnectionState" #define mqtt_topic_network_device (char*)"/maintenance/networkDevice" + +#define mqtt_topic_nuki_hub_config_action (char*)"/configuration/action" +#define mqtt_topic_nuki_hub_config_action_command_result (char*)"/configuration/commandResult" +#define mqtt_topic_nuki_hub_config_json (char*)"/configuration/json" + #define mqtt_topic_hybrid_state (char*)"/hybridConnected" #define mqtt_topic_gpio_prefix (char*)"/gpio" diff --git a/src/NukiNetwork.cpp b/src/NukiNetwork.cpp index ec656a1..a010aee 100644 --- a/src/NukiNetwork.cpp +++ b/src/NukiNetwork.cpp @@ -14,6 +14,7 @@ NukiNetwork* NukiNetwork::_inst = nullptr; +extern bool timeSynced; extern bool wifiFallback; extern bool disableNetwork; extern bool forceEnableWebServer; @@ -21,11 +22,12 @@ extern const uint8_t x509_crt_imported_bundle_bin_start[] asm("_binary_x509_crt_ extern const uint8_t x509_crt_imported_bundle_bin_end[] asm("_binary_x509_crt_bundle_end"); #ifndef NUKI_HUB_UPDATER -NukiNetwork::NukiNetwork(Preferences *preferences, Gpio* gpio, const String& maintenancePathPrefix, char* buffer, size_t bufferSize) +NukiNetwork::NukiNetwork(Preferences *preferences, Gpio* gpio, const String& maintenancePathPrefix, char* buffer, size_t bufferSize, ImportExport* importExport) : _preferences(preferences), _gpio(gpio), _buffer(buffer), - _bufferSize(bufferSize) + _bufferSize(bufferSize), + _importExport(importExport) #else NukiNetwork::NukiNetwork(Preferences *preferences) : _preferences(preferences) @@ -66,7 +68,7 @@ void NukiNetwork::setupDevice() { _ipConfiguration = new IPConfiguration(_preferences); int hardwareDetect = _preferences->getInt(preference_network_hardware, 0); - Log->print(("Hardware detect: ")); + Log->print("Hardware detect: "); Log->println(hardwareDetect); _firstBootAfterDeviceChange = _preferences->getBool(preference_ntw_reconfigure, false); @@ -94,13 +96,13 @@ void NukiNetwork::setupDevice() #ifndef CONFIG_IDF_TARGET_ESP32H2 if(!_firstBootAfterDeviceChange) { - Log->println(("Failed to connect to network. Wi-Fi fallback is disabled, rebooting.")); + Log->println("Failed to connect to network. Wi-Fi fallback is disabled, rebooting."); wifiFallback = false; sleep(5); restartEsp(RestartReason::NetworkDeviceCriticalFailureNoWifiFallback); } - Log->println(("Switching to Wi-Fi device as fallback.")); + Log->println("Switching to Wi-Fi device as fallback."); _networkDeviceType = NetworkDeviceType::WiFi; #else int custEth = _preferences->getInt(preference_network_custom_phy, 0); @@ -124,7 +126,7 @@ void NukiNetwork::setupDevice() _device = NetworkDeviceInstantiator::Create(_networkDeviceType, _hostname, _preferences, _ipConfiguration); - Log->print(("Network device: ")); + Log->print("Network device: "); Log->println(_device->deviceName()); #ifndef NUKI_HUB_UPDATER @@ -223,7 +225,7 @@ void NukiNetwork::initialize() strcpy(_hostnameArr, _hostname.c_str()); _device->initialize(); - Log->print(("Host name: ")); + Log->print("Host name: "); Log->println(_hostname); } @@ -273,7 +275,7 @@ void NukiNetwork::initialize() strcpy(_hostnameArr, _hostname.c_str()); _device->initialize(); - Log->print(("Host name: ")); + Log->print("Host name: "); Log->println(_hostname); String brokerAddr = _preferences->getString(preference_mqtt_broker); @@ -300,9 +302,9 @@ void NukiNetwork::initialize() } } - Log->print(("MQTT Broker: ")); + Log->print("MQTT Broker: "); Log->print(_mqttBrokerAddr); - Log->print((":")); + Log->print(":"); Log->println(_mqttPort); _device->mqttSetClientId(_hostnameArr); @@ -314,7 +316,7 @@ void NukiNetwork::initialize() if(rebGpio) { - Log->println(("Rebuild MQTT GPIO structure")); + Log->println("Rebuild MQTT GPIO structure"); } for (const auto &pinEntry: _gpio->pinConfiguration()) { @@ -412,7 +414,7 @@ bool NukiNetwork::update() if(_logIp && _device->isConnected() && !_device->localIP().equals("0.0.0.0")) { _logIp = false; - Log->print(("IP: ")); + Log->print("IP: "); Log->println(_device->localIP()); _firstDisconnected = true; } @@ -470,6 +472,12 @@ bool NukiNetwork::update() _lastRssi = rssi; } } + + if(_overwriteNukiHubConfigTS > 0 && espMillis() > _overwriteNukiHubConfigTS) + { + publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_json, "--", true); + _overwriteNukiHubConfigTS = -1; + } if(_lastMaintenanceTs == 0 || (ts - _lastMaintenanceTs) > 30000) { @@ -575,9 +583,9 @@ bool NukiNetwork::update() buildMqttPath(gpioPath, {mqtt_topic_gpio_prefix, (mqtt_topic_gpio_pin + std::to_string(pin)).c_str(), mqtt_topic_gpio_state}); publishInt(_lockPath.c_str(), gpioPath, pinState, _retainGpio); - Log->print(("GPIO ")); + Log->print("GPIO "); Log->print(pin); - Log->print((" (Input) --> ")); + Log->print(" (Input) --> "); Log->println(pinState); } } @@ -597,31 +605,31 @@ void NukiNetwork::onMqttDisconnect(const espMqttClientTypes::DisconnectReason &r switch(reason) { case espMqttClientTypes::DisconnectReason::USER_OK: - Log->println(("USER_OK")); + Log->println("USER_OK"); break; case espMqttClientTypes::DisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION: - Log->println(("MQTT_UNACCEPTABLE_PROTOCOL_VERSION")); + Log->println("MQTT_UNACCEPTABLE_PROTOCOL_VERSION"); break; case espMqttClientTypes::DisconnectReason::MQTT_IDENTIFIER_REJECTED: - Log->println(("MQTT_IDENTIFIER_REJECTED")); + Log->println("MQTT_IDENTIFIER_REJECTED"); break; case espMqttClientTypes::DisconnectReason::MQTT_SERVER_UNAVAILABLE: - Log->println(("MQTT_SERVER_UNAVAILABLE")); + Log->println("MQTT_SERVER_UNAVAILABLE"); break; case espMqttClientTypes::DisconnectReason::MQTT_MALFORMED_CREDENTIALS: - Log->println(("MQTT_MALFORMED_CREDENTIALS")); + Log->println("MQTT_MALFORMED_CREDENTIALS"); break; case espMqttClientTypes::DisconnectReason::MQTT_NOT_AUTHORIZED: - Log->println(("MQTT_NOT_AUTHORIZED")); + Log->println("MQTT_NOT_AUTHORIZED"); break; case espMqttClientTypes::DisconnectReason::TLS_BAD_FINGERPRINT: - Log->println(("TLS_BAD_FINGERPRINT")); + Log->println("TLS_BAD_FINGERPRINT"); break; case espMqttClientTypes::DisconnectReason::TCP_DISCONNECTED: - Log->println(("TCP_DISCONNECTED")); + Log->println("TCP_DISCONNECTED"); break; default: - Log->println(("Unknown")); + Log->println("Unknown"); break; } } @@ -634,7 +642,7 @@ bool NukiNetwork::reconnect() { if(strcmp(_mqttBrokerAddr, "") == 0) { - Log->println(("MQTT Broker not configured, aborting connection attempt.")); + Log->println("MQTT Broker not configured, aborting connection attempt."); _nextReconnect = espMillis() + 5000; if(_device->isConnected()) @@ -644,17 +652,17 @@ bool NukiNetwork::reconnect() return false; } - Log->println(("Attempting MQTT connection")); + Log->println("Attempting MQTT connection"); _connectReplyReceived = false; if(strlen(_mqttUser) == 0) { - Log->println(("MQTT: Connecting without credentials")); + Log->println("MQTT: Connecting without credentials"); } else { - Log->print(("MQTT: Connecting with user: ")); + Log->print("MQTT: Connecting with user: "); Log->println(_mqttUser); _device->mqttSetCredentials(_mqttUser, _mqttPass); } @@ -677,7 +685,7 @@ bool NukiNetwork::reconnect() if (_device->mqttConnected()) { - Log->println(("MQTT connected")); + Log->println("MQTT connected"); _mqttConnectedTs = millis(); _mqttConnectionState = 1; delay(100); @@ -766,6 +774,18 @@ bool NukiNetwork::reconnect() subscribe(_maintenancePathPrefix, mqtt_topic_update); } + if(_preferences->getBool(preference_publish_config, false)) + { + initTopic(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_json, "--"); + } + + if(_preferences->getBool(preference_config_from_mqtt, false) || _preferences->getBool(preference_publish_config, false)) + { + initTopic(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action, "--"); + subscribe(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action); + initTopic(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action_command_result, "--"); + } + initTopic(_maintenancePathPrefix, mqtt_topic_webserver_action, "--"); subscribe(_maintenancePathPrefix, mqtt_topic_webserver_action); initTopic(_maintenancePathPrefix, mqtt_topic_webserver_state, (_preferences->getBool(preference_webserver_enabled, true) || forceEnableWebServer ? "1" : "0")); @@ -792,7 +812,7 @@ bool NukiNetwork::reconnect() } else { - Log->print(("MQTT connect failed")); + Log->print("MQTT connect failed"); _mqttConnectionState = 0; _nextReconnect = espMillis() + 5000; //_device->mqttDisconnect(true); @@ -911,14 +931,14 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns if(comparePrefixedPath(topic, mqtt_topic_reset) && strcmp(data, "1") == 0 && !mqttRecentlyConnected()) { - Log->println(("Restart requested via MQTT.")); + Log->println("Restart requested via MQTT."); clearWifiFallback(); delay(200); restartEsp(RestartReason::RequestedViaMqtt); } else if(comparePrefixedPath(topic, mqtt_topic_update) && strcmp(data, "1") == 0 && _preferences->getBool(preference_update_from_mqtt, false) && !mqttRecentlyConnected()) { - Log->println(("Update requested via MQTT.")); + Log->println("Update requested via MQTT."); bool otaManifestSuccess = false; JsonDocument doc; @@ -959,13 +979,13 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns { if(strcmp(NUKI_HUB_VERSION, doc["release"]["fullversion"].as()) == 0 && strcmp(NUKI_HUB_BUILD, doc["release"]["build"].as()) == 0 && strcmp(NUKI_HUB_DATE, doc["release"]["time"].as()) == 0) { - Log->println(("Nuki Hub is already on the latest release version, OTA update aborted.")); + Log->println("Nuki Hub is already on the latest release version, OTA update aborted."); } else { _preferences->putString(preference_ota_updater_url, GITHUB_LATEST_UPDATER_BINARY_URL); _preferences->putString(preference_ota_main_url, GITHUB_LATEST_RELEASE_BINARY_URL); - Log->println(("Updating to latest release version.")); + Log->println("Updating to latest release version."); delay(200); restartEsp(RestartReason::OTAReboot); } @@ -974,13 +994,13 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns { if(strcmp(NUKI_HUB_VERSION, doc["beta"]["fullversion"].as()) == 0 && strcmp(NUKI_HUB_BUILD, doc["beta"]["build"].as()) == 0 && strcmp(NUKI_HUB_DATE, doc["beta"]["time"].as()) == 0) { - Log->println(("Nuki Hub is already on the latest beta version, OTA update aborted.")); + Log->println("Nuki Hub is already on the latest beta version, OTA update aborted."); } else { _preferences->putString(preference_ota_updater_url, GITHUB_BETA_UPDATER_BINARY_URL); _preferences->putString(preference_ota_main_url, GITHUB_BETA_RELEASE_BINARY_URL); - Log->println(("Updating to latest beta version.")); + Log->println("Updating to latest beta version."); delay(200); restartEsp(RestartReason::OTAReboot); } @@ -989,13 +1009,13 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns { if(strcmp(NUKI_HUB_VERSION, doc["master"]["fullversion"].as()) == 0 && strcmp(NUKI_HUB_BUILD, doc["master"]["build"].as()) == 0 && strcmp(NUKI_HUB_DATE, doc["master"]["time"].as()) == 0) { - Log->println(("Nuki Hub is already on the latest development version, OTA update aborted.")); + Log->println("Nuki Hub is already on the latest development version, OTA update aborted."); } else { _preferences->putString(preference_ota_updater_url, GITHUB_MASTER_UPDATER_BINARY_URL); _preferences->putString(preference_ota_main_url, GITHUB_MASTER_RELEASE_BINARY_URL); - Log->println(("Updating to latest developmemt version.")); + Log->println("Updating to latest developmemt version."); delay(200); restartEsp(RestartReason::OTAReboot); } @@ -1004,13 +1024,13 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns { if(strcmp(NUKI_HUB_VERSION, doc["release"]["fullversion"].as()) == 0 && strcmp(NUKI_HUB_BUILD, doc["release"]["build"].as()) == 0 && strcmp(NUKI_HUB_DATE, doc["release"]["time"].as()) == 0) { - Log->println(("Nuki Hub is already on the latest release version, OTA update aborted.")); + Log->println("Nuki Hub is already on the latest release version, OTA update aborted."); } else { _preferences->putString(preference_ota_updater_url, GITHUB_LATEST_UPDATER_BINARY_URL); _preferences->putString(preference_ota_main_url, GITHUB_LATEST_RELEASE_BINARY_URL); - Log->println(("Updating to latest release version.")); + Log->println("Updating to latest release version."); delay(200); restartEsp(RestartReason::OTAReboot); } @@ -1018,7 +1038,7 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns } else { - Log->println(("Failed to retrieve OTA manifest, OTA update aborted.")); + Log->println("Failed to retrieve OTA manifest, OTA update aborted."); } } else if(comparePrefixedPath(topic, mqtt_topic_webserver_action) && !mqttRecentlyConnected()) @@ -1035,7 +1055,7 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns { return; } - Log->println(("Webserver enabled, restarting.")); + Log->println("Webserver enabled, restarting."); _preferences->putBool(preference_webserver_enabled, true); } else if (strcmp(data, "0") == 0) @@ -1044,20 +1064,194 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns { return; } - Log->println(("Webserver disabled, restarting.")); + Log->println("Webserver disabled, restarting."); _preferences->putBool(preference_webserver_enabled, false); } clearWifiFallback(); delay(200); restartEsp(RestartReason::ReconfigureWebServer); } + else if(comparePrefixedPath(topic, mqtt_topic_nuki_hub_config_action) && !mqttRecentlyConnected()) + { + if(strcmp(data, "") == 0 || strcmp(data, "--") == 0) + { + return; + } + else + { + Log->println("JSON config update received"); + JsonDocument doc; + + DeserializationError error = deserializeJson(doc, data); + if (error) + { + Log->println("Invalid JSON for import/export"); + publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action_command_result, "{\"error\": \"jsonInvalid\"}", false); + publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action, "--", true); + } + else + { + if(_preferences->getBool(preference_cred_duo_approval, false) && (_importExport->getTOTPEnabled() || _importExport->getDuoEnabled())) + { + if(_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); + return; + } + } + else if (!timeSynced) + { + publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action_command_result, "{\"error\": \"duoTimeNotSynced\"}", false); + publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action, "--", true); + return; + } + else if (_importExport->startDuoAuth((char*)"Approve Nuki Hub setting change")) + { + int duoResult = 2; + + while (duoResult == 2) + { + duoResult = _importExport->checkDuoApprove(); + delay(2000); + esp_task_wdt_reset(); + } + + if (duoResult != 1) + { + publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action_command_result, "{\"error\": \"duoApprovalFailed\"}", false); + publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action, "--", true); + return; + } + } + } + + if(!doc["exportHTTPS"].isNull() && _device->isEncrypted()) + { + if(_preferences->getBool(preference_publish_config, false)) + { + 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); + } + } + else + { + publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action_command_result, "{\"error\": \"mqttExportNotEncrypted\"}", false); + } + } + else + { + publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action_command_result, "{\"error\": \"mqttExportNotEnabled\"}", false); + } + } + else if(!doc["exportMQTTS"].isNull()) + { + if(_preferences->getBool(preference_publish_config, false)) + { + if(_device->isEncrypted()) + { + JsonDocument json; + _importExport->exportMqttsJson(json); + serializeJson(json, _buffer, _bufferSize); + publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_json, _buffer, false); + + if (doc["exportMQTTS"].as() > 0) + { + _overwriteNukiHubConfigTS = espMillis() + (doc["exportMQTTS"].as() * 1000); + } + } + else + { + publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action_command_result, "{\"error\": \"mqttExportNotEncrypted\"}", false); + } + } + else + { + publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action_command_result, "{\"error\": \"mqttExportNotEnabled\"}", false); + } + } + else if(!doc["exportNH"].isNull()) + { + if(_preferences->getBool(preference_publish_config, false)) + { + bool redacted = false; + if(!doc["redacted"].isNull()) + { + if(_device->isEncrypted()) + { + redacted = true; + } + else + { + publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action_command_result, "{\"error\": \"mqttExportNotEncrypted\"}", false); + } + } + bool pairing = false; + if(!doc["pairing"].isNull()) + { + if(_device->isEncrypted()) + { + pairing = true; + } + else + { + publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action_command_result, "{\"error\": \"mqttExportNotEncrypted\"}", false); + } + } + JsonDocument json; + _importExport->exportNukiHubJson(json, redacted, pairing, _preferences->getBool(preference_lock_enabled, true), _preferences->getBool(preference_opener_enabled, false)); + serializeJson(json, _buffer, _bufferSize); + publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_json, _buffer, false); + + if (doc["exportNH"].as() > 0) + { + _overwriteNukiHubConfigTS = espMillis() + (doc["exportNH"].as() * 1000); + } + } + else + { + publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action_command_result, "{\"error\": \"mqttExportNotEnabled\"}", false); + } + } + else + { + if(_preferences->getBool(preference_config_from_mqtt, false)) + { + JsonDocument json; + json = _importExport->importJson(doc); + serializeJson(json, _buffer, _bufferSize); + publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_json, _buffer, false); + publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action, "--", true); + delay(200); + restartEsp(RestartReason::ConfigurationUpdated); + } + else + { + publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action_command_result, "{\"error\": \"mqttImportNotEnabled\"}", false); + } + } + publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action, "--", true); + } + } + } } void NukiNetwork::parseGpioTopics(const espMqttClientTypes::MessageProperties &properties, const char *topic, const uint8_t *payload, size_t& len, size_t& index, size_t& total) { char gpioPath[250]; buildMqttPath(gpioPath, {_lockPath.c_str(), mqtt_topic_gpio_prefix, mqtt_topic_gpio_pin}); -// /nuki_t/gpio/pin_17/state + size_t gpioLen = strlen(gpioPath); if(strncmp(gpioPath, topic, gpioLen) == 0) { @@ -1073,9 +1267,9 @@ void NukiNetwork::parseGpioTopics(const espMqttClientTypes::MessageProperties &p if(_gpio->getPinRole(pin) == PinRole::GeneralOutput) { const uint8_t pinState = strcmp((const char*)payload, "1") == 0 ? HIGH : LOW; - Log->print(("GPIO ")); + Log->print("GPIO "); Log->print(pin); - Log->print((" (Output) --> ")); + Log->print(" (Output) --> "); Log->println(pinState); digitalWrite(pin, pinState); } @@ -1177,7 +1371,7 @@ void NukiNetwork::removeTopic(const String& mqttPath, const String& mqttTopic) publish(path.c_str(), "", true); #ifdef DEBUG_NUKIHUB - Log->print(("Removing MQTT topic: ")); + Log->print("Removing MQTT topic: "); Log->println(path.c_str()); #endif } diff --git a/src/NukiNetwork.h b/src/NukiNetwork.h index 3f68481..860c279 100644 --- a/src/NukiNetwork.h +++ b/src/NukiNetwork.h @@ -16,6 +16,7 @@ #include #include "NukiConstants.h" #include "HomeAssistantDiscovery.h" +#include "ImportExport.h" #endif class NukiNetwork @@ -42,7 +43,7 @@ public: #ifdef NUKI_HUB_UPDATER explicit NukiNetwork(Preferences* preferences); #else - explicit NukiNetwork(Preferences* preferences, Gpio* gpio, const String& maintenancePathPrefix, char* buffer, size_t bufferSize); + explicit NukiNetwork(Preferences* preferences, Gpio* gpio, const String& maintenancePathPrefix, char* buffer, size_t bufferSize, ImportExport* importExport); void registerMqttReceiver(MqttReceiver* receiver); void disableAutoRestarts(); // disable on OTA start @@ -128,13 +129,14 @@ private: String _lockPath; HomeAssistantDiscovery* _hadiscovery = nullptr; - + ImportExport* _importExport; Gpio* _gpio; int _mqttConnectionState = 0; int _mqttConnectCounter = 0; int _mqttPort = 1883; long _mqttConnectedTs = -1; + long _overwriteNukiHubConfigTS = -1; bool _connectReplyReceived = false; bool _firstDisconnected = true; diff --git a/src/NukiNetworkLock.cpp b/src/NukiNetworkLock.cpp index 4a79b01..b7189c4 100644 --- a/src/NukiNetworkLock.cpp +++ b/src/NukiNetworkLock.cpp @@ -237,7 +237,7 @@ void NukiNetworkLock::onMqttDataReceived(const char* topic, byte* payload, const return; } - Log->print(("Lock action received: ")); + Log->print("Lock action received: "); Log->println(data); LockActionResult lockActionResult = LockActionResult::Failed; if(_lockActionReceivedCallback != NULL) diff --git a/src/NukiNetworkOpener.cpp b/src/NukiNetworkOpener.cpp index 7031720..8574ebd 100644 --- a/src/NukiNetworkOpener.cpp +++ b/src/NukiNetworkOpener.cpp @@ -175,7 +175,7 @@ void NukiNetworkOpener::onMqttDataReceived(const char* topic, byte* payload, con return; } - Log->print(("Opener action received: ")); + Log->print("Opener action received: "); Log->println(data); LockActionResult lockActionResult = LockActionResult::Failed; if(_lockActionReceivedCallback != NULL) @@ -667,12 +667,12 @@ void NukiNetworkOpener::publishAuthorizationInfo(const std::listprintln(("Nuki opener: Ring detected (Locked)")); + Log->println("Nuki opener: Ring detected (Locked)"); publishRing(true); } else { - Log->println(("Nuki opener: Ring detected (Open)")); + Log->println("Nuki opener: Ring detected (Open)"); publishRing(false); } } diff --git a/src/NukiOfficial.cpp b/src/NukiOfficial.cpp index 7628dd7..5c778ab 100644 --- a/src/NukiOfficial.cpp +++ b/src/NukiOfficial.cpp @@ -86,15 +86,15 @@ void NukiOfficial::onOfficialUpdateReceived(const char *topic, const char *value memset(&str, 0, sizeof(str)); Log->println("Official Nuki change received"); - Log->print(("Topic: ")); + Log->print("Topic: "); Log->println(topic); - Log->print(("Value: ")); + Log->print("Value: "); Log->println(value); if(strcmp(topic, mqtt_topic_official_connected) == 0) { - Log->print(("Connected: ")); - Log->println((strcmp(value, "true") == 0 ? 1 : 0)); + Log->print("Connected: "); + Log->println(strcmp(value, "true") == 0 ? 1 : 0); offConnected = (strcmp(value, "true") == 0 ? 1 : 0); _publisher->publishBool(mqtt_topic_hybrid_state, offConnected, true); } @@ -102,12 +102,12 @@ void NukiOfficial::onOfficialUpdateReceived(const char *topic, const char *value { offState = atoi(value); _statusUpdated = true; - Log->println(("Lock: Updating status on Hybrid state change")); + Log->println("Lock: Updating status on Hybrid state change"); _publisher->publishBool(mqtt_topic_hybrid_state, offConnected, true); NukiLock::lockstateToString((NukiLock::LockState)offState, str); _publisher->publishString(mqtt_topic_lock_state, str, true); - Log->print(("Lockstate: ")); + Log->print("Lockstate: "); Log->println(str); _offStateToPublish = (NukiLock::LockState)offState; @@ -117,11 +117,11 @@ void NukiOfficial::onOfficialUpdateReceived(const char *topic, const char *value { offDoorsensorState = atoi(value); _statusUpdated = true; - Log->println(("Lock: Updating status on Hybrid door sensor state change")); + Log->println("Lock: Updating status on Hybrid door sensor state change"); _publisher->publishBool(mqtt_topic_lock_status_updated, _statusUpdated, true); NukiLock::doorSensorStateToString((NukiLock::DoorSensorState)offDoorsensorState, str); - Log->print(("Doorsensor state: ")); + Log->print("Doorsensor state: "); Log->println(str); _publisher->publishString(mqtt_topic_lock_door_sensor_state, str, true); @@ -130,7 +130,7 @@ void NukiOfficial::onOfficialUpdateReceived(const char *topic, const char *value { offCritical = (strcmp(value, "true") == 0 ? 1 : 0); - Log->print(("Battery critical: ")); + Log->print("Battery critical: "); Log->println(offCritical); if(!_disableNonJSON) @@ -143,7 +143,7 @@ void NukiOfficial::onOfficialUpdateReceived(const char *topic, const char *value { offCharging = (strcmp(value, "true") == 0 ? 1 : 0); - Log->print(("Battery charging: ")); + Log->print("Battery charging: "); Log->println(offCharging); if(!_disableNonJSON) @@ -156,7 +156,7 @@ void NukiOfficial::onOfficialUpdateReceived(const char *topic, const char *value { offChargeState = atoi(value); - Log->print(("Battery level: ")); + Log->print("Battery level: "); Log->println(offChargeState); if(!_disableNonJSON) diff --git a/src/NukiOpenerWrapper.cpp b/src/NukiOpenerWrapper.cpp index 807bcc7..2706dc8 100644 --- a/src/NukiOpenerWrapper.cpp +++ b/src/NukiOpenerWrapper.cpp @@ -14,14 +14,16 @@ NukiOpenerWrapper* nukiOpenerInst; Preferences* nukiOpenerPreferences = nullptr; -NukiOpenerWrapper::NukiOpenerWrapper(const std::string& deviceName, NukiDeviceId* deviceId, BleScanner::Scanner* scanner, NukiNetworkOpener* network, Gpio* gpio, Preferences* preferences) +NukiOpenerWrapper::NukiOpenerWrapper(const std::string& deviceName, NukiDeviceId* deviceId, BleScanner::Scanner* scanner, NukiNetworkOpener* network, Gpio* gpio, Preferences* preferences, char* buffer, size_t bufferSize) : _deviceName(deviceName), _deviceId(deviceId), _nukiOpener(deviceName, _deviceId->get()), _bleScanner(scanner), _network(network), _gpio(gpio), - _preferences(preferences) + _preferences(preferences), + _buffer(buffer), + _bufferSize(bufferSize) { Log->print("Device id opener: "); Log->println(_deviceId->get()); @@ -83,7 +85,7 @@ void NukiOpenerWrapper::readSettings() #else if(pwrLvl >= 20) { - powerLevel = ESP_PWR_LVL_P20; + powerLevel = ESP_PWR_LVL_P20; } else if(pwrLvl >= 18) { @@ -199,11 +201,11 @@ void NukiOpenerWrapper::readSettings() _preferences->putInt(preference_restart_ble_beacon_lost, _restartBeaconTimeout); } - Log->print(("Opener state interval: ")); + Log->print("Opener state interval: "); Log->print(_intervalLockstate); - Log->print((" | Battery interval: ")); + Log->print(" | Battery interval: "); Log->print(_intervalBattery); - Log->print((" | Publish auth data: ")); + Log->print(" | Publish auth data: "); Log->println(_publishAuthData ? "yes" : "no"); if(!_publishAuthData) @@ -220,7 +222,7 @@ void NukiOpenerWrapper::update() wdt_hal_write_protect_enable(&rtc_wdt_ctx); if(!_paired) { - Log->println(("Nuki opener start pairing")); + Log->println("Nuki opener start pairing"); _network->publishBleAddress(""); Nuki::AuthorizationIdType idType = _preferences->getBool(preference_register_opener_as_app) ? @@ -229,7 +231,7 @@ void NukiOpenerWrapper::update() if(_nukiOpener.pairNuki(idType) == NukiOpener::PairingResult::Success) { - Log->println(("Nuki opener paired")); + Log->println("Nuki opener paired"); _paired = true; _network->publishBleAddress(_nukiOpener.getBleAddress().toString()); } @@ -272,14 +274,14 @@ void NukiOpenerWrapper::update() _network->publishCommandResult(resultStr); - Log->print(("Opener action result: ")); + Log->print("Opener action result: "); Log->println(resultStr); if(cmdResult != Nuki::CmdResult::Success) { - Log->print(("Opener: Last command failed, retrying after ")); + Log->print("Opener: Last command failed, retrying after "); Log->print(_retryDelay); - Log->print((" milliseconds. Retry ")); + Log->print(" milliseconds. Retry "); Log->print(retryCount + 1); Log->print(" of "); Log->println(_nrOfRetries); @@ -299,7 +301,7 @@ void NukiOpenerWrapper::update() _network->publishRetry("--"); retryCount = 0; _statusUpdated = true; - Log->println(("Opener: updating status after action")); + Log->println("Opener: updating status after action"); _statusUpdatedTs = ts; if(_intervalLockstate > 10) { @@ -308,7 +310,7 @@ void NukiOpenerWrapper::update() } else { - Log->println(("Opener: Maximum number of retries exceeded, aborting.")); + Log->println("Opener: Maximum number of retries exceeded, aborting."); _network->publishRetry("failed"); retryCount = 0; _nextLockAction = (NukiOpener::LockAction) 0xff; @@ -434,11 +436,6 @@ void NukiOpenerWrapper::deactivateCM() _nextLockAction = NukiOpener::LockAction::DeactivateCM; } -bool NukiOpenerWrapper::isPinSet() -{ - return _nukiOpener.getSecurityPincode() != 0; -} - bool NukiOpenerWrapper::isPinValid() { return _preferences->getInt(preference_opener_pin_status, (int)NukiPinState::NotConfigured) == (int)NukiPinState::Valid; @@ -477,7 +474,7 @@ bool NukiOpenerWrapper::updateKeyTurnerState() while(result != Nuki::CmdResult::Success && retryCount < _nrOfRetries + 1) { - Log->print(("Result (attempt ")); + Log->print("Result (attempt "); Log->print(retryCount + 1); Log->print("): "); result =_nukiOpener.requestOpenerState(&_keyTurnerState); @@ -496,7 +493,7 @@ bool NukiOpenerWrapper::updateKeyTurnerState() postponeBleWatchdog(); if(_retryLockstateCount < _nrOfRetries + 1) { - Log->print(("Query opener state retrying in ")); + Log->print("Query opener state retrying in "); Log->print(_retryDelay); Log->println("ms"); _nextLockStateUpdateTs = espMillis() + _retryDelay; @@ -519,7 +516,7 @@ bool NukiOpenerWrapper::updateKeyTurnerState() _lastKeyTurnerState.lockState == NukiOpener::LockState::Locked && _lastKeyTurnerState.nukiState == _keyTurnerState.nukiState) { - Log->println(("Nuki opener: Ring detected (Locked)")); + Log->println("Nuki opener: Ring detected (Locked)"); _network->publishRing(true); } else @@ -529,17 +526,17 @@ bool NukiOpenerWrapper::updateKeyTurnerState() _keyTurnerState.lockState == NukiOpener::LockState::Open && _keyTurnerState.trigger == NukiOpener::Trigger::Manual) { - Log->println(("Nuki opener: Ring detected (Open)")); + Log->println("Nuki opener: Ring detected (Open)"); _network->publishRing(false); } if(_publishAuthData) { - Log->println(("Publishing auth data")); + Log->println("Publishing auth data"); updateAuthData(false); - Log->println(("Done publishing auth data")); + Log->println("Done publishing auth data"); } - + if(_keyTurnerState.lockState == NukiOpener::LockState::Undefined) { if (_nextLockStateUpdateTs > espMillis() + 60000) @@ -554,12 +551,12 @@ bool NukiOpenerWrapper::updateKeyTurnerState() if((_keyTurnerState.lockState == NukiOpener::LockState::Open || _keyTurnerState.lockState == NukiOpener::LockState::Opening) && espMillis() < _statusUpdatedTs + 10000) { updateStatus = true; - Log->println(("Opener: Keep updating status on intermediate lock state")); + Log->println("Opener: Keep updating status on intermediate lock state"); } if(_keyTurnerState.nukiState == NukiOpener::State::ContinuousMode) { - Log->println(("Continuous Mode")); + Log->println("Continuous Mode"); } char lockStateStr[20]; @@ -568,7 +565,7 @@ bool NukiOpenerWrapper::updateKeyTurnerState() } postponeBleWatchdog(); - Log->println(("Done querying opener state")); + Log->println("Done querying opener state"); return updateStatus; } @@ -579,7 +576,7 @@ void NukiOpenerWrapper::updateBatteryState() while(retryCount < _nrOfRetries + 1) { - Log->print(("Querying opener battery state: ")); + Log->print("Querying opener battery state: "); result = _nukiOpener.requestBatteryReport(&_batteryReport); delay(250); if(result != Nuki::CmdResult::Success) @@ -598,7 +595,7 @@ void NukiOpenerWrapper::updateBatteryState() _network->publishBatteryReport(_batteryReport); } postponeBleWatchdog(); - Log->println(("Done querying opener battery state")); + Log->println("Done querying opener battery state"); } void NukiOpenerWrapper::updateConfig() @@ -613,7 +610,7 @@ void NukiOpenerWrapper::updateConfig() { char uidString[20]; itoa(_nukiConfig.nukiId, uidString, 16); - Log->print(("Saving Opener Nuki ID to preferences (")); + Log->print("Saving Opener Nuki ID to preferences ("); Log->print(_nukiConfig.nukiId); Log->print(" / "); Log->print(uidString); @@ -642,61 +639,49 @@ void NukiOpenerWrapper::updateConfig() const int pinStatus = _preferences->getInt(preference_opener_pin_status, (int)NukiPinState::NotConfigured); - if(isPinSet()) + Nuki::CmdResult result = (Nuki::CmdResult)-1; + int retryCount = 0; + + while(retryCount < _nrOfRetries + 1) { - Nuki::CmdResult result = (Nuki::CmdResult)-1; - int retryCount = 0; - Log->println(("Nuki opener PIN is set")); - - while(retryCount < _nrOfRetries + 1) - { - result = _nukiOpener.verifySecurityPin(); - - if(result != Nuki::CmdResult::Success) - { - ++retryCount; - } - else - { - break; - } - } + result = _nukiOpener.verifySecurityPin(); if(result != Nuki::CmdResult::Success) { - Log->println(("Nuki opener PIN is invalid")); - if(pinStatus != 2) - { - _preferences->putInt(preference_opener_pin_status, (int)NukiPinState::Invalid); - } + ++retryCount; } else { - Log->println(("Nuki opener PIN is valid")); - if(pinStatus != 1) - { - _preferences->putInt(preference_opener_pin_status, (int)NukiPinState::Valid); - } + break; + } + } + + if(result != Nuki::CmdResult::Success) + { + Log->println("Nuki opener PIN is invalid or not set"); + if(pinStatus != 2) + { + _preferences->putInt(preference_opener_pin_status, (int)NukiPinState::Invalid); } } else { - Log->println(("Nuki opener PIN is not set")); - if(pinStatus != 0) + Log->println("Nuki opener PIN is valid"); + if(pinStatus != 1) { - _preferences->putInt(preference_opener_pin_status, (int)NukiPinState::NotSet); + _preferences->putInt(preference_opener_pin_status, (int)NukiPinState::Valid); } } } else { - Log->println(("Invalid/Unexpected opener config received, ID does not matched saved ID")); + Log->println("Invalid/Unexpected opener config received, ID does not matched saved ID"); expectedConfig = false; } } else { - Log->println(("Invalid/Unexpected opener config received, Config is not valid")); + Log->println("Invalid/Unexpected opener config received, Config is not valid"); expectedConfig = false; } @@ -713,7 +698,7 @@ void NukiOpenerWrapper::updateConfig() } else { - Log->println(("Invalid/Unexpected opener advanced config received, Advanced config is not valid")); + Log->println("Invalid/Unexpected opener advanced config received, Advanced config is not valid"); expectedConfig = false; } } @@ -721,12 +706,12 @@ void NukiOpenerWrapper::updateConfig() if(expectedConfig && _nukiConfigValid && _nukiAdvancedConfigValid) { _retryConfigCount = 0; - Log->println(("Done retrieving opener config and advanced config")); + Log->println("Done retrieving opener config and advanced config"); } else { ++_retryConfigCount; - Log->println(("Invalid/Unexpected opener config and/or advanced config received, retrying in 10 seconds")); + Log->println("Invalid/Unexpected opener config and/or advanced config received, retrying in 10 seconds"); int64_t ts = espMillis(); _nextConfigUpdateTs = ts + 10000; } @@ -736,7 +721,7 @@ void NukiOpenerWrapper::updateAuthData(bool retrieved) { if(!isPinValid()) { - Log->println(("No valid PIN set")); + Log->println("No valid PIN set"); return; } @@ -747,7 +732,7 @@ void NukiOpenerWrapper::updateAuthData(bool retrieved) while(retryCount < _nrOfRetries + 1) { - Log->print(("Retrieve log entries: ")); + Log->print("Retrieve log entries: "); result = _nukiOpener.retrieveLogEntries(0, _preferences->getInt(preference_authlog_max_entries, MAX_AUTHLOG), 1, false); if(result != Nuki::CmdResult::Success) @@ -801,7 +786,7 @@ void NukiOpenerWrapper::updateAuthData(bool retrieved) return a.index < b.index; }); - Log->print(("Log size: ")); + Log->print("Log size: "); Log->println(log.size()); if(log.size() > 0) @@ -822,7 +807,7 @@ void NukiOpenerWrapper::updateKeypad(bool retrieved) if(!isPinValid()) { - Log->println(("No valid Nuki Opener PIN set")); + Log->println("No valid Nuki Opener PIN set"); return; } @@ -833,7 +818,7 @@ void NukiOpenerWrapper::updateKeypad(bool retrieved) while(retryCount < _nrOfRetries + 1) { - Log->print(("Querying opener keypad: ")); + Log->print("Querying opener keypad: "); result = _nukiOpener.retrieveKeypadEntries(0, _preferences->getInt(preference_keypad_max_entries, MAX_KEYPAD)); if(result != Nuki::CmdResult::Success) @@ -857,7 +842,7 @@ void NukiOpenerWrapper::updateKeypad(bool retrieved) std::list entries; _nukiOpener.getKeypadEntries(&entries); - Log->print(("Opener keypad codes: ")); + Log->print("Opener keypad codes: "); Log->println(entries.size()); entries.sort([](const NukiOpener::KeypadEntry& a, const NukiOpener::KeypadEntry& b) @@ -902,7 +887,7 @@ void NukiOpenerWrapper::updateTimeControl(bool retrieved) if(!isPinValid()) { - Log->println(("No valid Nuki Opener PIN set")); + Log->println("No valid Nuki Opener PIN set"); return; } @@ -913,7 +898,7 @@ void NukiOpenerWrapper::updateTimeControl(bool retrieved) while(retryCount < _nrOfRetries + 1) { - Log->print(("Querying opener timecontrol: ")); + Log->print("Querying opener timecontrol: "); result = _nukiOpener.retrieveTimeControlEntries(); if(result != Nuki::CmdResult::Success) @@ -937,7 +922,7 @@ void NukiOpenerWrapper::updateTimeControl(bool retrieved) std::list timeControlEntries; _nukiOpener.getTimeControlEntries(&timeControlEntries); - Log->print(("Opener timecontrol entries: ")); + Log->print("Opener timecontrol entries: "); Log->println(timeControlEntries.size()); timeControlEntries.sort([](const NukiOpener::TimeControlEntry& a, const NukiOpener::TimeControlEntry& b) @@ -974,7 +959,7 @@ void NukiOpenerWrapper::updateAuth(bool retrieved) { if(!isPinValid()) { - Log->println(("No valid Nuki Lock PIN set")); + Log->println("No valid Nuki Lock PIN set"); return; } @@ -990,7 +975,7 @@ void NukiOpenerWrapper::updateAuth(bool retrieved) while(retryCount < _nrOfRetries) { - Log->print(("Querying opener authorization: ")); + Log->print("Querying opener authorization: "); result = _nukiOpener.retrieveAuthorizationEntries(0, _preferences->getInt(preference_auth_max_entries, MAX_AUTH)); delay(250); if(result != Nuki::CmdResult::Success) @@ -1014,7 +999,7 @@ void NukiOpenerWrapper::updateAuth(bool retrieved) std::list authEntries; _nukiOpener.getAuthorizationEntries(&authEntries); - Log->print(("Opener authorization entries: ")); + Log->print("Opener authorization entries: "); Log->println(authEntries.size()); authEntries.sort([](const NukiOpener::AuthorizationEntry& a, const NukiOpener::AuthorizationEntry& b) @@ -1560,23 +1545,21 @@ Nuki::BatteryType NukiOpenerWrapper::batteryTypeToEnum(const char* str) void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) { - JsonDocument jsonResult; - char _resbuf[2048]; if(!_nukiConfigValid) { jsonResult["general"] = "configNotReady"; - serializeJson(jsonResult, _resbuf, sizeof(_resbuf)); - _network->publishConfigCommandResult(_resbuf); + serializeJson(jsonResult, _buffer, _bufferSize); + _network->publishConfigCommandResult(_buffer); return; } if(!isPinValid()) { jsonResult["general"] = "noValidPinSet"; - serializeJson(jsonResult, _resbuf, sizeof(_resbuf)); - _network->publishConfigCommandResult(_resbuf); + serializeJson(jsonResult, _buffer, _bufferSize); + _network->publishConfigCommandResult(_buffer); return; } @@ -1586,8 +1569,8 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) if(jsonError) { jsonResult["general"] = "invalidJson"; - serializeJson(jsonResult, _resbuf, sizeof(_resbuf)); - _network->publishConfigCommandResult(_resbuf); + serializeJson(jsonResult, _buffer, _bufferSize); + _network->publishConfigCommandResult(_buffer); return; } @@ -2439,8 +2422,8 @@ void NukiOpenerWrapper::onConfigUpdateReceived(const char *value) _nextConfigUpdateTs = espMillis() + 300; - serializeJson(jsonResult, _resbuf, sizeof(_resbuf)); - _network->publishConfigCommandResult(_resbuf); + serializeJson(jsonResult, _buffer, _bufferSize); + _network->publishConfigCommandResult(_buffer); return; } @@ -2765,7 +2748,7 @@ void NukiOpenerWrapper::onKeypadJsonCommandReceived(const char *value) { auto it1 = std::find(_keypadCodeIds.begin(), _keypadCodeIds.end(), codeId); int index = it1 - _keypadCodeIds.begin(); - Log->print(("Check keypad code: ")); + Log->print("Check keypad code: "); if(code == _keypadCodes[index]) { @@ -2804,7 +2787,7 @@ void NukiOpenerWrapper::onKeypadJsonCommandReceived(const char *value) if(idExists) { result = _nukiOpener.deleteKeypadEntry(codeId); - Log->print(("Delete keypad code: ")); + Log->print("Delete keypad code: "); Log->println((int)result); } else @@ -3011,7 +2994,7 @@ void NukiOpenerWrapper::onKeypadJsonCommandReceived(const char *value) } result = _nukiOpener.addKeypadEntry(entry); - Log->print(("Add keypad code: ")); + Log->print("Add keypad code: "); Log->println((int)result); } else if (strcmp(action, "update") == 0) @@ -3177,7 +3160,7 @@ void NukiOpenerWrapper::onKeypadJsonCommandReceived(const char *value) } result = _nukiOpener.updateKeypadEntry(entry); - Log->print(("Update keypad code: ")); + Log->print("Update keypad code: "); Log->println((int)result); } } @@ -3304,7 +3287,7 @@ void NukiOpenerWrapper::onTimeControlCommandReceived(const char *value) if(idExists) { result = _nukiOpener.removeTimeControlEntry(entryId); - Log->print(("Delete timecontrol: ")); + Log->print("Delete timecontrol: "); Log->println((int)result); } else @@ -3383,7 +3366,7 @@ void NukiOpenerWrapper::onTimeControlCommandReceived(const char *value) entry.lockAction = timeControlLockAction; result = _nukiOpener.addTimeControlEntry(entry); - Log->print(("Add timecontrol: ")); + Log->print("Add timecontrol: "); Log->println((int)result); } else if (strcmp(action, "update") == 0) @@ -3460,7 +3443,7 @@ void NukiOpenerWrapper::onTimeControlCommandReceived(const char *value) entry.lockAction = timeControlLockAction; result = _nukiOpener.updateTimeControlEntry(entry); - Log->print(("Update timecontrol: ")); + Log->print("Update timecontrol: "); Log->println((int)result); } } @@ -3614,7 +3597,7 @@ void NukiOpenerWrapper::onAuthCommandReceived(const char *value) if(idExists) { result = _nukiOpener.deleteAuthorizationEntry(authId); - Log->print(("Delete authorization: ")); + Log->print("Delete authorization: "); Log->println((int)result); } else @@ -3833,7 +3816,7 @@ void NukiOpenerWrapper::onAuthCommandReceived(const char *value) } result = _nukiOpener.addAuthorizationEntry(entry); - Log->print(("Add authorization: ")); + Log->print("Add authorization: "); Log->println((int)result); } else if (strcmp(action, "update") == 0) @@ -3998,7 +3981,7 @@ void NukiOpenerWrapper::onAuthCommandReceived(const char *value) } result = _nukiOpener.updateAuthorizationEntry(entry); - Log->print(("Update authorization: ")); + Log->print("Update authorization: "); Log->println((int)result); } } @@ -4111,7 +4094,7 @@ void NukiOpenerWrapper::readConfig() char resultStr[20]; NukiOpener::cmdResultToString(result, resultStr); - Log->print(("Opener config result: ")); + Log->print("Opener config result: "); Log->print(resultStr); Log->print("("); Log->print(result); @@ -4141,7 +4124,7 @@ void NukiOpenerWrapper::readAdvancedConfig() char resultStr[20]; NukiOpener::cmdResultToString(result, resultStr); - Log->print(("Opener advanced config result: ")); + Log->print("Opener advanced config result: "); Log->println(resultStr); postponeBleWatchdog(); } @@ -4200,7 +4183,7 @@ void NukiOpenerWrapper::updateTime() { if(!isPinValid()) { - Log->println(("No valid PIN set")); + Log->println("No valid PIN set"); return; } @@ -4211,7 +4194,7 @@ void NukiOpenerWrapper::updateTime() if (int(tm.tm_year + 1900) < int(2025)) { - Log->println(("NTP Time not valid, not updating Nuki device")); + Log->println("NTP Time not valid, not updating Nuki device"); return; } @@ -4228,6 +4211,6 @@ void NukiOpenerWrapper::updateTime() char resultStr[15] = {0}; NukiOpener::cmdResultToString(cmdResult, resultStr); - Log->print(("Opener time update result: ")); + Log->print("Opener time update result: "); Log->println(resultStr); } \ No newline at end of file diff --git a/src/NukiOpenerWrapper.h b/src/NukiOpenerWrapper.h index 8c7a4fb..f3caec9 100644 --- a/src/NukiOpenerWrapper.h +++ b/src/NukiOpenerWrapper.h @@ -11,7 +11,7 @@ class NukiOpenerWrapper : public NukiOpener::SmartlockEventHandler { public: - NukiOpenerWrapper(const std::string& deviceName, NukiDeviceId* deviceId, BleScanner::Scanner* scanner, NukiNetworkOpener* network, Gpio* gpio, Preferences* preferences); + NukiOpenerWrapper(const std::string& deviceName, NukiDeviceId* deviceId, BleScanner::Scanner* scanner, NukiNetworkOpener* network, Gpio* gpio, Preferences* preferences, char* buffer, size_t bufferSize); virtual ~NukiOpenerWrapper(); void initialize(); @@ -25,7 +25,6 @@ public: void deactivateRTO(); void deactivateCM(); - bool isPinSet(); bool isPinValid(); void setPin(const uint16_t pin); uint16_t getPin(); @@ -159,4 +158,7 @@ private: std::string _firmwareVersion = ""; std::string _hardwareVersion = ""; NukiOpener::LockAction _nextLockAction = (NukiOpener::LockAction)0xff; + + char* _buffer; + const size_t _bufferSize; }; diff --git a/src/NukiWrapper.cpp b/src/NukiWrapper.cpp index d59f6b2..7ef9542 100644 --- a/src/NukiWrapper.cpp +++ b/src/NukiWrapper.cpp @@ -12,7 +12,7 @@ NukiWrapper* nukiInst = nullptr; -NukiWrapper::NukiWrapper(const std::string& deviceName, NukiDeviceId* deviceId, BleScanner::Scanner* scanner, NukiNetworkLock* network, NukiOfficial* nukiOfficial, Gpio* gpio, Preferences* preferences) +NukiWrapper::NukiWrapper(const std::string& deviceName, NukiDeviceId* deviceId, BleScanner::Scanner* scanner, NukiNetworkLock* network, NukiOfficial* nukiOfficial, Gpio* gpio, Preferences* preferences, char* buffer, size_t bufferSize) : _deviceName(deviceName), _deviceId(deviceId), _bleScanner(scanner), @@ -20,7 +20,9 @@ NukiWrapper::NukiWrapper(const std::string& deviceName, NukiDeviceId* deviceId, _network(network), _nukiOfficial(nukiOfficial), _gpio(gpio), - _preferences(preferences) + _preferences(preferences), + _buffer(buffer), + _bufferSize(bufferSize) { Log->print("Device id lock: "); @@ -213,11 +215,11 @@ void NukiWrapper::readSettings() _preferences->putInt(preference_restart_ble_beacon_lost, _restartBeaconTimeout); } - Log->print(("Lock state interval: ")); + Log->print("Lock state interval: "); Log->print(_intervalLockstate); - Log->print((" | Battery interval: ")); + Log->print(" | Battery interval: "); Log->print(_intervalBattery); - Log->print((" | Publish auth data: ")); + Log->print(" | Publish auth data: "); Log->println(_publishAuthData ? "yes" : "no"); if(!_publishAuthData) @@ -234,8 +236,8 @@ void NukiWrapper::update(bool reboot) wdt_hal_write_protect_enable(&rtc_wdt_ctx); if(!_paired) { - Log->println(("Nuki lock start pairing")); - _preferences->getBool(preference_register_as_app) ? Log->println(("Pairing as app")) : Log->println(("Pairing as bridge")); + Log->println("Nuki lock start pairing"); + _preferences->getBool(preference_register_as_app) ? Log->println("Pairing as app") : Log->println("Pairing as bridge"); _network->publishBleAddress(""); Nuki::AuthorizationIdType idType = _preferences->getBool(preference_register_as_app) ? @@ -244,7 +246,7 @@ void NukiWrapper::update(bool reboot) if(_nukiLock.pairNuki(idType) == Nuki::PairingResult::Success) { - Log->println(("Nuki paired")); + Log->println("Nuki paired"); _paired = true; _network->publishBleAddress(_nukiLock.getBleAddress().toString()); } @@ -291,14 +293,14 @@ void NukiWrapper::update(bool reboot) NukiLock::cmdResultToString(cmdResult, resultStr); _network->publishCommandResult(resultStr); - Log->print(("Lock action result: ")); + Log->print("Lock action result: "); Log->println(resultStr); if(cmdResult != Nuki::CmdResult::Success) { - Log->print(("Lock: Last command failed, retrying after ")); + Log->print("Lock: Last command failed, retrying after "); Log->print(_retryDelay); - Log->print((" milliseconds. Retry ")); + Log->print(" milliseconds. Retry "); Log->print(retryCount + 1); Log->print(" of "); Log->println(_nrOfRetries); @@ -321,7 +323,7 @@ void NukiWrapper::update(bool reboot) { _statusUpdated = true; } - Log->println(("Lock: updating status after action")); + Log->println("Lock: updating status after action"); _statusUpdatedTs = ts; if(_intervalLockstate > 10) { @@ -330,7 +332,7 @@ void NukiWrapper::update(bool reboot) } else { - Log->println(("Lock: Maximum number of retries exceeded, aborting.")); + Log->println("Lock: Maximum number of retries exceeded, aborting."); _network->publishRetry("failed"); retryCount = 0; _nextLockAction = (NukiLock::LockAction) 0xff; @@ -451,18 +453,6 @@ void NukiWrapper::lockngounlatch() _nextLockAction = NukiLock::LockAction::LockNgoUnlatch; } -bool NukiWrapper::isPinSet() -{ - if (_isUltra) - { - return _nukiLock.getUltraPincode() != 0; - } - else - { - return _nukiLock.getSecurityPincode() != 0; - } -} - bool NukiWrapper::isPinValid() { return _preferences->getInt(preference_lock_pin_status, (int)NukiPinState::NotConfigured) == (int)NukiPinState::Valid; @@ -509,13 +499,13 @@ bool NukiWrapper::updateKeyTurnerState() Nuki::CmdResult result = (Nuki::CmdResult)-1; int retryCount = 0; - Log->println(("Querying lock state")); + Log->println("Querying lock state"); while(result != Nuki::CmdResult::Success && retryCount < _nrOfRetries + 1) { - Log->print(("Result (attempt ")); + Log->print("Result (attempt "); Log->print(retryCount + 1); - Log->print(("): ")); + Log->print("): "); result =_nukiLock.requestKeyTurnerState(&_keyTurnerState); ++retryCount; } @@ -532,7 +522,7 @@ bool NukiWrapper::updateKeyTurnerState() postponeBleWatchdog(); if(_retryLockstateCount < _nrOfRetries + 1) { - Log->print(("Query lock state retrying in ")); + Log->print("Query lock state retrying in "); Log->print(_retryDelay); Log->println("ms"); _nextLockStateUpdateTs = espMillis() + _retryDelay; @@ -558,9 +548,9 @@ bool NukiWrapper::updateKeyTurnerState() { if(_publishAuthData && (lockState == NukiLock::LockState::Locked || lockState == NukiLock::LockState::Unlocked)) { - Log->println(("Publishing auth data")); + Log->println("Publishing auth data"); updateAuthData(false); - Log->println(("Done publishing auth data")); + Log->println("Done publishing auth data"); } updateGpioOutputs(); @@ -568,7 +558,7 @@ bool NukiWrapper::updateKeyTurnerState() else if(!_nukiOfficial->getOffConnected() && espMillis() < _statusUpdatedTs + 10000) { updateStatus = true; - Log->println(("Lock: Keep updating status on intermediate lock state")); + Log->println("Lock: Keep updating status on intermediate lock state"); } else if(lockState == NukiLock::LockState::Undefined) { @@ -584,7 +574,7 @@ bool NukiWrapper::updateKeyTurnerState() Log->println(lockStateStr); postponeBleWatchdog(); - Log->println(("Done querying lock state")); + Log->println("Done querying lock state"); return updateStatus; } @@ -597,7 +587,7 @@ void NukiWrapper::updateBatteryState() while(retryCount < _nrOfRetries + 1) { - Log->print(("Result (attempt ")); + Log->print("Result (attempt "); Log->print(retryCount + 1); Log->print("): "); result = _nukiLock.requestBatteryReport(&_batteryReport); @@ -618,7 +608,7 @@ void NukiWrapper::updateBatteryState() _network->publishBatteryReport(_batteryReport); } postponeBleWatchdog(); - Log->println(("Done querying lock battery state")); + Log->println("Done querying lock battery state"); } void NukiWrapper::updateConfig() @@ -633,7 +623,7 @@ void NukiWrapper::updateConfig() { char uidString[20]; itoa(_nukiConfig.nukiId, uidString, 16); - Log->print(("Saving Lock Nuki ID to preferences (")); + Log->print("Saving Lock Nuki ID to preferences ("); Log->print(_nukiConfig.nukiId); Log->print(" / "); Log->print(uidString); @@ -661,60 +651,48 @@ void NukiWrapper::updateConfig() const int pinStatus = _preferences->getInt(preference_lock_pin_status, (int)NukiPinState::NotConfigured); - if(isPinSet()) + Nuki::CmdResult result = (Nuki::CmdResult)-1; + int retryCount = 0; + + while(retryCount < _nrOfRetries + 1) { - Nuki::CmdResult result = (Nuki::CmdResult)-1; - int retryCount = 0; - Log->println(("Nuki Lock PIN is set")); - - while(retryCount < _nrOfRetries + 1) - { - result = _nukiLock.verifySecurityPin(); - if(result != Nuki::CmdResult::Success) - { - ++retryCount; - } - else - { - break; - } - } - + result = _nukiLock.verifySecurityPin(); if(result != Nuki::CmdResult::Success) { - Log->println(("Nuki Lock PIN is invalid")); - if(pinStatus != 2) - { - _preferences->putInt(preference_lock_pin_status, (int)NukiPinState::Invalid); - } + ++retryCount; } else { - Log->println(("Nuki Lock PIN is valid")); - if(pinStatus != 1) - { - _preferences->putInt(preference_lock_pin_status, (int)NukiPinState::Valid); - } + break; + } + } + + if(result != Nuki::CmdResult::Success) + { + Log->println("Nuki Lock PIN is invalid or not set"); + if(pinStatus != 2) + { + _preferences->putInt(preference_lock_pin_status, (int)NukiPinState::Invalid); } } else { - Log->println(("Nuki Lock PIN is not set")); - if(pinStatus != 0) + Log->println("Nuki Lock PIN is valid"); + if(pinStatus != 1) { - _preferences->putInt(preference_lock_pin_status, (int)NukiPinState::NotSet); + _preferences->putInt(preference_lock_pin_status, (int)NukiPinState::Valid); } } } else { - Log->println(("Invalid/Unexpected lock config received, ID does not matched saved ID")); + Log->println("Invalid/Unexpected lock config received, ID does not matched saved ID"); expectedConfig = false; } } else { - Log->println(("Invalid/Unexpected lock config received, Config is not valid")); + Log->println("Invalid/Unexpected lock config received, Config is not valid"); expectedConfig = false; } @@ -731,7 +709,7 @@ void NukiWrapper::updateConfig() } else { - Log->println(("Invalid/Unexpected lock advanced config received, Advanced config is not valid")); + Log->println("Invalid/Unexpected lock advanced config received, Advanced config is not valid"); expectedConfig = false; } } @@ -739,12 +717,12 @@ void NukiWrapper::updateConfig() if(expectedConfig && _nukiConfigValid && _nukiAdvancedConfigValid) { _retryConfigCount = 0; - Log->println(("Done retrieving lock config and advanced config")); + Log->println("Done retrieving lock config and advanced config"); } else { ++_retryConfigCount; - Log->println(("Invalid/Unexpected lock config and/or advanced config received, retrying in 10 seconds")); + Log->println("Invalid/Unexpected lock config and/or advanced config received, retrying in 10 seconds"); int64_t ts = espMillis(); _nextConfigUpdateTs = ts + 10000; } @@ -754,7 +732,7 @@ void NukiWrapper::updateAuthData(bool retrieved) { if(!isPinValid()) { - Log->println(("No valid Nuki Lock PIN set")); + Log->println("No valid Nuki Lock PIN set"); return; } @@ -765,7 +743,7 @@ void NukiWrapper::updateAuthData(bool retrieved) while(retryCount < _nrOfRetries + 1) { - Log->print(("Retrieve log entries: ")); + Log->print("Retrieve log entries: "); result = _nukiLock.retrieveLogEntries(0, _preferences->getInt(preference_authlog_max_entries, MAX_AUTHLOG), 1, false); if(result != Nuki::CmdResult::Success) { @@ -817,7 +795,7 @@ void NukiWrapper::updateAuthData(bool retrieved) return a.index < b.index; }); - Log->print(("Log size: ")); + Log->print("Log size: "); Log->println(log.size()); if(log.size() > 0) @@ -838,7 +816,7 @@ void NukiWrapper::updateKeypad(bool retrieved) if(!isPinValid()) { - Log->println(("No valid Nuki Lock PIN set")); + Log->println("No valid Nuki Lock PIN set"); return; } @@ -849,7 +827,7 @@ void NukiWrapper::updateKeypad(bool retrieved) while(retryCount < _nrOfRetries + 1) { - Log->print(("Querying lock keypad: ")); + Log->print("Querying lock keypad: "); result = _nukiLock.retrieveKeypadEntries(0, _preferences->getInt(preference_keypad_max_entries, MAX_KEYPAD)); if(result != Nuki::CmdResult::Success) { @@ -872,7 +850,7 @@ void NukiWrapper::updateKeypad(bool retrieved) std::list entries; _nukiLock.getKeypadEntries(&entries); - Log->print(("Lock keypad codes: ")); + Log->print("Lock keypad codes: "); Log->println(entries.size()); entries.sort([](const NukiLock::KeypadEntry& a, const NukiLock::KeypadEntry& b) @@ -917,7 +895,7 @@ void NukiWrapper::updateTimeControl(bool retrieved) if(!isPinValid()) { - Log->println(("No valid Nuki Lock PIN set")); + Log->println("No valid Nuki Lock PIN set"); return; } @@ -928,7 +906,7 @@ void NukiWrapper::updateTimeControl(bool retrieved) while(retryCount < _nrOfRetries + 1) { - Log->print(("Querying lock timecontrol: ")); + Log->print("Querying lock timecontrol: "); result = _nukiLock.retrieveTimeControlEntries(); if(result != Nuki::CmdResult::Success) { @@ -951,7 +929,7 @@ void NukiWrapper::updateTimeControl(bool retrieved) std::list timeControlEntries; _nukiLock.getTimeControlEntries(&timeControlEntries); - Log->print(("Lock timecontrol entries: ")); + Log->print("Lock timecontrol entries: "); Log->println(timeControlEntries.size()); timeControlEntries.sort([](const NukiLock::TimeControlEntry& a, const NukiLock::TimeControlEntry& b) @@ -988,7 +966,7 @@ void NukiWrapper::updateAuth(bool retrieved) { if(!isPinValid()) { - Log->println(("No valid Nuki Lock PIN set")); + Log->println("No valid Nuki Lock PIN set"); return; } @@ -1004,7 +982,7 @@ void NukiWrapper::updateAuth(bool retrieved) while(retryCount < _nrOfRetries) { - Log->print(("Querying lock authorization: ")); + Log->print("Querying lock authorization: "); result = _nukiLock.retrieveAuthorizationEntries(0, _preferences->getInt(preference_auth_max_entries, MAX_AUTH)); delay(250); if(result != Nuki::CmdResult::Success) @@ -1028,7 +1006,7 @@ void NukiWrapper::updateAuth(bool retrieved) std::list authEntries; _nukiLock.getAuthorizationEntries(&authEntries); - Log->print(("Lock authorization entries: ")); + Log->print("Lock authorization entries: "); Log->println(authEntries.size()); authEntries.sort([](const NukiLock::AuthorizationEntry& a, const NukiLock::AuthorizationEntry& b) @@ -1496,21 +1474,20 @@ void NukiWrapper::onOfficialUpdateReceived(const char *topic, const char *value) void NukiWrapper::onConfigUpdateReceived(const char *value) { JsonDocument jsonResult; - char _resbuf[2048]; if(!_nukiConfigValid) { jsonResult["general"] = "configNotReady"; - serializeJson(jsonResult, _resbuf, sizeof(_resbuf)); - _network->publishConfigCommandResult(_resbuf); + serializeJson(jsonResult, _buffer, _bufferSize); + _network->publishConfigCommandResult(_buffer); return; } if(!isPinValid()) { jsonResult["general"] = "noValidPinSet"; - serializeJson(jsonResult, _resbuf, sizeof(_resbuf)); - _network->publishConfigCommandResult(_resbuf); + serializeJson(jsonResult, _buffer, _bufferSize); + _network->publishConfigCommandResult(_buffer); return; } @@ -1520,15 +1497,11 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) if(jsonError) { jsonResult["general"] = "invalidJson"; - serializeJson(jsonResult, _resbuf, sizeof(_resbuf)); - _network->publishConfigCommandResult(_resbuf); + serializeJson(jsonResult, _buffer, _bufferSize); + _network->publishConfigCommandResult(_buffer); return; } - Log->println(value); - serializeJson(json, _resbuf, sizeof(_resbuf)); - Log->println(_resbuf); - Nuki::CmdResult cmdResult; const char *basicKeys[16] = {"name", "latitude", "longitude", "autoUnlatch", "pairingEnabled", "buttonEnabled", "ledEnabled", "ledBrightness", "timeZoneOffset", "dstMode", "fobAction1", "fobAction2", "fobAction3", "singleLock", "advertisingMode", "timeZone"}; const char *advancedKeys[25] = {"unlockedPositionOffsetDegrees", "lockedPositionOffsetDegrees", "singleLockedPositionOffsetDegrees", "unlockedToLockedTransitionOffsetDegrees", "lockNgoTimeout", "singleButtonPressAction", "doubleButtonPressAction", "detachedCylinder", "batteryType", "automaticBatteryTypeDetection", "unlatchDuration", "autoLockTimeOut", "autoUnLockDisabled", "nightModeEnabled", "nightModeStartTime", "nightModeEndTime", "nightModeAutoLockEnabled", "nightModeAutoUnlockDisabled", "nightModeImmediateLockOnStart", "autoLockEnabled", "immediateAutoLockEnabled", "autoUpdateEnabled", "rebootNuki", "motorSpeed", "enableSlowSpeedDuringNightMode"}; @@ -2501,8 +2474,8 @@ void NukiWrapper::onConfigUpdateReceived(const char *value) _nextConfigUpdateTs = espMillis() + 300; - serializeJson(jsonResult, _resbuf, sizeof(_resbuf)); - _network->publishConfigCommandResult(_resbuf); + serializeJson(jsonResult, _buffer, _bufferSize); + _network->publishConfigCommandResult(_buffer); return; } @@ -2875,7 +2848,7 @@ void NukiWrapper::onKeypadJsonCommandReceived(const char *value) { auto it1 = std::find(_keypadCodeIds.begin(), _keypadCodeIds.end(), codeId); int index = it1 - _keypadCodeIds.begin(); - Log->print(("Check keypad code: ")); + Log->print("Check keypad code: "); if(code == _keypadCodes[index]) { @@ -2915,7 +2888,7 @@ void NukiWrapper::onKeypadJsonCommandReceived(const char *value) if(idExists) { result = _nukiLock.deleteKeypadEntry(codeId); - Log->print(("Delete keypad code: ")); + Log->print("Delete keypad code: "); Log->println((int)result); } else @@ -3122,7 +3095,7 @@ void NukiWrapper::onKeypadJsonCommandReceived(const char *value) } result = _nukiLock.addKeypadEntry(entry); - Log->print(("Add keypad code: ")); + Log->print("Add keypad code: "); Log->println((int)result); } else if (strcmp(action, "update") == 0) @@ -3288,7 +3261,7 @@ void NukiWrapper::onKeypadJsonCommandReceived(const char *value) } result = _nukiLock.updateKeypadEntry(entry); - Log->print(("Update keypad code: ")); + Log->print("Update keypad code: "); Log->println((int)result); } } @@ -3415,7 +3388,7 @@ void NukiWrapper::onTimeControlCommandReceived(const char *value) if(idExists) { result = _nukiLock.removeTimeControlEntry(entryId); - Log->print(("Delete timecontrol: ")); + Log->print("Delete timecontrol: "); Log->println((int)result); } else @@ -3495,7 +3468,7 @@ void NukiWrapper::onTimeControlCommandReceived(const char *value) entry.lockAction = timeControlLockAction; result = _nukiLock.addTimeControlEntry(entry); - Log->print(("Add timecontrol: ")); + Log->print("Add timecontrol: "); Log->println((int)result); } else if (strcmp(action, "update") == 0) @@ -3573,7 +3546,7 @@ void NukiWrapper::onTimeControlCommandReceived(const char *value) entry.lockAction = timeControlLockAction; result = _nukiLock.updateTimeControlEntry(entry); - Log->print(("Update timecontrol: ")); + Log->print("Update timecontrol: "); Log->println((int)result); } } @@ -3728,7 +3701,7 @@ void NukiWrapper::onAuthCommandReceived(const char *value) { result = _nukiLock.deleteAuthorizationEntry(authId); delay(250); - Log->print(("Delete authorization: ")); + Log->print("Delete authorization: "); Log->println((int)result); } else @@ -3948,7 +3921,7 @@ void NukiWrapper::onAuthCommandReceived(const char *value) result = _nukiLock.addAuthorizationEntry(entry); delay(250); - Log->print(("Add authorization: ")); + Log->print("Add authorization: "); Log->println((int)result); } else if (strcmp(action, "update") == 0) @@ -4114,7 +4087,7 @@ void NukiWrapper::onAuthCommandReceived(const char *value) result = _nukiLock.updateAuthorizationEntry(entry); delay(250); - Log->print(("Update authorization: ")); + Log->print("Update authorization: "); Log->println((int)result); } } @@ -4218,7 +4191,7 @@ void NukiWrapper::readConfig() char resultStr[20]; NukiLock::cmdResultToString(result, resultStr); - Log->print(("Lock config result: ")); + Log->print("Lock config result: "); Log->println(resultStr); if(result != Nuki::CmdResult::Success) @@ -4246,7 +4219,7 @@ void NukiWrapper::readAdvancedConfig() char resultStr[20]; NukiLock::cmdResultToString(result, resultStr); - Log->print(("Lock advanced config result: ")); + Log->print("Lock advanced config result: "); Log->println(resultStr); if(result != Nuki::CmdResult::Success) @@ -4326,7 +4299,7 @@ void NukiWrapper::updateTime() { if(!isPinValid()) { - Log->println(("No valid PIN set")); + Log->println("No valid PIN set"); return; } @@ -4337,7 +4310,7 @@ void NukiWrapper::updateTime() if (int(tm.tm_year + 1900) < int(2025)) { - Log->println(("NTP Time not valid, not updating Nuki device")); + Log->println("NTP Time not valid, not updating Nuki device"); return; } @@ -4354,6 +4327,6 @@ void NukiWrapper::updateTime() char resultStr[15] = {0}; NukiLock::cmdResultToString(cmdResult, resultStr); - Log->print(("Lock time update result: ")); + Log->print("Lock time update result: "); Log->println(resultStr); } \ No newline at end of file diff --git a/src/NukiWrapper.h b/src/NukiWrapper.h index 7dbe709..76ddbd8 100644 --- a/src/NukiWrapper.h +++ b/src/NukiWrapper.h @@ -14,7 +14,7 @@ class NukiWrapper : public Nuki::SmartlockEventHandler { public: - NukiWrapper(const std::string& deviceName, NukiDeviceId* deviceId, BleScanner::Scanner* scanner, NukiNetworkLock* network, NukiOfficial* nukiOfficial, Gpio* gpio, Preferences* preferences); + NukiWrapper(const std::string& deviceName, NukiDeviceId* deviceId, BleScanner::Scanner* scanner, NukiNetworkLock* network, NukiOfficial* nukiOfficial, Gpio* gpio, Preferences* preferences, char* buffer, size_t bufferSize); virtual ~NukiWrapper(); void initialize(); @@ -27,7 +27,6 @@ public: void lockngo(); void lockngounlatch(); - bool isPinSet(); bool isPinValid(); void setPin(const uint16_t pin); void setUltraPin(const uint32_t pin); @@ -168,4 +167,7 @@ private: std::string _firmwareVersion = ""; std::string _hardwareVersion = ""; volatile NukiLock::LockAction _nextLockAction = (NukiLock::LockAction)0xff; + + char* _buffer; + const size_t _bufferSize; }; diff --git a/src/PreferencesKeys.h b/src/PreferencesKeys.h index 1aea27a..ac9b8d5 100644 --- a/src/PreferencesKeys.h +++ b/src/PreferencesKeys.h @@ -87,10 +87,15 @@ #define preference_cred_session_lifetime_remember (char*)"credLfRmbr" #define preference_cred_session_lifetime_duo (char*)"credLfDuo" #define preference_cred_session_lifetime_duo_remember (char*)"credLfDuoRmbr" +#define preference_cred_session_lifetime_totp (char*)"credLfTotp" +#define preference_cred_session_lifetime_totp_remember (char*)"credLfTotpRmbr" #define preference_cred_duo_approval (char*)"duoApprove" #define preference_cred_bypass_boot_btn_enabled (char*)"bypassBtBtn" #define preference_cred_bypass_gpio_high (char*)"bypassHigh" #define preference_cred_bypass_gpio_low (char*)"bypassLow" +#define preference_publish_config (char*)"nhPubConfig" +#define preference_config_from_mqtt (char*)"nhCntrlEnabled" +#define preference_totp_secret (char*)"totpsecret" // CHANGE DOES NOT REQUIRE REBOOT TO TAKE EFFECT #define preference_find_best_rssi (char*)"nwbestrssi" @@ -241,6 +246,8 @@ inline void initPreferences(Preferences* preferences) preferences->putBool(preference_cred_duo_enabled, false); preferences->putBool(preference_cred_duo_approval, false); preferences->putBool(preference_cred_bypass_boot_btn_enabled, false); + preferences->putBool(preference_publish_config, false); + preferences->putBool(preference_config_from_mqtt, false); preferences->putInt(preference_mqtt_broker_port, 1883); preferences->putInt(preference_buffer_size, CHAR_BUFFER_SIZE); @@ -264,6 +271,8 @@ inline void initPreferences(Preferences* preferences) preferences->putInt(preference_cred_session_lifetime_remember, 720); preferences->putInt(preference_cred_session_lifetime_duo, 3600); preferences->putInt(preference_cred_session_lifetime_duo_remember, 720); + preferences->putInt(preference_cred_session_lifetime_totp, 3600); + preferences->putInt(preference_cred_session_lifetime_totp_remember, 720); preferences->putInt(preference_cred_bypass_gpio_high, -1); preferences->putInt(preference_cred_bypass_gpio_low, -1); @@ -504,8 +513,8 @@ private: preference_mqtt_broker, preference_mqtt_broker_port, preference_mqtt_user, preference_mqtt_password, preference_mqtt_log_enabled, preference_check_updates, preference_webserver_enabled, preference_lock_enabled, preference_lock_pin_status, preference_mqtt_lock_path, preference_opener_enabled, preference_opener_pin_status, preference_opener_continuous_mode, preference_lock_max_keypad_code_count, preference_opener_max_keypad_code_count, preference_update_time, preference_time_server, - preference_lock_max_timecontrol_entry_count, preference_opener_max_timecontrol_entry_count, preference_enable_bootloop_reset, preference_mqtt_ca, preference_mqtt_crt, - preference_mqtt_key, preference_mqtt_hass_discovery, preference_mqtt_hass_cu_url, preference_buffer_size, preference_ip_dhcp_enabled, preference_ip_address, + preference_lock_max_timecontrol_entry_count, preference_opener_max_timecontrol_entry_count, preference_enable_bootloop_reset, + preference_mqtt_hass_discovery, preference_mqtt_hass_cu_url, preference_buffer_size, preference_ip_dhcp_enabled, preference_ip_address, preference_ip_subnet, preference_ip_gateway, preference_ip_dns_server, preference_network_hardware, preference_http_auth_type, preference_lock_gemini_pin, preference_rssi_publish_interval, preference_hostname, preference_network_timeout, preference_restart_on_disconnect, preference_hybrid_reboot_on_disconnect, preference_restart_ble_beacon_lost, preference_query_interval_lockstate, preference_timecontrol_topic_per_entry, preference_keypad_topic_per_entry, @@ -525,7 +534,8 @@ private: preference_lock_force_id, preference_lock_force_doorsensor, preference_lock_force_keypad, preference_opener_force_id, preference_opener_force_keypad, preference_nukihub_id, 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_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 }; std::vector _redact = { @@ -544,7 +554,8 @@ private: preference_keypad_check_code_enabled, preference_disable_network_not_connected, preference_find_best_rssi, preference_cred_bypass_boot_btn_enabled, preference_debug_connect, preference_debug_communication, preference_debug_readable_data, preference_debug_hex_data, preference_debug_command, preference_connect_mode, preference_lock_force_id, preference_lock_force_doorsensor, preference_lock_force_keypad, preference_opener_force_id, preference_opener_force_keypad, preference_mqtt_ssl_enabled, - preference_hybrid_reboot_on_disconnect, preference_lock_gemini_enabled, preference_enable_debug_mode, preference_cred_duo_enabled, preference_cred_duo_approval + preference_hybrid_reboot_on_disconnect, preference_lock_gemini_enabled, preference_enable_debug_mode, preference_cred_duo_enabled, preference_cred_duo_approval, + preference_publish_config, preference_config_from_mqtt }; std::vector _bytePrefs = { @@ -563,7 +574,7 @@ private: preference_network_custom_irq, preference_network_custom_rst, preference_network_custom_cs, preference_network_custom_sck, preference_network_custom_miso, preference_network_custom_mosi, preference_network_custom_pwr, preference_network_custom_mdio, preference_http_auth_type, preference_cred_session_lifetime, preference_cred_session_lifetime_remember, preference_cred_session_lifetime_duo, preference_cred_session_lifetime_duo_remember, - preference_cred_bypass_gpio_high, preference_cred_bypass_gpio_low + preference_cred_bypass_gpio_high, preference_cred_bypass_gpio_low, preference_cred_session_lifetime_totp, preference_cred_session_lifetime_totp_remember }; std::vector _uintPrefs = { diff --git a/src/WebCfgServer.cpp b/src/WebCfgServer.cpp index 5906140..3598eb2 100644 --- a/src/WebCfgServer.cpp +++ b/src/WebCfgServer.cpp @@ -15,7 +15,6 @@ #include #endif #include -#include #include "driver/gpio.h" extern const uint8_t x509_crt_imported_bundle_bin_start[] asm("_binary_x509_crt_bundle_start"); @@ -27,7 +26,7 @@ extern bool timeSynced; #include #include "ArduinoJson.h" -WebCfgServer::WebCfgServer(NukiWrapper* nuki, NukiOpenerWrapper* nukiOpener, NukiNetwork* network, Gpio* gpio, Preferences* preferences, bool allowRestartToPortal, uint8_t partitionType, PsychicHttpServer* psychicServer) +WebCfgServer::WebCfgServer(NukiWrapper* nuki, NukiOpenerWrapper* nukiOpener, NukiNetwork* network, Gpio* gpio, Preferences* preferences, bool allowRestartToPortal, uint8_t partitionType, PsychicHttpServer* psychicServer, ImportExport* importExport) : _nuki(nuki), _nukiOpener(nukiOpener), _network(network), @@ -35,14 +34,16 @@ WebCfgServer::WebCfgServer(NukiWrapper* nuki, NukiOpenerWrapper* nukiOpener, Nuk _preferences(preferences), _allowRestartToPortal(allowRestartToPortal), _partitionType(partitionType), - _psychicServer(psychicServer) + _psychicServer(psychicServer), + _importExport(importExport) #else -WebCfgServer::WebCfgServer(NukiNetwork* network, Preferences* preferences, bool allowRestartToPortal, uint8_t partitionType, PsychicHttpServer* psychicServer) +WebCfgServer::WebCfgServer(NukiNetwork* network, Preferences* preferences, bool allowRestartToPortal, uint8_t partitionType, PsychicHttpServer* psychicServer, ImportExport* importExport) : _network(network), _preferences(preferences), _allowRestartToPortal(allowRestartToPortal), _partitionType(partitionType), - _psychicServer(psychicServer) + _psychicServer(psychicServer), + _importExport(importExport) #endif { _hostname = _preferences->getString(preference_hostname, ""); @@ -50,28 +51,10 @@ WebCfgServer::WebCfgServer(NukiNetwork* network, Preferences* preferences, bool str = _preferences->getString(preference_cred_user, ""); _isSSL = (psychicServer->getPort() == 443); - if (_preferences->getBool(preference_cred_duo_enabled, false)) - { - _duoEnabled = true; - _duoHost = _preferences->getString(preference_cred_duo_host, ""); - _duoIkey = _preferences->getString(preference_cred_duo_ikey, ""); - _duoSkey = _preferences->getString(preference_cred_duo_skey, ""); - _duoUser = _preferences->getString(preference_cred_duo_user, ""); - - if (_duoHost == "" || _duoIkey == "" || _duoSkey == "" || _duoUser == "" || !_preferences->getBool(preference_update_time, false)) - { - _duoEnabled = false; - } - else if (_preferences->getBool(preference_cred_bypass_boot_btn_enabled, false) || _preferences->getInt(preference_cred_bypass_gpio_high, -1) > -1 || _preferences->getInt(preference_cred_bypass_gpio_low, -1) > -1) - { - if (_preferences->getBool(preference_cred_bypass_boot_btn_enabled, false)) - { - _bypassGPIO = true; - } - _bypassGPIOHigh = _preferences->getInt(preference_cred_bypass_gpio_high, -1); - _bypassGPIOLow = _preferences->getInt(preference_cred_bypass_gpio_low, -1); - } - } + _duoEnabled = _importExport->getDuoEnabled(); + _bypassGPIO = _importExport->getBypassGPIOEnabled(); + _bypassGPIOHigh = _importExport->getBypassGPIOHigh(); + _bypassGPIOLow = _importExport->getBypassGPIOLow(); if(str.length() > 0) { @@ -92,47 +75,45 @@ WebCfgServer::WebCfgServer(NukiNetwork* network, Preferences* preferences, bool if (_duoEnabled) { - loadSessions(true); + loadSessions(1); + } + + if (_importExport->getTOTPEnabled()) + { + loadSessions(2); } } _confirmCode = generateConfirmCode(); #ifndef NUKI_HUB_UPDATER - _pinsConfigured = true; - - if(_nuki != nullptr && !_nuki->isPinSet()) - { - _pinsConfigured = false; - } - if(_nukiOpener != nullptr && !_nukiOpener->isPinSet()) - { - _pinsConfigured = false; - } - _brokerConfigured = _preferences->getString(preference_mqtt_broker).length() > 0 && _preferences->getInt(preference_mqtt_broker_port) > 0; #endif } -bool WebCfgServer::isAuthenticated(PsychicRequest *request, bool duo) +bool WebCfgServer::isAuthenticated(PsychicRequest *request, int type) { String cookieKey = "sessionId"; - if (duo) + if (type == 1) { cookieKey = "duoId"; } + else if (type == 2) + { + cookieKey = "totpId"; + } if (request->hasCookie(cookieKey.c_str())) { String cookie = request->getCookie(cookieKey.c_str()); - if ((!duo && _httpSessions[cookie].is()) || (duo && _duoSessions[cookie].is())) + if ((type == 0 && _httpSessions[cookie].is()) || (type == 1 && _importExport->_duoSessions[cookie].is()) || (type == 2 && _importExport->_totpSessions[cookie].is())) { struct timeval time; gettimeofday(&time, NULL); int64_t time_us = (int64_t)time.tv_sec * 1000000L + (int64_t)time.tv_usec; - if ((!duo && _httpSessions[cookie].as() > time_us) || (duo && _duoSessions[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)) { return true; } @@ -142,7 +123,6 @@ bool WebCfgServer::isAuthenticated(PsychicRequest *request, bool duo) } } } - Log->println("Authentication Failed"); return false; } @@ -183,19 +163,41 @@ esp_err_t WebCfgServer::logoutSession(PsychicRequest *request, PsychicResponse* if (request->hasCookie("duoId")) { String cookie2 = request->getCookie("duoId"); - _duoSessions.remove(cookie2); - saveSessions(true); + _importExport->_duoSessions.remove(cookie2); + saveSessions(1); } else { - Log->print("No session cookie found"); + Log->print("No duo session cookie found"); + } + } + + if (_importExport->getTOTPEnabled()) + { + if (!_isSSL) + { + resp->setCookie("totpId", "", 0, "HttpOnly"); + } + else + { + resp->setCookie("totpId", "", 0, "Secure; HttpOnly"); + } + + if (request->hasCookie("totpId")) { + String cookie2 = request->getCookie("totpId"); + _importExport->_totpSessions.remove(cookie2); + saveSessions(2); + } + else + { + Log->print("No totp session cookie found"); } } return buildConfirmHtml(request, resp, "Logging out", 3, true); } -void WebCfgServer::saveSessions(bool duo) +void WebCfgServer::saveSessions(int type) { if(_preferences->getBool(preference_update_time, false)) { @@ -207,21 +209,27 @@ void WebCfgServer::saveSessions(bool duo) { File file; - if (!duo) + if (type == 0) { file = SPIFFS.open("/sessions.json", "w"); serializeJson(_httpSessions, file); } - else + else if (type == 1) { file = SPIFFS.open("/duosessions.json", "w"); - serializeJson(_duoSessions, file); + serializeJson(_importExport->_duoSessions, file); + } + else if (type == 2) + { + file = SPIFFS.open("/totpsessions.json", "w"); + serializeJson(_importExport->_totpSessions, file); } file.close(); } } } -void WebCfgServer::loadSessions(bool duo) + +void WebCfgServer::loadSessions(int type) { if(_preferences->getBool(preference_update_time, false)) { @@ -233,7 +241,7 @@ void WebCfgServer::loadSessions(bool duo) { File file; - if (!duo) + if (type == 0) { file = SPIFFS.open("/sessions.json", "r"); @@ -245,7 +253,7 @@ void WebCfgServer::loadSessions(bool duo) deserializeJson(_httpSessions, file); } } - else + else if (type == 1) { file = SPIFFS.open("/duosessions.json", "r"); @@ -254,7 +262,19 @@ void WebCfgServer::loadSessions(bool duo) } else { - deserializeJson(_duoSessions, file); + deserializeJson(_importExport->_duoSessions, file); + } + } + else if (type == 2) + { + file = SPIFFS.open("/totpsessions.json", "r"); + + if (!file || file.isDirectory()) { + Log->println("totpsessions.json not found"); + } + else + { + deserializeJson(_importExport->_totpSessions, file); } } file.close(); @@ -271,13 +291,17 @@ void WebCfgServer::clearSessions() else { _httpSessions.clear(); - _duoSessions.clear(); + _importExport->_duoSessions.clear(); + _importExport->_totpSessions.clear(); File file; file = SPIFFS.open("/sessions.json", "w"); serializeJson(_httpSessions, file); file.close(); file = SPIFFS.open("/duosessions.json", "w"); - serializeJson(_duoSessions, file); + serializeJson(_importExport->_duoSessions, file); + file.close(); + file = SPIFFS.open("/totpsessions.json", "w"); + serializeJson(_importExport->_totpSessions, file); file.close(); } } @@ -295,225 +319,71 @@ int WebCfgServer::doAuthentication(PsychicRequest *request) { if (!isAuthenticated(request)) { + Log->println("Authentication Failed"); return savedAuthType; } - else if (_duoEnabled && !isAuthenticated(request, true)) - { - if (_bypassGPIO) - { - if (digitalRead(BOOT_BUTTON_GPIO) == LOW) - { - Log->print("Duo bypassed because boot button pressed"); - return 4; - } - } - if (_bypassGPIOHigh > -1) - { - if (digitalRead(_bypassGPIOHigh) == HIGH) - { - Log->print("Duo bypassed because bypass GPIO pin pulled high"); - return 4; - } - } - if (_bypassGPIOLow > -1) - { - if (digitalRead(_bypassGPIOLow) == LOW) - { - Log->print("Duo bypassed because bypass GPIO pin pulled low"); - return 4; - } - } - return 3; - } } else { if (!request->authenticate(_credUser, _credPassword)) { + Log->println("Authentication Failed"); return savedAuthType; } - else if (_duoEnabled && !isAuthenticated(request, true)) + } + + if (_duoEnabled || _importExport->getTOTPEnabled()) + { + if (_bypassGPIO) { - if (_bypassGPIO) + if (digitalRead(BOOT_BUTTON_GPIO) == LOW) { - if (digitalRead(BOOT_BUTTON_GPIO) == LOW) - { - Log->print("Duo bypassed because boot button pressed"); - return 4; - } + Log->print("Duo bypassed because boot button pressed"); + return 4; } - if (_bypassGPIOHigh > -1) - { - if (digitalRead(_bypassGPIOHigh) == HIGH) - { - Log->print("Duo bypassed because bypass GPIO pin pulled high"); - return 4; - } - } - if (_bypassGPIOLow > -1) - { - if (digitalRead(_bypassGPIOLow) == LOW) - { - Log->print("Duo bypassed because bypass GPIO pin pulled low"); - return 4; - } - } - return 3; } + if (_bypassGPIOHigh > -1) + { + if (digitalRead(_bypassGPIOHigh) == HIGH) + { + Log->print("Duo bypassed because bypass GPIO pin pulled high"); + return 4; + } + } + if (_bypassGPIOLow > -1) + { + if (digitalRead(_bypassGPIOLow) == LOW) + { + Log->print("Duo bypassed because bypass GPIO pin pulled low"); + return 4; + } + } + + if(_duoEnabled && isAuthenticated(request, 1)) + { + _importExport->_sessionsOpts[request->client()->localIP().toString() + "totp"] = false; + return 4; + } + else if(_importExport->getTOTPEnabled() && isAuthenticated(request, 2)) + { + _importExport->_sessionsOpts[request->client()->localIP().toString() + "totp"] = true; + return 4; + } + + Log->println("Authentication Failed"); + + if(_importExport->getTOTPEnabled() && _importExport->_sessionsOpts[request->client()->localIP().toString() + "totp"]) + { + return 5; + } + + return 3; } } return 4; } -bool WebCfgServer::startDuoAuth(char* pushType) -{ - int64_t timeout = esp_timer_get_time() - (30 * 1000 * 1000L); - if(!_duoActiveRequest || timeout > _duoRequestTS) - { - const char* duo_host = _duoHost.c_str(); - const char* duo_ikey = _duoIkey.c_str(); - const char* duo_skey = _duoSkey.c_str(); - const char* duo_user = _duoUser.c_str(); - - DuoAuthLib duoAuth; - bool duoRequestResult; - duoAuth.begin(duo_host, duo_ikey, duo_skey, &timeinfo); - duoAuth.setPushType(pushType); - duoRequestResult = duoAuth.pushAuth((char*)duo_user, true); - - if(duoRequestResult == true) - { - _duoTransactionId = duoAuth.getAuthTxId(); - _duoActiveRequest = true; - _duoRequestTS = esp_timer_get_time(); - Log->println("Duo MFA Auth sent"); - return true; - } - else - { - Log->println("Failed Duo MFA Auth"); - return false; - } - } - return true; -} - -int WebCfgServer::checkDuoAuth(PsychicRequest *request) -{ - const char* duo_host = _duoHost.c_str(); - const char* duo_ikey = _duoIkey.c_str(); - const char* duo_skey = _duoSkey.c_str(); - const char* duo_user = _duoUser.c_str(); - - if (request->hasParam("id")) { - const PsychicWebParameter* p = request->getParam("id"); - String cookie2 = p->value(); - DuoAuthLib duoAuth; - if(_duoActiveRequest && _duoCheckIP == request->client()->localIP().toString() && cookie2 == _duoCheckId) - { - duoAuth.begin(duo_host, duo_ikey, duo_skey, &timeinfo); - - Log->println("Checking Duo Push Status..."); - duoAuth.authStatus(_duoTransactionId); - - if(duoAuth.pushWaiting()) - { - Log->println("Duo Push Waiting..."); - return 2; - } - else - { - if (duoAuth.authSuccessful()) - { - Log->println("Successful Duo MFA Auth"); - _duoActiveRequest = false; - _duoTransactionId = ""; - _duoCheckIP = ""; - _duoCheckId = ""; - int64_t durationLength = 60*60*_preferences->getInt(preference_cred_session_lifetime_duo_remember, 720); - - if (!_sessionsOpts[request->client()->localIP().toString()]) - { - durationLength = _preferences->getInt(preference_cred_session_lifetime_duo, 3600); - } - struct timeval time; - gettimeofday(&time, NULL); - int64_t time_us = (int64_t)time.tv_sec * 1000000L + (int64_t)time.tv_usec; - _duoSessions[cookie2] = time_us + (durationLength*1000000L); - saveSessions(true); - if (_preferences->getBool(preference_mfa_reconfigure, false)) - { - _preferences->putBool(preference_mfa_reconfigure, false); - } - return 1; - } - else - { - Log->println("Failed Duo MFA Auth"); - _duoActiveRequest = false; - _duoTransactionId = ""; - _duoCheckIP = ""; - _duoCheckId = ""; - if (_preferences->getBool(preference_mfa_reconfigure, false)) - { - _preferences->putBool(preference_cred_duo_enabled, false); - _duoEnabled = false; - _preferences->putBool(preference_mfa_reconfigure, false); - } - return 0; - } - } - } - } - return 0; -} - -int WebCfgServer::checkDuoApprove() -{ - const char* duo_host = _duoHost.c_str(); - const char* duo_ikey = _duoIkey.c_str(); - const char* duo_skey = _duoSkey.c_str(); - const char* duo_user = _duoUser.c_str(); - - DuoAuthLib duoAuth; - if(_duoActiveRequest) - { - duoAuth.begin(duo_host, duo_ikey, duo_skey, &timeinfo); - - Log->println("Checking Duo Push Status..."); - duoAuth.authStatus(_duoTransactionId); - - if(duoAuth.pushWaiting()) - { - Log->println("Duo Push Waiting..."); - return 2; - } - else - { - if (duoAuth.authSuccessful()) - { - Log->println("Successful Duo MFA Auth"); - _duoActiveRequest = false; - _duoTransactionId = ""; - _duoCheckIP = ""; - _duoCheckId = ""; - return 1; - } - else - { - Log->println("Failed Duo MFA Auth"); - _duoActiveRequest = false; - _duoTransactionId = ""; - _duoCheckIP = ""; - _duoCheckId = ""; - return 0; - } - } - } - return 0; -} - void WebCfgServer::initialize() { //_psychicServer->onOpen([&](PsychicClient* client) { Log->printf("[http] connection #%u connected from %s\n", client->socket(), client->localIP().toString().c_str()); }); @@ -553,6 +423,7 @@ void WebCfgServer::initialize() return resp->redirect("/get?page=login"); break; case 3: + case 5: case 4: default: break; @@ -588,6 +459,7 @@ void WebCfgServer::initialize() return resp->redirect("/get?page=login"); break; case 3: + case 5: case 4: default: break; @@ -658,6 +530,14 @@ void WebCfgServer::initialize() return resp->redirect("/get?page=duoauth"); } break; + case 5: + if (value != "totp") + { + resp->setCode(302); + resp->addHeader("Cache-Control", "no-cache"); + return resp->redirect("/get?page=totp"); + } + break; case 4: default: break; @@ -675,13 +555,17 @@ void WebCfgServer::initialize() { return buildLoginHtml(request, resp); } + else if (value == "totp") + { + return buildTOTPHtml(request, resp, 0); + } else if (value == "logout") { return logoutSession(request, resp); } else if (value == "duoauth") { - return buildDuoHtml(request, resp); + return buildDuoHtml(request, resp, 0); } else if (value == "duocheck") { @@ -693,13 +577,13 @@ void WebCfgServer::initialize() } else if (value == "reboot") { - String value = ""; + String value2 = ""; if(request->hasParam("CONFIRMTOKEN")) { const PsychicWebParameter* p = request->getParam("CONFIRMTOKEN"); if(p->value() != "") { - value = p->value(); + value2 = p->value(); } } else @@ -707,7 +591,7 @@ void WebCfgServer::initialize() return buildConfirmHtml(request, resp, "No confirm code set.", 3, true); } - if(value != _confirmCode) + if(value2 != _confirmCode) { resp->setCode(302); resp->addHeader("Cache-Control", "no-cache"); @@ -735,31 +619,38 @@ void WebCfgServer::initialize() } else if (value == "export") { - if(_preferences->getBool(preference_cred_duo_approval, false)) + if(!_preferences->getBool(preference_cred_duo_approval, false) || (!_importExport->getTOTPEnabled() && !_duoEnabled)) { - if (!timeSynced) - { - return buildConfirmHtml(request, resp, "NTP time not synced yet, Duo not available, please wait for NTP to sync", 3, true); - } - else if (startDuoAuth((char*)"Approve Nuki Hub export")) - { - int duoResult = 2; + return sendSettings(request, resp); + } - while (duoResult == 2) + if(_importExport->_sessionsOpts[request->client()->localIP().toString() + "approve"]) + { + _importExport->_sessionsOpts[request->client()->localIP().toString() + "approve"] = false; + return sendSettings(request, resp); + } + else if(request->hasParam("totpkey") && _importExport->getTOTPEnabled()) + { + const PsychicWebParameter* pass = request->getParam("totpkey"); + if(pass->value() != "") + { + String totpkey = pass->value(); + if (_importExport->checkTOTP(&totpkey)) { - duoResult = checkDuoApprove(); - delay(2000); - esp_task_wdt_reset(); - } - - if (duoResult != 1) - { - return buildConfirmHtml(request, resp, "Duo approval failed, redirecting to main menu", 3, true); + _importExport->_sessionsOpts[request->client()->localIP().toString() + "approve"] = false; + return sendSettings(request, resp); } } } - return sendSettings(request, resp); + if(_importExport->_sessionsOpts[request->client()->localIP().toString() + "totp"] && _importExport->getTOTPEnabled()) + { + return buildTOTPHtml(request, resp, 1); + } + else + { + return buildDuoHtml(request, resp, 1); + } } else if (value == "impexpcfg") { @@ -828,20 +719,20 @@ void WebCfgServer::initialize() } else if (value == "wifimanager") { - String value = ""; + String value2 = ""; if(request->hasParam("CONFIRMTOKEN")) { const PsychicWebParameter* p = request->getParam("CONFIRMTOKEN"); if(p->value() != "") { - value = p->value(); + value2 = p->value(); } } else { return buildConfirmHtml(request, resp, "No confirm code set.", 3, true); } - if(value != _confirmCode) + if(value2 != _confirmCode) { resp->setCode(302); resp->addHeader("Cache-Control", "no-cache"); @@ -869,13 +760,13 @@ void WebCfgServer::initialize() } else if (value == "reboottoota") { - String value = ""; + String value2 = ""; if(request->hasParam("CONFIRMTOKEN")) { const PsychicWebParameter* p = request->getParam("CONFIRMTOKEN"); if(p->value() != "") { - value = p->value(); + value2 = p->value(); } } else @@ -883,7 +774,7 @@ void WebCfgServer::initialize() return buildConfirmHtml(request, resp, "No confirm code set.", 3, true); } - if(value != _confirmCode) + if(value2 != _confirmCode) { resp->setCode(302); resp->addHeader("Cache-Control", "no-cache"); @@ -925,7 +816,7 @@ void WebCfgServer::initialize() } } - if (value != "login") + if (value != "login" && value != "totp") { int authReq = doAuthentication(request); @@ -947,32 +838,58 @@ void WebCfgServer::initialize() resp->addHeader("Cache-Control", "no-cache"); return resp->redirect("/get?page=duoauth"); break; + case 5: + resp->setCode(302); + resp->addHeader("Cache-Control", "no-cache"); + return resp->redirect("/get?page=totp"); + break; case 4: default: break; } - if(_preferences->getBool(preference_cred_duo_approval, false)) + if(_preferences->getBool(preference_cred_duo_approval, false) && (_importExport->getTOTPEnabled() && _duoEnabled)) { - if (!timeSynced) + if(!_importExport->_sessionsOpts[request->client()->localIP().toString() + "approve"]) { - return buildConfirmHtml(request, resp, "NTP time not synced yet, Duo not available, please wait for NTP to sync", 3, true); + bool approved = false; + if(request->hasParam("totpkey") && _importExport->getTOTPEnabled()) + { + const PsychicWebParameter* pass = request->getParam("totpkey"); + if(pass->value() != "") + { + String totpkey = pass->value(); + if (_importExport->checkTOTP(&totpkey)) + { + _importExport->_sessionsOpts[request->client()->localIP().toString() + "approve"] = false; + approved = true; + } + } + } + + if (!approved) + { + int posttype = 3; + + if (value == "import") + { + posttype = 2; + } + + if(_importExport->_sessionsOpts[request->client()->localIP().toString() + "totp"] && _importExport->getTOTPEnabled()) + { + + return buildTOTPHtml(request, resp, posttype); + } + else + { + return buildDuoHtml(request, resp, posttype); + } + } } - else if (startDuoAuth((char*)"Approve Nuki Hub setting change")) + else { - int duoResult = 2; - - while (duoResult == 2) - { - duoResult = checkDuoApprove(); - delay(2000); - esp_task_wdt_reset(); - } - - if (duoResult != 1) - { - return buildConfirmHtml(request, resp, "Duo approval failed, redirecting to main menu", 3, true); - } + _importExport->_sessionsOpts[request->client()->localIP().toString() + "approve"] = false; } } } @@ -993,6 +910,22 @@ void WebCfgServer::initialize() return resp->redirect("/get?page=login"); } } + else if (value == "totp") + { + bool loggedIn = processTOTP(request, resp); + if (loggedIn) + { + resp->setCode(302); + resp->addHeader("Cache-Control", "no-cache"); + return resp->redirect("/"); + } + else + { + resp->setCode(302); + resp->addHeader("Cache-Control", "no-cache"); + return resp->redirect("/get?page=totp"); + } + } #ifndef NUKI_HUB_UPDATER else if (value == "savecfg") { @@ -1015,7 +948,7 @@ void WebCfgServer::initialize() { processGpioArgs(request, resp); esp_err_t res = buildConfirmHtml(request, resp, "Saving GPIO configuration. Restarting.", 3, true); - Log->println(("Restarting")); + Log->println("Restarting"); waitAndProcess(true, 1000); restartEsp(RestartReason::GpioConfigurationUpdated); return res; @@ -1077,6 +1010,8 @@ void WebCfgServer::initialize() return ESP_FAIL; case 3: return ESP_FAIL; + case 5: + return ESP_FAIL; case 4: default: break; @@ -1106,6 +1041,11 @@ void WebCfgServer::initialize() resp->addHeader("Cache-Control", "no-cache"); return resp->redirect("/get?page=duoauth"); break; + case 5: + resp->setCode(302); + resp->addHeader("Cache-Control", "no-cache"); + return resp->redirect("/get?page=totp"); + break; case 4: default: break; @@ -1164,6 +1104,11 @@ void WebCfgServer::initialize() resp->addHeader("Cache-Control", "no-cache"); return resp->redirect("/get?page=duoauth"); break; + case 5: + resp->setCode(302); + resp->addHeader("Cache-Control", "no-cache"); + return resp->redirect("/get?page=totp"); + break; case 4: default: break; @@ -1790,7 +1735,7 @@ esp_err_t WebCfgServer::handleOtaUpload(PsychicRequest *request, const String& f return(ESP_FAIL); } } - Log->print(("Progress: 100%")); + Log->print("Progress: 100%"); Log->println(); Log->print("handleFileUpload Total Size: "); Log->println(index+len); @@ -1916,7 +1861,88 @@ esp_err_t WebCfgServer::buildLoginHtml(PsychicRequest *request, PsychicResponse* response.print("

Nuki Hub login

"); response.print("
"); response.print(""); - response.print("
"); + if (_importExport->getTOTPEnabled() || _duoEnabled) + { + if (_importExport->getTOTPEnabled()) { + response.print(""); + } + if (_duoEnabled) { + response.print(""); + } + } + else { + response.print(""); + } + response.print(""); + response.print("
"); + return response.endSend(); +} + +esp_err_t WebCfgServer::buildTOTPHtml(PsychicRequest *request, PsychicResponse* resp, int type) +{ + 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"; + + if(type == 0) + { + response.print("
"); + } + else + { + if(type == 1) + { + typeText = "Export"; + response.print((String)"uri() + "\" method=\"get\" target=\"_blank\">"); + } + else if(type == 2) + { + typeText = "Import"; + response.print((String)"uri() + "\" method=\"post\">"); + } + else + { + typeText = "Save"; + response.print((String)"uri() + "\" method=\"post\">"); + } + int params = request->params(); + + for(int index = 0; index < params; index++) + { + const PsychicWebParameter* p = request->getParam(index); + if (p->name() != "totpkey") + { + response.print((String)"name() + "\" value='" + p->value() + "' />"); + } + } + } + + response.print("
"); + response.print(""); + /* + if (!timeSynced) + { + response.print(""); + } + */ + response.print("
"); response.print("
"); return response.endSend(); } @@ -1924,7 +1950,7 @@ esp_err_t WebCfgServer::buildLoginHtml(PsychicRequest *request, PsychicResponse* esp_err_t WebCfgServer::buildDuoCheckHtml(PsychicRequest *request, PsychicResponse* resp) { char valueStr[2]; - itoa(checkDuoAuth(request), valueStr, 10); + itoa(_importExport->checkDuoAuth(request), valueStr, 10); resp->setCode(200); resp->setContentType("text/plain"); resp->setContent(valueStr); @@ -1945,29 +1971,48 @@ esp_err_t WebCfgServer::buildCoredumpHtml(PsychicRequest *request, PsychicRespon Log->println("coredump.hex not found"); } else - { + { PsychicFileResponse response(resp, file, "coredump.hex"); String name = "coredump.txt"; char buf[26 + name.length()]; snprintf(buf, sizeof(buf), "attachment; filename=\"%s\"", name.c_str()); response.addHeader("Content-Disposition", buf); - return response.send(); + return response.send(); } } - + resp->setCode(302); resp->addHeader("Cache-Control", "no-cache"); return resp->redirect("/"); } -esp_err_t WebCfgServer::buildDuoHtml(PsychicRequest *request, PsychicResponse* resp) +esp_err_t WebCfgServer::buildDuoHtml(PsychicRequest *request, PsychicResponse* resp, int type) { if (!timeSynced) { return buildConfirmHtml(request, resp, "NTP time not synced yet, Duo not available, please wait for NTP to sync", 3, true); } - - bool duo = startDuoAuth((char*)"Approve Nuki Hub login"); + + String duoText; + + if (type == 0) + { + duoText = "login"; + } + else if (type == 1) + { + duoText = "export"; + } + else if (type == 2) + { + duoText = "import"; + } + else if (type == 3) + { + duoText = "save"; + } + + bool duo = _importExport->startDuoAuth((char*)((String("Approve Nuki Hub ") + duoText).c_str())); if (!duo) { @@ -1982,33 +2027,61 @@ esp_err_t WebCfgServer::buildDuoHtml(PsychicRequest *request, PsychicResponse* r sprintf(buffer + (i * 8), "%08lx", (unsigned long int)esp_random()); } - int64_t durationLength = 60*60*_preferences->getInt(preference_cred_session_lifetime_duo_remember, 720); - - if (!_sessionsOpts[request->client()->localIP().toString()]) + if(type == 0) { - durationLength = _preferences->getInt(preference_cred_session_lifetime_duo, 3600); + int64_t durationLength = 60*60*_preferences->getInt(preference_cred_session_lifetime_duo_remember, 720); + + if (!_importExport->_sessionsOpts[request->client()->localIP().toString()]) + { + durationLength = _preferences->getInt(preference_cred_session_lifetime_duo, 3600); + } + + if (!_isSSL) + { + response.setCookie("duoId", buffer, durationLength, "HttpOnly"); + } + else + { + response.setCookie("duoId", buffer, durationLength, "Secure; HttpOnly"); + } } - if (!_isSSL) - { - response.setCookie("duoId", buffer, durationLength, "HttpOnly"); - } - else - { - response.setCookie("duoId", buffer, durationLength, "Secure; HttpOnly"); - } - - _duoCheckIP = request->client()->localIP().toString(); - _duoCheckId = buffer; + _importExport->setDuoCheckIP(request->client()->localIP().toString()); + _importExport->setDuoCheckId(buffer); response.beginSend(); response.print(""); response.print(""); - response.print((String)""); - response.print("

Nuki Hub login

"); + response.print((String)""); + response.print("

Nuki Hub " + duoText + "

"); response.print("
Duo Push sent

"); - response.print("Please confirm login in the Duo app

"); + response.print("Please confirm " + duoText + " in the Duo app

"); response.print(""); + if (type > 1) + { + response.print((String)"
uri() + "\" method=\"post\">"); + int params = request->params(); + + for(int index = 0; index < params; index++) + { + const PsychicWebParameter* p = request->getParam(index); + response.print((String)"name() + "\" value='" + p->value() + "' />"); + } + response.print("
"); + } response.print("
"); return response.endSend(); @@ -2035,7 +2108,7 @@ bool WebCfgServer::processLogin(PsychicRequest *request, PsychicResponse* resp) durationLength = _preferences->getInt(preference_cred_session_lifetime, 3600); } - _sessionsOpts[request->client()->localIP().toString()] = request->hasParam("remember"); + _importExport->_sessionsOpts[request->client()->localIP().toString()] = request->hasParam("remember"); if (!_isSSL) { @@ -2051,6 +2124,52 @@ bool WebCfgServer::processLogin(PsychicRequest *request, PsychicResponse* resp) int64_t time_us = (int64_t)time.tv_sec * 1000000L + (int64_t)time.tv_usec; _httpSessions[buffer] = time_us + (durationLength*1000000L); saveSessions(); + + _importExport->_sessionsOpts[request->client()->localIP().toString() + "totp"] = request->hasParam("totp"); + + return true; + } + } + } + return false; +} + +bool WebCfgServer::processTOTP(PsychicRequest *request, PsychicResponse* resp) +{ + if(request->hasParam("totpkey")) + { + const PsychicWebParameter* pass = request->getParam("totpkey"); + if(pass->value() != "") + { + String totpkey = pass->value(); + if (_importExport->checkTOTP(&totpkey)) + { + char buffer[33]; + int i; + int64_t durationLength = 60*60*_preferences->getInt(preference_cred_session_lifetime_totp_remember, 720); + for (i = 0; i < 4; i++) { + sprintf(buffer + (i * 8), "%08lx", (unsigned long int)esp_random()); + } + + if (!_importExport->_sessionsOpts[request->client()->localIP().toString()]) + { + durationLength = _preferences->getInt(preference_cred_session_lifetime_totp, 3600); + } + + if (!_isSSL) + { + resp->setCookie("totpId", buffer, durationLength, "HttpOnly"); + } + else + { + resp->setCookie("totpId", buffer, durationLength, "Secure; HttpOnly"); + } + + struct timeval time; + gettimeofday(&time, NULL); + int64_t time_us = (int64_t)time.tv_sec * 1000000L + (int64_t)time.tv_usec; + _importExport->_totpSessions[buffer] = time_us + (durationLength*1000000L); + saveSessions(2); return true; } } @@ -2072,118 +2191,12 @@ esp_err_t WebCfgServer::sendSettings(PsychicRequest *request, PsychicResponse* r if(p->value() == "https") { name = "nuki_hub_http_ssl.json"; - if (!SPIFFS.begin(true)) { - Log->println("SPIFFS Mount Failed"); - } - else - { - File file = SPIFFS.open("/http_ssl.crt"); - if (!file || file.isDirectory()) { - Log->println("http_ssl.crt not found"); - } - else - { - Log->println("Reading http_ssl.crt"); - size_t filesize = file.size(); - char cert[filesize + 1]; - - file.read((uint8_t *)cert, sizeof(cert)); - file.close(); - cert[filesize] = '\0'; - json["http_ssl.crt"] = cert; - } - } - - if (!SPIFFS.begin(true)) { - Log->println("SPIFFS Mount Failed"); - } - else - { - File file = SPIFFS.open("/http_ssl.key"); - if (!file || file.isDirectory()) { - Log->println("http_ssl.key not found"); - } - else - { - Log->println("Reading http_ssl.key"); - size_t filesize = file.size(); - char key[filesize + 1]; - - file.read((uint8_t *)key, sizeof(key)); - file.close(); - key[filesize] = '\0'; - json["http_ssl.key"] = key; - } - } + _importExport->exportHttpsJson(json); } else { name = "nuki_hub_mqtt_ssl.json"; - if (!SPIFFS.begin(true)) { - Log->println("SPIFFS Mount Failed"); - } - else - { - File file = SPIFFS.open("/mqtt_ssl.ca"); - if (!file || file.isDirectory()) { - Log->println("mqtt_ssl.ca not found"); - } - else - { - Log->println("Reading mqtt_ssl.ca"); - size_t filesize = file.size(); - char ca[filesize + 1]; - - file.read((uint8_t *)ca, sizeof(ca)); - file.close(); - ca[filesize] = '\0'; - json["mqtt_ssl.ca"] = ca; - } - } - - if (!SPIFFS.begin(true)) { - Log->println("SPIFFS Mount Failed"); - } - else - { - File file = SPIFFS.open("/mqtt_ssl.crt"); - if (!file || file.isDirectory()) { - Log->println("mqtt_ssl.crt not found"); - } - else - { - Log->println("Reading mqtt_ssl.crt"); - size_t filesize = file.size(); - char cert[filesize + 1]; - - file.read((uint8_t *)cert, sizeof(cert)); - file.close(); - cert[filesize] = '\0'; - json["mqtt_ssl.crt"] = cert; - } - } - - if (!SPIFFS.begin(true)) { - Log->println("SPIFFS Mount Failed"); - } - else - { - File file = SPIFFS.open("/mqtt_ssl.key"); - if (!file || file.isDirectory()) { - Log->println("mqtt_ssl.key not found"); - } - else - { - Log->println("Reading mqtt_ssl.key"); - size_t filesize = file.size(); - char key[filesize + 1]; - - file.read((uint8_t *)key, sizeof(key)); - file.close(); - key[filesize] = '\0'; - json["mqtt_ssl.key"] = key; - } - } + _importExport->exportMqttsJson(json); } } else @@ -2208,188 +2221,7 @@ esp_err_t WebCfgServer::sendSettings(PsychicRequest *request, PsychicResponse* r pairing = true; } } - - DebugPreferences debugPreferences; - - const std::vector keysPrefs = debugPreferences.getPreferencesKeys(); - const std::vector boolPrefs = debugPreferences.getPreferencesBoolKeys(); - const std::vector redactedPrefs = debugPreferences.getPreferencesRedactedKeys(); - const std::vector bytePrefs = debugPreferences.getPreferencesByteKeys(); - - for(const auto& key : keysPrefs) - { - if(strcmp(key, preference_show_secrets) == 0) - { - continue; - } - if(strcmp(key, preference_latest_version) == 0) - { - continue; - } - if(!redacted) if(std::find(redactedPrefs.begin(), redactedPrefs.end(), key) != redactedPrefs.end()) - { - continue; - } - if(!_preferences->isKey(key)) - { - json[key] = ""; - } - else if(std::find(boolPrefs.begin(), boolPrefs.end(), key) != boolPrefs.end()) - { - json[key] = _preferences->getBool(key) ? "1" : "0"; - } - else - { - switch(_preferences->getType(key)) - { - case PT_I8: - json[key] = String(_preferences->getChar(key)); - break; - case PT_I16: - json[key] = String(_preferences->getShort(key)); - break; - case PT_I32: - json[key] = String(_preferences->getInt(key)); - break; - case PT_I64: - json[key] = String(_preferences->getLong64(key)); - break; - case PT_U8: - json[key] = String(_preferences->getUChar(key)); - break; - case PT_U16: - json[key] = String(_preferences->getUShort(key)); - break; - case PT_U32: - json[key] = String(_preferences->getUInt(key)); - break; - case PT_U64: - json[key] = String(_preferences->getULong64(key)); - break; - case PT_STR: - json[key] = _preferences->getString(key); - break; - default: - json[key] = _preferences->getString(key); - break; - } - } - } - - if(pairing) - { - if(_nuki != nullptr) - { - unsigned char currentBleAddress[6]; - unsigned char authorizationId[4] = {0x00}; - unsigned char secretKeyK[32] = {0x00}; - uint16_t storedPincode = 0000; - uint32_t storedUltraPincode = 000000; - bool isUltra = false; - Preferences nukiBlePref; - nukiBlePref.begin("NukiHub", false); - nukiBlePref.getBytes("bleAddress", currentBleAddress, 6); - nukiBlePref.getBytes("secretKeyK", secretKeyK, 32); - nukiBlePref.getBytes("authorizationId", authorizationId, 4); - nukiBlePref.getBytes("securityPinCode", &storedPincode, 2); - nukiBlePref.getBytes("ultraPinCode", &storedUltraPincode, 4); - isUltra = nukiBlePref.getBool("isUltra", false); - nukiBlePref.end(); - char text[255]; - text[0] = '\0'; - for(int i = 0 ; i < 6 ; i++) - { - size_t offset = strlen(text); - sprintf(&(text[offset]), "%02x", currentBleAddress[i]); - } - json["bleAddressLock"] = text; - memset(text, 0, sizeof(text)); - text[0] = '\0'; - for(int i = 0 ; i < 32 ; i++) - { - size_t offset = strlen(text); - sprintf(&(text[offset]), "%02x", secretKeyK[i]); - } - json["secretKeyKLock"] = text; - memset(text, 0, sizeof(text)); - text[0] = '\0'; - for(int i = 0 ; i < 4 ; i++) - { - size_t offset = strlen(text); - sprintf(&(text[offset]), "%02x", authorizationId[i]); - } - json["authorizationIdLock"] = text; - memset(text, 0, sizeof(text)); - json["securityPinCodeLock"] = storedPincode; - json["ultraPinCodeLock"] = storedUltraPincode; - json["isUltra"] = isUltra ? "1" : "0"; - } - if(_nukiOpener != nullptr) - { - unsigned char currentBleAddressOpn[6]; - unsigned char authorizationIdOpn[4] = {0x00}; - unsigned char secretKeyKOpn[32] = {0x00}; - uint16_t storedPincodeOpn = 0000; - Preferences nukiBlePref; - nukiBlePref.begin("NukiHubopener", false); - nukiBlePref.getBytes("bleAddress", currentBleAddressOpn, 6); - nukiBlePref.getBytes("secretKeyK", secretKeyKOpn, 32); - nukiBlePref.getBytes("authorizationId", authorizationIdOpn, 4); - nukiBlePref.getBytes("securityPinCode", &storedPincodeOpn, 2); - nukiBlePref.end(); - char text[255]; - text[0] = '\0'; - for(int i = 0 ; i < 6 ; i++) - { - size_t offset = strlen(text); - sprintf(&(text[offset]), "%02x", currentBleAddressOpn[i]); - } - json["bleAddressOpener"] = text; - memset(text, 0, sizeof(text)); - text[0] = '\0'; - for(int i = 0 ; i < 32 ; i++) - { - size_t offset = strlen(text); - sprintf(&(text[offset]), "%02x", secretKeyKOpn[i]); - } - json["secretKeyKOpener"] = text; - memset(text, 0, sizeof(text)); - text[0] = '\0'; - for(int i = 0 ; i < 4 ; i++) - { - size_t offset = strlen(text); - sprintf(&(text[offset]), "%02x", authorizationIdOpn[i]); - } - json["authorizationIdOpener"] = text; - memset(text, 0, sizeof(text)); - json["securityPinCodeOpener"] = storedPincodeOpn; - } - } - - for(const auto& key : bytePrefs) - { - size_t storedLength = _preferences->getBytesLength(key); - if(storedLength == 0) - { - continue; - } - uint8_t serialized[storedLength]; - memset(serialized, 0, sizeof(serialized)); - size_t size = _preferences->getBytes(key, serialized, sizeof(serialized)); - if(size == 0) - { - continue; - } - char text[255]; - text[0] = '\0'; - for(int i = 0 ; i < size ; i++) - { - size_t offset = strlen(text); - sprintf(&(text[offset]), "%02x", serialized[i]); - } - json[key] = text; - memset(text, 0, sizeof(text)); - } + _importExport->exportNukiHubJson(json, redacted, pairing, (_nuki != nullptr), (_nukiOpener != nullptr)); } serializeJsonPretty(json, jsonPretty); @@ -2454,7 +2286,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getString(preference_mqtt_broker, "") != value) { _preferences->putString(preference_mqtt_broker, value); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -2464,7 +2296,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getInt(preference_mqtt_broker_port, 0) != value.toInt()) { _preferences->putInt(preference_mqtt_broker_port, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -2480,7 +2312,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getString(preference_mqtt_user, "") != value) { _preferences->putString(preference_mqtt_user, value); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -2493,7 +2325,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getString(preference_mqtt_password, "") != value) { _preferences->putString(preference_mqtt_password, value); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -2504,7 +2336,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getString(preference_mqtt_lock_path, "") != value) { _preferences->putString(preference_mqtt_lock_path, value); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -2536,10 +2368,10 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S else { if (!SPIFFS.remove("/mqtt_ssl.ca")) { - Serial.println("Failed to delete /mqtt_ssl.ca"); + Log->println("Failed to delete /mqtt_ssl.ca"); } } - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -2572,10 +2404,10 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S else { if (!SPIFFS.remove("/mqtt_ssl.crt")) { - Serial.println("Failed to delete /mqtt_ssl.crt"); + Log->println("Failed to delete /mqtt_ssl.crt"); } } - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -2608,10 +2440,10 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S else { if (!SPIFFS.remove("/mqtt_ssl.key")) { - Serial.println("Failed to delete /mqtt_ssl.key"); + Log->println("Failed to delete /mqtt_ssl.key"); } } - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -2645,10 +2477,10 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S else { if (!SPIFFS.remove("/http_ssl.crt")) { - Serial.println("Failed to delete /http_ssl.crt"); + Log->println("Failed to delete /http_ssl.crt"); } } - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -2681,10 +2513,10 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S else { if (!SPIFFS.remove("/http_ssl.key")) { - Serial.println("Failed to delete /http_ssl.key"); + Log->println("Failed to delete /http_ssl.key"); } } - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -2696,7 +2528,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_update_time, false) != (value == "1")) { _preferences->putBool(preference_update_time, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -2706,7 +2538,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getString(preference_time_server, "pool.ntp.org") != value) { _preferences->putString(preference_time_server, value); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -2724,7 +2556,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S } } _preferences->putInt(preference_network_hardware, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -2735,7 +2567,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S { networkReconfigure = true; _preferences->putInt(preference_network_custom_phy, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -2746,7 +2578,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S { networkReconfigure = true; _preferences->putInt(preference_network_custom_addr, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -2757,7 +2589,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S { networkReconfigure = true; _preferences->putInt(preference_network_custom_irq, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -2768,7 +2600,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S { networkReconfigure = true; _preferences->putInt(preference_network_custom_rst, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -2779,7 +2611,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S { networkReconfigure = true; _preferences->putInt(preference_network_custom_cs, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -2790,7 +2622,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S { networkReconfigure = true; _preferences->putInt(preference_network_custom_sck, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -2801,7 +2633,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S { networkReconfigure = true; _preferences->putInt(preference_network_custom_miso, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -2812,7 +2644,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S { networkReconfigure = true; _preferences->putInt(preference_network_custom_mosi, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -2823,7 +2655,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S { networkReconfigure = true; _preferences->putInt(preference_network_custom_pwr, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -2834,7 +2666,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S { networkReconfigure = true; _preferences->putInt(preference_network_custom_mdio, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -2845,7 +2677,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S { networkReconfigure = true; _preferences->putInt(preference_network_custom_mdc, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -2856,7 +2688,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S { networkReconfigure = true; _preferences->putInt(preference_network_custom_clk, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -2866,7 +2698,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getInt(preference_rssi_publish_interval, 60) != value.toInt()) { _preferences->putInt(preference_rssi_publish_interval, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -2876,7 +2708,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getString(preference_https_fqdn, "") != value) { _preferences->putString(preference_https_fqdn, value); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -2888,7 +2720,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getString(preference_cred_duo_host, "") != value) { _preferences->putString(preference_cred_duo_host, value); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; clearSession = true; @@ -2903,7 +2735,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getString(preference_cred_duo_ikey, "") != value) { _preferences->putString(preference_cred_duo_ikey, value); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; clearSession = true; @@ -2918,7 +2750,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getString(preference_cred_duo_skey, "") != value) { _preferences->putString(preference_cred_duo_skey, value); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; clearSession = true; @@ -2933,7 +2765,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getString(preference_cred_duo_user, "") != value) { _preferences->putString(preference_cred_duo_user, value); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; clearSession = true; @@ -2949,7 +2781,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if (value == "1") { _preferences->putBool(preference_update_time, true); } - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; clearSession = true; @@ -2961,7 +2793,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_cred_bypass_boot_btn_enabled, false) != (value == "1")) { _preferences->putBool(preference_cred_bypass_boot_btn_enabled, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -2971,7 +2803,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getInt(preference_cred_bypass_gpio_high, -1) != value.toInt()) { _preferences->putInt(preference_cred_bypass_gpio_high, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -2981,7 +2813,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getInt(preference_cred_bypass_gpio_low, -1) != value.toInt()) { _preferences->putInt(preference_cred_bypass_gpio_low, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -2991,7 +2823,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_cred_duo_approval, false) != (value == "1")) { _preferences->putBool(preference_cred_duo_approval, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3001,7 +2833,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getInt(preference_cred_session_lifetime, 3600) != value.toInt()) { _preferences->putInt(preference_cred_session_lifetime, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; clearSession = true; @@ -3012,7 +2844,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getInt(preference_cred_session_lifetime_remember, 720) != value.toInt()) { _preferences->putInt(preference_cred_session_lifetime_remember, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; clearSession = true; @@ -3023,7 +2855,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getInt(preference_cred_session_lifetime_duo, 3600) != value.toInt()) { _preferences->putInt(preference_cred_session_lifetime_duo, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; clearSession = true; @@ -3034,7 +2866,29 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getInt(preference_cred_session_lifetime_duo_remember, 720) != value.toInt()) { _preferences->putInt(preference_cred_session_lifetime_duo_remember, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); + Log->println(key); + configChanged = true; + clearSession = true; + } + } + else if(key == "CREDTOTPLFTM") + { + if(_preferences->getInt(preference_cred_session_lifetime_totp, 3600) != value.toInt()) + { + _preferences->putInt(preference_cred_session_lifetime_totp, value.toInt()); + Log->print("Setting changed: "); + Log->println(key); + configChanged = true; + clearSession = true; + } + } + else if(key == "CREDTOTPLFTMRMBR") + { + if(_preferences->getInt(preference_cred_session_lifetime_totp_remember, 720) != value.toInt()) + { + _preferences->putInt(preference_cred_session_lifetime_totp_remember, value.toInt()); + Log->print("Setting changed: "); Log->println(key); configChanged = true; clearSession = true; @@ -3046,7 +2900,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S { _network->disableHASS(); _preferences->putBool(preference_hass_device_discovery, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3057,7 +2911,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S { _network->disableHASS(); _preferences->putBool(preference_mqtt_hass_enabled, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3068,7 +2922,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S { _network->disableHASS(); _preferences->putString(preference_mqtt_hass_discovery, value); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3078,7 +2932,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_opener_continuous_mode, false) != (value == "1")) { _preferences->putBool(preference_opener_continuous_mode, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3088,7 +2942,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getString(preference_mqtt_hass_cu_url, "") != value) { _preferences->putString(preference_mqtt_hass_cu_url, value); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3098,7 +2952,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getString(preference_hostname, "") != value) { _preferences->putString(preference_hostname, value); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3108,7 +2962,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getInt(preference_network_timeout, 60) != value.toInt()) { _preferences->putInt(preference_network_timeout, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3118,7 +2972,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_find_best_rssi, false) != (value == "1")) { _preferences->putBool(preference_find_best_rssi, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3128,7 +2982,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_restart_on_disconnect, false) != (value == "1")) { _preferences->putBool(preference_restart_on_disconnect, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3138,7 +2992,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_mqtt_log_enabled, false) != (value == "1")) { _preferences->putBool(preference_mqtt_log_enabled, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3148,7 +3002,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_mqtt_ssl_enabled, false) != (value == "1")) { _preferences->putBool(preference_mqtt_ssl_enabled, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3158,7 +3012,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_webserial_enabled, false) != (value == "1")) { _preferences->putBool(preference_webserial_enabled, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3168,7 +3022,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_check_updates, false) != (value == "1")) { _preferences->putBool(preference_check_updates, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3178,7 +3032,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_update_from_mqtt, false) != (value == "1")) { _preferences->putBool(preference_update_from_mqtt, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3192,7 +3046,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S { _preferences->putBool(preference_register_as_app, true); } - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3206,7 +3060,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S { _preferences->putBool(preference_register_as_app, true); } - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3216,7 +3070,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getInt(preference_query_interval_hybrid_lockstate, 600) != value.toInt()) { _preferences->putInt(preference_query_interval_hybrid_lockstate, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3226,7 +3080,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_official_hybrid_retry, false) != (value == "1")) { _preferences->putBool(preference_official_hybrid_retry, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3236,7 +3090,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_hybrid_reboot_on_disconnect, false) != (value == "1")) { _preferences->putBool(preference_hybrid_reboot_on_disconnect, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3246,7 +3100,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_disable_non_json, false) != (value == "1")) { _preferences->putBool(preference_disable_non_json, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3256,7 +3110,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_ip_dhcp_enabled, true) != (value == "1")) { _preferences->putBool(preference_ip_dhcp_enabled, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3266,7 +3120,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getString(preference_ip_address, "") != value) { _preferences->putString(preference_ip_address, value); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3276,7 +3130,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getString(preference_ip_subnet, "") != value) { _preferences->putString(preference_ip_subnet, value); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3286,7 +3140,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getString(preference_ip_gateway, "") != value) { _preferences->putString(preference_ip_gateway, value); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3296,7 +3150,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getString(preference_ip_dns_server, "") != value) { _preferences->putString(preference_ip_dns_server, value); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3306,7 +3160,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getInt(preference_query_interval_lockstate, 1800) != value.toInt()) { _preferences->putInt(preference_query_interval_lockstate, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3316,7 +3170,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getInt(preference_query_interval_configuration, 3600) != value.toInt()) { _preferences->putInt(preference_query_interval_configuration, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3326,7 +3180,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getInt(preference_query_interval_battery, 1800) != value.toInt()) { _preferences->putInt(preference_query_interval_battery, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3336,7 +3190,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getInt(preference_query_interval_keypad, 1800) != value.toInt()) { _preferences->putInt(preference_query_interval_keypad, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3346,7 +3200,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getInt(preference_command_nr_of_retries, 3) != value.toInt()) { _preferences->putInt(preference_command_nr_of_retries, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3356,7 +3210,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getInt(preference_command_retry_delay, 100) != value.toInt()) { _preferences->putInt(preference_command_retry_delay, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3372,7 +3226,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getInt(preference_ble_tx_power, 9) != value.toInt()) { _preferences->putInt(preference_ble_tx_power, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3383,7 +3237,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getInt(preference_restart_ble_beacon_lost, 60) != value.toInt()) { _preferences->putInt(preference_restart_ble_beacon_lost, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3395,7 +3249,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getInt(preference_task_size_network, NETWORK_TASK_SIZE) != value.toInt()) { _preferences->putInt(preference_task_size_network, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3408,7 +3262,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getInt(preference_task_size_nuki, NUKI_TASK_SIZE) != value.toInt()) { _preferences->putInt(preference_task_size_nuki, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3421,7 +3275,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getInt(preference_authlog_max_entries, MAX_AUTHLOG) != value.toInt()) { _preferences->putInt(preference_authlog_max_entries, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3434,7 +3288,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getInt(preference_keypad_max_entries, MAX_KEYPAD) != value.toInt()) { _preferences->putInt(preference_keypad_max_entries, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3447,7 +3301,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getInt(preference_timecontrol_max_entries, MAX_TIMECONTROL) != value.toInt()) { _preferences->putInt(preference_timecontrol_max_entries, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3460,7 +3314,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getInt(preference_auth_max_entries, MAX_AUTH) != value.toInt()) { _preferences->putInt(preference_auth_max_entries, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3473,7 +3327,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getInt(preference_buffer_size, CHAR_BUFFER_SIZE) != value.toInt()) { _preferences->putInt(preference_buffer_size, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3484,7 +3338,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_enable_bootloop_reset, false) != (value == "1")) { _preferences->putBool(preference_enable_bootloop_reset, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3494,7 +3348,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_disable_network_not_connected, false) != (value == "1")) { _preferences->putBool(preference_disable_network_not_connected, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3504,7 +3358,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getString(preference_ota_updater_url, "") != value) { _preferences->putString(preference_ota_updater_url, value); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3514,7 +3368,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getString(preference_ota_main_url, "") != value) { _preferences->putString(preference_ota_main_url, value); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3524,7 +3378,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_show_secrets, false) != (value == "1")) { _preferences->putBool(preference_show_secrets, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3534,7 +3388,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_debug_connect, false) != (value == "1")) { _preferences->putBool(preference_debug_connect, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3544,7 +3398,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_debug_communication, false) != (value == "1")) { _preferences->putBool(preference_debug_communication, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3554,7 +3408,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_publish_debug_info, false) != (value == "1")) { _preferences->putBool(preference_publish_debug_info, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3564,7 +3418,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_debug_readable_data, false) != (value == "1")) { _preferences->putBool(preference_debug_readable_data, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3574,7 +3428,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_debug_hex_data, false) != (value == "1")) { _preferences->putBool(preference_debug_hex_data, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3584,7 +3438,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_debug_command, false) != (value == "1")) { _preferences->putBool(preference_debug_command, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3594,7 +3448,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_lock_force_id, false) != (value == "1")) { _preferences->putBool(preference_lock_force_id, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); } } @@ -3603,7 +3457,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_lock_force_keypad, false) != (value == "1")) { _preferences->putBool(preference_lock_force_keypad, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); } } @@ -3612,7 +3466,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_lock_force_doorsensor, false) != (value == "1")) { _preferences->putBool(preference_lock_force_doorsensor, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); } } @@ -3621,7 +3475,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_opener_force_id, false) != (value == "1")) { _preferences->putBool(preference_opener_force_id, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); } } @@ -3630,7 +3484,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_opener_force_keypad, false) != (value == "1")) { _preferences->putBool(preference_opener_force_keypad, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); } } @@ -3643,17 +3497,45 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_conf_info_enabled, true) != (value == "1")) { _preferences->putBool(preference_conf_info_enabled, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } } + else if(key == "CONFNHPUB") + { + if(_preferences->getBool(preference_publish_config, false) != (value == "1")) + { + if(_preferences->getBool(preference_config_from_mqtt, false) && _preferences->getInt(preference_buffer_size, CHAR_BUFFER_SIZE) < 8192) + { + _preferences->putInt(preference_buffer_size, 8192); + } + _preferences->putBool(preference_publish_config, (value == "1")); + Log->print("Setting changed: "); + Log->println(key); + configChanged = true; + } + } + else if(key == "CONFNHCTRL") + { + if(_preferences->getBool(preference_config_from_mqtt, false) != (value == "1")) + { + if(_preferences->getBool(preference_config_from_mqtt, false) && _preferences->getInt(preference_buffer_size, CHAR_BUFFER_SIZE) < 8192) + { + _preferences->putInt(preference_buffer_size, 8192); + } + _preferences->putBool(preference_config_from_mqtt, (value == "1")); + Log->print("Setting changed: "); + Log->println(key); + configChanged = true; + } + } else if(key == "KPPUB") { if(_preferences->getBool(preference_keypad_info_enabled, false) != (value == "1")) { _preferences->putBool(preference_keypad_info_enabled, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3663,7 +3545,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_keypad_publish_code, false) != (value == "1")) { _preferences->putBool(preference_keypad_publish_code, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3673,7 +3555,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_keypad_check_code_enabled, false) != (value == "1")) { _preferences->putBool(preference_keypad_check_code_enabled, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3683,7 +3565,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_keypad_control_enabled, false) != (value == "1")) { _preferences->putBool(preference_keypad_control_enabled, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3693,7 +3575,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_timecontrol_info_enabled, false) != (value == "1")) { _preferences->putBool(preference_timecontrol_info_enabled, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3703,7 +3585,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_auth_info_enabled, false) != (value == "1")) { _preferences->putBool(preference_auth_info_enabled, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3713,7 +3595,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_keypad_topic_per_entry, false) != (value == "1")) { _preferences->putBool(preference_keypad_topic_per_entry, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3723,7 +3605,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_timecontrol_topic_per_entry, false) != (value == "1")) { _preferences->putBool(preference_timecontrol_topic_per_entry, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3733,7 +3615,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_timecontrol_control_enabled, false) != (value == "1")) { _preferences->putBool(preference_timecontrol_control_enabled, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3743,7 +3625,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_auth_topic_per_entry, false) != (value == "1")) { _preferences->putBool(preference_auth_topic_per_entry, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3753,7 +3635,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_auth_control_enabled, false) != (value == "1")) { _preferences->putBool(preference_auth_control_enabled, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -3763,7 +3645,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_publish_authdata, false) != (value == "1")) { _preferences->putBool(preference_publish_authdata, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -3773,7 +3655,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getInt(preference_http_auth_type, 0) != value.toInt()) { _preferences->putInt(preference_http_auth_type, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; clearSession = true; @@ -3786,7 +3668,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getString(preference_bypass_proxy, "") != value) { _preferences->putString(preference_bypass_proxy, value); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; clearSession = true; @@ -4170,7 +4052,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_register_as_app, false) != (value == "1")) { _preferences->putBool(preference_register_as_app, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -4180,7 +4062,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_register_opener_as_app, false) != (value == "1")) { _preferences->putBool(preference_register_opener_as_app, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); //configChanged = true; } @@ -4190,7 +4072,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_lock_enabled, true) != (value == "1")) { _preferences->putBool(preference_lock_enabled, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -4207,7 +4089,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S _preferences->putBool(preference_official_hybrid_enabled, true); _preferences->putBool(preference_official_hybrid_actions, true); } - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -4217,7 +4099,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_opener_enabled, false) != (value == "1")) { _preferences->putBool(preference_opener_enabled, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -4227,7 +4109,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getBool(preference_connect_mode, true) != (value == "1")) { _preferences->putBool(preference_connect_mode, (value == "1")); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -4243,7 +4125,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getString(preference_cred_user, "") != value) { _preferences->putString(preference_cred_user, value); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; clearSession = true; @@ -4258,6 +4140,21 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S { pass2 = value; } + else if(key == "CREDTOTP") + { + if(value != "*") + { + if(_preferences->getString(preference_totp_secret, "") != value) + { + _preferences->putString(preference_totp_secret, value); + Log->print("Setting changed: "); + Log->println(key); + configChanged = true; + clearSession = true; + newMFA = true; + } + } + } else if(key == "NUKIPIN" && _nuki != nullptr) { if(value == "#") @@ -4273,7 +4170,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S message = "Nuki Lock PIN cleared"; _nuki->setPin(0xffff); } - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -4286,7 +4183,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S message = "Nuki Lock Ultra PIN saved"; _nuki->setUltraPin(value.toInt()); _preferences->putInt(preference_lock_gemini_pin, value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -4297,7 +4194,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S { message = "Nuki Lock PIN saved"; _nuki->setPin(value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -4310,7 +4207,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S { message = "Nuki Opener PIN cleared"; _nukiOpener->setPin(0xffff); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -4320,7 +4217,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S { message = "Nuki Opener PIN saved"; _nukiOpener->setPin(value.toInt()); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println(key); configChanged = true; } @@ -4389,7 +4286,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(manPairLck) { - Log->println(("Changing lock pairing")); + Log->println("Changing lock pairing"); Preferences nukiBlePref; nukiBlePref.begin("NukiHub", false); nukiBlePref.putBytes("bleAddress", currentBleAddress, 6); @@ -4400,14 +4297,14 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S nukiBlePref.putBool("isUltra", isUltra); nukiBlePref.end(); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println("Lock pairing data"); configChanged = true; } if(manPairOpn) { - Log->println(("Changing opener pairing")); + Log->println("Changing opener pairing"); Preferences nukiBlePref; nukiBlePref.begin("NukiHubopener", false); nukiBlePref.putBytes("bleAddress", currentBleAddressOpn, 6); @@ -4415,7 +4312,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S nukiBlePref.putBytes("authorizationId", authorizationIdOpn, 4); nukiBlePref.putBytes("securityPinCode", pincode, 2); nukiBlePref.end(); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println("Opener pairing data"); configChanged = true; } @@ -4425,7 +4322,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getString(preference_cred_password, "") != pass1) { _preferences->putString(preference_cred_password, pass1); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println("CREDPASS"); configChanged = true; clearSession = true; @@ -4437,14 +4334,14 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getString(preference_mqtt_user, "") != "") { _preferences->putString(preference_mqtt_user, ""); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println("MQTTUSER"); configChanged = true; } if(_preferences->getString(preference_mqtt_password, "") != "") { _preferences->putString(preference_mqtt_password, ""); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println("MQTTPASS"); configChanged = true; } @@ -4455,7 +4352,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getString(preference_cred_user, "") != "") { _preferences->putString(preference_cred_user, ""); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println("CREDUSER"); configChanged = true; clearSession = true; @@ -4463,7 +4360,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(_preferences->getString(preference_cred_password, "") != "") { _preferences->putString(preference_cred_password, ""); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println("CREDPASS"); configChanged = true; clearSession = true; @@ -4488,7 +4385,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(curAclPrefs[i] != aclPrefs[i]) { _preferences->putBytes(preference_acl, (byte*)(&aclPrefs), sizeof(aclPrefs)); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println("ACLPREFS"); //configChanged = true; break; @@ -4499,7 +4396,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(curBasicLockConfigAclPrefs[i] != basicLockConfigAclPrefs[i]) { _preferences->putBytes(preference_conf_lock_basic_acl, (byte*)(&basicLockConfigAclPrefs), sizeof(basicLockConfigAclPrefs)); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println("ACLCONFBASICLOCK"); //configChanged = true; break; @@ -4510,7 +4407,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(curAdvancedLockConfigAclPrefs[i] != advancedLockConfigAclPrefs[i]) { _preferences->putBytes(preference_conf_lock_advanced_acl, (byte*)(&advancedLockConfigAclPrefs), sizeof(advancedLockConfigAclPrefs)); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println("ACLCONFADVANCEDLOCK"); //configChanged = true; break; @@ -4522,7 +4419,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(curBasicOpenerConfigAclPrefs[i] != basicOpenerConfigAclPrefs[i]) { _preferences->putBytes(preference_conf_opener_basic_acl, (byte*)(&basicOpenerConfigAclPrefs), sizeof(basicOpenerConfigAclPrefs)); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println("ACLCONFBASICOPENER"); //configChanged = true; break; @@ -4533,7 +4430,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(curAdvancedOpenerConfigAclPrefs[i] != advancedOpenerConfigAclPrefs[i]) { _preferences->putBytes(preference_conf_opener_advanced_acl, (byte*)(&advancedOpenerConfigAclPrefs), sizeof(advancedOpenerConfigAclPrefs)); - Log->print(("Setting changed: ")); + Log->print("Setting changed: "); Log->println("ACLCONFADVANCEDOPENER"); //configChanged = true; break; @@ -4548,24 +4445,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S if(newMFA) { _preferences->putBool(preference_mfa_reconfigure, true); - - if (_preferences->getBool(preference_cred_duo_enabled, false)) - { - _duoEnabled = true; - _duoHost = _preferences->getString(preference_cred_duo_host, ""); - _duoIkey = _preferences->getString(preference_cred_duo_ikey, ""); - _duoSkey = _preferences->getString(preference_cred_duo_skey, ""); - _duoUser = _preferences->getString(preference_cred_duo_user, ""); - - if (_duoHost == "" || _duoIkey == "" || _duoSkey == "" || _duoUser == "" || !_preferences->getBool(preference_update_time, false)) - { - _duoEnabled = false; - } - } - else - { - _duoEnabled = false; - } + _importExport->readSettings(); } if(configChanged) { @@ -4593,13 +4473,6 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S bool WebCfgServer::processImport(PsychicRequest *request, PsychicResponse* resp, String& message) { bool configChanged = false; - unsigned char currentBleAddress[6]; - unsigned char authorizationId[4] = {0x00}; - unsigned char secretKeyK[32] = {0x00}; - unsigned char currentBleAddressOpn[6]; - unsigned char authorizationIdOpn[4] = {0x00}; - unsigned char secretKeyKOpn[32] = {0x00}; - int params = request->params(); for(int index = 0; index < params; index++) @@ -4617,222 +4490,7 @@ bool WebCfgServer::processImport(PsychicRequest *request, PsychicResponse* resp, return configChanged; } - DebugPreferences debugPreferences; - - const std::vector keysPrefs = debugPreferences.getPreferencesKeys(); - const std::vector boolPrefs = debugPreferences.getPreferencesBoolKeys(); - const std::vector bytePrefs = debugPreferences.getPreferencesByteKeys(); - const std::vector intPrefs = debugPreferences.getPreferencesIntKeys(); - const std::vector uintPrefs = debugPreferences.getPreferencesUIntKeys(); - const std::vector uint64Prefs = debugPreferences.getPreferencesUInt64Keys(); - - for(const auto& key : keysPrefs) - { - if(doc[key].isNull()) - { - continue; - } - if(strcmp(key, preference_show_secrets) == 0) - { - continue; - } - if(strcmp(key, preference_latest_version) == 0) - { - continue; - } - if(std::find(boolPrefs.begin(), boolPrefs.end(), key) != boolPrefs.end()) - { - if (doc[key].as().length() > 0) - { - _preferences->putBool(key, (doc[key].as() == "1" ? true : false)); - } - else - { - _preferences->remove(key); - } - continue; - } - if(std::find(intPrefs.begin(), intPrefs.end(), key) != intPrefs.end()) - { - if (doc[key].as().length() > 0) - { - _preferences->putInt(key, doc[key].as()); - } - else - { - _preferences->remove(key); - } - continue; - } - if(std::find(uintPrefs.begin(), uintPrefs.end(), key) != uintPrefs.end()) - { - if (doc[key].as().length() > 0) - { - _preferences->putUInt(key, doc[key].as()); - } - else - { - _preferences->remove(key); - } - continue; - } - if(std::find(uint64Prefs.begin(), uint64Prefs.end(), key) != uint64Prefs.end()) - { - if (doc[key].as().length() > 0) - { - _preferences->putULong64(key, doc[key].as()); - } - else - { - _preferences->remove(key); - } - continue; - } - if (doc[key].as().length() > 0) - { - _preferences->putString(key, doc[key].as()); - } - else - { - _preferences->remove(key); - } - } - - for(const auto& key : bytePrefs) - { - if(!doc[key].isNull() && doc[key].is()) - { - String value = doc[key].as(); - unsigned char tmpchar[32]; - for(int i=0; iputBytes(key, (byte*)(&tmpchar), (value.length() / 2)); - memset(tmpchar, 0, sizeof(tmpchar)); - } - } - - Preferences nukiBlePref; - nukiBlePref.begin("NukiHub", false); - - if(!doc["bleAddressLock"].isNull()) - { - if (doc["bleAddressLock"].as().length() == 12) - { - String value = doc["bleAddressLock"].as(); - for(int i=0; i().length() == 64) - { - String value = doc["secretKeyKLock"].as(); - for(int i=0; i().length() == 8) - { - String value = doc["authorizationIdLock"].as(); - for(int i=0; i().length() >0) - { - nukiBlePref.putBool("isUltra", (doc["isUltra"].as() == "1" ? true : false)); - } - } - nukiBlePref.end(); - if(!doc["securityPinCodeLock"].isNull() && _nuki != nullptr) - { - if(doc["securityPinCodeLock"].as().length() > 0) - { - _nuki->setPin(doc["securityPinCodeLock"].as()); - } - else - { - _nuki->setPin(0xffff); - } - } - if(!doc["ultraPinCodeLock"].isNull() && _nuki != nullptr) - { - if(doc["ultraPinCodeLock"].as().length() > 0) - { - _nuki->setUltraPin(doc["ultraPinCodeLock"].as()); - _preferences->putInt(preference_lock_gemini_pin, doc["ultraPinCodeLock"].as()); - } - else - { - _nuki->setUltraPin(0xffffffff); - _preferences->putInt(preference_lock_gemini_pin, 0); - } - } - nukiBlePref.begin("NukiHubopener", false); - if(!doc["bleAddressOpener"].isNull()) - { - if (doc["bleAddressOpener"].as().length() == 12) - { - String value = doc["bleAddressOpener"].as(); - for(int i=0; i().length() == 64) - { - String value = doc["secretKeyKOpener"].as(); - for(int i=0; i().length() == 8) - { - String value = doc["authorizationIdOpener"].as(); - for(int i=0; i().length() > 0) - { - _nukiOpener->setPin(doc["securityPinCodeOpener"].as()); - } - else - { - _nukiOpener->setPin(0xffff); - } - } + _importExport->importJson(doc); configChanged = true; } @@ -5006,13 +4664,13 @@ esp_err_t WebCfgServer::buildHtml(PsychicRequest *request, PsychicResponse* resp printParameter(&response, "Latest Firmware", _preferences->getString(preference_latest_version).c_str(), "/get?page=ota", "ota"); } response.print("
"); - + response.print("
    "); buildNavigationMenuEntry(&response, "Network Configuration", "/get?page=ntwconfig"); buildNavigationMenuEntry(&response, "MQTT Configuration", "/get?page=mqttconfig", _brokerConfigured ? "" : "Please configure MQTT broker"); buildNavigationMenuEntry(&response, "Nuki Configuration", "/get?page=nukicfg"); buildNavigationMenuEntry(&response, "Access Level Configuration", "/get?page=acclvl"); - buildNavigationMenuEntry(&response, "Credentials", "/get?page=cred", _pinsConfigured ? "" : "Please configure PIN"); + buildNavigationMenuEntry(&response, "Credentials", "/get?page=cred"); buildNavigationMenuEntry(&response, "GPIO Configuration", "/get?page=gpiocfg"); buildNavigationMenuEntry(&response, "Firmware update", "/get?page=ota"); buildNavigationMenuEntry(&response, "Import/Export Configuration", "/get?page=impexpcfg"); @@ -5047,6 +4705,14 @@ esp_err_t WebCfgServer::buildHtml(PsychicRequest *request, PsychicResponse* resp esp_err_t WebCfgServer::buildCredHtml(PsychicRequest *request, PsychicResponse* resp) { + 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'; + PsychicStreamResponse response(resp, "text/html"); response.beginSend(); buildHtmlHeader(&response); @@ -5064,18 +4730,24 @@ esp_err_t WebCfgServer::buildCredHtml(PsychicRequest *request, PsychicResponse* printDropDown(&response, "CREDDIGEST", "HTTP Authentication type", String(_preferences->getInt(preference_http_auth_type, 0)), httpAuthOptions, ""); printInputField(&response, "CREDTRUSTPROXY", "Bypass authentication for reverse proxy with IP", _preferences->getString(preference_bypass_proxy, "").c_str(), 255, ""); printCheckBox(&response, "DUOENA", "Duo Push authentication enabled", _preferences->getBool(preference_cred_duo_enabled, false), ""); - printCheckBox(&response, "DUOAPPROVAL", "Require Duo Push authentication for all sensitive Nuki Hub operations (changing/exporting settings)", _preferences->getBool(preference_cred_duo_approval, false), ""); - printCheckBox(&response, "DUOBYPASS", "Bypass Duo Push authentication by pressing the BOOT button while logging in", _preferences->getBool(preference_cred_bypass_boot_btn_enabled, false), ""); - printInputField(&response, "DUOBYPASSHIGH", "Bypass Duo Push authentication by pulling GPIO High", _preferences->getInt(preference_cred_bypass_gpio_high, -1), 2, ""); - printInputField(&response, "DUOBYPASSLOW", "Bypass Duo Push authentication by pulling GPIO Low", _preferences->getInt(preference_cred_bypass_gpio_low, -1), 2, ""); + printCheckBox(&response, "DUOAPPROVAL", "Require MFA (Duo/TOTP) authentication for all sensitive Nuki Hub operations (changing/exporting settings)", _preferences->getBool(preference_cred_duo_approval, false), ""); + printCheckBox(&response, "DUOBYPASS", "Bypass MFA (Duo/TOTP) authentication by pressing the BOOT button while logging in", _preferences->getBool(preference_cred_bypass_boot_btn_enabled, false), ""); + printInputField(&response, "DUOBYPASSHIGH", "Bypass MFA (Duo/TOTP) authentication by pulling GPIO High", _preferences->getInt(preference_cred_bypass_gpio_high, -1), 2, ""); + printInputField(&response, "DUOBYPASSLOW", "Bypass MFA (Duo/TOTP) authentication by pulling GPIO Low", _preferences->getInt(preference_cred_bypass_gpio_low, -1), 2, ""); printInputField(&response, "DUOHOST", "Duo API hostname", "*", 255, "", true, false); printInputField(&response, "DUOIKEY", "Duo integration key", "*", 255, "", true, false); printInputField(&response, "DUOSKEY", "Duo secret key", "*", 255, "", true, false); printInputField(&response, "DUOUSER", "Duo user", "*", 255, "", true, false); + printInputField(&response, "CREDTOTP", "TOTP Secret Key (requires Form authentication)", "*", 16, "", 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, ""); printInputField(&response, "CREDDUOLFTMRMBR", "Duo Session validity remember (in hours)", _preferences->getInt(preference_cred_session_lifetime_duo_remember, 720), 12, ""); + printInputField(&response, "CREDTOTPLFTM", "TOTP Session validity (in seconds)", _preferences->getInt(preference_cred_session_lifetime_totp, 3600), 12, ""); + printInputField(&response, "CREDTOTPLFTMRMBR", "TOTP Session validity remember (in hours)", _preferences->getInt(preference_cred_session_lifetime_totp_remember, 720), 12, ""); response.print(""); response.print("
    "); response.print(""); @@ -5646,6 +5318,8 @@ esp_err_t WebCfgServer::buildAccLvlHtml(PsychicRequest *request, PsychicResponse response.print(""); response.print("

    Nuki General Access Control

    "); response.print(""); + printCheckBox(&response, "CONFNHPUB", "Publish Nuki Hub configuration information", _preferences->getBool(preference_publish_config, false), ""); + printCheckBox(&response, "CONFNHCTRL", "Modify Nuki Hub configuration over MQTT", _preferences->getBool(preference_config_from_mqtt, false), ""); printCheckBox(&response, "CONFPUB", "Publish Nuki configuration information", _preferences->getBool(preference_conf_info_enabled, true), ""); if((_nuki != nullptr && _nuki->hasKeypad()) || (_nukiOpener != nullptr && _nukiOpener->hasKeypad())) @@ -6047,6 +5721,17 @@ esp_err_t WebCfgServer::buildInfoHtml(PsychicRequest *request, PsychicResponse* response.print(_preferences->getInt(preference_cred_session_lifetime_duo_remember, 720)); } + response.print("\nTOTP MFA enabled: "); + response.print(_importExport->getTOTPEnabled() ? "Yes" : "No"); + + if (_importExport->getTOTPEnabled()) + { + response.print("\nTOTP Session validity (in seconds): "); + response.print(_preferences->getInt(preference_cred_session_lifetime_totp, 3600)); + response.print("\nTOTP Session validity remember (in hours): "); + response.print(_preferences->getInt(preference_cred_session_lifetime_totp_remember, 720)); + } + response.print("\nWeb configurator enabled: "); response.print(_preferences->getBool(preference_webserver_enabled, true) ? "Yes" : "No"); response.print("\nHTTP SSL: "); @@ -6277,7 +5962,7 @@ esp_err_t WebCfgServer::buildInfoHtml(PsychicRequest *request, PsychicResponse* response.print(_preferences->getInt(preference_auth_max_entries, MAX_AUTH)); response.print("\n\n------------ HOME ASSISTANT ------------"); response.print("\nHome Assistant auto discovery enabled: "); - if(_preferences->getString(preference_mqtt_hass_discovery, "").length() > 0) + if(_preferences->getBool(preference_mqtt_hass_enabled, false) && _preferences->getString(preference_mqtt_hass_discovery, "").length() > 0) { response.print("Yes"); response.print("\nHome Assistant auto discovery topic: "); diff --git a/src/WebCfgServer.h b/src/WebCfgServer.h index cf1a2c1..6e3e763 100644 --- a/src/WebCfgServer.h +++ b/src/WebCfgServer.h @@ -15,6 +15,7 @@ #include "NukiNetworkLock.h" #include "NukiOpenerWrapper.h" #include "Gpio.h" +#include "ImportExport.h" extern TaskHandle_t nukiTaskHandle; @@ -32,6 +33,7 @@ enum class TokenType #else #include "NukiNetwork.h" +#include "ImportExport.h" #endif extern TaskHandle_t networkTaskHandle; @@ -40,9 +42,9 @@ class WebCfgServer { public: #ifndef NUKI_HUB_UPDATER - WebCfgServer(NukiWrapper* nuki, NukiOpenerWrapper* nukiOpener, NukiNetwork* network, Gpio* gpio, Preferences* preferences, bool allowRestartToPortal, uint8_t partitionType, PsychicHttpServer* psychicServer); + WebCfgServer(NukiWrapper* nuki, NukiOpenerWrapper* nukiOpener, NukiNetwork* network, Gpio* gpio, Preferences* preferences, bool allowRestartToPortal, uint8_t partitionType, PsychicHttpServer* psychicServer, ImportExport* importExport); #else - WebCfgServer(NukiNetwork* network, Preferences* preferences, bool allowRestartToPortal, uint8_t partitionType, PsychicHttpServer* psychicServer); + WebCfgServer(NukiNetwork* network, Preferences* preferences, bool allowRestartToPortal, uint8_t partitionType, PsychicHttpServer* psychicServer, ImportExport* importExport); #endif ~WebCfgServer() = default; @@ -93,7 +95,6 @@ private: NukiWrapper* _nuki = nullptr; NukiOpenerWrapper* _nukiOpener = nullptr; Gpio* _gpio = nullptr; - bool _pinsConfigured = false; bool _brokerConfigured = false; bool _rebootRequired = false; #endif @@ -103,19 +104,18 @@ private: String generateConfirmCode(); String _confirmCode = "----"; - int checkDuoAuth(PsychicRequest *request); - int checkDuoApprove(); - bool startDuoAuth(char* pushType = (char*)""); - void saveSessions(bool duo = false); - void loadSessions(bool duo = false); + void saveSessions(int type = 0); + void loadSessions(int type = 0); void clearSessions(); esp_err_t logoutSession(PsychicRequest *request, PsychicResponse* resp); - bool isAuthenticated(PsychicRequest *request, bool duo = false); + bool isAuthenticated(PsychicRequest *request, int type = 0); bool processLogin(PsychicRequest *request, PsychicResponse* resp); + bool processTOTP(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 buildDuoHtml(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); esp_err_t buildSSIDListHtml(PsychicRequest *request, PsychicResponse* resp); esp_err_t buildConfirmHtml(PsychicRequest *request, PsychicResponse* resp, const String &message, uint32_t redirectDelay = 5, bool redirect = false, String redirectTo = "/"); @@ -138,6 +138,7 @@ private: PsychicHttpServer* _psychicServer = nullptr; NukiNetwork* _network = nullptr; Preferences* _preferences = nullptr; + ImportExport* _importExport; char _credUser[31] = {0}; char _credPassword[31] = {0}; @@ -147,20 +148,8 @@ private: size_t _otaContentLen = 0; String _hostname; JsonDocument _httpSessions; - JsonDocument _duoSessions; - JsonDocument _sessionsOpts; - struct tm timeinfo; - bool _duoActiveRequest; bool _duoEnabled = false; bool _bypassGPIO = false; int _bypassGPIOHigh = -1; int _bypassGPIOLow = -1; - int64_t _duoRequestTS = 0; - String _duoTransactionId; - String _duoHost; - String _duoSkey; - String _duoIkey; - String _duoUser; - String _duoCheckId; - String _duoCheckIP; }; diff --git a/src/WebCfgServerConstants.h b/src/WebCfgServerConstants.h index 1bb2264..96f5389 100644 --- a/src/WebCfgServerConstants.h +++ b/src/WebCfgServerConstants.h @@ -3,7 +3,7 @@ // escaped by https://www.cescaper.com/ // source: https://cdn.jsdelivr.net/npm/@exampledev/new.css@1.1.2/new.min.css -const char stylecss[] = ":root{--nc-font-sans:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';--nc-font-mono:Consolas,monaco,'Ubuntu Mono','Liberation Mono','Courier New',Courier,monospace;--nc-tx-1:#000;--nc-tx-2:#1a1a1a;--nc-bg-1:#fff;--nc-bg-2:#f6f8fa;--nc-bg-3:#e5e7eb;--nc-lk-1:#0070f3;--nc-lk-2:#0366d6;--nc-lk-tx:#fff;--nc-ac-1:#79ffe1;--nc-ac-tx:#0c4047}@media(prefers-color-scheme:dark){:root{--nc-tx-1:#fff;--nc-tx-2:#eee;--nc-bg-1:#000;--nc-bg-2:#111;--nc-bg-3:#222;--nc-lk-1:#3291ff;--nc-lk-2:#0070f3;--nc-lk-tx:#fff;--nc-ac-1:#7928ca;--nc-ac-tx:#fff}}*{margin:0;padding:0}img,input,option,p,table,textarea,ul{margin-bottom:1rem}button,html,input,select{font-family:var(--nc-font-sans)}body{margin:0 auto;max-width:750px;padding:2rem;border-radius:6px;overflow-x:hidden;word-break:normal;overflow-wrap:anywhere;background:var(--nc-bg-1);color:var(--nc-tx-2);font-size:1.03rem;line-height:1.5}::selection{background:var(--nc-ac-1);color:var(--nc-ac-tx)}h1,h2,h3,h4,h5,h6{line-height:1;color:var(--nc-tx-1);padding-top:.875rem}h1,h2,h3{color:var(--nc-tx-1);padding-bottom:2px;margin-bottom:8px;border-bottom:1px solid var(--nc-bg-2)}h4,h5,h6{margin-bottom:.3rem}h1{font-size:2.25rem}h2{font-size:1.85rem}h3{font-size:1.55rem}h4{font-size:1.25rem}h5{font-size:1rem}h6{font-size:.875rem}a{color:var(--nc-lk-1)}a:hover{color:var(--nc-lk-2) !important;}abbr{cursor:help}abbr:hover{cursor:help}a button,button,input[type=button],input[type=reset],input[type=submit]{font-size:1rem;display:inline-block;padding:6px 12px;text-align:center;text-decoration:none;white-space:nowrap;background:var(--nc-lk-1);color:var(--nc-lk-tx);border:0;border-radius:4px;box-sizing:border-box;cursor:pointer;color:var(--nc-lk-tx)}a button[disabled],button[disabled],input[type=button][disabled],input[type=reset][disabled],input[type=submit][disabled]{cursor:default;opacity:.5;cursor:not-allowed}.button:focus,.button:hover,button:focus,button:hover,input[type=button]:focus,input[type=button]:hover,input[type=reset]:focus,input[type=reset]:hover,input[type=submit]:focus,input[type=submit]:hover{background:var(--nc-lk-2)}table{border-collapse:collapse;width:100%}td,th{border:1px solid var(--nc-bg-3);text-align:left;padding:.5rem}th{background:var(--nc-bg-2)}tr{background:var(--nc-bg-2)}textarea{max-width:100%}input,select,textarea{padding:6px 12px;margin-bottom:.5rem;background:var(--nc-bg-2);color:var(--nc-tx-2);border:1px solid var(--nc-bg-3);border-radius:4px;box-shadow:none;box-sizing:border-box}img{max-width:100%}td>input{margin-top:0;margin-bottom:0}td>textarea{margin-top:0;margin-bottom:0}td>select{margin-top:0;margin-bottom:0}.warning{color:red}@media only screen and (max-width:600px){.adapt td{display:block}.adapt input[type=text],.adapt input[type=password],.adapt input[type=submit],.adapt textarea,.adapt select{width:100%}.adapt td:has(input[type=checkbox]){text-align:center}.adapt input[type=checkbox]{width:1.5em;height:1.5em}.adapt table td:first-child{border-bottom:0}.adapt table td:last-child{border-top:0}#tblnav a li>span{max-width:140px}}#tblnav a{border:0;border-bottom:1px solid;display:block;font-size:1rem;font-weight:bold;padding:.6rem 0;line-height:1;color:var(--nc-tx-1);text-decoration:none;background:linear-gradient(to left,transparent 50%,rgba(255,255,255,0.4) 50%) right;background-size:200% 100%;transition:all .2s ease}#tblnav a{background:linear-gradient(to left,var(--nc-bg-2) 50%,rgba(255,255,255,0.4) 50%) right;background-size:200% 100%}#tblnav a:hover{background-position:left;transition:all .45s ease}#tblnav a:active{background:var(--nc-lk-1);transition:all .15s ease}#tblnav a li{list-style:none;padding:.5rem;display:inline-block;width:100%}#tblnav a li>span{float:right;text-align:right;margin-right:10px;color:#f70;font-weight:100;font-style:italic;display:block}.tdbtn{text-align:center;vertical-align:middle}.naventry{float:left;max-width:375px;width:100%}"; +const char stylecss[] = ":root{--nc-font-sans:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';--nc-font-mono:Consolas,monaco,'Ubuntu Mono','Liberation Mono','Courier New',Courier,monospace;--nc-tx-1:#000;--nc-tx-2:#1a1a1a;--nc-bg-1:#fff;--nc-bg-2:#f6f8fa;--nc-bg-3:#e5e7eb;--nc-lk-1:#0070f3;--nc-lk-2:#0366d6;--nc-lk-tx:#fff;--nc-ac-1:#79ffe1;--nc-ac-tx:#0c4047}@media(prefers-color-scheme:dark){:root{--nc-tx-1:#fff;--nc-tx-2:#eee;--nc-bg-1:#000;--nc-bg-2:#111;--nc-bg-3:#222;--nc-lk-1:#3291ff;--nc-lk-2:#0070f3;--nc-lk-tx:#fff;--nc-ac-1:#7928ca;--nc-ac-tx:#fff}}*{margin:0;padding:0}img,input,option,p,table,textarea,ul{margin-bottom:1rem}button,html,input,select{font-family:var(--nc-font-sans)}body{margin:0 auto;max-width:750px;padding:2rem;border-radius:6px;overflow-x:hidden;word-break:normal;overflow-wrap:anywhere;background:var(--nc-bg-1);color:var(--nc-tx-2);font-size:1.03rem;line-height:1.5}::selection{background:var(--nc-ac-1);color:var(--nc-ac-tx)}h1,h2,h3,h4,h5,h6{line-height:1;color:var(--nc-tx-1);padding-top:.875rem}h1,h2,h3{color:var(--nc-tx-1);padding-bottom:2px;margin-bottom:8px;border-bottom:1px solid var(--nc-bg-2)}h4,h5,h6{margin-bottom:.3rem}h1{font-size:2.25rem}h2{font-size:1.85rem}h3{font-size:1.55rem}h4{font-size:1.25rem}h5{font-size:1rem}h6{font-size:.875rem}a{color:var(--nc-lk-1)}a:hover{color:var(--nc-lk-2) !important;}abbr{cursor:help}abbr:hover{cursor:help}a button,button,input[type=button],input[type=reset],input[type=submit]{font-size:1rem;display:inline-block;padding:6px 12px;text-align:center;text-decoration:none;white-space:nowrap;background:var(--nc-lk-1);color:var(--nc-lk-tx);border:0;border-radius:4px;box-sizing:border-box;cursor:pointer;color:var(--nc-lk-tx)}a button[disabled],button[disabled],input[type=button][disabled],input[type=reset][disabled],input[type=submit][disabled]{cursor:default;opacity:.5;cursor:not-allowed}.button:focus,.button:hover,button:focus,button:hover,input[type=button]:focus,input[type=button]:hover,input[type=reset]:focus,input[type=reset]:hover,input[type=submit]:focus,input[type=submit]:hover{background:var(--nc-lk-2)}table{border-collapse:collapse;width:100%}td,th{border:1px solid var(--nc-bg-3);text-align:left;padding:.5rem}th{background:var(--nc-bg-2)}tr:nth-child(even){background:var(--nc-bg-2)}textarea{max-width:100%}input,select,textarea{padding:6px 12px;margin-bottom:.5rem;background:var(--nc-bg-2);color:var(--nc-tx-2);border:1px solid var(--nc-bg-3);border-radius:4px;box-shadow:none;box-sizing:border-box}img{max-width:100%}td>input{margin-top:0;margin-bottom:0}td>textarea{margin-top:0;margin-bottom:0}td>select{margin-top:0;margin-bottom:0}.warning{color:red}@media only screen and (max-width:600px){.adapt td{display:block}.adapt input[type=text],.adapt input[type=password],.adapt input[type=submit],.adapt textarea,.adapt select{width:100%}.adapt td:has(input[type=checkbox]){text-align:center}.adapt input[type=checkbox]{width:1.5em;height:1.5em}.adapt table td:first-child{border-bottom:0}.adapt table td:last-child{border-top:0}#tblnav a li>span{max-width:140px}}#tblnav a{border:0;border-bottom:1px solid;display:block;font-size:1rem;font-weight:bold;padding:.6rem 0;line-height:1;color:var(--nc-tx-1);text-decoration:none;background:linear-gradient(to left,transparent 50%,rgba(255,255,255,0.4) 50%) right;background-size:200% 100%;transition:all .2s ease}#tblnav a{background:linear-gradient(to left,var(--nc-bg-2) 50%,rgba(255,255,255,0.4) 50%) right;background-size:200% 100%}#tblnav a:hover{background-position:left;transition:all .45s ease}#tblnav a:active{background:var(--nc-lk-1);transition:all .15s ease}#tblnav a li{list-style:none;padding:.5rem;display:inline-block;width:100%}#tblnav a li>span{float:right;text-align:right;margin-right:10px;color:#f70;font-weight:100;font-style:italic;display:block}.tdbtn{text-align:center;vertical-align:middle}.naventry{float:left;max-width:375px;width:100%}"; // converted to char array by https://notisrac.github.io/FileToCArray/ const uint8_t favicon_32x32[] = { @@ -59,4 +59,4 @@ const uint8_t favicon_32x32[] = { 0x2e, 0x66, 0x20, 0x0a, 0x56, 0x51, 0x09, 0x33, 0x93, 0xfc, 0x01, 0xf3, 0x6f, 0x2d, 0xfb, 0x03, 0xed, 0x06, 0xb0, 0xce, 0xb5, 0xc4, 0xb4, 0x59, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82 -}; \ No newline at end of file +}; diff --git a/src/main.cpp b/src/main.cpp index 9da9be3..142450f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -31,6 +31,7 @@ #include "RestartReason.h" #include "EspMillis.h" #include "NimBLEDevice.h" +#include "ImportExport.h" /* #ifdef DEBUG_NUKIHUB @@ -65,6 +66,7 @@ int64_t restartTs = (pow(2,63) - (5 * 1000 * 60000)) / 1000; #include "../../src/RestartReason.h" #include "../../src/NukiNetwork.h" #include "../../src/EspMillis.h" +#include "../../src/ImportExport.h" int64_t restartTs = 10 * 60 * 1000; @@ -78,6 +80,7 @@ NukiNetwork* network = nullptr; WebCfgServer* webCfgServer = nullptr; WebCfgServer* webCfgServerSSL = nullptr; Preferences* preferences = nullptr; +ImportExport* importExport = nullptr; RTC_NOINIT_ATTR int espRunning; RTC_NOINIT_ATTR int restartReason; @@ -163,9 +166,9 @@ void setReroute() uint8_t checkPartition() { const esp_partition_t* running_partition = esp_ota_get_running_partition(); - Log->print(("Partition size: ")); + Log->print("Partition size: "); Log->println(running_partition->size); - Log->print(("Partition subtype: ")); + Log->print("Partition subtype: "); Log->println(running_partition->subtype); if(running_partition->size == 1966080) @@ -203,7 +206,7 @@ void networkTask(void *pvParameters) if(bootloopCounter > 0) { bootloopCounter = (int8_t)0; - Log->println(("Bootloop counter reset")); + Log->println("Bootloop counter reset"); } } @@ -339,12 +342,12 @@ void bootloopDetection() esp_reset_reason() == esp_reset_reason_t::ESP_RST_WDT) { bootloopCounter++; - Log->print(("Bootloop counter incremented: ")); + Log->print("Bootloop counter incremented: "); Log->println(bootloopCounter); if(bootloopCounter == 10) { - Log->print(("Bootloop detected.")); + Log->print("Bootloop detected."); preferences->putInt(preference_buffer_size, CHAR_BUFFER_SIZE); preferences->putInt(preference_task_size_network, NETWORK_TASK_SIZE); @@ -440,7 +443,7 @@ void otaTask(void *pvParameter) { .http_config = &config, }; - Log->print(("Attempting to download update from ")); + Log->print("Attempting to download update from "); Log->println(config.url); int retryMax = 3; @@ -649,9 +652,9 @@ void setup() } #ifdef NUKI_HUB_UPDATER - Log->print(("Nuki Hub OTA version ")); + Log->print("Nuki Hub OTA version "); Log->println(NUKI_HUB_VERSION); - Log->print(("Nuki Hub OTA build ")); + Log->print("Nuki Hub OTA build "); Log->println(); if(preferences->getString(preference_updater_version, "") != NUKI_HUB_VERSION) @@ -666,6 +669,8 @@ void setup() { preferences->putString(preference_updater_date, NUKI_HUB_DATE); } + + importExport = new ImportExport(preferences); network = new NukiNetwork(preferences); network->initialize(); @@ -735,7 +740,7 @@ void setup() psychicSSLServer->ssl_config.httpd.max_open_sockets = 8; psychicSSLServer->setCertificate(cert, key); psychicSSLServer->config.stack_size = HTTPD_TASK_SIZE; - webCfgServerSSL = new WebCfgServer(network, preferences, network->networkDeviceType() == NetworkDeviceType::WiFi, partitionType, psychicSSLServer); + webCfgServerSSL = new WebCfgServer(network, preferences, network->networkDeviceType() == NetworkDeviceType::WiFi, partitionType, psychicSSLServer, importExport); webCfgServerSSL->initialize(); psychicSSLServer->onNotFound([](PsychicRequest* request, PsychicResponse* response) { return response->redirect("/"); @@ -751,7 +756,7 @@ void setup() #endif psychicServer = new PsychicHttpServer; psychicServer->config.stack_size = HTTPD_TASK_SIZE; - webCfgServer = new WebCfgServer(network, preferences, network->networkDeviceType() == NetworkDeviceType::WiFi, partitionType, psychicServer); + webCfgServer = new WebCfgServer(network, preferences, network->networkDeviceType() == NetworkDeviceType::WiFi, partitionType, psychicServer, importExport); webCfgServer->initialize(); psychicServer->onNotFound([](PsychicRequest* request, PsychicResponse* response) { return response->redirect("/"); @@ -767,9 +772,9 @@ void setup() bootloopDetection(); } - Log->print(("Nuki Hub version ")); + Log->print("Nuki Hub version "); Log->println(NUKI_HUB_VERSION); - Log->print(("Nuki Hub build ")); + Log->print("Nuki Hub build "); Log->println(NUKI_HUB_BUILD); uint32_t devIdOpener = preferences->getUInt(preference_device_id_opener); @@ -791,8 +796,10 @@ 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); + network = new NukiNetwork(preferences, gpio, mqttLockPath, CharBuffer::get(), buffer_size, importExport); network->initialize(); lockEnabled = preferences->getBool(preference_lock_enabled); @@ -826,7 +833,7 @@ void setup() networkLock->initialize(); } - nuki = new NukiWrapper("NukiHub", deviceIdLock, bleScanner, networkLock, nukiOfficial, gpio, preferences); + nuki = new NukiWrapper("NukiHub", deviceIdLock, bleScanner, networkLock, nukiOfficial, gpio, preferences, CharBuffer::get(), buffer_size); nuki->initialize(); } @@ -840,7 +847,7 @@ void setup() networkOpener->initialize(); } - nukiOpener = new NukiOpenerWrapper("NukiHub", deviceIdOpener, bleScanner, networkOpener, gpio, preferences); + nukiOpener = new NukiOpenerWrapper("NukiHub", deviceIdOpener, bleScanner, networkOpener, gpio, preferences, CharBuffer::get(), buffer_size); nukiOpener->initialize(); } @@ -911,7 +918,7 @@ void setup() psychicSSLServer->ssl_config.httpd.max_open_sockets = 8; psychicSSLServer->setCertificate(cert, key); psychicSSLServer->config.stack_size = HTTPD_TASK_SIZE; - webCfgServerSSL = new WebCfgServer(nuki, nukiOpener, network, gpio, preferences, network->networkDeviceType() == NetworkDeviceType::WiFi, partitionType, psychicSSLServer); + webCfgServerSSL = new WebCfgServer(nuki, nukiOpener, network, gpio, preferences, network->networkDeviceType() == NetworkDeviceType::WiFi, partitionType, psychicSSLServer, importExport); webCfgServerSSL->initialize(); psychicSSLServer->onNotFound([](PsychicRequest* request, PsychicResponse* response) { return response->redirect("/"); @@ -927,7 +934,7 @@ void setup() #endif psychicServer = new PsychicHttpServer; psychicServer->config.stack_size = HTTPD_TASK_SIZE; - webCfgServer = new WebCfgServer(nuki, nukiOpener, network, gpio, preferences, network->networkDeviceType() == NetworkDeviceType::WiFi, partitionType, psychicServer); + webCfgServer = new WebCfgServer(nuki, nukiOpener, network, gpio, preferences, network->networkDeviceType() == NetworkDeviceType::WiFi, partitionType, psychicServer, importExport); webCfgServer->initialize(); psychicServer->onNotFound([](PsychicRequest* request, PsychicResponse* response) { return response->redirect("/"); @@ -949,27 +956,27 @@ void setup() } #endif */ - - String timeserver = preferences->getString(preference_time_server, "pool.ntp.org"); - esp_sntp_config_t config = ESP_NETIF_SNTP_DEFAULT_CONFIG(timeserver.c_str()); - config.start = false; - config.server_from_dhcp = true; - config.renew_servers_after_new_IP = true; - config.index_of_first_server = 1; - - if (network->networkDeviceType() == NetworkDeviceType::WiFi) - { - config.ip_event_to_renew = IP_EVENT_STA_GOT_IP; - } - else - { - config.ip_event_to_renew = IP_EVENT_ETH_GOT_IP; - } - config.sync_cb = cbSyncTime; - esp_netif_sntp_init(&config); } #endif + String timeserver = preferences->getString(preference_time_server, "pool.ntp.org"); + esp_sntp_config_t config = ESP_NETIF_SNTP_DEFAULT_CONFIG(timeserver.c_str()); + config.start = false; + config.server_from_dhcp = true; + config.renew_servers_after_new_IP = true; + config.index_of_first_server = 1; + + if (network->networkDeviceType() == NetworkDeviceType::WiFi) + { + config.ip_event_to_renew = IP_EVENT_STA_GOT_IP; + } + else + { + config.ip_event_to_renew = IP_EVENT_ETH_GOT_IP; + } + config.sync_cb = cbSyncTime; + esp_netif_sntp_init(&config); + if(doOta) { setupTasks(true); diff --git a/src/networkDevices/EthernetDevice.cpp b/src/networkDevices/EthernetDevice.cpp index 8be245f..f3ef974 100644 --- a/src/networkDevices/EthernetDevice.cpp +++ b/src/networkDevices/EthernetDevice.cpp @@ -72,7 +72,7 @@ void EthernetDevice::initialize() if(ethCriticalFailure) { ethCriticalFailure = false; - Log->println(("Failed to initialize ethernet hardware")); + Log->println("Failed to initialize ethernet hardware"); Log->println("Network device has a critical failure, enable fallback to Wi-Fi and reboot."); wifiFallback = true; delay(200); @@ -80,11 +80,11 @@ void EthernetDevice::initialize() return; } - Log->println(("Init Ethernet")); + Log->println("Init Ethernet"); if(_useSpi) { - Log->println(("Use SPI")); + Log->println("Use SPI"); ethCriticalFailure = true; SPI.begin(_spi_sck, _spi_miso, _spi_mosi); _hardwareInitialized = ETH.begin(_type, _phy_addr, _cs, _irq, _rst, SPI); @@ -93,7 +93,7 @@ void EthernetDevice::initialize() #ifdef CONFIG_IDF_TARGET_ESP32 else { - Log->println(("Use RMII")); + Log->println("Use RMII"); // Workaround for failing RMII initialization with pioarduino 3.1.0 // Revoke all GPIO's some of them set by init PSRAM in IDF @@ -118,7 +118,7 @@ void EthernetDevice::initialize() if(_hardwareInitialized) { - Log->println(("Ethernet hardware Initialized")); + Log->println("Ethernet hardware Initialized"); wifiFallback = false; if(_useSpi && !_ipConfiguration->dhcpEnabled()) @@ -133,7 +133,7 @@ void EthernetDevice::initialize() } else { - Log->println(("Failed to initialize ethernet hardware")); + Log->println("Failed to initialize ethernet hardware"); Log->println("Network device has a critical failure, enable fallback to Wi-Fi and reboot."); wifiFallback = true; delay(200); @@ -150,7 +150,7 @@ void EthernetDevice::update() { if(_ipConfiguration->ipAddress() != ETH.localIP()) { - Log->println(("ETH Set static IP")); + Log->println("ETH Set static IP"); ETH.config(_ipConfiguration->ipAddress(), _ipConfiguration->defaultGateway(), _ipConfiguration->subnet(), _ipConfiguration->dnsServer()); _checkIpTs = espMillis() + 5000; return; diff --git a/src/networkDevices/IPConfiguration.cpp b/src/networkDevices/IPConfiguration.cpp index 3dc5423..9dc9df0 100644 --- a/src/networkDevices/IPConfiguration.cpp +++ b/src/networkDevices/IPConfiguration.cpp @@ -16,20 +16,20 @@ IPConfiguration::IPConfiguration(Preferences *preferences) _gateway.fromString(_preferences->getString(preference_ip_gateway, "")); _dnsServer.fromString(_preferences->getString(preference_ip_dns_server, "")); - Log->print(("IP configuration: ")); + Log->print("IP configuration: "); if(dhcpEnabled()) { - Log->println(("DHCP")); + Log->println("DHCP"); } else { - Log->print(("IP address: ")); + Log->print("IP address: "); Log->print(ipAddress()); - Log->print((", Subnet: ")); + Log->print(", Subnet: "); Log->print(subnet()); - Log->print((", Gateway: ")); + Log->print(", Gateway: "); Log->print(defaultGateway()); - Log->print((", DNS: ")); + Log->print(", DNS: "); Log->println(dnsServer()); } } diff --git a/src/networkDevices/NetworkDevice.cpp b/src/networkDevices/NetworkDevice.cpp index ebd70b0..b921d71 100644 --- a/src/networkDevices/NetworkDevice.cpp +++ b/src/networkDevices/NetworkDevice.cpp @@ -32,7 +32,7 @@ void NetworkDevice::init() if(ca_cert.length() > 1) { _useEncryption = true; - Log->println(("MQTT over TLS.")); + Log->println("MQTT over TLS."); _mqttClientSecure = new espMqttClientSecure(espMqttClientTypes::UseInternalTask::NO); _mqttClientSecure->setCACert(caDest); @@ -57,7 +57,7 @@ void NetworkDevice::init() if(cert.length() > 1 && key.length() > 1) { - Log->println(("MQTT with client certificate.")); + Log->println("MQTT with client certificate."); _mqttClientSecure->setCertificate(certDest); _mqttClientSecure->setPrivateKey(keyDest); } @@ -69,7 +69,7 @@ void NetworkDevice::init() if (!_useEncryption) { - Log->println(("MQTT without TLS.")); + Log->println("MQTT without TLS."); _mqttClient = new espMqttClient(espMqttClientTypes::UseInternalTask::NO); } @@ -251,6 +251,11 @@ void NetworkDevice::mqttDisable() _mqttEnabled = false; } +bool NetworkDevice::isEncrypted() +{ + return _useEncryption; +} + MqttClient *NetworkDevice::getMqttClient() const { if (_useEncryption) diff --git a/src/networkDevices/NetworkDevice.h b/src/networkDevices/NetworkDevice.h index d2d19a3..667bf64 100644 --- a/src/networkDevices/NetworkDevice.h +++ b/src/networkDevices/NetworkDevice.h @@ -29,6 +29,7 @@ public: virtual String BSSIDstr() = 0; #ifndef NUKI_HUB_UPDATER + virtual bool isEncrypted(); virtual bool mqttConnect(); virtual bool mqttDisconnect(bool force); virtual void mqttDisable(); diff --git a/src/util/NetworkUtil.cpp b/src/util/NetworkUtil.cpp index eef08af..50308a5 100644 --- a/src/util/NetworkUtil.cpp +++ b/src/util/NetworkUtil.cpp @@ -50,7 +50,7 @@ NetworkDeviceType NetworkUtil::GetDeviceTypeFromPreference(int hardwareDetect, i return NetworkDeviceType::LilyGO_T_ETH_ELite; break; default: - Log->println(("Unknown hardware selected, falling back to Wi-Fi.")); + Log->println("Unknown hardware selected, falling back to Wi-Fi."); return NetworkDeviceType::WiFi; break; } diff --git a/updater/platformio.ini b/updater/platformio.ini index 6ceb202..99e37d8 100644 --- a/updater/platformio.ini +++ b/updater/platformio.ini @@ -52,7 +52,8 @@ lib_deps = PsychicHttp=symlink://../lib/PsychicHttp ArduinoJson=symlink://../lib/ArduinoJson DuoAuthLibrary=symlink://../lib/DuoAuthLibrary - + Arduino-Base32-Decode=symlink://../lib/Arduino-Base32-Decode + Arduino-TOTP-RFC6238-generator=symlink://../lib/Arduino-TOTP-RFC6238-generator monitor_speed = 115200 monitor_filters = esp32_exception_decoder diff --git a/updater/src/CMakeLists.txt b/updater/src/CMakeLists.txt index b418139..d317a2b 100644 --- a/updater/src/CMakeLists.txt +++ b/updater/src/CMakeLists.txt @@ -6,10 +6,12 @@ list(APPEND app_sources ../../src/PreferencesKeys.h) list(APPEND app_sources ../../src/RestartReason.h) list(APPEND app_sources ../../src/WebCfgServer.h) list(APPEND app_sources ../../src/WebCfgServerConstants.h) +list(APPEND app_sources ../../src/ImportExport.h) list(APPEND app_sources ../../src/Logger.cpp) list(APPEND app_sources ../../src/NukiNetwork.cpp) list(APPEND app_sources ../../src/WebCfgServer.cpp) +list(APPEND app_sources ../../src/ImportExport.cpp) list(APPEND app_sources ../../src/enums/NetworkDeviceType.h) list(APPEND app_sources ../../src/networkDevices/EthernetDevice.h)
    SettingEnabled