Merge pull request #628 from iranl/mfa-import-export-improvements

Update esp-nimble and pioarduino 3.1.3, various improvements and fixes for HA, SPIFFS and TOTP, add self-signed SSL certificate generator, add bypass MFA, add admin key, add build for GL-S10
This commit is contained in:
iranl
2025-02-18 18:43:51 +01:00
committed by GitHub
37 changed files with 1178 additions and 114 deletions

View File

@@ -12,7 +12,7 @@ jobs:
strategy:
fail-fast: false
matrix:
board: [esp32, esp32-s3, esp32-s3-oct, esp32-c3, esp32-c6, esp32-h2, esp32-solo1]
board: [esp32, esp32-s3, esp32-s3-oct, esp32-c3, esp32-c6, esp32-h2, esp32-solo1, esp32-gl-s10]
build: [release]
env:
BOARD: ${{ matrix.board }}

View File

@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
matrix:
board: [esp32, esp32-s3, esp32-s3-oct, esp32-c3, esp32-c6, esp32-h2, esp32-solo1]
board: [esp32, esp32-s3, esp32-s3-oct, esp32-c3, esp32-c6, esp32-h2, esp32-solo1, esp32-gl-s10]
build: [release]
env:
BOARD: ${{ matrix.board }}

View File

@@ -34,7 +34,7 @@ jobs:
strategy:
fail-fast: false
matrix:
board: [esp32, esp32-s3, esp32-s3-oct, esp32-c3, esp32-c6, esp32-h2, esp32-solo1]
board: [esp32, esp32-s3, esp32-s3-oct, esp32-c3, esp32-c6, esp32-h2, esp32-solo1, esp32-gl-s10]
build: [release]
env:
BOARD: ${{ matrix.board }}

View File

@@ -12,7 +12,7 @@ jobs:
strategy:
fail-fast: false
matrix:
board: [esp32, esp32-s3, esp32-s3-oct, esp32-c3, esp32-c6, esp32-h2, esp32-solo1]
board: [esp32, esp32-s3, esp32-s3-oct, esp32-c3, esp32-c6, esp32-h2, esp32-solo1, esp32-gl-s10]
build: [release]
env:
BOARD: ${{ matrix.board }}

View File

