TOTP
This commit is contained in:
39
README.md
39
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.<br><br>
|
||||
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.<br>
|
||||
<br>
|
||||
The firmware supports SSL encryption for MQTT, however most people don't use this.<br>
|
||||
The firmware supports SSL encryption for MQTT.<br>
|
||||
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.<br>
|
||||
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.<br>
|
||||
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.<br>
|
||||
According to Nuki this is by design and part of the specification of Wi-Fi/Thread enabled locks.<br>
|
||||
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.<br>
|
||||
Or you can use Nuki Hub in Hybrid mode using Wi-Fi or Thread, see [hybrid mode](/HYBRID.md)<br>
|
||||
|
||||
|
||||
@@ -72,6 +72,11 @@ file(GLOB_RECURSE SRCFILESREC
|
||||
lib/espMqttClient/src/Packets/*.h
|
||||
lib/espMqttClient/src/Transport/*.cpp
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
21
lib/Arduino-Base32-Decode/LICENSE.txt
Normal file
21
lib/Arduino-Base32-Decode/LICENSE.txt
Normal file
@@ -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).
|
||||
60
lib/Arduino-Base32-Decode/README.md
Normal file
60
lib/Arduino-Base32-Decode/README.md
Normal file
@@ -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);
|
||||
@@ -0,0 +1,127 @@
|
||||
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include <Base32-Decode.h>
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
12
lib/Arduino-Base32-Decode/library.properties
Normal file
12
lib/Arduino-Base32-Decode/library.properties
Normal file
@@ -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 <dirkx@webweaving.org>
|
||||
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
|
||||
174
lib/Arduino-Base32-Decode/src/Base32-Decode.cpp
Normal file
174
lib/Arduino-Base32-Decode/src/Base32-Decode.cpp
Normal file
@@ -0,0 +1,174 @@
|
||||
#include <Arduino.h>
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
|
||||
#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 <assert.h>
|
||||
#include <stdio.h>
|
||||
|
||||
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
|
||||
30
lib/Arduino-Base32-Decode/src/Base32-Decode.h
Normal file
30
lib/Arduino-Base32-Decode/src/Base32-Decode.h
Normal file
@@ -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
|
||||
12
lib/Arduino-TOTP-RFC6238-generator/LICENSE.txt
Normal file
12
lib/Arduino-TOTP-RFC6238-generator/LICENSE.txt
Normal file
@@ -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.
|
||||
24
lib/Arduino-TOTP-RFC6238-generator/README.md
Normal file
24
lib/Arduino-TOTP-RFC6238-generator/README.md
Normal file
@@ -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.
|
||||
@@ -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 <WiFi.h>
|
||||
#include <ESPmDNS.h>
|
||||
#include <ArduinoOTA.h>
|
||||
#include <WebServer.h>
|
||||
#include <lwip/apps/sntp.h>
|
||||
|
||||
#include <TOTP-generator.hpp>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
12
lib/Arduino-TOTP-RFC6238-generator/library.properties
Normal file
12
lib/Arduino-TOTP-RFC6238-generator/library.properties
Normal file
@@ -0,0 +1,12 @@
|
||||
name=TOTP-generator
|
||||
version=1.0.1
|
||||
author=Dirk-Willem van Gulik
|
||||
license=ASLv2
|
||||
maintainer=Dirk-Willem van Gulik <dirkx@webweaving.org>
|
||||
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
|
||||
121
lib/Arduino-TOTP-RFC6238-generator/src/TOTP-RC6236-generator.hpp
Normal file
121
lib/Arduino-TOTP-RFC6238-generator/src/TOTP-RC6236-generator.hpp
Normal file
@@ -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 <mbedtls/md.h>
|
||||
|
||||
// Needed for base32 decode - origin
|
||||
// https://github.com/dirkx/Arduino-Base32-Decode/releases
|
||||
//
|
||||
#include <Base32-Decode.h>
|
||||
|
||||
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
|
||||
121
lib/Arduino-TOTP-RFC6238-generator/src/TOTP-generator.hpp
Normal file
121
lib/Arduino-TOTP-RFC6238-generator/src/TOTP-generator.hpp
Normal file
@@ -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 <mbedtls/md.h>
|
||||
|
||||
// Needed for base32 decode - origin
|
||||
// https://github.com/dirkx/Arduino-Base32-Decode/releases
|
||||
//
|
||||
#include <Base32-Decode.h>
|
||||
|
||||
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
|
||||
@@ -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
|
||||
|
||||
# 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
|
||||
@@ -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"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "Logger.h"
|
||||
#include "PreferencesKeys.h"
|
||||
#include <DuoAuthLib.h>
|
||||
#include <TOTP-RC6236-generator.hpp>
|
||||
|
||||
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;
|
||||
@@ -123,11 +132,21 @@ int ImportExport::checkDuoAuth(PsychicRequest *request)
|
||||
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,6 +167,9 @@ int ImportExport::checkDuoAuth(PsychicRequest *request)
|
||||
_duoTransactionId = "";
|
||||
_duoCheckIP = "";
|
||||
_duoCheckId = "";
|
||||
|
||||
if(type==0)
|
||||
{
|
||||
int64_t durationLength = 60*60*_preferences->getInt(preference_cred_session_lifetime_duo_remember, 720);
|
||||
|
||||
if (!_sessionsOpts[request->client()->localIP().toString()])
|
||||
@@ -157,12 +179,17 @@ int ImportExport::checkDuoAuth(PsychicRequest *request)
|
||||
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);
|
||||
_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
|
||||
@@ -172,12 +199,20 @@ int ImportExport::checkDuoAuth(PsychicRequest *request)
|
||||
_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;
|
||||
}
|
||||
}
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<char*> _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<char*> _uintPrefs =
|
||||
{
|
||||
|
||||
@@ -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<JsonVariant>()) || (duo && _importExport->_duoSessions[cookie].is<JsonVariant>()))
|
||||
if ((type == 0 && _httpSessions[cookie].is<JsonVariant>()) || (type == 1 && _importExport->_duoSessions[cookie].is<JsonVariant>()) || (type == 2 && _importExport->_totpSessions[cookie].is<JsonVariant>()))
|
||||
{
|
||||
struct timeval time;
|
||||
gettimeofday(&time, NULL);
|
||||
int64_t time_us = (int64_t)time.tv_sec * 1000000L + (int64_t)time.tv_usec;
|
||||
|
||||
if ((!duo && _httpSessions[cookie].as<signed long long>() > time_us) || (duo && _importExport->_duoSessions[cookie].as<signed long long>() > time_us))
|
||||
if ((type == 0 && _httpSessions[cookie].as<signed long long>() > time_us) || (type == 1 && _importExport->_duoSessions[cookie].as<signed long long>() > time_us) || (type == 2 && _importExport->_totpSessions[cookie].as<signed long long>() > time_us))
|
||||
{
|
||||
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,44 +319,20 @@ 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)
|
||||
{
|
||||
@@ -331,8 +358,26 @@ int WebCfgServer::doAuthentication(PsychicRequest *request)
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
return 3;
|
||||
|
||||
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,32 +619,39 @@ 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;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sendSettings(request, resp);
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
_importExport->_sessionsOpts[request->client()->localIP().toString() + "approve"] = false;
|
||||
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")
|
||||
{
|
||||
return buildImportExportHtml(request, resp);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
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)
|
||||
if (!approved)
|
||||
{
|
||||
return buildConfirmHtml(request, resp, "Duo approval failed, redirecting to main menu", 3, true);
|
||||
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
|
||||
{
|
||||
_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("</head><body><center><h2>Nuki Hub login</h2><form action=\"/post?page=login\" method=\"post\">");
|
||||
response.print("<div class=\"container\"><label for=\"username\"><b>Username</b></label><input type=\"text\" placeholder=\"Enter Username\" name=\"username\" required>");
|
||||
response.print("<label for=\"password\"><b>Password</b></label><input type=\"password\" placeholder=\"Enter Password\" name=\"password\" required>");
|
||||
response.print("<button type=\"submit\">Login</button><label><input type=\"checkbox\" name=\"remember\"> Remember me</label></div>");
|
||||
if (_importExport->getTOTPEnabled() || _duoEnabled)
|
||||
{
|
||||
if (_importExport->getTOTPEnabled()) {
|
||||
response.print("<button name=\"totp\" type=\"submit\" formaction=\"/post?page=login&totp=1\">Login with TOTP</button>");
|
||||
}
|
||||
if (_duoEnabled) {
|
||||
response.print("<button name=\"duo\" type=\"submit\">Login with Duo</button>");
|
||||
}
|
||||
}
|
||||
else {
|
||||
response.print("<button type=\"submit\">Login</button>");
|
||||
}
|
||||
response.print("<label><input type=\"checkbox\" name=\"remember\"> Remember me</label></div>");
|
||||
response.print("</form></center></body></html>");
|
||||
return response.endSend();
|
||||
}
|
||||
|
||||
esp_err_t WebCfgServer::buildTOTPHtml(PsychicRequest *request, PsychicResponse* resp, int type)
|
||||
{
|
||||
PsychicStreamResponse response(resp, "text/html");
|
||||
response.beginSend();
|
||||
response.print("<html><head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
|
||||
response.print("<style>form{border:3px solid #f1f1f1; max-width: 400px;}input[type=password],input[type=text]{width:100%;padding:12px 20px;margin:8px 0;display:inline-block;border:1px solid #ccc;box-sizing:border-box}button{background-color:#04aa6d;color:#fff;padding:14px 20px;margin:8px 0;border:none;cursor:pointer;width:100%}button:hover{opacity:.8}.container{padding:16px}span.password{float:right;padding-top:16px}@media screen and (max-width:300px){span.psw{display:block;float:none}}</style>");
|
||||
/*
|
||||
if (!timeSynced)
|
||||
{
|
||||
char millis[20];
|
||||
itoa(espMillis(), millis, 10);
|
||||
response.print((String)"<script>window.onload = function() { var startTime = Date.now(); var interval = setInterval(function() { var elapsedTime = Date.now() - startTime; document.getElementById(\"timestamp\").innerHTML = (elapsedTime / 1000).toFixed(3) + " + millis + ";}, 100); }</script>");
|
||||
}
|
||||
*/
|
||||
response.print("</head><body><center><h2>Nuki Hub TOTP</h2>");
|
||||
|
||||
String typeText = "Login";
|
||||
|
||||
if(type == 0)
|
||||
{
|
||||
response.print("<form action=\"/post?page=totp\" method=\"post\">");
|
||||
}
|
||||
else
|
||||
{
|
||||
if(type == 1)
|
||||
{
|
||||
typeText = "Export";
|
||||
response.print((String)"<form action=\"" + request->uri() + "\" method=\"get\" target=\"_blank\">");
|
||||
}
|
||||
else if(type == 2)
|
||||
{
|
||||
typeText = "Import";
|
||||
response.print((String)"<form action=\"" + request->uri() + "\" method=\"post\">");
|
||||
}
|
||||
else
|
||||
{
|
||||
typeText = "Save";
|
||||
response.print((String)"<form action=\"" + request->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)"<input type=\"hidden\" name=\"" + p->name() + "\" value='" + p->value() + "' />");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.print("<div class=\"container\">");
|
||||
response.print("<label for=\"totpkey\"><b>TOTP</b></label><input type=\"text\" placeholder=\"Enter TOTP code\" name=\"totpkey\">");
|
||||
/*
|
||||
if (!timeSynced)
|
||||
{
|
||||
response.print("<label for=\"timestamp\"><b>Timestamp</b></label><span type=\"text\" id=\"timestamp\"></span>");
|
||||
}
|
||||
*/
|
||||
response.print("<button type=\"submit\" ");
|
||||
if(type == 1)
|
||||
{
|
||||
response.print("onclick=\"setTimeout(function() { window.location.href = '/' }, 1000);\"");
|
||||
}
|
||||
response.print((String)">" + typeText + "</button></div>");
|
||||
response.print("</form></center></body></html>");
|
||||
return response.endSend();
|
||||
}
|
||||
@@ -1785,14 +1986,33 @@ esp_err_t WebCfgServer::buildCoredumpHtml(PsychicRequest *request, PsychicRespon
|
||||
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 = _importExport->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)
|
||||
{
|
||||
@@ -1807,6 +2027,8 @@ esp_err_t WebCfgServer::buildDuoHtml(PsychicRequest *request, PsychicResponse* r
|
||||
sprintf(buffer + (i * 8), "%08lx", (unsigned long int)esp_random());
|
||||
}
|
||||
|
||||
if(type == 0)
|
||||
{
|
||||
int64_t durationLength = 60*60*_preferences->getInt(preference_cred_session_lifetime_duo_remember, 720);
|
||||
|
||||
if (!_importExport->_sessionsOpts[request->client()->localIP().toString()])
|
||||
@@ -1822,6 +2044,7 @@ esp_err_t WebCfgServer::buildDuoHtml(PsychicRequest *request, PsychicResponse* r
|
||||
{
|
||||
response.setCookie("duoId", buffer, durationLength, "Secure; HttpOnly");
|
||||
}
|
||||
}
|
||||
|
||||
_importExport->setDuoCheckIP(request->client()->localIP().toString());
|
||||
_importExport->setDuoCheckId(buffer);
|
||||
@@ -1829,11 +2052,36 @@ esp_err_t WebCfgServer::buildDuoHtml(PsychicRequest *request, PsychicResponse* r
|
||||
response.beginSend();
|
||||
response.print("<html><head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
|
||||
response.print("<style>.container{border:3px solid #f1f1f1; max-width: 400px; padding:16px}</style>");
|
||||
response.print((String)"<script>let intervalId; let stop = 0; window.onload = function() { updateInfo(); intervalId = setInterval(updateInfo, 2000); }; function updateInfo() { var request = new XMLHttpRequest(); request.open('GET', '/get?page=duocheck&id=" + String(buffer) + "', true); request.onload = () => { const obj = request.responseText; if ((obj == \"1\" || obj == \"0\") && stop == 0) { stop = 1; clearInterval(intervalId); if (obj == \"1\") { document.getElementById('duoresult').innerHTML = 'Login approved<br>Redirecting...'; setTimeout(function() { window.location.href = \"/\"; }, 2000); } else { document.getElementById('duoresult').innerHTML = 'Login failed<br>Refresh to retry'; } } }; request.send(); }</script>");
|
||||
response.print("</head><body><center><h2>Nuki Hub login</h2>");
|
||||
response.print((String)"<script>let intervalId; let stop = 0; window.onload = function() { updateInfo(); intervalId = setInterval(updateInfo, 2000); }; function updateInfo() { var request = new XMLHttpRequest(); request.open('GET', '/get?page=duocheck&id=" + String(buffer) + "&type=" + type + "', true); request.onload = () => { const obj = request.responseText; if ((obj == \"1\" || obj == \"0\") && stop == 0) { stop = 1; clearInterval(intervalId); if (obj == \"1\") { document.getElementById('duoresult').innerHTML = '" + duoText + " approved<br>Redirecting...'; ");
|
||||
if(type == 0)
|
||||
{
|
||||
response.print("setTimeout(function() { window.location.href = \"/\"");
|
||||
}
|
||||
else if (type == 1)
|
||||
{
|
||||
response.print((String)"window.open('" + request->uri() + "'); setTimeout(function() { window.location.href = \"/\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
response.print("setTimeout(function() { document.getElementById('frmrepost').submit()");
|
||||
}
|
||||
response.print("; }, 2000); } else { document.getElementById('duoresult').innerHTML = '" + duoText + " failed<br>Refresh to retry'; } } }; request.send(); }</script>");
|
||||
response.print("</head><body><center><h2>Nuki Hub " + duoText + "</h2>");
|
||||
response.print("<div class=\"container\">Duo Push sent<br><br>");
|
||||
response.print("Please confirm login in the Duo app<br><br><div id=\"duoresult\"></div></div>");
|
||||
response.print("Please confirm " + duoText + " in the Duo app<br><br><div id=\"duoresult\"></div></div>");
|
||||
response.print("</div>");
|
||||
if (type > 1)
|
||||
{
|
||||
response.print((String)"<form id=\"frmrepost\" action=\"" + request->uri() + "\" method=\"post\">");
|
||||
int params = request->params();
|
||||
|
||||
for(int index = 0; index < params; index++)
|
||||
{
|
||||
const PsychicWebParameter* p = request->getParam(index);
|
||||
response.print((String)"<input type=\"hidden\" name=\"" + p->name() + "\" value='" + p->value() + "' />");
|
||||
}
|
||||
response.print("</form>");
|
||||
}
|
||||
response.print("</center></body></html>");
|
||||
|
||||
return response.endSend();
|
||||
@@ -1876,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;
|
||||
}
|
||||
}
|
||||
@@ -2578,6 +2872,28 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S
|
||||
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;
|
||||
}
|
||||
}
|
||||
else if(key == "HADEVDISC")
|
||||
{
|
||||
if(_preferences->getBool(preference_hass_device_discovery, false) != (value == "1"))
|
||||
@@ -3824,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 == "#")
|
||||
@@ -4374,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);
|
||||
@@ -4391,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("<tr id=\"totpgentr\" ><td><input type=\"button\" id=\"totpgen\" onclick=\"document.getElementsByName('CREDTOTP')[0].type='text'; document.getElementsByName('CREDTOTP')[0].value='");
|
||||
response.print(randomstr);
|
||||
response.print("'; document.getElementById('totpgentr').style.display='none';\" value=\"Generate new TOTP key\"></td></tr>");
|
||||
printInputField(&response, "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("</table>");
|
||||
response.print("<br><input type=\"submit\" name=\"submit\" value=\"Save\">");
|
||||
response.print("</form><script>function testcreds() { var input_user = document.getElementById(\"inputuser\").value; var input_pass = document.getElementById(\"inputpass\").value; var input_pass2 = document.getElementById(\"inputpass2\").value; var pattern = /^[ -~]*$/; if(input_user == '#' || input_user == '') { return true; } if (input_pass != input_pass2) { alert('Passwords do not match'); return false;} if(!pattern.test(input_user) || !pattern.test(input_pass)) { alert('Only non unicode characters are allowed in username and password'); return false;} else { return true; } }</script>");
|
||||
@@ -5376,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: ");
|
||||
|
||||
@@ -104,16 +104,18 @@ private:
|
||||
String generateConfirmCode();
|
||||
String _confirmCode = "----";
|
||||
|
||||
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 = "/");
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user