diff --git a/README.md b/README.md
index 88d5b32..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
@@ -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
@@ -324,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
@@ -463,8 +466,8 @@ 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)"
@@ -520,7 +523,7 @@ Note that the following options can break Nuki Hub and cause bootloops that will
- 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
+## Import and Export Nuki Hub settings over MQTT (BETA)
Consider this when deciding if you want to enable the following functionality:
@@ -546,7 +549,7 @@ A general explanation of the exported values can be found in the [PreferencesKey
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 Duo Push authentication for all sensitive Nuki Hub operations (changing/exporting settings)` you will first need to approve the Duo Push before the settings will be exported.
+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
@@ -557,7 +560,7 @@ The expected values and format is the same as the JSON files/values that can be
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 Duo Push authentication for all sensitive Nuki Hub operations (changing/exporting settings)` you will first need to approve the Duo Push before the settings will be changed/imported.
+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.
@@ -565,7 +568,7 @@ A general explanation of the values that can be imported can be found in the [Pr
## 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
@@ -587,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" }` |
@@ -631,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" }` |
@@ -961,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 f21ed17..60ad55d 100644
--- a/clion/CMakeLists.txt
+++ b/clion/CMakeLists.txt
@@ -71,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(
@@ -81,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 c7e4c00..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-04"
+#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/ImportExport.cpp b/src/ImportExport.cpp
index 452411c..a38e877 100644
--- a/src/ImportExport.cpp
+++ b/src/ImportExport.cpp
@@ -3,6 +3,7 @@
#include "Logger.h"
#include "PreferencesKeys.h"
#include
+#include
ImportExport::ImportExport(Preferences *preferences)
: _preferences(preferences)
@@ -34,6 +35,9 @@ void ImportExport::readSettings()
_bypassGPIOLow = _preferences->getInt(preference_cred_bypass_gpio_low, -1);
}
}
+
+ _totpKey = _preferences->getString(preference_totp_secret, "");
+ _totpEnabled = _totpKey.length() > 0;
}
bool ImportExport::getDuoEnabled()
@@ -41,6 +45,11 @@ bool ImportExport::getDuoEnabled()
return _duoEnabled;
}
+bool ImportExport::getTOTPEnabled()
+{
+ return _totpEnabled;
+}
+
bool ImportExport::getBypassGPIOEnabled()
{
return _bypassGPIO;
@@ -90,7 +99,7 @@ bool ImportExport::startDuoAuth(char* pushType)
void ImportExport::setDuoCheckIP(String duoCheckIP)
{
- _duoCheckIP = duoCheckIP;
+ _duoCheckIP = duoCheckIP;
}
void ImportExport::setDuoCheckId(String duoCheckId)
@@ -122,12 +131,22 @@ int ImportExport::checkDuoAuth(PsychicRequest *request)
const char* duo_ikey = _duoIkey.c_str();
const char* duo_skey = _duoSkey.c_str();
const char* duo_user = _duoUser.c_str();
+
+ int type = 0;
+ if(request->hasParam("type"))
+ {
+ const PsychicWebParameter* p = request->getParam("type");
+ if(p->value() != "")
+ {
+ type = p->value().toInt();
+ }
+ }
if (request->hasParam("id")) {
const PsychicWebParameter* p = request->getParam("id");
- String cookie2 = p->value();
+ String id = p->value();
DuoAuthLib duoAuth;
- if(_duoActiveRequest && _duoCheckIP == request->client()->localIP().toString() && cookie2 == _duoCheckId)
+ if(_duoActiveRequest && _duoCheckIP == request->client()->localIP().toString() && id == _duoCheckId)
{
duoAuth.begin(duo_host, duo_ikey, duo_skey, &timeinfo);
@@ -148,20 +167,28 @@ int ImportExport::checkDuoAuth(PsychicRequest *request)
_duoTransactionId = "";
_duoCheckIP = "";
_duoCheckId = "";
- int64_t durationLength = 60*60*_preferences->getInt(preference_cred_session_lifetime_duo_remember, 720);
+
+ 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);
+ 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);
+ }
}
- 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();
- if (_preferences->getBool(preference_mfa_reconfigure, false))
+ else
{
- _preferences->putBool(preference_mfa_reconfigure, false);
+ _sessionsOpts[request->client()->localIP().toString() + "approve"] = true;
}
return 1;
}
@@ -172,11 +199,19 @@ int ImportExport::checkDuoAuth(PsychicRequest *request)
_duoTransactionId = "";
_duoCheckIP = "";
_duoCheckId = "";
- if (_preferences->getBool(preference_mfa_reconfigure, false))
+
+ if(type==0)
{
- _preferences->putBool(preference_cred_duo_enabled, false);
- _duoEnabled = false;
- _preferences->putBool(preference_mfa_reconfigure, false);
+ 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;
}
@@ -231,6 +266,32 @@ int ImportExport::checkDuoApprove()
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)) {
@@ -1000,6 +1061,6 @@ JsonDocument ImportExport::importJson(JsonDocument &doc)
}
}
}
-
+
return json;
}
\ No newline at end of file
diff --git a/src/ImportExport.h b/src/ImportExport.h
index 263983c..341d4dc 100644
--- a/src/ImportExport.h
+++ b/src/ImportExport.h
@@ -15,6 +15,8 @@ public:
int checkDuoAuth(PsychicRequest *request);
int checkDuoApprove();
bool startDuoAuth(char* pushType = (char*)"");
+ bool getTOTPEnabled();
+ bool checkTOTP(String* totpKey);
bool getDuoEnabled();
bool getBypassGPIOEnabled();
int getBypassGPIOHigh();
@@ -23,11 +25,13 @@ public:
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;
@@ -41,5 +45,6 @@ private:
String _duoUser;
String _duoCheckId;
String _duoCheckIP;
+ String _totpKey;
};
diff --git a/src/NukiNetwork.cpp b/src/NukiNetwork.cpp
index 26bbe0f..a010aee 100644
--- a/src/NukiNetwork.cpp
+++ b/src/NukiNetwork.cpp
@@ -1091,9 +1091,19 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns
}
else
{
- if(_preferences->getBool(preference_cred_duo_approval, false))
+ if(_preferences->getBool(preference_cred_duo_approval, false) && (_importExport->getTOTPEnabled() || _importExport->getDuoEnabled()))
{
- if (!timeSynced)
+ 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);
diff --git a/src/PreferencesKeys.h b/src/PreferencesKeys.h
index 5b02550..ac9b8d5 100644
--- a/src/PreferencesKeys.h
+++ b/src/PreferencesKeys.h
@@ -87,12 +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"
@@ -268,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);
@@ -530,7 +535,7 @@ private:
preference_cred_duo_host, preference_cred_duo_ikey, preference_cred_duo_skey, preference_cred_duo_user, preference_cred_duo_enabled, preference_https_fqdn, preference_bypass_proxy,
preference_cred_session_lifetime, preference_cred_session_lifetime_remember, preference_cred_session_lifetime_duo, preference_cred_session_lifetime_duo_remember,
preference_cred_duo_approval, preference_cred_bypass_boot_btn_enabled, preference_cred_bypass_gpio_high, preference_cred_bypass_gpio_low, preference_publish_config,
- preference_config_from_mqtt
+ preference_config_from_mqtt, preference_totp_secret, preference_cred_session_lifetime_totp, preference_cred_session_lifetime_totp_remember
};
std::vector _redact =
{
@@ -569,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 a8757d0..3598eb2 100644
--- a/src/WebCfgServer.cpp
+++ b/src/WebCfgServer.cpp
@@ -75,7 +75,12 @@ WebCfgServer::WebCfgServer(NukiNetwork* network, Preferences* preferences, bool
if (_duoEnabled)
{
- loadSessions(true);
+ loadSessions(1);
+ }
+
+ if (_importExport->getTOTPEnabled())
+ {
+ loadSessions(2);
}
}
_confirmCode = generateConfirmCode();
@@ -85,26 +90,30 @@ WebCfgServer::WebCfgServer(NukiNetwork* network, Preferences* preferences, bool
#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 && _importExport->_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 && _importExport->_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;
}
@@ -114,7 +123,6 @@ bool WebCfgServer::isAuthenticated(PsychicRequest *request, bool duo)
}
}
}
- Log->println("Authentication Failed");
return false;
}
@@ -156,18 +164,40 @@ esp_err_t WebCfgServer::logoutSession(PsychicRequest *request, PsychicResponse*
if (request->hasCookie("duoId")) {
String cookie2 = request->getCookie("duoId");
_importExport->_duoSessions.remove(cookie2);
- saveSessions(true);
+ 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))
{
@@ -179,22 +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(_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))
{
@@ -206,7 +241,7 @@ void WebCfgServer::loadSessions(bool duo)
{
File file;
- if (!duo)
+ if (type == 0)
{
file = SPIFFS.open("/sessions.json", "r");
@@ -218,7 +253,7 @@ void WebCfgServer::loadSessions(bool duo)
deserializeJson(_httpSessions, file);
}
}
- else
+ else if (type == 1)
{
file = SPIFFS.open("/duosessions.json", "r");
@@ -230,6 +265,18 @@ void WebCfgServer::loadSessions(bool duo)
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();
}
}
@@ -245,6 +292,7 @@ void WebCfgServer::clearSessions()
{
_httpSessions.clear();
_importExport->_duoSessions.clear();
+ _importExport->_totpSessions.clear();
File file;
file = SPIFFS.open("/sessions.json", "w");
serializeJson(_httpSessions, file);
@@ -252,6 +300,9 @@ void WebCfgServer::clearSessions()
file = SPIFFS.open("/duosessions.json", "w");
serializeJson(_importExport->_duoSessions, file);
file.close();
+ file = SPIFFS.open("/totpsessions.json", "w");
+ serializeJson(_importExport->_totpSessions, file);
+ file.close();
}
}
@@ -268,71 +319,65 @@ 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;
}
}
@@ -378,6 +423,7 @@ void WebCfgServer::initialize()
return resp->redirect("/get?page=login");
break;
case 3:
+ case 5:
case 4:
default:
break;
@@ -413,6 +459,7 @@ void WebCfgServer::initialize()
return resp->redirect("/get?page=login");
break;
case 3:
+ case 5:
case 4:
default:
break;
@@ -483,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;
@@ -500,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")
{
@@ -518,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
@@ -532,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");
@@ -560,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 (_importExport->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 = _importExport->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")
{
@@ -653,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");
@@ -694,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
@@ -708,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");
@@ -750,7 +816,7 @@ void WebCfgServer::initialize()
}
}
- if (value != "login")
+ if (value != "login" && value != "totp")
{
int authReq = doAuthentication(request);
@@ -772,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 (_importExport->startDuoAuth((char*)"Approve Nuki Hub setting change"))
+ else
{
- int duoResult = 2;
-
- while (duoResult == 2)
- {
- duoResult = _importExport->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;
}
}
}
@@ -818,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")
{
@@ -902,6 +1010,8 @@ void WebCfgServer::initialize()
return ESP_FAIL;
case 3:
return ESP_FAIL;
+ case 5:
+ return ESP_FAIL;
case 4:
default:
break;
@@ -931,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;
@@ -989,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;
@@ -1741,7 +1861,88 @@ esp_err_t WebCfgServer::buildLoginHtml(PsychicRequest *request, PsychicResponse*
response.print("