@@ -15,7 +15,7 @@ Feel free to join us on Discord: https://discord.gg/9nPq85bP4p
## Supported devices
<b>Supported ESP32 devices:</b>
- Nuki Hub is compiled against all ESP32 models with Wi-Fi and Bluetooh Low Energy (BLE) which are supported by ESP-IDF 5.3.2 and Arduino Core 3.1.0.
- Nuki Hub is compiled against all ESP32 models with Wi-Fi and Bluetooh Low Energy (BLE) which are supported by ESP-IDF 5.3.2 and Arduino Core 3.1.1.
- Tested stable builds are provided for the ESP32, ESP32-S3, ESP32-C3, ESP32-C6 and ESP32-H2.
- Untested builds are provided for the ESP32-Solo1 (as the developers don't own one).
@@ -333,6 +333,8 @@ Note: All of the following requires the Nuki security code / PIN to be set, see
- Duo secret key: Set to the Duo secret key
- Duo user: Set to the Duo user that you want to receive the push notification
- TOTP Secret Key: Set a TOTP secret key to enable TOTP MFA. Enter the TOTP secret key in an authenticator application (Password manager, Microsoft/Google Authenticator etc.) to generate TOTP codes.
- One-time MFA Bypass: Set a 32 character long alphanumeric string that can be used as a one-time MFA bypass when the ESP32 is unable to sync it's time and TOTP and Duo are unavailable as a result.
- Admin key: Set a 32 character long alphanumeric string that can be used in combination with a TOTP code to export and import settings without needing to log in (for use with automated systems).
- Session validity (in seconds): Session validity to use with form authentication when the "Remember me" checkbox is disabled, default 3600 seconds.
- Session validity remember (in hours): Session validity to use with form authentication when the "Remember me" checkbox is enabled, default 720 hours.
- Duo Session validity (in seconds): Session validity to use with Duo authentication when the "Remember me" checkbox is disabled, default 3600 seconds.
@@ -527,7 +529,7 @@ Note that the following options can break Nuki Hub and cause bootloops that will
Consider this when deciding if you want to enable the following functionality:
- Any application/actor that has read access to `nukihub/configuration/action` and `nukihub/configuration/json` can view your changes and exports.
- Any application/actor that has read access to `nukihub/configuration/action` and `nukihub/configuration/json` can view your changes and exports.
- If you have not enabled the setting to require MFA when changing settings any application/actor that has write access to `nukihub/configuration/action` can change Nuki Hub settings (including pairing data and credentials)
### Export Nuki Hub settings over MQTT
@@ -562,7 +564,7 @@ After the import is complete the ESP32 will reboot.
If you have enabled `Require MFA (Duo/TOTP) authentication for all sensitive Nuki Hub operations (changing/exporting settings)` you will need to either provide a currently valid TOTP code as part of the sent JSON in the `totp` node or approve the Duo Push before the settings will be changed/imported.
Note: When importing settings using MQTT there are less/no checks on the values entered. These checks are only available when changing settings through the WebConfigurator.
Note: When importing settings using MQTT there are less/no checks on the values entered. These checks are only available when changing settings through the WebConfigurator.
Consider testing your configuration values by changing them in the Web Configurator before trying to use MQTT to change configuration.
A general explanation of the values that can be imported can be found in the [PreferencesKeys.h](/src/PreferencesKeys.h) file

View File

@@ -0,0 +1,37 @@
{
"build": {
"arduino":{
"ldscript": "esp32_out.ld"
},
"core": "esp32",
"extra_flags": "-DARDUINO_ESP32_DEV -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue",
"f_cpu": "240000000L",
"f_flash": "40000000L",
"flash_mode": "dio",
"mcu": "esp32",
"variant": "doitESP32devkitV1"
},
"connectivity": [
"wifi",
"bluetooth",
"ethernet",
"can"
],
"debug": {
"openocd_board": "esp-wroom-32.cfg"
},
"frameworks": [
"arduino",
"espidf"
],
"name": "DOIT ESP32 DEVKIT V1",
"upload": {
"flash_size": "4MB",
"maximum_ram_size": 327680,
"maximum_size": 4194304,
"require_upload_port": true,
"speed": 460800
},
"url": "http://www.doit.am/",
"vendor": "DOIT"
}

View File

@@ -2,3 +2,4 @@
# List of Authors
Gary Oppel <gaoppel@cisco.com>
iranl <25727444+iranl@users.noreply.github.com>

View File

@@ -5,3 +5,8 @@ v1.0.0 (08/26/2020)
------
* Initial Library Release
v1.1.0 (01/20/2025)
------
* Enable using ESP32 CA Certificate bundle

View File

@@ -1,9 +1,9 @@
name=Duo Auth Library
version=1.0.0
author=Gary Oppel
maintainer=Gary Oppel
version=1.1.0
author=iranl
maintainer=iranl
sentence=Enables Duo Authentication within your ESP32 Wi-Fi Projects
paragraph=Extends Duo Authentication API's for Push, Passcode, and Asynchronous Push Authentication requests.
category=Other
url=https://github.com/CiscoDevNet/Arduino-DuoAuthLibrary-ESP32
url=https://github.com/technyon/nuki_hub/lib/DuoAuthLibrary
architectures=esp32

View File

@@ -2,6 +2,7 @@
*@license
*
*Copyright 2020 Cisco Systems, Inc. or its affiliates
*Modifications copyright (C) 2025 iranl / Nuki Hub
*
*Licensed under the Apache License, Version 2.0 (the "License");
*you may not use this file except in compliance with the License.
@@ -24,6 +25,11 @@
* @url https://github.com/CiscoDevNet/Arduino-DuoAuthLibrary-ESP32
* @version 1.0.0
* @author Gary Oppel <gaoppel@cisco.com>
* @url https://github.com/technyon/nuki_hub/lib/DuoAuthLibrary
* @version 1.1.0
* @author iranl <25727444+iranl@users.noreply.github.com>
* Modified to enable using ESP32 CA Certificate bundle
*/
//Include DuoAuthLib Library Header

View File

@@ -2,6 +2,7 @@
*@license
*
*Copyright 2020 Cisco Systems, Inc. or its affiliates
*Modifications copyright (C) 2025 iranl / Nuki Hub
*
*Licensed under the Apache License, Version 2.0 (the "License");
*you may not use this file except in compliance with the License.
@@ -24,6 +25,11 @@
* @url https://github.com/CiscoDevNet/Arduino-DuoAuthLibrary-ESP32
* @version 1.0.0
* @author Gary Oppel <gaoppel@cisco.com>
* @url https://github.com/technyon/nuki_hub/lib/DuoAuthLibrary
* @version 1.1.0
* @author iranl <25727444+iranl@users.noreply.github.com>
* Modified to enable using ESP32 CA Certificate bundle
*/
//Verify that the Duo Auth Library descriptor is only included once

View File

@@ -1 +1 @@
# Espressif ESP32 Partition Table
# Espressif ESP32 Partition Table
1 # Espressif ESP32 Partition Table # Name Type SubType Offset Size Flags nvs data nvs 0x9000 0x5000 otadata data ota 0xe000 0x2000 app0 app ota_0 0x10000 0x270000 app1 app ota_1 0x280000 0x150000 0x130000 spiffs data spiffs 0x3D0000 0x3B0000 0x20000 0x40000 coredump data coredump 0x3F0000 0x10000

View File

@@ -9,6 +9,8 @@ def get_board_name(env):
if env.get('BOARD') == 'nuki-esp32solo1':
board = 'esp32solo1'
elif env.get('BOARD') == 'nuki-esp32gls10':
board = 'esp32gls10'
elif env.get('BOARD') == 'nuki-esp32-s3-oct':
board = 'esp32s3oct'
return board

View File

@@ -13,7 +13,7 @@ default_envs = esp32
boards_dir = boards
[env]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.11/platform-espressif32.zip
platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.13/platform-espressif32.zip
platform_packages =
framework = arduino, espidf
board_build.embed_txtfiles =
@@ -78,6 +78,13 @@ build_flags =
-DCONFIG_NIMBLE_CPP_LOG_LEVEL=0
-DCONFIG_BT_NIMBLE_LOG_LEVEL=0
[env:esp32-gl-s10]
extends = env:esp32
board = nuki-esp32gls10
board_build.cmake_extra_args =
-DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.release.defaults;sdkconfig.defaults.esp32;sdkconfig.gls10.defaults"
-DNUKI_TARGET_GL_S10=y
[env:esp32-c3]
extends = env:esp32
board = esp32-c3-devkitc-02
@@ -136,6 +143,18 @@ build_flags =
-DCONFIG_BT_NIMBLE_LOG_LEVEL=0
-DDEBUG_NUKIHUB
[env:esp32-gl-s10_dbg]
extends = env:esp32-gl-s10
custom_build = debug
board_build.cmake_extra_args =
-DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.debug.defaults;sdkconfig.defaults.esp32;sdkconfig.gls10.defaults"
build_flags =
${env.build_flags}
-DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG
-DCONFIG_NIMBLE_CPP_LOG_LEVEL=0
-DCONFIG_BT_NIMBLE_LOG_LEVEL=0
-DDEBUG_NUKIHUB
[env:esp32-c3_dbg]
extends = env:esp32-c3
custom_build = debug

View File

@@ -29,6 +29,13 @@ e000 boot_app0.bin
10000 nuki_hub_esp32.bin
280000 nuki_hub_updater_esp32.bin
ESP32-GL-S10
e000 boot_app0.bin
1000 bootloader.bin
8000 nuki_hub.partitions.bin
10000 nuki_hub_esp32.bin
280000 nuki_hub_updater_esp32.bin
ESP32-S3
e000 boot_app0.bin
0 bootloader.bin
@@ -87,6 +94,10 @@ As an alternative to the Download Tools, you can also use the esptool from the E
esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq keep --flash_size detect 0xe000 boot_app0.bin 0x1000 bootloader.bin 0x10000 nuki_hub_esp32.bin 0x280000 nuki_hub_updater_esp32.bin 0x8000 nuki_hub.partitions.bin
## ESP32-GL-S10
esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq keep --flash_size detect 0xe000 boot_app0.bin 0x1000 bootloader.bin 0x10000 nuki_hub_esp32.bin 0x280000 nuki_hub_updater_esp32.bin 0x8000 nuki_hub.partitions.bin
## ESP32-S3
esptool.py --chip esp32s3 --port /dev/ttyUSB0 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq keep --flash_size detect 0xe000 boot_app0.bin 0x0 bootloader.bin 0x10000 nuki_hub_esp32s3.bin 0x280000 nuki_hub_updater_esp32s3.bin 0x8000 nuki_hub.partitions.bin

View File

@@ -3,6 +3,7 @@ CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_SIZE=y
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_ESPTOOLPY_HEADER_FLASHSIZE_UPDATE=y
CONFIG_SPIFFS_GC_MAX_RUNS=512
# ARDUINO
CONFIG_AUTOSTART_ARDUINO=y
@@ -39,7 +40,7 @@ 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_CUSTOM_CERTIFICATE_BUNDLE_PATH="resources/root_ca.pem"
CONFIG_MBEDTLS_DYNAMIC_BUFFER=y
# RTC WDT

1
sdkconfig.gls10.defaults Normal file
View File

@@ -0,0 +1 @@
CONFIG_D0WD_PSRAM_CLK_IO=6

View File

@@ -5,7 +5,7 @@
#define NUKI_HUB_VERSION "9.09"
#define NUKI_HUB_VERSION_INT (uint32_t)909
#define NUKI_HUB_BUILD "unknownbuildnr"
#define NUKI_HUB_DATE "2025-02-10"
#define NUKI_HUB_DATE "2025-02-18"
#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"
@@ -107,6 +107,21 @@
#define GITHUB_MASTER_UPDATER_BINARY_URL_DBG (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/debug/master/nuki_hub_updater_esp32-solo1.bin"
#define NUKI_HUB_HW (char*)"ESP32-SOLO1"
#define BOOT_BUTTON_GPIO (gpio_num_t)0
#elif defined(NUKI_TARGET_GL_S10)
#define GITHUB_LATEST_RELEASE_BINARY_URL "https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/nuki_hub_esp32-gl-s10.bin"
#define GITHUB_LATEST_UPDATER_BINARY_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/nuki_hub_updater_esp32.bin"
#define GITHUB_BETA_RELEASE_BINARY_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/beta/nuki_hub_esp32-gl-s10.bin"
#define GITHUB_BETA_UPDATER_BINARY_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/beta/nuki_hub_updater_esp32.bin"
#define GITHUB_MASTER_RELEASE_BINARY_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/master/nuki_hub_esp32-gl-s10.bin"
#define GITHUB_MASTER_UPDATER_BINARY_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/master/nuki_hub_updater_esp32.bin"
#define GITHUB_LATEST_RELEASE_BINARY_URL_DBG (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/debug/nuki_hub_esp32-gl-s10.bin"
#define GITHUB_LATEST_UPDATER_BINARY_URL_DBG (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/debug/nuki_hub_updater_esp32.bin"
#define GITHUB_BETA_RELEASE_BINARY_URL_DBG (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/debug/beta/nuki_hub_esp32-gl-s10.bin"
#define GITHUB_BETA_UPDATER_BINARY_URL_DBG (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/debug/beta/nuki_hub_updater_esp32.bin"
#define GITHUB_MASTER_RELEASE_BINARY_URL_DBG (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/debug/master/nuki_hub_esp32-gl-s10.bin"
#define GITHUB_MASTER_UPDATER_BINARY_URL_DBG (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/debug/master/nuki_hub_updater_esp32.bin"
#define NUKI_HUB_HW (char*)"ESP32-GL-S10"
#define BOOT_BUTTON_GPIO (gpio_num_t)0
#else
#define GITHUB_LATEST_RELEASE_BINARY_URL "https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/nuki_hub_esp32.bin"
#define GITHUB_LATEST_UPDATER_BINARY_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/nuki_hub_updater_esp32.bin"

View File

@@ -216,12 +216,12 @@ void HomeAssistantDiscovery::publishHASSNukiHubConfig()
"",
{ { (char*)"en", (char*)"true" }});
// Nuki Hub Webserver enabled
// Webserver enabled
publishHassTopic("switch",
"webserver",
_nukiHubUidString,
"_webserver",
"Nuki Hub webserver enabled",
"Webserver enabled",
_hostname.c_str(),
_baseTopic.c_str(),
String("~") + mqtt_topic_webserver_state,
@@ -317,12 +317,12 @@ void HomeAssistantDiscovery::publishHASSNukiHubConfig()
{(char*)"ic", (char*)"mdi:counter"}
});
// Nuki Hub restart reason
// Restart reason
publishHassTopic("sensor",
"nuki_hub_restart_reason",
_nukiHubUidString,
"_nuki_hub_restart_reason",
"Nuki Hub restart reason",
"Restart reason",
_hostname.c_str(),
_baseTopic.c_str(),
String("~") + mqtt_topic_restart_reason_fw,
@@ -333,12 +333,12 @@ void HomeAssistantDiscovery::publishHASSNukiHubConfig()
"",
{ { (char*)"en", (char*)"true" }});
// Nuki Hub restart reason ESP
// Restart reason ESP
publishHassTopic("sensor",
"nuki_hub_restart_reason_esp",
_nukiHubUidString,
"_nuki_hub_restart_reason_esp",
"Nuki Hub restart reason ESP",
"Restart reason ESP",
_hostname.c_str(),
_baseTopic.c_str(),
String("~") + mqtt_topic_restart_reason_esp,
@@ -351,12 +351,12 @@ void HomeAssistantDiscovery::publishHASSNukiHubConfig()
if(_checkUpdates)
{
// NUKI Hub latest
// Nuki Hub latest
publishHassTopic("sensor",
"nuki_hub_latest",
_nukiHubUidString,
"_nuki_hub_latest",
"NUKI Hub latest",
"Nuki Hub latest",
_hostname.c_str(),
_baseTopic.c_str(),
String("~") + mqtt_topic_info_nuki_hub_latest,
@@ -370,7 +370,7 @@ void HomeAssistantDiscovery::publishHASSNukiHubConfig()
{(char*)"ic", (char*)"mdi:counter"}
});
// NUKI Hub update
// Nuki Hub update
char latest_version_topic[250];
_baseTopic.toCharArray(latest_version_topic,_baseTopic.length() + 1);
strcat(latest_version_topic, mqtt_topic_info_nuki_hub_latest);
@@ -381,7 +381,7 @@ void HomeAssistantDiscovery::publishHASSNukiHubConfig()
"nuki_hub_update",
_nukiHubUidString,
"_nuki_hub_update",
"NUKI Hub firmware update",
"Nuki Hub firmware update",
_hostname.c_str(),
_baseTopic.c_str(),
String("~") + mqtt_topic_info_nuki_hub_version,
@@ -403,7 +403,7 @@ void HomeAssistantDiscovery::publishHASSNukiHubConfig()
"nuki_hub_update",
_nukiHubUidString,
"_nuki_hub_update",
"NUKI Hub firmware update",
"Nuki Hub firmware update",
_hostname.c_str(),
_baseTopic.c_str(),
String("~") + mqtt_topic_info_nuki_hub_version,
@@ -427,12 +427,12 @@ void HomeAssistantDiscovery::publishHASSNukiHubConfig()
removeHassTopic((char*)"update", (char*)"nuki_hub_update", _nukiHubUidString);
}
// Nuki Hub IP Address
// IP Address
publishHassTopic("sensor",
"nuki_hub_ip",
_nukiHubUidString,
"_nuki_hub_ip",
"Nuki Hub IP",
"IP",
_hostname.c_str(),
_baseTopic.c_str(),
String("~") + mqtt_topic_info_nuki_hub_ip,

View File

@@ -1,4 +1,5 @@
#include "ImportExport.h"
#include "EspMillis.h"
#include "SPIFFS.h"
#include "Logger.h"
#include "PreferencesKeys.h"
@@ -38,6 +39,8 @@ void ImportExport::readSettings()
_totpKey = _preferences->getString(preference_totp_secret, "");
_totpEnabled = _totpKey.length() > 0;
_bypassKey = _preferences->getString(preference_bypass_secret, "");
_bypassEnabled = _bypassKey.length() > 0;
}
bool ImportExport::getDuoEnabled()
@@ -50,6 +53,11 @@ bool ImportExport::getTOTPEnabled()
return _totpEnabled;
}
bool ImportExport::getBypassEnabled()
{
return _bypassEnabled;
}
bool ImportExport::getBypassGPIOEnabled()
{
return _bypassGPIO;
@@ -131,7 +139,7 @@ int ImportExport::checkDuoAuth(PsychicRequest *request)
const char* duo_ikey = _duoIkey.c_str();
const char* duo_skey = _duoSkey.c_str();
const char* duo_user = _duoUser.c_str();
int type = 0;
if(request->hasParam("type"))
{
@@ -167,7 +175,7 @@ int ImportExport::checkDuoAuth(PsychicRequest *request)
_duoTransactionId = "";
_duoCheckIP = "";
_duoCheckId = "";
if(type==0)
{
int64_t durationLength = 60*60*_preferences->getInt(preference_cred_session_lifetime_duo_remember, 720);
@@ -199,7 +207,7 @@ int ImportExport::checkDuoAuth(PsychicRequest *request)
_duoTransactionId = "";
_duoCheckIP = "";
_duoCheckId = "";
if(type==0)
{
if (_preferences->getBool(preference_mfa_reconfigure, false))
@@ -268,10 +276,18 @@ int ImportExport::checkDuoApprove()
bool ImportExport::checkTOTP(String* totpKey)
{
String key(totpKey->c_str());
if(_totpEnabled)
{
if((pow(_invalidCount, 5) + _lastCodeCheck) > espMillis())
{
_lastCodeCheck = espMillis();
return false;
}
_lastCodeCheck = espMillis();
String key(totpKey->c_str());
time_t now;
time(&now);
int totpTime = -60;
@@ -279,19 +295,45 @@ bool ImportExport::checkTOTP(String* totpKey)
while (totpTime <= 60)
{
String key2(TOTP::currentOTP(now, _totpKey, 30, 6, totpTime)->c_str());
if(key.toInt() == key2.toInt())
{
_invalidCount = 0;
Log->println("Successful TOTP MFA Auth");
return true;
}
totpTime += 30;
}
_invalidCount++;
Log->println("Failed TOTP MFA Auth");
}
return false;
}
bool ImportExport::checkBypass(String bypass)
{
if(_bypassEnabled)
{
if((pow(_invalidCount2, 5) + _lastCodeCheck2) > espMillis())
{
_lastCodeCheck2 = espMillis();
return false;
}
_lastCodeCheck2 = espMillis();
if(bypass == _bypassKey)
{
_invalidCount2 = 0;
Log->println("Successful Bypass MFA Auth");
return true;
}
_invalidCount2++;
Log->println("Failed Bypass MFA Auth");
}
return false;
}
void ImportExport::exportHttpsJson(JsonDocument &json)
{
if (!SPIFFS.begin(true)) {
@@ -427,10 +469,22 @@ void ImportExport::exportNukiHubJson(JsonDocument &json, bool redacted, bool pai
{
continue;
}
if(strcmp(key, preference_totp_secret) == 0)
{
continue;
}
if(strcmp(key, preference_bypass_secret) == 0)
{
continue;
}
if(strcmp(key, preference_admin_secret) == 0)
{
continue;
}
if(!redacted) if(std::find(redactedPrefs.begin(), redactedPrefs.end(), key) != redactedPrefs.end())
{
continue;
}
{
continue;
}
if(!_preferences->isKey(key))
{
json[key] = "";

View File

@@ -16,7 +16,9 @@ public:
int checkDuoApprove();
bool startDuoAuth(char* pushType = (char*)"");
bool getTOTPEnabled();
bool getBypassEnabled();
bool checkTOTP(String* totpKey);
bool checkBypass(String bypass);
bool getDuoEnabled();
bool getBypassGPIOEnabled();
int getBypassGPIOHigh();
@@ -27,11 +29,17 @@ public:
JsonDocument _duoSessions;
JsonDocument _totpSessions;
JsonDocument _sessionsOpts;
JsonDocument _bypassSessions;
int64_t _lastCodeCheck = 0;
int64_t _lastCodeCheck2 = 0;
int _invalidCount = 0;
int _invalidCount2 = 0;
private:
void saveSessions();
Preferences* _preferences;
struct tm timeinfo;
bool _totpEnabled = false;
bool _bypassEnabled = false;
bool _duoActiveRequest;
bool _duoEnabled = false;
bool _bypassGPIO = false;
@@ -45,6 +53,7 @@ private:
String _duoUser;
String _duoCheckId;
String _duoCheckIP;
String _totpKey;
String _totpKey;
String _bypassKey;
};

View File

@@ -385,6 +385,16 @@ bool NukiNetwork::update()
int64_t ts = espMillis();
_device->update();
if(_importExport->getTOTPEnabled() && _importExport->_invalidCount > 0 && (ts - (120000 * _importExport->_invalidCount)) > _importExport->_lastCodeCheck)
{
_importExport->_invalidCount--;
}
if(_importExport->getBypassEnabled() && _importExport->_invalidCount2 > 0 && (ts - (120000 * _importExport->_invalidCount2)) > _importExport->_lastCodeCheck2)
{
_importExport->_invalidCount2--;
}
if(disableNetwork || !_mqttEnabled || _device->isApOpen())
{
return false;
@@ -472,7 +482,7 @@ bool NukiNetwork::update()
_lastRssi = rssi;
}
}
if(_overwriteNukiHubConfigTS > 0 && espMillis() > _overwriteNukiHubConfigTS)
{
publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_json, "--", true);
@@ -1093,10 +1103,10 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns
{
if(_preferences->getBool(preference_cred_duo_approval, false) && (_importExport->getTOTPEnabled() || _importExport->getDuoEnabled()))
{
if(_importExport->getTOTPEnabled() && !doc["totp"].isNull())
if(timeSynced && _importExport->getTOTPEnabled() && !doc["totp"].isNull())
{
String jsonTotp = doc["totp"];
if (!_importExport->checkTOTP(&jsonTotp)) {
publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action_command_result, "{\"error\": \"totpIncorrect\"}", false);
publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action, "--", true);
@@ -1109,15 +1119,19 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns
publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_action, "--", true);
return;
}
else if (_importExport->startDuoAuth((char*)"Approve Nuki Hub setting change"))
else
{
bool duoRes = _importExport->startDuoAuth((char*)"Approve Nuki Hub setting change");
int duoResult = 2;
while (duoResult == 2)
if (duoRes)
{
duoResult = _importExport->checkDuoApprove();
delay(2000);
esp_task_wdt_reset();
while (duoResult == 2)
{
duoResult = _importExport->checkDuoApprove();
delay(2000);
esp_task_wdt_reset();
}
}
if (duoResult != 1)
@@ -1128,18 +1142,18 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns
}
}
}
if(!doc["exportHTTPS"].isNull() && _device->isEncrypted())
{
if(_preferences->getBool(preference_publish_config, false))
{
if(_device->isEncrypted())
if(_device->isEncrypted())
{
JsonDocument json;
_importExport->exportHttpsJson(json);
serializeJson(json, _buffer, _bufferSize);
publishString(_maintenancePathPrefix, mqtt_topic_nuki_hub_config_json, _buffer, false);
if (doc["exportHTTPS"].as<int>() > 0)
{
_overwriteNukiHubConfigTS = espMillis() + (doc["exportHTTPS"].as<int>() * 1000);
@@ -1159,7 +1173,7 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns
{
if(_preferences->getBool(preference_publish_config, false))
{
if(_device->isEncrypted())
if(_device->isEncrypted())
{
JsonDocument json;
_importExport->exportMqttsJson(json);
@@ -1188,7 +1202,7 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns
bool redacted = false;
if(!doc["redacted"].isNull())
{
if(_device->isEncrypted())
if(_device->isEncrypted())
{
redacted = true;
}
@@ -1200,7 +1214,7 @@ void NukiNetwork::onMqttDataReceived(const char* topic, byte* payload, const uns
bool pairing = false;
if(!doc["pairing"].isNull())
{
if(_device->isEncrypted())
if(_device->isEncrypted())
{
pairing = true;
}

View File

@@ -108,7 +108,7 @@ private:
int _retryConfigCount = 0;
int _retryLockstateCount = 0;
int64_t _nextRetryTs = 0;
int64_t _invalidCount = 0;
int _invalidCount = 0;
int64_t _lastCodeCheck = 0;
std::vector<uint16_t> _keypadCodeIds;
std::vector<uint32_t> _keypadCodes;

View File

@@ -62,7 +62,7 @@ void NukiWrapper::initialize()
_nukiLock.setDebugHexData(_preferences->getBool(preference_debug_hex_data, false));
_nukiLock.setDebugCommand(_preferences->getBool(preference_debug_command, false));
_nukiLock.registerLogger(Log);
if (_preferences->getInt(preference_lock_gemini_pin, 0) > 0 && _preferences->getBool(preference_lock_gemini_enabled, false))
{
_nukiLock.saveUltraPincode(_preferences->getInt(preference_lock_gemini_pin, 0), false);
@@ -90,7 +90,7 @@ void NukiWrapper::readSettings()
#else
if(pwrLvl >= 20)
{
powerLevel = ESP_PWR_LVL_P20;
powerLevel = ESP_PWR_LVL_P20;
}
else if(pwrLvl >= 18)
{
@@ -486,7 +486,7 @@ void NukiWrapper::unpair()
nukiBlePref.clear();
nukiBlePref.end();
_deviceId->assignNewId();
if (!_forceId)
if (!_forceId)
{
_preferences->remove(preference_nuki_id_lock);
}
@@ -4302,18 +4302,18 @@ void NukiWrapper::updateTime()
Log->println("No valid PIN set");
return;
}
time_t now;
tm tm;
time(&now);
localtime_r(&now, &tm);
if (int(tm.tm_year + 1900) < int(2025))
{
Log->println("NTP Time not valid, not updating Nuki device");
return;
}
Nuki::TimeValue nukiTime;
nukiTime.year = tm.tm_year + 1900;
nukiTime.month = tm.tm_mon + 1;

View File

@@ -108,7 +108,7 @@ private:
bool _publishAuthData = false;
bool _clearAuthData = false;
bool _checkKeypadCodes = false;
int64_t _invalidCount = 0;
int _invalidCount = 0;
int64_t _lastCodeCheck = 0;
std::vector<uint16_t> _keypadCodeIds;
std::vector<uint32_t> _keypadCodes;

View File

@@ -96,6 +96,8 @@
#define preference_publish_config (char*)"nhPubConfig"
#define preference_config_from_mqtt (char*)"nhCntrlEnabled"
#define preference_totp_secret (char*)"totpsecret"
#define preference_bypass_secret (char*)"bypassecret"
#define preference_admin_secret (char*)"adminsecret"
// CHANGE DOES NOT REQUIRE REBOOT TO TAKE EFFECT
#define preference_find_best_rssi (char*)"nwbestrssi"
@@ -535,12 +537,14 @@ private:
preference_cred_duo_host, preference_cred_duo_ikey, preference_cred_duo_skey, preference_cred_duo_user, preference_cred_duo_enabled, preference_https_fqdn, preference_bypass_proxy,
preference_cred_session_lifetime, preference_cred_session_lifetime_remember, preference_cred_session_lifetime_duo, preference_cred_session_lifetime_duo_remember,
preference_cred_duo_approval, preference_cred_bypass_boot_btn_enabled, preference_cred_bypass_gpio_high, preference_cred_bypass_gpio_low, preference_publish_config,
preference_config_from_mqtt, preference_totp_secret, preference_cred_session_lifetime_totp, preference_cred_session_lifetime_totp_remember
preference_config_from_mqtt, preference_totp_secret, preference_cred_session_lifetime_totp, preference_cred_session_lifetime_totp_remember, preference_bypass_secret,
preference_admin_secret
};
std::vector<char*> _redact =
{
preference_mqtt_user, preference_mqtt_password, preference_cred_user, preference_cred_password, preference_nuki_id_lock, preference_nuki_id_opener, preference_wifi_pass,
preference_lock_gemini_pin, preference_cred_duo_host, preference_cred_duo_ikey, preference_cred_duo_skey, preference_cred_duo_user, preference_https_fqdn, preference_bypass_proxy
preference_lock_gemini_pin, preference_cred_duo_host, preference_cred_duo_ikey, preference_cred_duo_skey, preference_cred_duo_user, preference_bypass_proxy,
preference_totp_secret, preference_bypass_secret, preference_admin_secret
};
std::vector<char*> _boolPrefs =
{

View File

@@ -9,6 +9,7 @@
#include "esp_random.h"
#ifdef CONFIG_SOC_SPIRAM_SUPPORTED
#include "esp_psram.h"
#include "util/SSLCert.hpp"
#endif
#ifndef CONFIG_IDF_TARGET_ESP32H2
#include <esp_wifi.h>
@@ -102,18 +103,22 @@ bool WebCfgServer::isAuthenticated(PsychicRequest *request, int type)
{
cookieKey = "totpId";
}
else if (type == 3)
{
cookieKey = "bypassId";
}
if (request->hasCookie(cookieKey.c_str()))
{
String cookie = request->getCookie(cookieKey.c_str());
if ((type == 0 && _httpSessions[cookie].is<JsonVariant>()) || (type == 1 && _importExport->_duoSessions[cookie].is<JsonVariant>()) || (type == 2 && _importExport->_totpSessions[cookie].is<JsonVariant>()))
if ((type == 0 && _httpSessions[cookie].is<JsonVariant>()) || (type == 1 && _importExport->_duoSessions[cookie].is<JsonVariant>()) || (type == 2 && _importExport->_totpSessions[cookie].is<JsonVariant>()) || (type == 3 && _importExport->_bypassSessions[cookie].is<JsonVariant>()))
{
struct timeval time;
gettimeofday(&time, NULL);
int64_t time_us = (int64_t)time.tv_sec * 1000000L + (int64_t)time.tv_usec;
if ((type == 0 && _httpSessions[cookie].as<signed long long>() > time_us) || (type == 1 && _importExport->_duoSessions[cookie].as<signed long long>() > time_us) || (type == 2 && _importExport->_totpSessions[cookie].as<signed long long>() > time_us))
if ((type == 0 && _httpSessions[cookie].as<signed long long>() > time_us) || (type == 1 && _importExport->_duoSessions[cookie].as<signed long long>() > time_us) || (type == 2 && _importExport->_totpSessions[cookie].as<signed long long>() > time_us) || (type == 3 && _importExport->_bypassSessions[cookie].as<signed long long>() > time_us))
{
return true;
}
@@ -194,6 +199,23 @@ esp_err_t WebCfgServer::logoutSession(PsychicRequest *request, PsychicResponse*
}
}
if (_importExport->getBypassEnabled())
{
if (!_isSSL)
{
resp->setCookie("bypassId", "", 0, "HttpOnly");
}
else
{
resp->setCookie("bypassId", "", 0, "Secure; HttpOnly");
}
if (request->hasCookie("bypassId")) {
String cookie2 = request->getCookie("bypassId");
_importExport->_bypassSessions.remove(cookie2);
}
}
return buildConfirmHtml(request, resp, "Logging out", 3, true);
}
@@ -369,6 +391,11 @@ int WebCfgServer::doAuthentication(PsychicRequest *request)
_importExport->_sessionsOpts[request->client()->localIP().toString() + "totp"] = true;
return 4;
}
else if(!timeSynced && _importExport->getBypassEnabled() && isAuthenticated(request, 3))
{
_importExport->_sessionsOpts[request->client()->localIP().toString() + "totp"] = false;
return 4;
}
Log->println("Authentication Failed");
@@ -505,10 +532,38 @@ void WebCfgServer::initialize()
value = p->value();
}
}
int authReq = doAuthentication(request);
if (value != "status" && value != "login" && value != "duocheck")
bool adminKeyValid = false;
if(value == "export" && timeSynced && request->hasParam("adminkey") && request->hasParam("totpkey") && _importExport->getTOTPEnabled())
{
String value2 = "";
if(request->hasParam("adminkey"))
{
const PsychicWebParameter* p = request->getParam("adminkey");
if(p->value() != "")
{
value2 = p->value();
}
}
String value3 = "";
if(request->hasParam("totpkey"))
{
const PsychicWebParameter* p = request->getParam("totpkey");
if(p->value() != "")
{
value3 = p->value();
}
}
if (value2.length() > 0 && value2 == _preferences->getString(preference_admin_secret, "") && _importExport->checkTOTP(&value3))
{
adminKeyValid = true;
}
}
if (!adminKeyValid && value != "status" && value != "login" && value != "duocheck" && value != "bypass")
{
int authReq = doAuthentication(request);
switch (authReq)
{
case 0:
@@ -543,14 +598,16 @@ void WebCfgServer::initialize()
break;
}
}
else if (value == "status" && authReq != 4)
else if (value == "status")
{
resp->setCode(200);
resp->setContentType("application/json");
resp->setContent("{}");
return resp->send();
if (doAuthentication(request) != 4)
{
resp->setCode(200);
resp->setContentType("application/json");
resp->setContent("{}");
return resp->send();
}
}
if (value == "login")
{
return buildLoginHtml(request, resp);
@@ -559,6 +616,15 @@ void WebCfgServer::initialize()
{
return buildTOTPHtml(request, resp, 0);
}
else if (value == "bypass")
{
return buildBypassHtml(request, resp);
}
else if (value == "newbypass" && _newBypass)
{
_newBypass = false;
return buildConfirmHtml(request, resp, "Logged in using Bypass. New bypass: " + _preferences->getString(preference_bypass_secret, "") + " <br/><br/><a href=\"/\">Home page</a>", 3, false);
}
else if (value == "logout")
{
return logoutSession(request, resp);
@@ -624,12 +690,17 @@ void WebCfgServer::initialize()
return sendSettings(request, resp);
}
if(adminKeyValid)
{
return sendSettings(request, resp, true);
}
if(_importExport->_sessionsOpts[request->client()->localIP().toString() + "approve"])
{
_importExport->_sessionsOpts[request->client()->localIP().toString() + "approve"] = false;
return sendSettings(request, resp);
}
else if(request->hasParam("totpkey") && _importExport->getTOTPEnabled())
else if(timeSynced && request->hasParam("totpkey") && _importExport->getTOTPEnabled())
{
const PsychicWebParameter* pass = request->getParam("totpkey");
if(pass->value() != "")
@@ -704,6 +775,10 @@ void WebCfgServer::initialize()
{
return buildHttpSSLConfigHtml(request, resp, 2);
}
else if (value == "selfsignhttps")
{
return buildHttpSSLConfigHtml(request, resp, 3);
}
else if (value == "nukicfg")
{
return buildNukiConfigHtml(request, resp);
@@ -816,7 +891,33 @@ void WebCfgServer::initialize()
}
}
if (value != "login" && value != "totp")
bool adminKeyValid = false;
if(value == "import" && timeSynced && request->hasParam("adminkey") && request->hasParam("totpkey") && _importExport->getTOTPEnabled())
{
String value2 = "";
if(request->hasParam("adminkey"))
{
const PsychicWebParameter* p = request->getParam("adminkey");
if(p->value() != "")
{
value2 = p->value();
}
}
String value3 = "";
if(request->hasParam("totpkey"))
{
const PsychicWebParameter* p = request->getParam("totpkey");
if(p->value() != "")
{
value3 = p->value();
}
}
if (value2.length() > 0 && value2 == _preferences->getString(preference_admin_secret, "") && _importExport->checkTOTP(&value3))
{
adminKeyValid = true;
}
}
if(!adminKeyValid && value != "login" && value != "totp" && value != "bypass")
{
int authReq = doAuthentication(request);
@@ -853,7 +954,7 @@ void WebCfgServer::initialize()
if(!_importExport->_sessionsOpts[request->client()->localIP().toString() + "approve"])
{
bool approved = false;
if(request->hasParam("totpkey") && _importExport->getTOTPEnabled())
if(timeSynced && request->hasParam("totpkey") && _importExport->getTOTPEnabled())
{
const PsychicWebParameter* pass = request->getParam("totpkey");
if(pass->value() != "")
@@ -866,6 +967,11 @@ void WebCfgServer::initialize()
}
}
}
else if(!timeSynced && _importExport->getBypassEnabled() && isAuthenticated(request, 3))
{
_importExport->_sessionsOpts[request->client()->localIP().toString() + "approve"] = false;
approved = true;
}
if (!approved)
{
@@ -926,6 +1032,23 @@ void WebCfgServer::initialize()
return resp->redirect("/get?page=totp");
}
}
else if (value == "bypass")
{
bool loggedIn = processBypass(request, resp);
if (loggedIn)
{
resp->setCode(302);
resp->addHeader("Cache-Control", "no-cache");
_newBypass = true;
return resp->redirect("/get?page=newbypass");
}
else
{
resp->setCode(302);
resp->addHeader("Cache-Control", "no-cache");
return resp->redirect("/");
}
}
#ifndef NUKI_HUB_UPDATER
else if (value == "savecfg")
{
@@ -969,7 +1092,23 @@ void WebCfgServer::initialize()
{
String message = "";
bool restart = processImport(request, resp, message);
return buildConfirmHtml(request, resp, message, 3, true);
if(adminKeyValid)
{
resp->setCode(200);
resp->setContentType("application/json");
resp->setContent("{ \"result\": \"success\"}");
esp_err_t res = resp->send();
if(restart)
{
restartEsp(RestartReason::RequestedViaWebServer);
}
return res;
}
else
{
return buildConfirmHtml(request, resp, message, 3, true);
}
}
#endif
else
@@ -1880,18 +2019,20 @@ esp_err_t WebCfgServer::buildLoginHtml(PsychicRequest *request, PsychicResponse*
esp_err_t WebCfgServer::buildTOTPHtml(PsychicRequest *request, PsychicResponse* resp, int type)
{
if (!timeSynced)
{
return buildConfirmHtml(request, resp, "NTP time not synced yet, TOTP not available, please wait for NTP to sync or use <a href=\"/get?page=bypass\">one-time bypass</a>", 3, true);
}
if((pow(_importExport->_invalidCount, 5) + _importExport->_lastCodeCheck) > espMillis())
{
return buildConfirmHtml(request, resp, "Too many invalid TOTP tries, please wait before retrying or use <a href=\"/get?page=bypass\">one-time bypass</a>", 3, true);
}
PsychicStreamResponse response(resp, "text/html");
response.beginSend();
response.print("<html><head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
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";
@@ -1931,12 +2072,6 @@ esp_err_t WebCfgServer::buildTOTPHtml(PsychicRequest *request, PsychicResponse*
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)
{
@@ -1947,6 +2082,32 @@ esp_err_t WebCfgServer::buildTOTPHtml(PsychicRequest *request, PsychicResponse*
return response.endSend();
}
esp_err_t WebCfgServer::buildBypassHtml(PsychicRequest *request, PsychicResponse* resp)
{
if (timeSynced)
{
return buildConfirmHtml(request, resp, "One-time bypass is only available if NTP time is not synced</a>", 3, true);
}
if((pow(_importExport->_invalidCount2, 5) + _importExport->_lastCodeCheck2) > espMillis())
{
return buildConfirmHtml(request, resp, "Too many invalid bypass tries, please wait before retrying", 3, true);
}
PsychicStreamResponse response(resp, "text/html");
response.beginSend();
response.print("<html><head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
response.print("<style>form{border:3px solid #f1f1f1; max-width: 400px;}input[type=password],input[type=text]{width:100%;padding:12px 20px;margin:8px 0;display:inline-block;border:1px solid #ccc;box-sizing:border-box}button{background-color:#04aa6d;color:#fff;padding:14px 20px;margin:8px 0;border:none;cursor:pointer;width:100%}button:hover{opacity:.8}.container{padding:16px}span.password{float:right;padding-top:16px}@media screen and (max-width:300px){span.psw{display:block;float:none}}</style>");
response.print("</head><body><center><h2>Nuki Hub One-time Bypass</h2>");
response.print("<form action=\"/post?page=bypass\" method=\"post\">");
response.print("<div class=\"container\">");
response.print("<label for=\"bypass\"><b>Bypass code</b></label><input type=\"text\" placeholder=\"Enter bypass code\" name=\"bypass\">");
response.print("<button type=\"submit\" ");
response.print(">Login</button></div>");
response.print("</form></center></body></html>");
return response.endSend();
}
esp_err_t WebCfgServer::buildDuoCheckHtml(PsychicRequest *request, PsychicResponse* resp)
{
char valueStr[2];
@@ -1990,7 +2151,7 @@ esp_err_t WebCfgServer::buildDuoHtml(PsychicRequest *request, PsychicResponse* r
{
if (!timeSynced)
{
return buildConfirmHtml(request, resp, "NTP time not synced yet, Duo not available, please wait for NTP to sync", 3, true);
return buildConfirmHtml(request, resp, "NTP time not synced yet, Duo not available, please wait for NTP to sync or use <a href=\"/get?page=bypass\">one-time bypass</a>", 3, true);
}
String duoText;
@@ -2134,9 +2295,55 @@ bool WebCfgServer::processLogin(PsychicRequest *request, PsychicResponse* resp)
return false;
}
bool WebCfgServer::processBypass(PsychicRequest *request, PsychicResponse* resp)
{
if(!timeSynced && request->hasParam("bypass"))
{
const PsychicWebParameter* pass = request->getParam("bypass");
if(pass->value() != "")
{
String bypass = pass->value();
if (_importExport->checkBypass(bypass))
{
char buffer[33];
int i;
for (i = 0; i < 4; i++) {
sprintf(buffer + (i * 8), "%08lx", (unsigned long int)esp_random());
}
if (!_isSSL)
{
resp->setCookie("bypassId", buffer, 3600, "HttpOnly");
}
else
{
resp->setCookie("bypassId", buffer, 3600, "Secure; HttpOnly");
}
struct timeval time;
gettimeofday(&time, NULL);
int64_t time_us = (int64_t)time.tv_sec * 1000000L + (int64_t)time.tv_usec;
_importExport->_bypassSessions[buffer] = time_us + ((int64_t)3600*1000000L);
char randomstr2[33];
randomSeed(analogRead(0));
char chars[] = {'1', '2', '3','4', '5', '6','7', '8', '9', '0', 'A', 'B', 'C', 'D','E', 'F', 'G','H', 'I', 'J','K', 'L', 'M', 'N', 'O','P', 'Q','R', 'S', 'T','U', 'V', 'W','X', 'Y', 'Z'};
for(int i = 0;i < 32; i++){
randomstr2[i] = chars[random(36)];
}
randomstr2[32] = '\0';
_preferences->putString(preference_bypass_secret, randomstr2);
return true;
}
}
}
return false;
}
bool WebCfgServer::processTOTP(PsychicRequest *request, PsychicResponse* resp)
{
if(request->hasParam("totpkey"))
if(timeSynced && request->hasParam("totpkey"))
{
const PsychicWebParameter* pass = request->getParam("totpkey");
if(pass->value() != "")
@@ -2178,7 +2385,7 @@ bool WebCfgServer::processTOTP(PsychicRequest *request, PsychicResponse* resp)
}
#ifndef NUKI_HUB_UPDATER
esp_err_t WebCfgServer::sendSettings(PsychicRequest *request, PsychicResponse* resp)
esp_err_t WebCfgServer::sendSettings(PsychicRequest *request, PsychicResponse* resp, bool adminKey)
{
JsonDocument json;
String jsonPretty;
@@ -2227,7 +2434,10 @@ esp_err_t WebCfgServer::sendSettings(PsychicRequest *request, PsychicResponse* r
serializeJsonPretty(json, jsonPretty);
char buf[26 + name.length()];
snprintf(buf, sizeof(buf), "attachment; filename=\"%s\"", name.c_str());
resp->addHeader("Content-Disposition", buf);
if(!adminKey)
{
resp->addHeader("Content-Disposition", buf);
}
resp->setCode(200);
resp->setContentType("application/json");
resp->setContent(jsonPretty.c_str());
@@ -2522,6 +2732,13 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S
}
}
}
else if(key == "HTTPGEN")
{
createSSLCertificate();
Log->print("Setting changed: ");
Log->println(key);
configChanged = true;
}
#endif
else if(key == "UPTIME")
{
@@ -4155,6 +4372,32 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S
}
}
}
else if(key == "CREDBYPASS")
{
if(value != "*")
{
if(_preferences->getString(preference_bypass_secret, "") != value)
{
_preferences->putString(preference_bypass_secret, value);
Log->print("Setting changed: ");
Log->println(key);
configChanged = true;
}
}
}
else if(key == "CREDADMIN")
{
if(value != "*")
{
if(_preferences->getString(preference_admin_secret, "") != value)
{
_preferences->putString(preference_admin_secret, value);
Log->print("Setting changed: ");
Log->println(key);
configChanged = true;
}
}
}
else if(key == "NUKIPIN" && _nuki != nullptr)
{
if(value == "#")
@@ -4705,13 +4948,27 @@ esp_err_t WebCfgServer::buildHtml(PsychicRequest *request, PsychicResponse* resp
esp_err_t WebCfgServer::buildCredHtml(PsychicRequest *request, PsychicResponse* resp)
{
char chars[] = {'2', '3','4', '5', '6','7', 'A', 'B', 'C', 'D','E', 'F', 'G','H', 'I', 'J','K', 'L', 'M', 'N', 'O','P', 'Q','R', 'S', 'T','U', 'V', 'W','X', 'Y', 'Z'};
char chars2[] = {'1', '2', '3','4', '5', '6','7', '8', '9', '0', 'A', 'B', 'C', 'D','E', 'F', 'G','H', 'I', 'J','K', 'L', 'M', 'N', 'O','P', 'Q','R', 'S', 'T','U', 'V', 'W','X', 'Y', 'Z'};
char randomstr[17];
randomSeed(analogRead(0));
char chars[] = {'2', '3','4', '5', '6','7', 'A', 'B', 'C', 'D','E', 'F', 'G','H', 'I', 'J','K', 'L', 'M', 'N', 'O','P', 'Q','R', 'S', 'T','U', 'V', 'W','X', 'Y', 'Z'};
for(int i = 0;i < 16; i++){
randomstr[i] = chars[random(32)];
}
randomstr[16] = '\0';
char randomstr2[33];
randomSeed(analogRead(0));
for(int i = 0;i < 32; i++){
randomstr2[i] = chars2[random(36)];
}
randomstr2[32] = '\0';
char randomstr3[33];
randomSeed(analogRead(0));
for(int i = 0;i < 32; i++){
randomstr3[i] = chars2[random(36)];
}
randomstr3[32] = '\0';
PsychicStreamResponse response(resp, "text/html");
response.beginSend();
@@ -4738,10 +4995,18 @@ esp_err_t WebCfgServer::buildCredHtml(PsychicRequest *request, PsychicResponse*
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);
printInputField(&response, "CREDTOTP", "TOTP Secret Key", "*", 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, "CREDBYPASS", "One-time MFA Bypass", "*", 32, "", true, false);
response.print("<tr id=\"bypassgentr\" ><td><input type=\"button\" id=\"bypassgen\" onclick=\"document.getElementsByName('CREDBYPASS')[0].type='text'; document.getElementsByName('CREDBYPASS')[0].value='");
response.print(randomstr2);
response.print("'; document.getElementById('bypassgentr').style.display='none';\" value=\"Generate new Bypass\"></td></tr>");
printInputField(&response, "CREDADMIN", "Admin key", "*", 32, "", true, false);
response.print("<tr id=\"admingentr\" ><td><input type=\"button\" id=\"admingen\" onclick=\"document.getElementsByName('CREDADMIN')[0].type='text'; document.getElementsByName('CREDADMIN')[0].value='");
response.print(randomstr3);
response.print("'; document.getElementById('admingentr').style.display='none';\" value=\"Generate new Admin key\"></td></tr>");
printInputField(&response, "CREDLFTM", "Session validity (in seconds)", _preferences->getInt(preference_cred_session_lifetime, 3600), 12, "");
printInputField(&response, "CREDLFTMRMBR", "Session validity remember (in hours)", _preferences->getInt(preference_cred_session_lifetime_remember, 720), 12, "");
printInputField(&response, "CREDDUOLFTM", "Duo Session validity (in seconds)", _preferences->getInt(preference_cred_session_lifetime_duo, 3600), 12, "");
@@ -4844,6 +5109,7 @@ esp_err_t WebCfgServer::buildNetworkConfigHtml(PsychicRequest *request, PsychicR
{
response.print("<tr><td>Set HTTP SSL Certificate</td><td><button title=\"Set HTTP SSL Certificate\" onclick=\" window.open('/get?page=httpcrtconfig', '_self'); return false;\">Change</button></td></tr>");
response.print("<tr><td>Set HTTP SSL Key</td><td><button title=\"Set HTTP SSL Key\" onclick=\" window.open('/get?page=httpkeyconfig', '_self'); return false;\">Change</button></td></tr>");
response.print("<tr><td>Generate self-signed HTTP SSL Certificate and key</td><td><button title=\"Generate HTTP SSL Certificate and key\" onclick=\" window.open('/get?page=selfsignhttps', '_self'); return false;\">Generate</button></td></tr>");
printInputField(&response, "HTTPSFQDN", "Nuki Hub FQDN for HTTP redirect", _preferences->getString(preference_https_fqdn, "").c_str(), 255, "");
}
#endif
@@ -5073,7 +5339,7 @@ esp_err_t WebCfgServer::buildHttpSSLConfigHtml(PsychicRequest *request, PsychicR
printTextarea(&response, "HTTPCRT", "HTTP SSL Certificate (*, optional)", "", 4400, true, true);
}
}
else
else if (type == 2)
{
bool found = false;
@@ -5107,6 +5373,11 @@ esp_err_t WebCfgServer::buildHttpSSLConfigHtml(PsychicRequest *request, PsychicR
printTextarea(&response, "HTTPKEY", "HTTP SSL Key (*, optional)", "", 2200, true, true);
}
}
else
{
response.print("<input type=\"hidden\" name=\"HTTPGEN\" value=\"1\">");
response.print("<tr><td>Click save to generate a HTTPS SSL Certificate and key</td></tr>");
}
response.print("</table>");
response.print("<br><input type=\"submit\" name=\"submit\" value=\"Save\">");
response.print("</form>");
@@ -5677,6 +5948,11 @@ esp_err_t WebCfgServer::buildInfoHtml(PsychicRequest *request, PsychicResponse*
response.print(uxTaskGetStackHighWaterMark(networkTaskHandle));
response.print("\nNuki task stack high watermark: ");
response.print(uxTaskGetStackHighWaterMark(nukiTaskHandle));
SPIFFS.begin(true);
response.print("\n\n------------ SPIFFS ------------");
response.printf("\nSPIFFS Total Bytes: %u", SPIFFS.totalBytes());
response.printf("\nSPIFFS Used Bytes: %u", SPIFFS.usedBytes());
response.printf("\nSPIFFS Free Bytes: %u", SPIFFS.totalBytes() - SPIFFS.usedBytes());
response.print("\n\n------------ GENERAL SETTINGS ------------");
response.print("\nNetwork task stack size: ");
response.print(_preferences->getInt(preference_task_size_network, NETWORK_TASK_SIZE));
@@ -6752,4 +7028,59 @@ const String WebCfgServer::getPreselectionForGpio(const uint8_t &pin) const
return String((int8_t)PinRole::Disabled);
}
#ifdef CONFIG_SOC_SPIRAM_SUPPORTED
void WebCfgServer::createSSLCertificate()
{
SSLCert* cert;
cert = new SSLCert();
int createCertResult = createSelfSignedCert(
*cert,
KEYSIZE_2048,
"CN=nukihub.local,O=NukiHub,C=DE",
"20250101000000",
"20350101000000"
);
if (createCertResult == 0) {
if (!SPIFFS.begin(true)) {
Log->println("SPIFFS Mount Failed");
}
else
{
File file = SPIFFS.open("/http_ssl.crt", FILE_WRITE);
if (!file) {
Log->println("Failed to open /http_ssl.crt for writing");
}
else
{
if (!file.print(cert->getCertPEM()))
{
Log->println("Failed to write /http_ssl.crt");
}
file.close();
}
File file2 = SPIFFS.open("/http_ssl.key", FILE_WRITE);
if (!file2) {
Log->println("Failed to open /http_ssl.key for writing");
}
else
{
if (!file2.print(cert->getKeyPEM()))
{
Log->println("Failed to write /http_ssl.key");
}
file2.close();
}
}
}
else
{
Log->print("SSL Self sign failed: ");
Log->println(createCertResult);
}
}
#endif
#endif

View File

@@ -52,7 +52,7 @@ public:
private:
#ifndef NUKI_HUB_UPDATER
esp_err_t sendSettings(PsychicRequest *request, PsychicResponse* resp);
esp_err_t sendSettings(PsychicRequest *request, PsychicResponse* resp, bool adminKey = false);
bool processArgs(PsychicRequest *request, PsychicResponse* resp, String& message);
bool processImport(PsychicRequest *request, PsychicResponse* resp, String& message);
void processGpioArgs(PsychicRequest *request, PsychicResponse* resp);
@@ -86,7 +86,9 @@ private:
#if defined(CONFIG_IDF_TARGET_ESP32)
const std::vector<std::pair<String, String>> getNetworkCustomCLKOptions() const;
#endif
#ifdef CONFIG_SOC_SPIRAM_SUPPORTED
void createSSLCertificate();
#endif
const String getPreselectionForGpio(const uint8_t& pin) const;
const String pinStateToString(const NukiPinState& value) const;
@@ -111,9 +113,11 @@ private:
bool isAuthenticated(PsychicRequest *request, int type = 0);
bool processLogin(PsychicRequest *request, PsychicResponse* resp);
bool processTOTP(PsychicRequest *request, PsychicResponse* resp);
bool processBypass(PsychicRequest *request, PsychicResponse* resp);
int doAuthentication(PsychicRequest *request);
esp_err_t buildCoredumpHtml(PsychicRequest *request, PsychicResponse* resp);
esp_err_t buildLoginHtml(PsychicRequest *request, PsychicResponse* resp);
esp_err_t buildBypassHtml(PsychicRequest *request, PsychicResponse* resp);
esp_err_t buildTOTPHtml(PsychicRequest *request, PsychicResponse* resp, int type);
esp_err_t buildDuoHtml(PsychicRequest *request, PsychicResponse* resp, int type);
esp_err_t buildDuoCheckHtml(PsychicRequest *request, PsychicResponse* resp);
@@ -150,6 +154,7 @@ private:
JsonDocument _httpSessions;
bool _duoEnabled = false;
bool _bypassGPIO = false;
bool _newBypass = false;
int _bypassGPIOHigh = -1;
int _bypassGPIOLow = -1;
};

View File

@@ -4,7 +4,7 @@ dependencies:
esp-nimble-cpp:
git: https://github.com/h2zero/esp-nimble-cpp.git
version: fa468d360a56712f3f39a1fba74b97340ebca8a9
version: 74b5c59887a381ca02c8193384bd89968c8409a6
espressif/libsodium: "^1.0.20~2"

View File

@@ -185,6 +185,43 @@ uint8_t checkPartition()
}
}
void listDir(fs::FS &fs, const char *dirname, uint8_t levels) {
Serial.printf("Listing directory: %s\r\n", dirname);
File root = fs.open(dirname);
if (!root) {
Serial.println("- failed to open directory");
return;
}
if (!root.isDirectory()) {
Serial.println(" - not a directory");
return;
}
File file = root.openNextFile();
while (file) {
if (file.isDirectory()) {
Serial.print(" DIR : ");
Serial.println(file.name());
if (levels) {
listDir(fs, file.path(), levels - 1);
}
} else {
Serial.print(" FILE: ");
Serial.print(file.name());
Serial.print("\tSIZE: ");
Serial.println(file.size());
}
if (file.size() > (int)(SPIFFS.totalBytes() * 0.4))
{
SPIFFS.remove((String)"/" + file.name());
}
file = root.openNextFile();
}
}
void cbSyncTime(struct timeval *tv) {
Log->println("NTP time synced");
timeSynced = true;
@@ -518,7 +555,7 @@ void logCoreDump()
{
coredumpPrinted = false;
delay(500);
Serial.println("Printing coredump and saving to coredump.hex on SPIFFS");
Log->println("Printing coredump and saving to coredump.hex on SPIFFS");
size_t size = 0;
size_t address = 0;
if (esp_core_dump_image_get(&address, &size) == ESP_OK)
@@ -528,6 +565,7 @@ void logCoreDump()
if (pt != NULL)
{
File file;
uint8_t bf[256];
char str_dst[640];
int16_t toRead;
@@ -536,17 +574,19 @@ void logCoreDump()
{
Log->println("SPIFFS Mount Failed");
}
File file = SPIFFS.open("/coredump.hex", FILE_WRITE);
if (!file) {
Log->println("Failed to open /coredump.hex for writing");
}
else
{
file.printf("%s\r\n", NUKI_HUB_HW);
file.printf("%s\r\n", NUKI_HUB_BUILD);
file = SPIFFS.open("/coredump.hex", FILE_WRITE);
if (!file) {
Log->println("Failed to open /coredump.hex for writing");
}
else
{
file.printf("%s\r\n", NUKI_HUB_HW);
file.printf("%s\r\n", NUKI_HUB_BUILD);
}
}
Serial.printf("%s\r\n", NUKI_HUB_HW);
Serial.printf("%s\r\n", NUKI_HUB_BUILD);
@@ -581,7 +621,7 @@ void logCoreDump()
file.printf("%s", str_dst);
}
}
Serial.println("");
if (file) {
@@ -632,6 +672,11 @@ void setup()
{
logCoreDump();
}
if (SPIFFS.begin(true))
{
listDir(SPIFFS, "/", 1);
}
uint8_t partitionType = checkPartition();
@@ -669,7 +714,7 @@ void setup()
{
preferences->putString(preference_updater_date, NUKI_HUB_DATE);
}
importExport = new ImportExport(preferences);
network = new NukiNetwork(preferences);
@@ -796,7 +841,7 @@ void setup()
Log->print(gpioDesc.c_str());
const String mqttLockPath = preferences->getString(preference_mqtt_lock_path);
importExport = new ImportExport(preferences);
network = new NukiNetwork(preferences, gpio, mqttLockPath, CharBuffer::get(), buffer_size, importExport);

397
src/util/SSLCert.cpp Normal file
View File

@@ -0,0 +1,397 @@
/*
MIT License
Copyright (c) 2017 Frank Hessel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include "SSLCert.hpp"
SSLCert::SSLCert(uint16_t certLength, uint16_t pkLength, String keyPEM, String certPEM):
_certLength(certLength),
_pkLength(pkLength),
_keyPEM(keyPEM),
_certPEM(certPEM) {
}
SSLCert::~SSLCert() {
// TODO Auto-generated destructor stub
}
uint16_t SSLCert::getCertLength() {
return _certLength;
}
uint16_t SSLCert::getPKLength() {
return _pkLength;
}
String SSLCert::getKeyPEM() {
return _keyPEM;
}
String SSLCert::getCertPEM() {
return _certPEM;
}
void SSLCert::setPK(String keyPEM) {
_keyPEM = keyPEM;
_pkLength = keyPEM.length();
}
void SSLCert::setCert(String certPEM) {
_certPEM = certPEM;
_certLength = certPEM.length();
}
void SSLCert::clear() {
_certLength = 0;
_pkLength = 0;
_keyPEM = "";
_certPEM = "";
}
/**
* Returns the CN value from a DN, or "" if it cannot be found
*/
static std::string get_cn(std::string dn) {
size_t cnStart = dn.find("CN=");
if (cnStart == std::string::npos) {
return "";
}
cnStart += 3;
size_t cnStop = dn.find(",", cnStart);
if (cnStop == std::string::npos) {
cnStop = dn.length();
}
return dn.substr(cnStart, cnStop - cnStart);
}
/**
* Sets the DN as subjectAltName extension in the certificate
*/
static int add_subject_alt_name(mbedtls_x509write_cert *crt, std::string &cn) {
size_t bufsize = cn.length() + 8; // some additional space for tags and length fields
uint8_t buf[bufsize];
uint8_t *p = &buf[bufsize - 1];
uint8_t *start = buf;
int length = 0;
int ret; // used by MBEDTLS macro
// The ASN structure that we will construct as parameter for write_crt_set_extension is as follows:
// | 0x30 = Sequence | length | 0x82 = dNSName, context-specific | length | cn0 | cn1 | cn2 | cn3 | .. | cnn |
// ↑ : ↑ `-------------v------------------´:
// | : `-------------------´ :
// | `----------v------------------------------------------------------------------´
// `---------------´
// Let's encrypt has useful infos: https://letsencrypt.org/docs/a-warm-welcome-to-asn1-and-der/#choice-and-any-encoding
MBEDTLS_ASN1_CHK_ADD(length,
mbedtls_asn1_write_raw_buffer(&p, start, (uint8_t*)cn.c_str(), cn.length()));
MBEDTLS_ASN1_CHK_ADD(length,
mbedtls_asn1_write_len(&p, start, length));
MBEDTLS_ASN1_CHK_ADD(length,
mbedtls_asn1_write_tag(&p, start, MBEDTLS_ASN1_CONTEXT_SPECIFIC | 0x02)); // 0x02 = dNSName
MBEDTLS_ASN1_CHK_ADD(length,
mbedtls_asn1_write_len(&p, start, length));
MBEDTLS_ASN1_CHK_ADD(length,
mbedtls_asn1_write_tag(&p, start, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE ));
return mbedtls_x509write_crt_set_extension( crt,
MBEDTLS_OID_SUBJECT_ALT_NAME, MBEDTLS_OID_SIZE(MBEDTLS_OID_SUBJECT_ALT_NAME),
0, // not critical
p, length);
}
/**
* Function to create the key for a self-signed certificate.
*
* Writes private key as DER in certCtx
*
* Based on programs/pkey/gen_key.c
*/
static int gen_key(SSLCert &certCtx, SSLKeySize keySize) {
// Initialize the entropy source
mbedtls_entropy_context entropy;
mbedtls_entropy_init( &entropy );
// Initialize the RNG
mbedtls_ctr_drbg_context ctr_drbg;
mbedtls_ctr_drbg_init( &ctr_drbg );
int rngRes = mbedtls_ctr_drbg_seed(
&ctr_drbg, mbedtls_entropy_func, &entropy,
NULL, 0
);
if (rngRes != 0) {
mbedtls_entropy_free( &entropy );
return HTTPS_SERVER_ERROR_KEYGEN_RNG;
}
// Initialize the private key
mbedtls_pk_context key;
mbedtls_pk_init( &key );
int resPkSetup = mbedtls_pk_setup( &key, mbedtls_pk_info_from_type( MBEDTLS_PK_RSA ) );
if ( resPkSetup != 0) {
mbedtls_ctr_drbg_free( &ctr_drbg );
mbedtls_entropy_free( &entropy );
return HTTPS_SERVER_ERROR_KEYGEN_SETUP_PK;
}
// Actual key generation
int resPkGen = mbedtls_rsa_gen_key(
mbedtls_pk_rsa( key ),
mbedtls_ctr_drbg_random,
&ctr_drbg,
keySize,
65537
);
if ( resPkGen != 0) {
mbedtls_pk_free( &key );
mbedtls_ctr_drbg_free( &ctr_drbg );
mbedtls_entropy_free( &entropy );
return HTTPS_SERVER_ERROR_KEYGEN_GEN_PK;
}
// Free the entropy source and the RNG as they are no longer needed
mbedtls_ctr_drbg_free( &ctr_drbg );
mbedtls_entropy_free( &entropy );
// Allocate the space on the heap, as stack size is quite limited
unsigned char * output_buf = new unsigned char[4096];
if (output_buf == NULL) {
mbedtls_pk_free( &key );
return HTTPS_SERVER_ERROR_KEY_OUT_OF_MEM;
}
memset(output_buf, 0, 4096);
// Write the key to the temporary buffer and determine its length
int resPkWrite = mbedtls_pk_write_key_pem( &key, output_buf, 4096 );
if (resPkWrite < 0) {
delete[] output_buf;
mbedtls_pk_free( &key );
return HTTPS_SERVER_ERROR_KEY_WRITE_PK;
}
// Clean up the temporary buffer and clear the key context
mbedtls_pk_free( &key );
// Set the private key in the context
certCtx.setPK((char*)output_buf);
delete[] output_buf;
return 0;
}
static int parse_serial_decimal_format(unsigned char *obuf, size_t obufmax,
const char *ibuf, size_t *len)
{
unsigned long long int dec;
unsigned int remaining_bytes = sizeof(dec);
unsigned char *p = obuf;
unsigned char val;
char *end_ptr = NULL;
errno = 0;
dec = strtoull(ibuf, &end_ptr, 10);
if ((errno != 0) || (end_ptr == ibuf)) {
return -1;
}
*len = 0;
while (remaining_bytes > 0) {
if (obufmax < (*len + 1)) {
return -1;
}
val = (dec >> ((remaining_bytes - 1) * 8)) & 0xFF;
/* Skip leading zeros */
if ((val != 0) || (*len != 0)) {
*p = val;
(*len)++;
p++;
}
remaining_bytes--;
}
return 0;
}
/**
* Function to generate the X509 certificate data for a private key
*
* Writes certificate in certCtx
*
* Based on programs/x509/cert_write.c
*/
static int cert_write(SSLCert &certCtx, std::string dn, std::string validityFrom, std::string validityTo) {
int funcRes = 0;
int stepRes = 0;
mbedtls_entropy_context entropy;
mbedtls_ctr_drbg_context ctr_drbg;
mbedtls_pk_context key;
mbedtls_x509write_cert crt;
unsigned char * primary_buffer;
unsigned char *certOffset;
unsigned char * output_buffer;
size_t certLength;
const char *serial = "peer";
size_t serial_len;
// Make a C-friendly version of the distinguished name
char dn_cstr[dn.length()+1];
strcpy(dn_cstr, dn.c_str());
std::string cn = get_cn(dn);
if (cn == "") {
return HTTPS_SERVER_ERROR_CERTGEN_CN;
}
// Initialize the entropy source
mbedtls_entropy_init( &entropy );
// Initialize the RNG
mbedtls_ctr_drbg_init( &ctr_drbg );
stepRes = mbedtls_ctr_drbg_seed( &ctr_drbg, mbedtls_entropy_func, &entropy, NULL, 0 );
if (stepRes != 0) {
funcRes = HTTPS_SERVER_ERROR_CERTGEN_RNG;
goto error_after_entropy;
}
mbedtls_pk_init( &key );
stepRes = mbedtls_pk_parse_key( &key, (const unsigned char *)certCtx.getKeyPEM().c_str(), certCtx.getPKLength() + 1, NULL, 0, mbedtls_ctr_drbg_random, &ctr_drbg);
if (stepRes != 0) {
funcRes = HTTPS_SERVER_ERROR_CERTGEN_READKEY;
goto error_after_key;
}
// Start configuring the certificate
mbedtls_x509write_crt_init( &crt );
// Set version and hash algorithm
mbedtls_x509write_crt_set_version( &crt, MBEDTLS_X509_CRT_VERSION_3 );
mbedtls_x509write_crt_set_md_alg( &crt, MBEDTLS_MD_SHA256 );
// Set the keys (same key as we self-sign)
mbedtls_x509write_crt_set_subject_key( &crt, &key );
mbedtls_x509write_crt_set_issuer_key( &crt, &key );
// Set issuer and subject (same, as we self-sign)
stepRes = mbedtls_x509write_crt_set_subject_name( &crt, dn_cstr );
if (stepRes != 0) {
funcRes = HTTPS_SERVER_ERROR_CERTGEN_NAME;
goto error_after_cert;
}
stepRes = mbedtls_x509write_crt_set_issuer_name( &crt, dn_cstr );
if (stepRes != 0) {
funcRes = HTTPS_SERVER_ERROR_CERTGEN_NAME;
goto error_after_cert;
}
// Set the validity of the certificate. At the moment, it's fixed from 2019 to end of 2029.
stepRes = mbedtls_x509write_crt_set_validity( &crt, validityFrom.c_str(), validityTo.c_str());
if (stepRes != 0) {
funcRes = HTTPS_SERVER_ERROR_CERTGEN_VALIDITY;
goto error_after_cert;
}
// Make this a CA certificate
stepRes = mbedtls_x509write_crt_set_basic_constraints( &crt, 1, 0 );
if (stepRes != 0) {
funcRes = HTTPS_SERVER_ERROR_CERTGEN_VALIDITY;
goto error_after_cert;
}
stepRes = add_subject_alt_name( &crt, cn );
if (stepRes != 0) {
funcRes = HTTPS_SERVER_ERROR_CERTGEN_NAME;
goto error_after_cert;
}
// Initialize the serial number
stepRes = mbedtls_x509write_crt_set_serial_raw( &crt, (unsigned char *)serial, strlen(serial) );
if (stepRes != 0) {
funcRes = HTTPS_SERVER_ERROR_CERTGEN_SERIAL;
goto error_after_cert_serial;
}
// Create buffer to write the certificate
primary_buffer = new unsigned char[4096];
if (primary_buffer == NULL) {
funcRes = HTTPS_SERVER_ERROR_CERTGEN_OUT_OF_MEM;
goto error_after_cert_serial;
}
// Write the actual certificate
stepRes = mbedtls_x509write_crt_pem(&crt, primary_buffer, 4096, mbedtls_ctr_drbg_random, &ctr_drbg );
if (stepRes < 0) {
funcRes = HTTPS_SERVER_ERROR_CERTGEN_WRITE;
goto error_after_primary_buffer;
}
// Configure the cert in the context
certCtx.setCert((char*)primary_buffer);
// Run through the cleanup process
error_after_primary_buffer:
delete[] primary_buffer;
error_after_cert_serial:
error_after_cert:
mbedtls_x509write_crt_free( &crt );
error_after_key:
mbedtls_pk_free(&key);
error_after_entropy:
mbedtls_ctr_drbg_free( &ctr_drbg );
mbedtls_entropy_free( &entropy );
return funcRes;
}
int createSelfSignedCert(SSLCert &certCtx, SSLKeySize keySize, std::string dn, std::string validFrom, std::string validUntil) {
// Add the private key
int keyRes = gen_key(certCtx, keySize);
if (keyRes != 0) {
// Key-generation failed, return the failure code
return keyRes;
}
// Add the self-signed certificate
int certRes = cert_write(certCtx, dn, validFrom, validUntil);
if (certRes != 0) {
// Cert writing failed, reset the pk and return failure code
certCtx.setPK("");
return certRes;
}
// If all went well, return 0
return 0;
}

89
src/util/SSLCert.hpp Normal file
View File

@@ -0,0 +1,89 @@
/*
MIT License
Copyright (c) 2017 Frank Hessel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#ifndef SRC_SSLCERT_HPP_
#define SRC_SSLCERT_HPP_
#include <Arduino.h>
#include <string>
#include <mbedtls/rsa.h>
#include <mbedtls/entropy.h>
#include <mbedtls/ctr_drbg.h>
#include <mbedtls/pk.h>
#include <mbedtls/x509.h>
#include <mbedtls/x509_crt.h>
#include <mbedtls/x509_csr.h>
#include <mbedtls/asn1write.h>
#include <mbedtls/oid.h>
#define HTTPS_SERVER_ERROR_KEYGEN 0x0F
#define HTTPS_SERVER_ERROR_KEYGEN_RNG 0x02
#define HTTPS_SERVER_ERROR_KEYGEN_SETUP_PK 0x03
#define HTTPS_SERVER_ERROR_KEYGEN_GEN_PK 0x04
#define HTTPS_SERVER_ERROR_KEY_WRITE_PK 0x05
#define HTTPS_SERVER_ERROR_KEY_OUT_OF_MEM 0x06
#define HTTPS_SERVER_ERROR_CERTGEN 0x1F
#define HTTPS_SERVER_ERROR_CERTGEN_RNG 0x12
#define HTTPS_SERVER_ERROR_CERTGEN_READKEY 0x13
#define HTTPS_SERVER_ERROR_CERTGEN_WRITE 0x15
#define HTTPS_SERVER_ERROR_CERTGEN_OUT_OF_MEM 0x16
#define HTTPS_SERVER_ERROR_CERTGEN_NAME 0x17
#define HTTPS_SERVER_ERROR_CERTGEN_SERIAL 0x18
#define HTTPS_SERVER_ERROR_CERTGEN_VALIDITY 0x19
#define HTTPS_SERVER_ERROR_CERTGEN_CN 0x1a
class SSLCert {
public:
SSLCert(
uint16_t certLength = 0,
uint16_t pkLength = 0,
String keyPEM = "",
String certPEM = ""
);
virtual ~SSLCert();
uint16_t getCertLength();
uint16_t getPKLength();
String getCertPEM();
String getKeyPEM();
void setPK(String _keyPEM);
void setCert(String _certPEM);
void clear();
private:
uint16_t _certLength;
uint16_t _pkLength;
String _keyPEM;
String _certPEM;
};
enum SSLKeySize {
KEYSIZE_1024 = 1024,
KEYSIZE_2048 = 2048,
KEYSIZE_4096 = 4096
};
int createSelfSignedCert(SSLCert &certCtx, SSLKeySize keySize, std::string dn, std::string validFrom = "20190101000000", std::string validUntil = "20300101000000");
#endif /* SRC_SSLCERT_HPP_ */

View File

@@ -1 +1 @@
# Espressif ESP32 Partition Table
# Espressif ESP32 Partition Table
1 # Espressif ESP32 Partition Table # Name Type SubType Offset Size Flags nvs data nvs 0x9000 0x5000 otadata data ota 0xe000 0x2000 app0 app ota_0 0x10000 0x150000 0x130000

View File

@@ -8,6 +8,8 @@ def get_board_name(env):
board = env.get('BOARD_MCU')
if env.get('BOARD') == 'nuki-esp32solo1':
board = 'esp32solo1'
elif env.get('BOARD') == 'nuki-esp32gls10':
board = 'esp32gls10'
elif env.get('BOARD') == 'nuki-esp32-s3-oct':
board = 'esp32s3oct'
return board

View File

@@ -13,7 +13,7 @@ default_envs = updater_esp32
boards_dir = ../boards
[env]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.11/platform-espressif32.zip
platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.13/platform-espressif32.zip
platform_packages =
framework = arduino, espidf
board_build.embed_txtfiles =
@@ -67,6 +67,13 @@ extra_scripts =
board_build.cmake_extra_args =
-DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32"
[env:updater_esp32-gl-s10]
extends = env:updater_esp32
board = nuki-esp32gls10
board_build.cmake_extra_args =
-DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32;sdkconfig.gls10.defaults"
-DNUKI_TARGET_GL_S10=y
[env:updater_esp32-c3]
extends = env:updater_esp32
board = esp32-c3-devkitc-02

View File

@@ -0,0 +1 @@
CONFIG_D0WD_PSRAM_CLK_IO=6