From 43be1c9fb72ccd7f78785e5eea7bcdfc5fedfa69 Mon Sep 17 00:00:00 2001 From: Alfred Date: Thu, 11 Jun 2026 21:53:27 +0800 Subject: [PATCH 1/3] feat: add root password reset recovery firmware --- .gitignore | 1 + CMakeLists.txt | 38 +++ README.md | 4 +- components/ts_auth_layout/CMakeLists.txt | 4 + .../ts_auth_layout/include/ts_auth_layout.h | 48 +++ components/ts_security/CMakeLists.txt | 1 + components/ts_security/src/ts_auth.c | 45 ++- docs/FLASH_VIA_SERIAL.md | 6 +- docs/README.md | 2 + docs/ROOT_PASSWORD_RESET_FIRMWARE.md | 63 ++++ main/CMakeLists.txt | 79 +++-- main/root_password_reset_main.c | 285 ++++++++++++++++++ tools/build_root_reset.sh | 112 +++++++ 13 files changed, 626 insertions(+), 62 deletions(-) create mode 100644 components/ts_auth_layout/CMakeLists.txt create mode 100644 components/ts_auth_layout/include/ts_auth_layout.h create mode 100644 docs/ROOT_PASSWORD_RESET_FIRMWARE.md create mode 100644 main/root_password_reset_main.c create mode 100755 tools/build_root_reset.sh diff --git a/.gitignore b/.gitignore index cbfc4a4..4cf8c8a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Build intermediates build/* +build-root-reset/ !build/TianShanOS.bin !build/www.bin !build/ota_data_initial.bin diff --git a/CMakeLists.txt b/CMakeLists.txt index e69d7eb..8c39316 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,6 +56,33 @@ else() set(TIANSHAN_VERSION_BUILD "") endif() +if(ROOT_PASSWORD_RESET) + if(ROOT_PASSWORD_RESET_VERSION) + string(REGEX MATCH "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$" ROOT_RESET_VERSION_MATCH "${ROOT_PASSWORD_RESET_VERSION}") + if(NOT ROOT_RESET_VERSION_MATCH) + message(FATAL_ERROR "ROOT_PASSWORD_RESET_VERSION must use MAJOR.MINOR.PATCH format: ${ROOT_PASSWORD_RESET_VERSION}") + endif() + set(TIANSHAN_VERSION_MAJOR ${CMAKE_MATCH_1}) + set(TIANSHAN_VERSION_MINOR ${CMAKE_MATCH_2}) + set(TIANSHAN_VERSION_PATCH ${CMAKE_MATCH_3}) + set(TIANSHAN_VERSION_PRERELEASE "") + set(TIANSHAN_VERSION_BUILD "") + endif() + if(NOT ROOT_PASSWORD_RESET_RUN_ID) + string(TIMESTAMP ROOT_PASSWORD_RESET_RUN_ID "%H%M%S") + endif() + string(REGEX REPLACE "[^a-zA-Z0-9.]" "." ROOT_PASSWORD_RESET_RUN_ID "${ROOT_PASSWORD_RESET_RUN_ID}") + string(SUBSTRING "${ROOT_PASSWORD_RESET_RUN_ID}" 0 13 ROOT_PASSWORD_RESET_RUN_ID) + + set(TIANSHAN_VERSION_PRERELEASE "root-reset.${ROOT_PASSWORD_RESET_RUN_ID}") + set(TIANSHAN_VERSION_BUILD "") + set(TIANSHAN_VERSION_DISPLAY "${TIANSHAN_VERSION_MAJOR}.${TIANSHAN_VERSION_MINOR}.${TIANSHAN_VERSION_PATCH}-${TIANSHAN_VERSION_PRERELEASE}") + string(LENGTH "${TIANSHAN_VERSION_DISPLAY}" TIANSHAN_VERSION_DISPLAY_LEN) + if(TIANSHAN_VERSION_DISPLAY_LEN GREATER 31) + message(FATAL_ERROR "Root reset PROJECT_VER is too long for esp_app_desc: ${TIANSHAN_VERSION_DISPLAY}") + endif() +endif() + # 基础版本(不含构建元数据) set(TIANSHAN_VERSION "${TIANSHAN_VERSION_MAJOR}.${TIANSHAN_VERSION_MINOR}.${TIANSHAN_VERSION_PATCH}") if(TIANSHAN_VERSION_PRERELEASE) @@ -81,6 +108,11 @@ set(PARTITION_TABLE_FILE "${CMAKE_CURRENT_SOURCE_DIR}/partitions.csv" CACHE STRI # 包含 ESP-IDF include($ENV{IDF_PATH}/tools/cmake/project.cmake) +if(ROOT_PASSWORD_RESET) + idf_build_set_property(MINIMAL_BUILD ON) + idf_build_set_property(ROOT_PASSWORD_RESET ON) +endif() + # 项目定义 project(TianShanOS VERSION ${TIANSHAN_VERSION_MAJOR}.${TIANSHAN_VERSION_MINOR}.${TIANSHAN_VERSION_PATCH}) @@ -105,6 +137,12 @@ idf_build_set_property(COMPILE_DEFINITIONS "TIANSHAN_OS_VERSION_FULL=\"${TIANSHAN_VERSION_DISPLAY}\"" APPEND ) +if(ROOT_PASSWORD_RESET) + idf_build_set_property(COMPILE_DEFINITIONS + "ROOT_PASSWORD_RESET_RUN_ID=\"${ROOT_PASSWORD_RESET_RUN_ID}\"" + APPEND + ) +endif() # 打印项目信息 message(STATUS "") diff --git a/README.md b/README.md index 14a4c6b..0e2ec60 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ idf.py -p /dev/ttyACM0 flash monitor esptool.py --chip esp32s3 -p /dev/ttyACM0 write_flash \ 0x0 bootloader.bin \ 0x8000 partition-table.bin \ - 0x10000 TianshanOS.bin + 0x20000 TianShanOS.bin ``` ### VS Code 开发 @@ -139,6 +139,8 @@ esptool.py --chip esp32s3 -p /dev/ttyACM0 write_flash \ | 文档 | 描述 | |-----|------| | [快速入门](docs/QUICK_START.md) | 环境搭建与首次运行 | +| [串口刷机](docs/FLASH_VIA_SERIAL.md) | 串口烧录与分区偏移说明 | +| [Root 密码恢复固件](docs/ROOT_PASSWORD_RESET_FIRMWARE.md) | root 用户密码恢复固件的构建与刷写流程 | | [架构设计](docs/ARCHITECTURE_DESIGN.md) | 系统架构与设计决策 | | [配置系统设计](docs/CONFIG_SYSTEM_DESIGN.md) | 统一配置系统详细设计 | | [自动化引擎](docs/AUTOMATION_ENGINE.md) | 触发器-条件-动作系统 | diff --git a/components/ts_auth_layout/CMakeLists.txt b/components/ts_auth_layout/CMakeLists.txt new file mode 100644 index 0000000..f0f73a8 --- /dev/null +++ b/components/ts_auth_layout/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register( + INCLUDE_DIRS + "include" +) diff --git a/components/ts_auth_layout/include/ts_auth_layout.h b/components/ts_auth_layout/include/ts_auth_layout.h new file mode 100644 index 0000000..beaa07c --- /dev/null +++ b/components/ts_auth_layout/include/ts_auth_layout.h @@ -0,0 +1,48 @@ +/** + * @file ts_auth_layout.h + * @brief Shared on-flash auth credential layout. + */ + +#ifndef TS_AUTH_LAYOUT_H +#define TS_AUTH_LAYOUT_H + +#include +#include +#include + +#define TS_AUTH_NVS_NAMESPACE "ts_auth" +#define TS_AUTH_ROOT_CRED_KEY "cred_root" +#define TS_AUTH_CONFIG_VERSION_KEY "cfg_version" +#define TS_AUTH_DEFAULT_ROOT_PASSWORD "rm01" +#define TS_AUTH_CONFIG_VERSION 3 + +#define TS_AUTH_SALT_LEN 16 +#define TS_AUTH_HASH_LEN 32 + +typedef struct { + uint8_t salt[TS_AUTH_SALT_LEN]; + uint8_t hash[TS_AUTH_HASH_LEN]; + bool password_changed; + uint32_t failed_attempts; + uint32_t lockout_until; +} ts_auth_user_credential_t; + +#define TS_AUTH_USER_CREDENTIAL_SIZE 60 + +#if defined(__cplusplus) +static_assert(offsetof(ts_auth_user_credential_t, salt) == 0, "auth salt offset changed"); +static_assert(offsetof(ts_auth_user_credential_t, hash) == 16, "auth hash offset changed"); +static_assert(offsetof(ts_auth_user_credential_t, password_changed) == 48, "auth password_changed offset changed"); +static_assert(offsetof(ts_auth_user_credential_t, failed_attempts) == 52, "auth failed_attempts offset changed"); +static_assert(offsetof(ts_auth_user_credential_t, lockout_until) == 56, "auth lockout_until offset changed"); +static_assert(sizeof(ts_auth_user_credential_t) == TS_AUTH_USER_CREDENTIAL_SIZE, "auth credential size changed"); +#else +_Static_assert(offsetof(ts_auth_user_credential_t, salt) == 0, "auth salt offset changed"); +_Static_assert(offsetof(ts_auth_user_credential_t, hash) == 16, "auth hash offset changed"); +_Static_assert(offsetof(ts_auth_user_credential_t, password_changed) == 48, "auth password_changed offset changed"); +_Static_assert(offsetof(ts_auth_user_credential_t, failed_attempts) == 52, "auth failed_attempts offset changed"); +_Static_assert(offsetof(ts_auth_user_credential_t, lockout_until) == 56, "auth lockout_until offset changed"); +_Static_assert(sizeof(ts_auth_user_credential_t) == TS_AUTH_USER_CREDENTIAL_SIZE, "auth credential size changed"); +#endif + +#endif /* TS_AUTH_LAYOUT_H */ diff --git a/components/ts_security/CMakeLists.txt b/components/ts_security/CMakeLists.txt index fe1e3dc..e64f5a7 100644 --- a/components/ts_security/CMakeLists.txt +++ b/components/ts_security/CMakeLists.txt @@ -18,6 +18,7 @@ idf_component_register( REQUIRES ts_core ts_automation + ts_auth_layout mbedtls nvs_flash lwip diff --git a/components/ts_security/src/ts_auth.c b/components/ts_security/src/ts_auth.c index 03d92c0..d6d93ba 100644 --- a/components/ts_security/src/ts_auth.c +++ b/components/ts_security/src/ts_auth.c @@ -10,6 +10,7 @@ */ #include "ts_security.h" +#include "ts_auth_layout.h" #include "ts_crypto.h" #include "ts_log.h" #include "nvs_flash.h" @@ -20,23 +21,11 @@ #define TAG "ts_auth" -#define NVS_AUTH_NAMESPACE "ts_auth" -#define SALT_LEN 16 -#define HASH_LEN 32 #define DEFAULT_PASSWORD_ADMIN "rm01" -#define DEFAULT_PASSWORD_ROOT "rm01" #define MAX_LOGIN_ATTEMPTS 5 #define LOGIN_LOCKOUT_SEC 300 /* 5 分钟锁定 */ -#define AUTH_CONFIG_VERSION 3 /* 增加此版本号可强制重置所有用户密码 */ -/* 用户信息结构 */ -typedef struct { - uint8_t salt[SALT_LEN]; - uint8_t hash[HASH_LEN]; - bool password_changed; /* 是否已修改过初始密码 */ - uint32_t failed_attempts; - uint32_t lockout_until; /* 锁定截止时间 (秒) */ -} user_credential_t; +typedef ts_auth_user_credential_t user_credential_t; static nvs_handle_t s_auth_nvs; static bool s_auth_initialized = false; @@ -53,14 +42,14 @@ static esp_err_t compute_password_hash(const uint8_t *salt, const char *password { /* 拼接 salt + password */ size_t pwd_len = strlen(password); - size_t total_len = SALT_LEN + pwd_len; + size_t total_len = TS_AUTH_SALT_LEN + pwd_len; uint8_t *buf = malloc(total_len); if (!buf) return ESP_ERR_NO_MEM; - memcpy(buf, salt, SALT_LEN); - memcpy(buf + SALT_LEN, password, pwd_len); + memcpy(buf, salt, TS_AUTH_SALT_LEN); + memcpy(buf + TS_AUTH_SALT_LEN, password, pwd_len); - esp_err_t ret = ts_crypto_hash(TS_HASH_SHA256, buf, total_len, hash_out, HASH_LEN); + esp_err_t ret = ts_crypto_hash(TS_HASH_SHA256, buf, total_len, hash_out, TS_AUTH_HASH_LEN); /* 清除敏感数据 */ memset(buf, 0, total_len); @@ -114,7 +103,7 @@ static esp_err_t write_user_password_credential(const char *username, const char user_credential_t cred = {0}; - esp_fill_random(cred.salt, SALT_LEN); + esp_fill_random(cred.salt, TS_AUTH_SALT_LEN); esp_err_t ret = compute_password_hash(cred.salt, password, cred.hash); if (ret != ESP_OK) return ret; @@ -132,7 +121,7 @@ static esp_err_t write_user_password_credential(const char *username, const char static esp_err_t force_create_user(const char *username, ts_perm_level_t level) { /* 根据用户类型使用不同默认密码 */ - const char *default_pwd = (level == TS_PERM_ROOT) ? DEFAULT_PASSWORD_ROOT : DEFAULT_PASSWORD_ADMIN; + const char *default_pwd = (level == TS_PERM_ROOT) ? TS_AUTH_DEFAULT_ROOT_PASSWORD : DEFAULT_PASSWORD_ADMIN; TS_LOGI(TAG, "Creating/resetting user '%s'", username); return write_user_password_credential(username, default_pwd, false); @@ -172,7 +161,7 @@ esp_err_t ts_auth_init(void) { if (s_auth_initialized) return ESP_OK; - esp_err_t ret = nvs_open(NVS_AUTH_NAMESPACE, NVS_READWRITE, &s_auth_nvs); + esp_err_t ret = nvs_open(TS_AUTH_NVS_NAMESPACE, NVS_READWRITE, &s_auth_nvs); if (ret != ESP_OK) { TS_LOGE(TAG, "Failed to open NVS namespace: %s", esp_err_to_name(ret)); return ret; @@ -180,16 +169,16 @@ esp_err_t ts_auth_init(void) /* 检查配置版本,版本变化时强制重置所有用户 */ uint8_t stored_version = 0; - nvs_get_u8(s_auth_nvs, "cfg_version", &stored_version); + nvs_get_u8(s_auth_nvs, TS_AUTH_CONFIG_VERSION_KEY, &stored_version); - if (stored_version != AUTH_CONFIG_VERSION) { + if (stored_version != TS_AUTH_CONFIG_VERSION) { TS_LOGW(TAG, "Auth config version changed (%d -> %d), resetting all users", - stored_version, AUTH_CONFIG_VERSION); + stored_version, TS_AUTH_CONFIG_VERSION); /* 强制重新创建所有用户 */ force_create_user("admin", TS_PERM_ADMIN); force_create_user("root", TS_PERM_ROOT); /* 保存新版本号 */ - nvs_set_u8(s_auth_nvs, "cfg_version", AUTH_CONFIG_VERSION); + nvs_set_u8(s_auth_nvs, TS_AUTH_CONFIG_VERSION_KEY, TS_AUTH_CONFIG_VERSION); nvs_commit(s_auth_nvs); } else { /* 初始化默认用户(如果不存在) */ @@ -198,7 +187,7 @@ esp_err_t ts_auth_init(void) } s_auth_initialized = true; - TS_LOGI(TAG, "Auth module initialized (version %d)", AUTH_CONFIG_VERSION); + TS_LOGI(TAG, "Auth module initialized (version %d)", TS_AUTH_CONFIG_VERSION); return ESP_OK; } @@ -237,13 +226,13 @@ esp_err_t ts_auth_verify_password(const char *username, const char *password, } /* 计算输入密码的哈希 */ - uint8_t computed_hash[HASH_LEN]; + uint8_t computed_hash[TS_AUTH_HASH_LEN]; ret = compute_password_hash(cred.salt, password, computed_hash); if (ret != ESP_OK) return ret; /* 比较哈希(恒定时间比较防止时序攻击) */ uint8_t diff = 0; - for (int i = 0; i < HASH_LEN; i++) { + for (int i = 0; i < TS_AUTH_HASH_LEN; i++) { diff |= computed_hash[i] ^ cred.hash[i]; } @@ -420,7 +409,7 @@ esp_err_t ts_auth_reset_password(const char *username) if (strcmp(username, "admin") == 0) { default_pwd = DEFAULT_PASSWORD_ADMIN; } else if (strcmp(username, "root") == 0) { - default_pwd = DEFAULT_PASSWORD_ROOT; + default_pwd = TS_AUTH_DEFAULT_ROOT_PASSWORD; } else { return ESP_ERR_NOT_FOUND; } diff --git a/docs/FLASH_VIA_SERIAL.md b/docs/FLASH_VIA_SERIAL.md index ad9844b..39a4c62 100644 --- a/docs/FLASH_VIA_SERIAL.md +++ b/docs/FLASH_VIA_SERIAL.md @@ -48,7 +48,7 @@ idf.py build 构建成功后,可在 `build/` 下看到: -- `TianshanOS.bin` — 应用固件(烧录到 ota_0) +- `TianShanOS.bin` — 应用固件(烧录到 ota_0) - `bootloader.bin` — 引导程序 - `partition-table.bin` — 分区表 - `www.bin` — Web 资源 SPIFFS 镜像(烧录到 www 分区) @@ -117,7 +117,7 @@ idf.py -p flash monitor |------------|----------|------| | bootloader | 0x0 | build/bootloader/bootloader.bin | | 分区表 | 0x8000 | build/partition_table/partition-table.bin | -| 应用 ota_0 | 0x20000 | build/TianshanOS.bin | +| 应用 ota_0 | 0x20000 | build/TianShanOS.bin | | www | 0x6A0000 | build/www.bin | 在 `build` 目录下执行(把 `` 换成实际串口): @@ -128,7 +128,7 @@ cd build esptool.py --chip esp32s3 -p write_flash \ 0x0 bootloader/bootloader.bin \ 0x8000 partition_table/partition-table.bin \ - 0x20000 TianshanOS.bin \ + 0x20000 TianShanOS.bin \ 0x6A0000 www.bin ``` diff --git a/docs/README.md b/docs/README.md index e0c7029..78a9276 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,6 +13,8 @@ | 文档 | 描述 | |------|------| | [快速入门](QUICK_START.md) | 5 分钟快速上手指南:环境准备、编译烧录、首次配置 | +| [串口刷机指南](FLASH_VIA_SERIAL.md) | 串口烧录、手动分区偏移和常见刷机问题 | +| [Root 密码恢复固件](ROOT_PASSWORD_RESET_FIRMWARE.md) | root 用户密码恢复固件的构建、OTA 和串口使用流程 | | [命令参考](COMMAND_REFERENCE.md) | CLI 命令完整手册:系统、网络、LED、设备、配置等 | | [配置指南](CONFIGURATION_GUIDE.md) | 板级配置、GPIO 映射、运行时配置说明 | diff --git a/docs/ROOT_PASSWORD_RESET_FIRMWARE.md b/docs/ROOT_PASSWORD_RESET_FIRMWARE.md new file mode 100644 index 0000000..6aa3202 --- /dev/null +++ b/docs/ROOT_PASSWORD_RESET_FIRMWARE.md @@ -0,0 +1,63 @@ +# Root 密码恢复固件 + +Root 密码恢复固件是一个 app-only 临时固件,只修改 NVS 中 `ts_auth/cred_root`,把 `root` 密码恢复为默认值 `rm01`。它不会擦除 NVS,也不会主动修改 `admin` 密码、网络配置、证书、SSH key 或其他数据。 + +该固件不启动正常系统服务,不启动 WebUI、API、网络或控制台。写入并校验 root 凭据后,它会尝试切回另一个有效 OTA app 分区,等待 3 秒后重启。 + +恢复固件会先检查 `ts_auth/cfg_version` 是否等于当前认证布局版本。若版本缺失或不匹配,它会停机并拒绝写入,避免回到正常固件后触发认证模块的全用户重置流程。 + +## 构建 + +```bash +./tools/build_root_reset.sh +``` + +指定恢复固件基础版本号: + +```bash +ROOT_PASSWORD_RESET_VERSION=9.9.9 ./tools/build_root_reset.sh +``` + +构建产物: + +- `build-root-reset/TianShanOS.bin` +- `build-root-reset/TianShanOS-RootReset.bin` + +人工刷写和归档时统一使用 `build-root-reset/TianShanOS-RootReset.bin`,避免和普通固件混淆。 + +## 通过 WebUI 手动 OTA + +适用条件:当前设备仍可进入 WebUI,且 OTA 所需认证会话可用。 + +1. 打开 WebUI 的系统升级页面。 +2. 手动上传 `build-root-reset/TianShanOS-RootReset.bin`。 +3. 取消勾选“包含 WebUI”。 +4. 开始 OTA,随后通过串口日志确认恢复过程。 + +如果 OTA 需要认证而 `admin` 或现有 session 不可用,不能通过 WebUI 完成,只能走串口流程。 + +## 通过串口强制启动 ota_0 + +串口恢复用于 WebUI/OTA 不可用的场景。该流程会清除 OTA 选择信息并把恢复 app 写入 `ota_0`,不会擦 NVS。 + +这个流程只有在 `ota_1` 保留着要返回的正常固件时,才会自动回到那个正常固件。如果设备原本运行在 `ota_0`,下面的命令会覆盖该正常固件;恢复固件随后会尝试切到 `ota_1`,那里可能是旧固件、测试固件或空分区。无法确认另一个 slot 内容时,串口流程应视为“先恢复 root,再手动刷回正常固件”的救援手段。 + +```bash +esptool.py --chip esp32s3 -p erase_region 0x16000 0x2000 +esptool.py --chip esp32s3 -p write_flash 0x20000 build-root-reset/TianShanOS-RootReset.bin +``` + +`0x16000` 是 `otadata` 分区地址,`0x20000` 是 `ota_0` app 分区地址。恢复固件启动后会查找另一个 OTA slot 作为返回目标;如果另一个 slot 没有有效正常固件,或目标版本也是 `root-reset`,它会停机并要求手动刷回正常固件。 + +## 成功标准 + +串口日志应出现以下信息: + +- NVS 初始化完成。 +- 写入 `ts_auth/cred_root`。 +- `nvs_commit()` 完成。 +- 读回校验通过。 +- 打印当前分区、目标分区、目标 project/version。 +- 等待 3 秒后重启。 + +重启回同一认证布局的正常固件后,`root/rm01` 应可登录;`admin` 密码和其他 NVS 配置应保持不变。重复刷入同一个恢复固件应再次成功恢复 root。 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 913952e..a23a63a 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,33 +1,52 @@ # TianShanOS - Main Application # 主应用程序 -idf_component_register( - SRCS - "main.c" - "ts_core_init.c" - "ts_services.c" - INCLUDE_DIRS - "." - REQUIRES - ts_core - ts_config - ts_log - ts_event - ts_service - ts_storage - ts_console - ts_hal - ts_led - ts_drivers - ts_net - ts_api - ts_security - ts_cert - ts_config_pack - ts_pki_client - ts_https - ts_webui - ts_automation - ts_mempool - json # cJSON for PSRAM hooks -) +idf_build_get_property(ROOT_PASSWORD_RESET_BUILD ROOT_PASSWORD_RESET) + +if(ROOT_PASSWORD_RESET_BUILD) + idf_component_register( + SRCS + "root_password_reset_main.c" + INCLUDE_DIRS + "." + REQUIRES + nvs_flash + mbedtls + esp_partition + app_update + esp_system + esp_hw_support + ts_auth_layout + ) +else() + idf_component_register( + SRCS + "main.c" + "ts_core_init.c" + "ts_services.c" + INCLUDE_DIRS + "." + REQUIRES + ts_core + ts_config + ts_log + ts_event + ts_service + ts_storage + ts_console + ts_hal + ts_led + ts_drivers + ts_net + ts_api + ts_security + ts_cert + ts_config_pack + ts_pki_client + ts_https + ts_webui + ts_automation + ts_mempool + json # cJSON for PSRAM hooks + ) +endif() diff --git a/main/root_password_reset_main.c b/main/root_password_reset_main.c new file mode 100644 index 0000000..daaf05a --- /dev/null +++ b/main/root_password_reset_main.c @@ -0,0 +1,285 @@ +/** + * @file root_password_reset_main.c + * @brief One-purpose firmware for restoring the root password credential. + */ + +#include +#include +#include +#include + +#include "esp_err.h" +#include "esp_log.h" +#include "esp_ota_ops.h" +#include "esp_partition.h" +#include "esp_random.h" +#include "esp_system.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "mbedtls/sha256.h" +#include "nvs.h" +#include "nvs_flash.h" +#include "ts_auth_layout.h" + +#define TAG "root_reset" + +#ifndef TIANSHAN_OS_VERSION_FULL +#define TIANSHAN_OS_VERSION_FULL "unknown" +#endif + +#ifndef ROOT_PASSWORD_RESET_RUN_ID +#define ROOT_PASSWORD_RESET_RUN_ID "unknown" +#endif + +static bool string_contains(const char *haystack, const char *needle) +{ + if (!haystack || !needle) { + return false; + } + + return strstr(haystack, needle) != NULL; +} + +static void halt_forever(void) +{ + ESP_LOGE(TAG, "Recovery firmware halted. NVS was not erased."); + while (true) { + vTaskDelay(pdMS_TO_TICKS(1000)); + } +} + +static void halt_on_error(const char *operation, esp_err_t ret) +{ + ESP_LOGE(TAG, "%s failed: %s (0x%x)", operation, esp_err_to_name(ret), (unsigned)ret); + halt_forever(); +} + +static esp_err_t compute_root_hash(const uint8_t salt[TS_AUTH_SALT_LEN], + uint8_t hash_out[TS_AUTH_HASH_LEN]) +{ + static const char default_password[] = TS_AUTH_DEFAULT_ROOT_PASSWORD; + uint8_t material[TS_AUTH_SALT_LEN + sizeof(default_password) - 1]; + + memcpy(material, salt, TS_AUTH_SALT_LEN); + memcpy(material + TS_AUTH_SALT_LEN, default_password, sizeof(default_password) - 1); + + int ret = mbedtls_sha256(material, sizeof(material), hash_out, 0); + memset(material, 0, sizeof(material)); + + return ret == 0 ? ESP_OK : ESP_FAIL; +} + +static bool constant_time_equal(const uint8_t *a, const uint8_t *b, size_t len) +{ + uint8_t diff = 0; + + for (size_t i = 0; i < len; ++i) { + diff |= a[i] ^ b[i]; + } + + return diff == 0; +} + +static esp_err_t verify_auth_config_version(nvs_handle_t handle) +{ + uint8_t stored_version = 0; + esp_err_t ret = nvs_get_u8(handle, TS_AUTH_CONFIG_VERSION_KEY, &stored_version); + if (ret != ESP_OK) { + if (ret == ESP_ERR_NVS_NOT_FOUND) { + ESP_LOGE(TAG, "cfg_version key not found - auth module was never initialized on this device"); + } + return ret; + } + + if (stored_version != TS_AUTH_CONFIG_VERSION) { + ESP_LOGE(TAG, "Auth config version mismatch: expected %u, got %u", + (unsigned)TS_AUTH_CONFIG_VERSION, (unsigned)stored_version); + ESP_LOGE(TAG, "Refusing to proceed because normal firmware may reset all auth users"); + return ESP_ERR_INVALID_VERSION; + } + + return ESP_OK; +} + +static void reset_root_credential(void) +{ + ESP_LOGI(TAG, "Initializing NVS"); + esp_err_t ret = nvs_flash_init(); + if (ret != ESP_OK) { + halt_on_error("nvs_flash_init", ret); + } + + nvs_handle_t handle = 0; + bool handle_open = false; + ret = nvs_open(TS_AUTH_NVS_NAMESPACE, NVS_READWRITE, &handle); + if (ret != ESP_OK) { + halt_on_error("nvs_open(ts_auth)", ret); + } + handle_open = true; + + const char *failed_operation = NULL; + + ret = verify_auth_config_version(handle); + if (ret != ESP_OK) { + failed_operation = "verify_auth_config_version"; + goto fail; + } + + ts_auth_user_credential_t credential = {0}; + esp_fill_random(credential.salt, TS_AUTH_SALT_LEN); + + ret = compute_root_hash(credential.salt, credential.hash); + if (ret != ESP_OK) { + failed_operation = "compute_root_hash"; + goto fail; + } + + credential.password_changed = false; + credential.failed_attempts = 0; + credential.lockout_until = 0; + + ESP_LOGI(TAG, "Writing NVS blob %s/%s", TS_AUTH_NVS_NAMESPACE, TS_AUTH_ROOT_CRED_KEY); + ret = nvs_set_blob(handle, TS_AUTH_ROOT_CRED_KEY, &credential, sizeof(credential)); + if (ret != ESP_OK) { + failed_operation = "nvs_set_blob(cred_root)"; + goto fail; + } + + ret = nvs_commit(handle); + if (ret != ESP_OK) { + failed_operation = "nvs_commit"; + goto fail; + } + ESP_LOGI(TAG, "NVS commit complete"); + + ts_auth_user_credential_t readback = {0}; + size_t readback_len = sizeof(readback); + ret = nvs_get_blob(handle, TS_AUTH_ROOT_CRED_KEY, &readback, &readback_len); + nvs_close(handle); + handle_open = false; + if (ret != ESP_OK) { + halt_on_error("nvs_get_blob(cred_root)", ret); + } + if (readback_len != sizeof(readback)) { + ESP_LOGE(TAG, "Readback length mismatch: expected %u, got %u", + (unsigned)sizeof(readback), (unsigned)readback_len); + halt_forever(); + } + + uint8_t expected_hash[TS_AUTH_HASH_LEN] = {0}; + ret = compute_root_hash(readback.salt, expected_hash); + if (ret != ESP_OK) { + halt_on_error("compute_root_hash(readback)", ret); + } + + bool hash_ok = constant_time_equal(readback.hash, expected_hash, TS_AUTH_HASH_LEN); + memset(expected_hash, 0, sizeof(expected_hash)); + + if (!hash_ok || + readback.password_changed || + readback.failed_attempts != 0 || + readback.lockout_until != 0) { + ESP_LOGE(TAG, "Readback verification failed"); + halt_forever(); + } + + ESP_LOGI(TAG, "Root credential restored to default password"); + return; + +fail: + if (handle_open) { + nvs_close(handle); + } + halt_on_error(failed_operation, ret); +} + +static bool ota_state_rejected(const esp_partition_t *partition) +{ + esp_ota_img_states_t state = 0; + esp_err_t ret = esp_ota_get_state_partition(partition, &state); + + if (ret == ESP_OK) { + ESP_LOGI(TAG, "Target OTA state: %d", state); + return state == ESP_OTA_IMG_INVALID || state == ESP_OTA_IMG_ABORTED; + } + + ESP_LOGW(TAG, "Target OTA state unavailable: %s (0x%x)", + esp_err_to_name(ret), (unsigned)ret); + return false; +} + +static const esp_partition_t *find_return_partition(const esp_partition_t *running) +{ + esp_partition_subtype_t target_subtype; + + if (running->subtype == ESP_PARTITION_SUBTYPE_APP_OTA_0) { + target_subtype = ESP_PARTITION_SUBTYPE_APP_OTA_1; + } else if (running->subtype == ESP_PARTITION_SUBTYPE_APP_OTA_1) { + target_subtype = ESP_PARTITION_SUBTYPE_APP_OTA_0; + } else { + ESP_LOGE(TAG, "Running app is not an OTA slot: subtype=0x%x", running->subtype); + return NULL; + } + + return esp_partition_find_first(ESP_PARTITION_TYPE_APP, target_subtype, NULL); +} + +static void switch_back_to_normal_firmware(void) +{ + const esp_partition_t *running = esp_ota_get_running_partition(); + if (!running) { + ESP_LOGE(TAG, "Cannot resolve running partition"); + halt_forever(); + } + + ESP_LOGI(TAG, "Running partition: label=%s subtype=0x%x address=0x%lx size=0x%lx", + running->label, running->subtype, + (unsigned long)running->address, (unsigned long)running->size); + + const esp_partition_t *target = find_return_partition(running); + if (!target) { + ESP_LOGE(TAG, "No alternate OTA app partition found. Flash normal firmware manually."); + halt_forever(); + } + + esp_app_desc_t target_desc = {0}; + esp_err_t ret = esp_ota_get_partition_description(target, &target_desc); + if (ret != ESP_OK) { + halt_on_error("esp_ota_get_partition_description", ret); + } + + if (ota_state_rejected(target)) { + ESP_LOGE(TAG, "Refusing to boot target partition because it is invalid or aborted"); + halt_forever(); + } + + ESP_LOGI(TAG, "Return target: label=%s subtype=0x%x address=0x%lx size=0x%lx", + target->label, target->subtype, + (unsigned long)target->address, (unsigned long)target->size); + ESP_LOGI(TAG, "Return target app: project=%s version=%s", + target_desc.project_name, target_desc.version); + if (string_contains(target_desc.version, "root-reset")) { + ESP_LOGE(TAG, "Refusing to boot another root-reset firmware"); + halt_forever(); + } + + ret = esp_ota_set_boot_partition(target); + if (ret != ESP_OK) { + halt_on_error("esp_ota_set_boot_partition", ret); + } + + ESP_LOGI(TAG, "Boot partition updated. Restarting in 3 seconds."); + vTaskDelay(pdMS_TO_TICKS(3000)); + esp_restart(); +} + +void app_main(void) +{ + ESP_LOGI(TAG, "TianShanOS root password reset firmware"); + ESP_LOGI(TAG, "Version: %s", TIANSHAN_OS_VERSION_FULL); + ESP_LOGI(TAG, "Run ID: %s", ROOT_PASSWORD_RESET_RUN_ID); + ESP_LOGW(TAG, "Only root credential will be modified; NVS erase is never performed."); + + reset_root_credential(); + switch_back_to_normal_firmware(); +} diff --git a/tools/build_root_reset.sh b/tools/build_root_reset.sh new file mode 100755 index 0000000..0c76ff9 --- /dev/null +++ b/tools/build_root_reset.sh @@ -0,0 +1,112 @@ +#!/bin/bash +# Build the temporary root password reset firmware. + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +BUILD_DIR="build-root-reset" +SDKCONFIG_PATH="$PROJECT_DIR/$BUILD_DIR/sdkconfig" +ROOT_RESET_VERSION="${ROOT_PASSWORD_RESET_VERSION:-}" + +cd "$PROJECT_DIR" + +if [ -z "$IDF_PATH" ]; then + echo "正在激活 ESP-IDF 环境..." + source ~/esp/v5.5.2/esp-idf/export.sh 2>/dev/null || { + echo "错误: 无法找到 ESP-IDF,请先安装或手动 source export.sh" + exit 1 + } +fi + +GIT_SHORT_SHA="$(git rev-parse --short=6 HEAD 2>/dev/null || echo nogit)" +RUN_ID="$(date -u +%H%M%S).${GIT_SHORT_SHA}" + +echo "构建 root 密码恢复固件..." +echo "Run ID: ${RUN_ID}" +if [ -n "$ROOT_RESET_VERSION" ]; then + echo "Version override: ${ROOT_RESET_VERSION}" +fi + +mkdir -p "$BUILD_DIR" +rm -f "$BUILD_DIR/CMakeCache.txt" + +IDF_ARGS=( + -B "$BUILD_DIR" + -DSDKCONFIG="$SDKCONFIG_PATH" + -DROOT_PASSWORD_RESET=ON + -DROOT_PASSWORD_RESET_RUN_ID="$RUN_ID" +) +if [ -n "$ROOT_RESET_VERSION" ]; then + IDF_ARGS+=("-DROOT_PASSWORD_RESET_VERSION=$ROOT_RESET_VERSION") +fi + +IDF_COMPONENT_MANAGER=0 idf.py \ + "${IDF_ARGS[@]}" \ + build + +python3 - "$BUILD_DIR" <<'PY' +import json +import pathlib +import sys + +build_dir = pathlib.Path(sys.argv[1]) + +project = json.loads((build_dir / "project_description.json").read_text()) +version = project.get("project_version", "") +if "-root-reset." not in version: + raise SystemExit(f"错误: 构建产物版本不是 root-reset: {version}") + +components = set(project.get("build_components", [])) +forbidden_components = { + "ts_api", + "ts_automation", + "ts_cert", + "ts_config", + "ts_config_pack", + "ts_console", + "ts_core", + "ts_drivers", + "ts_event", + "ts_hal", + "ts_https", + "ts_jsonpath", + "ts_led", + "ts_log", + "ts_mempool", + "ts_net", + "ts_ota", + "ts_pki_client", + "ts_security", + "ts_service", + "ts_storage", + "ts_webui", +} +present_forbidden = sorted(components & forbidden_components) +if present_forbidden: + raise SystemExit("错误: 恢复固件包含正常系统组件: " + ", ".join(present_forbidden)) + +managed = sorted(component for component in components if component.startswith("espressif__")) +if managed: + raise SystemExit("错误: 恢复固件包含托管组件: " + ", ".join(managed)) + +compile_commands = json.loads((build_dir / "compile_commands.json").read_text()) +main_sources = sorted({ + pathlib.Path(command["file"]).name + for command in compile_commands + if "/main/" in command["file"] +}) +if main_sources != ["root_password_reset_main.c"]: + raise SystemExit("错误: main 源文件不符合恢复构建预期: " + ", ".join(main_sources)) + +print(f"恢复固件自检通过: {version}") +PY + +cp "$BUILD_DIR/TianShanOS.bin" "$BUILD_DIR/TianShanOS-RootReset.bin" + +echo "" +echo "恢复固件已生成:" +echo " $BUILD_DIR/TianShanOS.bin" +echo " $BUILD_DIR/TianShanOS-RootReset.bin" +echo "" +echo "人工操作请使用 $BUILD_DIR/TianShanOS-RootReset.bin。" From 2f6d2c7436a2208ac89e121b4f1404b8c9bef3fc Mon Sep 17 00:00:00 2001 From: Alfred Date: Thu, 11 Jun 2026 21:58:02 +0800 Subject: [PATCH 2/3] feat: add root password reset recovery firmware (#38) (#39) From 339f05e0ebda01172bea9b95651c9158db1812b0 Mon Sep 17 00:00:00 2001 From: Alfred Date: Tue, 16 Jun 2026 01:22:49 +0800 Subject: [PATCH 3/3] Restore admin in root reset firmware --- README.md | 2 +- .../ts_auth_layout/include/ts_auth_layout.h | 2 + components/ts_security/src/ts_auth.c | 9 +- docs/README.md | 2 +- docs/ROOT_PASSWORD_RESET_FIRMWARE.md | 19 +- main/root_password_reset_main.c | 200 ++++++++++++------ tools/build_root_reset.sh | 6 +- 7 files changed, 163 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 0e2ec60..190ff68 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ esptool.py --chip esp32s3 -p /dev/ttyACM0 write_flash \ |-----|------| | [快速入门](docs/QUICK_START.md) | 环境搭建与首次运行 | | [串口刷机](docs/FLASH_VIA_SERIAL.md) | 串口烧录与分区偏移说明 | -| [Root 密码恢复固件](docs/ROOT_PASSWORD_RESET_FIRMWARE.md) | root 用户密码恢复固件的构建与刷写流程 | +| [Root/Admin 密码恢复固件](docs/ROOT_PASSWORD_RESET_FIRMWARE.md) | root/admin 用户密码恢复固件的构建与刷写流程 | | [架构设计](docs/ARCHITECTURE_DESIGN.md) | 系统架构与设计决策 | | [配置系统设计](docs/CONFIG_SYSTEM_DESIGN.md) | 统一配置系统详细设计 | | [自动化引擎](docs/AUTOMATION_ENGINE.md) | 触发器-条件-动作系统 | diff --git a/components/ts_auth_layout/include/ts_auth_layout.h b/components/ts_auth_layout/include/ts_auth_layout.h index beaa07c..0364c3a 100644 --- a/components/ts_auth_layout/include/ts_auth_layout.h +++ b/components/ts_auth_layout/include/ts_auth_layout.h @@ -12,8 +12,10 @@ #define TS_AUTH_NVS_NAMESPACE "ts_auth" #define TS_AUTH_ROOT_CRED_KEY "cred_root" +#define TS_AUTH_ADMIN_CRED_KEY "cred_admin" #define TS_AUTH_CONFIG_VERSION_KEY "cfg_version" #define TS_AUTH_DEFAULT_ROOT_PASSWORD "rm01" +#define TS_AUTH_DEFAULT_ADMIN_PASSWORD "rm01" #define TS_AUTH_CONFIG_VERSION 3 #define TS_AUTH_SALT_LEN 16 diff --git a/components/ts_security/src/ts_auth.c b/components/ts_security/src/ts_auth.c index d6d93ba..a0f9b20 100644 --- a/components/ts_security/src/ts_auth.c +++ b/components/ts_security/src/ts_auth.c @@ -6,7 +6,7 @@ * * 安全设计: * - 密码哈希仅存储在 NVS,不导出到 SD 卡 - * - admin 密码可由 root 会话恢复,root 密码遗失需恢复出厂 + * - admin 密码可由 root 会话恢复;root/admin 均可通过受控恢复固件恢复默认密码 */ #include "ts_security.h" @@ -21,7 +21,6 @@ #define TAG "ts_auth" -#define DEFAULT_PASSWORD_ADMIN "rm01" #define MAX_LOGIN_ATTEMPTS 5 #define LOGIN_LOCKOUT_SEC 300 /* 5 分钟锁定 */ @@ -121,7 +120,9 @@ static esp_err_t write_user_password_credential(const char *username, const char static esp_err_t force_create_user(const char *username, ts_perm_level_t level) { /* 根据用户类型使用不同默认密码 */ - const char *default_pwd = (level == TS_PERM_ROOT) ? TS_AUTH_DEFAULT_ROOT_PASSWORD : DEFAULT_PASSWORD_ADMIN; + const char *default_pwd = (level == TS_PERM_ROOT) + ? TS_AUTH_DEFAULT_ROOT_PASSWORD + : TS_AUTH_DEFAULT_ADMIN_PASSWORD; TS_LOGI(TAG, "Creating/resetting user '%s'", username); return write_user_password_credential(username, default_pwd, false); @@ -407,7 +408,7 @@ esp_err_t ts_auth_reset_password(const char *username) /* 只允许重置 admin 和 root */ const char *default_pwd; if (strcmp(username, "admin") == 0) { - default_pwd = DEFAULT_PASSWORD_ADMIN; + default_pwd = TS_AUTH_DEFAULT_ADMIN_PASSWORD; } else if (strcmp(username, "root") == 0) { default_pwd = TS_AUTH_DEFAULT_ROOT_PASSWORD; } else { diff --git a/docs/README.md b/docs/README.md index 78a9276..5b95ed2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,7 +14,7 @@ |------|------| | [快速入门](QUICK_START.md) | 5 分钟快速上手指南:环境准备、编译烧录、首次配置 | | [串口刷机指南](FLASH_VIA_SERIAL.md) | 串口烧录、手动分区偏移和常见刷机问题 | -| [Root 密码恢复固件](ROOT_PASSWORD_RESET_FIRMWARE.md) | root 用户密码恢复固件的构建、OTA 和串口使用流程 | +| [Root/Admin 密码恢复固件](ROOT_PASSWORD_RESET_FIRMWARE.md) | root/admin 用户密码恢复固件的构建、OTA 和串口使用流程 | | [命令参考](COMMAND_REFERENCE.md) | CLI 命令完整手册:系统、网络、LED、设备、配置等 | | [配置指南](CONFIGURATION_GUIDE.md) | 板级配置、GPIO 映射、运行时配置说明 | diff --git a/docs/ROOT_PASSWORD_RESET_FIRMWARE.md b/docs/ROOT_PASSWORD_RESET_FIRMWARE.md index 6aa3202..b699c7d 100644 --- a/docs/ROOT_PASSWORD_RESET_FIRMWARE.md +++ b/docs/ROOT_PASSWORD_RESET_FIRMWARE.md @@ -1,10 +1,10 @@ -# Root 密码恢复固件 +# Root/Admin 密码恢复固件 -Root 密码恢复固件是一个 app-only 临时固件,只修改 NVS 中 `ts_auth/cred_root`,把 `root` 密码恢复为默认值 `rm01`。它不会擦除 NVS,也不会主动修改 `admin` 密码、网络配置、证书、SSH key 或其他数据。 +Root/Admin 密码恢复固件是一个 app-only 临时固件,只修改 NVS 中 `ts_auth/cred_root` 和 `ts_auth/cred_admin`,把 `root` 与 `admin` 密码都恢复为默认值 `rm01`。它不会擦除 NVS,也不会主动修改网络配置、证书、SSH key 或其他数据。 -该固件不启动正常系统服务,不启动 WebUI、API、网络或控制台。写入并校验 root 凭据后,它会尝试切回另一个有效 OTA app 分区,等待 3 秒后重启。 +该固件不启动正常系统服务,不启动 WebUI、API、网络或控制台。写入并校验 root/admin 凭据后,它会尝试切回另一个有效 OTA app 分区,等待 3 秒后重启。 -恢复固件会先检查 `ts_auth/cfg_version` 是否等于当前认证布局版本。若版本缺失或不匹配,它会停机并拒绝写入,避免回到正常固件后触发认证模块的全用户重置流程。 +恢复固件会先检查 `ts_auth/cfg_version` 是否等于当前认证布局版本。若版本缺失或不匹配,它会停机并拒绝写入任何 credential,避免回到正常固件后触发认证模块的全用户重置流程。 ## 构建 @@ -31,16 +31,16 @@ ROOT_PASSWORD_RESET_VERSION=9.9.9 ./tools/build_root_reset.sh 1. 打开 WebUI 的系统升级页面。 2. 手动上传 `build-root-reset/TianShanOS-RootReset.bin`。 -3. 取消勾选“包含 WebUI”。 +3. 取消勾选“包含 WebUI”,不要上传或刷写 `www.bin`。 4. 开始 OTA,随后通过串口日志确认恢复过程。 -如果 OTA 需要认证而 `admin` 或现有 session 不可用,不能通过 WebUI 完成,只能走串口流程。 +如果 OTA 需要认证而现有 session 不可用,不能通过 WebUI 完成,只能走串口流程。 ## 通过串口强制启动 ota_0 串口恢复用于 WebUI/OTA 不可用的场景。该流程会清除 OTA 选择信息并把恢复 app 写入 `ota_0`,不会擦 NVS。 -这个流程只有在 `ota_1` 保留着要返回的正常固件时,才会自动回到那个正常固件。如果设备原本运行在 `ota_0`,下面的命令会覆盖该正常固件;恢复固件随后会尝试切到 `ota_1`,那里可能是旧固件、测试固件或空分区。无法确认另一个 slot 内容时,串口流程应视为“先恢复 root,再手动刷回正常固件”的救援手段。 +这个流程只有在 `ota_1` 保留着要返回的正常固件时,才会自动回到那个正常固件。如果设备原本运行在 `ota_0`,下面的命令会覆盖该正常固件;恢复固件随后会尝试切到 `ota_1`,那里可能是旧固件、测试固件或空分区。无法确认另一个 slot 内容时,串口流程应视为“先恢复 root/admin,再手动刷回正常固件”的救援手段。 ```bash esptool.py --chip esp32s3 -p erase_region 0x16000 0x2000 @@ -55,9 +55,10 @@ esptool.py --chip esp32s3 -p write_flash 0x20000 build-root-reset/TianSha - NVS 初始化完成。 - 写入 `ts_auth/cred_root`。 +- 写入 `ts_auth/cred_admin`。 - `nvs_commit()` 完成。 -- 读回校验通过。 +- root/admin 读回校验通过。 - 打印当前分区、目标分区、目标 project/version。 - 等待 3 秒后重启。 -重启回同一认证布局的正常固件后,`root/rm01` 应可登录;`admin` 密码和其他 NVS 配置应保持不变。重复刷入同一个恢复固件应再次成功恢复 root。 +重启回同一认证布局的正常固件后,`root/rm01` 和 `admin/rm01` 都应可登录;网络配置、证书、SSH key 和其他 NVS 配置应保持不变。重复刷入同一个恢复固件应再次成功恢复 root/admin。 diff --git a/main/root_password_reset_main.c b/main/root_password_reset_main.c index daaf05a..abb5b46 100644 --- a/main/root_password_reset_main.c +++ b/main/root_password_reset_main.c @@ -1,6 +1,6 @@ /** * @file root_password_reset_main.c - * @brief One-purpose firmware for restoring the root password credential. + * @brief One-purpose firmware for restoring root and admin password credentials. */ #include @@ -31,6 +31,25 @@ #define ROOT_PASSWORD_RESET_RUN_ID "unknown" #endif +typedef struct { + const char *username; + const char *credential_key; + const char *default_password; +} credential_reset_target_t; + +static const credential_reset_target_t RESET_TARGETS[] = { + { + .username = "root", + .credential_key = TS_AUTH_ROOT_CRED_KEY, + .default_password = TS_AUTH_DEFAULT_ROOT_PASSWORD, + }, + { + .username = "admin", + .credential_key = TS_AUTH_ADMIN_CRED_KEY, + .default_password = TS_AUTH_DEFAULT_ADMIN_PASSWORD, + }, +}; + static bool string_contains(const char *haystack, const char *needle) { if (!haystack || !needle) { @@ -54,16 +73,26 @@ static void halt_on_error(const char *operation, esp_err_t ret) halt_forever(); } -static esp_err_t compute_root_hash(const uint8_t salt[TS_AUTH_SALT_LEN], - uint8_t hash_out[TS_AUTH_HASH_LEN]) +static esp_err_t compute_password_hash(const char *password, + const uint8_t salt[TS_AUTH_SALT_LEN], + uint8_t hash_out[TS_AUTH_HASH_LEN]) { - static const char default_password[] = TS_AUTH_DEFAULT_ROOT_PASSWORD; - uint8_t material[TS_AUTH_SALT_LEN + sizeof(default_password) - 1]; + if (!password || !salt || !hash_out) { + return ESP_ERR_INVALID_ARG; + } + + size_t password_len = strlen(password); + if (password_len < 4 || password_len > 64) { + return ESP_ERR_INVALID_ARG; + } + + uint8_t material[TS_AUTH_SALT_LEN + 64] = {0}; + size_t material_len = TS_AUTH_SALT_LEN + password_len; memcpy(material, salt, TS_AUTH_SALT_LEN); - memcpy(material + TS_AUTH_SALT_LEN, default_password, sizeof(default_password) - 1); + memcpy(material + TS_AUTH_SALT_LEN, password, password_len); - int ret = mbedtls_sha256(material, sizeof(material), hash_out, 0); + int ret = mbedtls_sha256(material, material_len, hash_out, 0); memset(material, 0, sizeof(material)); return ret == 0 ? ESP_OK : ESP_FAIL; @@ -101,7 +130,92 @@ static esp_err_t verify_auth_config_version(nvs_handle_t handle) return ESP_OK; } -static void reset_root_credential(void) +static esp_err_t build_default_credential(const credential_reset_target_t *target, + ts_auth_user_credential_t *credential) +{ + if (!target || !credential) { + return ESP_ERR_INVALID_ARG; + } + + memset(credential, 0, sizeof(*credential)); + esp_fill_random(credential->salt, TS_AUTH_SALT_LEN); + + esp_err_t ret = compute_password_hash(target->default_password, + credential->salt, + credential->hash); + if (ret != ESP_OK) { + return ret; + } + + credential->password_changed = false; + credential->failed_attempts = 0; + credential->lockout_until = 0; + + return ESP_OK; +} + +static esp_err_t write_default_credential(nvs_handle_t handle, + const credential_reset_target_t *target) +{ + ts_auth_user_credential_t credential = {0}; + esp_err_t ret = build_default_credential(target, &credential); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to build default credential for %s", target ? target->username : "unknown"); + return ret; + } + + ESP_LOGI(TAG, "Writing NVS blob %s/%s for %s", + TS_AUTH_NVS_NAMESPACE, target->credential_key, target->username); + ret = nvs_set_blob(handle, target->credential_key, &credential, sizeof(credential)); + memset(&credential, 0, sizeof(credential)); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to write %s credential", target->username); + } + return ret; +} + +static esp_err_t verify_default_credential(nvs_handle_t handle, + const credential_reset_target_t *target) +{ + ts_auth_user_credential_t readback = {0}; + size_t readback_len = sizeof(readback); + esp_err_t ret = nvs_get_blob(handle, target->credential_key, &readback, &readback_len); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to read back %s credential", target->username); + return ret; + } + if (readback_len != sizeof(readback)) { + ESP_LOGE(TAG, "%s readback length mismatch: expected %u, got %u", + target->username, (unsigned)sizeof(readback), (unsigned)readback_len); + memset(&readback, 0, sizeof(readback)); + return ESP_ERR_INVALID_SIZE; + } + + uint8_t expected_hash[TS_AUTH_HASH_LEN] = {0}; + ret = compute_password_hash(target->default_password, readback.salt, expected_hash); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to compute expected hash for %s readback", target->username); + memset(&readback, 0, sizeof(readback)); + return ret; + } + + bool hash_ok = constant_time_equal(readback.hash, expected_hash, TS_AUTH_HASH_LEN); + bool state_ok = !readback.password_changed && + readback.failed_attempts == 0 && + readback.lockout_until == 0; + memset(expected_hash, 0, sizeof(expected_hash)); + memset(&readback, 0, sizeof(readback)); + + if (!hash_ok || !state_ok) { + ESP_LOGE(TAG, "%s credential readback verification failed", target->username); + return ESP_FAIL; + } + + ESP_LOGI(TAG, "%s credential readback verified", target->username); + return ESP_OK; +} + +static void reset_auth_credentials(void) { ESP_LOGI(TAG, "Initializing NVS"); esp_err_t ret = nvs_flash_init(); @@ -125,24 +239,12 @@ static void reset_root_credential(void) goto fail; } - ts_auth_user_credential_t credential = {0}; - esp_fill_random(credential.salt, TS_AUTH_SALT_LEN); - - ret = compute_root_hash(credential.salt, credential.hash); - if (ret != ESP_OK) { - failed_operation = "compute_root_hash"; - goto fail; - } - - credential.password_changed = false; - credential.failed_attempts = 0; - credential.lockout_until = 0; - - ESP_LOGI(TAG, "Writing NVS blob %s/%s", TS_AUTH_NVS_NAMESPACE, TS_AUTH_ROOT_CRED_KEY); - ret = nvs_set_blob(handle, TS_AUTH_ROOT_CRED_KEY, &credential, sizeof(credential)); - if (ret != ESP_OK) { - failed_operation = "nvs_set_blob(cred_root)"; - goto fail; + for (size_t i = 0; i < sizeof(RESET_TARGETS) / sizeof(RESET_TARGETS[0]); ++i) { + ret = write_default_credential(handle, &RESET_TARGETS[i]); + if (ret != ESP_OK) { + failed_operation = "write_default_credential"; + goto fail; + } } ret = nvs_commit(handle); @@ -150,40 +252,20 @@ static void reset_root_credential(void) failed_operation = "nvs_commit"; goto fail; } - ESP_LOGI(TAG, "NVS commit complete"); + ESP_LOGI(TAG, "NVS commit complete for root/admin credentials"); - ts_auth_user_credential_t readback = {0}; - size_t readback_len = sizeof(readback); - ret = nvs_get_blob(handle, TS_AUTH_ROOT_CRED_KEY, &readback, &readback_len); - nvs_close(handle); - handle_open = false; - if (ret != ESP_OK) { - halt_on_error("nvs_get_blob(cred_root)", ret); - } - if (readback_len != sizeof(readback)) { - ESP_LOGE(TAG, "Readback length mismatch: expected %u, got %u", - (unsigned)sizeof(readback), (unsigned)readback_len); - halt_forever(); - } - - uint8_t expected_hash[TS_AUTH_HASH_LEN] = {0}; - ret = compute_root_hash(readback.salt, expected_hash); - if (ret != ESP_OK) { - halt_on_error("compute_root_hash(readback)", ret); + for (size_t i = 0; i < sizeof(RESET_TARGETS) / sizeof(RESET_TARGETS[0]); ++i) { + ret = verify_default_credential(handle, &RESET_TARGETS[i]); + if (ret != ESP_OK) { + failed_operation = "verify_default_credential"; + goto fail; + } } - bool hash_ok = constant_time_equal(readback.hash, expected_hash, TS_AUTH_HASH_LEN); - memset(expected_hash, 0, sizeof(expected_hash)); - - if (!hash_ok || - readback.password_changed || - readback.failed_attempts != 0 || - readback.lockout_until != 0) { - ESP_LOGE(TAG, "Readback verification failed"); - halt_forever(); - } + nvs_close(handle); + handle_open = false; - ESP_LOGI(TAG, "Root credential restored to default password"); + ESP_LOGI(TAG, "Root and admin credentials restored to default password"); return; fail: @@ -275,11 +357,11 @@ static void switch_back_to_normal_firmware(void) void app_main(void) { - ESP_LOGI(TAG, "TianShanOS root password reset firmware"); + ESP_LOGI(TAG, "TianShanOS root/admin password reset firmware"); ESP_LOGI(TAG, "Version: %s", TIANSHAN_OS_VERSION_FULL); ESP_LOGI(TAG, "Run ID: %s", ROOT_PASSWORD_RESET_RUN_ID); - ESP_LOGW(TAG, "Only root credential will be modified; NVS erase is never performed."); + ESP_LOGW(TAG, "Only root/admin credentials will be modified; NVS erase is never performed."); - reset_root_credential(); + reset_auth_credentials(); switch_back_to_normal_firmware(); } diff --git a/tools/build_root_reset.sh b/tools/build_root_reset.sh index 0c76ff9..400db02 100755 --- a/tools/build_root_reset.sh +++ b/tools/build_root_reset.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Build the temporary root password reset firmware. +# Build the temporary root/admin password reset firmware. set -e @@ -22,7 +22,7 @@ fi GIT_SHORT_SHA="$(git rev-parse --short=6 HEAD 2>/dev/null || echo nogit)" RUN_ID="$(date -u +%H%M%S).${GIT_SHORT_SHA}" -echo "构建 root 密码恢复固件..." +echo "构建 root/admin 密码恢复固件..." echo "Run ID: ${RUN_ID}" if [ -n "$ROOT_RESET_VERSION" ]; then echo "Version override: ${ROOT_RESET_VERSION}" @@ -105,7 +105,7 @@ PY cp "$BUILD_DIR/TianShanOS.bin" "$BUILD_DIR/TianShanOS-RootReset.bin" echo "" -echo "恢复固件已生成:" +echo "root/admin 密码恢复固件已生成:" echo " $BUILD_DIR/TianShanOS.bin" echo " $BUILD_DIR/TianShanOS-RootReset.bin" echo ""