Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion components/ts_api/src/ts_api.c
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ static const char *s_code_names[] = {
[TS_API_ERR_NO_MEM] = "NO_MEM",
[TS_API_ERR_INTERNAL] = "INTERNAL",
[TS_API_ERR_NOT_SUPPORTED] = "NOT_SUPPORTED",
[TS_API_ERR_HARDWARE] = "HARDWARE"
[TS_API_ERR_HARDWARE] = "HARDWARE",
[TS_API_ERR_CONNECTION] = "CONNECTION",
[TS_API_ERR_AUTH] = "AUTH"
};

static const char *s_category_names[] = {
Expand Down
118 changes: 117 additions & 1 deletion components/ts_api/src/ts_api_auth.c
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* @file ts_ap
* @brief Authentication API and
* 提供 auth.login / auth.lout / auth.status / auth.change_password API
* 提供 auth.login / auth.logout / auth.status / auth.change_password API
*
* @author TianShanOS Team
* @version 1.0.0
Expand Down Expand Up @@ -32,6 +32,35 @@ static const char *perm_level_to_string(ts_perm_level_t level)
}
}

static esp_err_t validate_root_token(const cJSON *token_item, ts_api_result_t *result)
{
if (!cJSON_IsString(token_item)) {
ts_api_result_error(result, TS_API_ERR_AUTH, "Missing token");
return ESP_ERR_INVALID_ARG;
}

uint32_t session_id;
esp_err_t ret = ts_security_validate_token(token_item->valuestring, &session_id);
if (ret != ESP_OK) {
ts_api_result_error(result, TS_API_ERR_AUTH, "Invalid or expired token");
return ret;
}

ts_session_t session;
ret = ts_security_validate_session(session_id, &session);
if (ret != ESP_OK) {
ts_api_result_error(result, TS_API_ERR_AUTH, "Session expired");
return ret;
}

if (session.level != TS_PERM_ROOT) {
ts_api_result_error(result, TS_API_ERR_NO_PERMISSION, "Root permission required");
return ESP_ERR_NOT_ALLOWED;
}

return ESP_OK;
}

/*===========================================================================*/
/* API Handlers */
/*===========================================================================*/
Expand Down Expand Up @@ -255,6 +284,79 @@ static esp_err_t api_auth_change_password(const cJSON *params, ts_api_result_t *
return ESP_OK;
}

/**
* @brief auth.admin.set_password - Root sets admin password
*
* Params: { "token": "...", "new_password": "..." }
* Returns: { "success": true, "username": "admin", "password_changed": true }
*/
static esp_err_t api_auth_admin_set_password(const cJSON *params, ts_api_result_t *result)
{
const cJSON *token_item = cJSON_GetObjectItem(params, "token");
const cJSON *new_pwd_item = cJSON_GetObjectItem(params, "new_password");

esp_err_t ret = validate_root_token(token_item, result);
if (ret != ESP_OK) return ret;

if (!cJSON_IsString(new_pwd_item)) {
ts_api_result_error(result, TS_API_ERR_INVALID_ARG,
"Missing required parameter: new_password");
return ESP_ERR_INVALID_ARG;
}

const char *new_password = new_pwd_item->valuestring;
size_t len = strlen(new_password);
if (len < 4 || len > 64) {
ts_api_result_error(result, TS_API_ERR_INVALID_ARG,
"Password must be 4-64 characters");
return ESP_ERR_INVALID_ARG;
}

ret = ts_auth_set_admin_password(new_password);
if (ret != ESP_OK) {
ts_api_result_error(result, TS_API_ERR_INTERNAL, "Failed to set admin password");
return ret;
}

cJSON *data = cJSON_CreateObject();
cJSON_AddBoolToObject(data, "success", true);
cJSON_AddStringToObject(data, "username", "admin");
cJSON_AddBoolToObject(data, "password_changed", true);

ts_api_result_ok(result, data);
TS_LOGI(TAG, "Root set admin password");
return ESP_OK;
}

/**
* @brief auth.admin.reset_password - Root resets admin password to default
*
* Params: { "token": "..." }
* Returns: { "success": true, "username": "admin", "password_changed": false }
*/
static esp_err_t api_auth_admin_reset_password(const cJSON *params, ts_api_result_t *result)
{
const cJSON *token_item = cJSON_GetObjectItem(params, "token");

esp_err_t ret = validate_root_token(token_item, result);
if (ret != ESP_OK) return ret;

ret = ts_auth_reset_password("admin");
if (ret != ESP_OK) {
ts_api_result_error(result, TS_API_ERR_INTERNAL, "Failed to reset admin password");
return ret;
}

cJSON *data = cJSON_CreateObject();
cJSON_AddBoolToObject(data, "success", true);
cJSON_AddStringToObject(data, "username", "admin");
cJSON_AddBoolToObject(data, "password_changed", false);

ts_api_result_ok(result, data);
TS_LOGI(TAG, "Root reset admin password to default");
return ESP_OK;
}

/*===========================================================================*/
/* Registration */
/*===========================================================================*/
Expand Down Expand Up @@ -288,6 +390,20 @@ static const ts_api_endpoint_t s_auth_endpoints[] = {
.handler = api_auth_change_password,
.requires_auth = false, /* 密码修改使用 token 验证 */
},
{
.name = "auth.admin.set_password",
.description = "Root sets admin password",
.category = TS_API_CAT_SECURITY,
.handler = api_auth_admin_set_password,
.requires_auth = false, /* Handler validates root token */
},
{
.name = "auth.admin.reset_password",
.description = "Root resets admin password to default",
.category = TS_API_CAT_SECURITY,
.handler = api_auth_admin_reset_password,
.requires_auth = false, /* Handler validates root token */
},
};

esp_err_t ts_api_auth_register(void)
Expand Down
7 changes: 7 additions & 0 deletions components/ts_security/include/ts_security.h
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,13 @@ esp_err_t ts_auth_verify_password(const char *username, const char *password,
esp_err_t ts_auth_change_password(const char *username, const char *old_password,
const char *new_password);

/**
* @brief Set admin password without requiring the old admin password
* @param new_password New admin password (4-64 chars)
* @return ESP_OK on success
*/
esp_err_t ts_auth_set_admin_password(const char *new_password);

/**
* @brief Check if user has changed the default password
*/
Expand Down
92 changes: 46 additions & 46 deletions components/ts_security/src/ts_auth.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*
* 安全设计:
* - 密码哈希仅存储在 NVS,不导出到 SD 卡
* - 忘记密码只能通过 idf.py erase-flash 恢复出厂
* - admin 密码可由 root 会话恢复,root 密码遗失需恢复出厂
*/

#include "ts_security.h"
Expand Down Expand Up @@ -99,30 +99,45 @@ static esp_err_t save_user_credential(const char *username, const user_credentia
}

/**
* @brief 强制重新创建用户凭据
* @brief 写入用户密码凭据,并清除失败计数/锁定状态
*/
static esp_err_t force_create_user(const char *username, ts_perm_level_t level)
static esp_err_t write_user_password_credential(const char *username, const char *password,
bool password_changed)
{
user_credential_t cred;

/* 根据用户类型使用不同默认密码 */
const char *default_pwd = (level == TS_PERM_ROOT) ? DEFAULT_PASSWORD_ROOT : DEFAULT_PASSWORD_ADMIN;
TS_LOGI(TAG, "Creating/resetting user '%s'", username);

/* 生成随机 salt */
if (!username || !password) return ESP_ERR_INVALID_ARG;

size_t pwd_len = strlen(password);
if (pwd_len < 4 || pwd_len > 64) {
TS_LOGW(TAG, "Password length invalid (4-64 chars required)");
return ESP_ERR_INVALID_ARG;
}

user_credential_t cred = {0};

esp_fill_random(cred.salt, SALT_LEN);

/* 计算默认密码哈希 */
esp_err_t ret = compute_password_hash(cred.salt, default_pwd, cred.hash);

esp_err_t ret = compute_password_hash(cred.salt, password, cred.hash);
if (ret != ESP_OK) return ret;
cred.password_changed = false;

cred.password_changed = password_changed;
cred.failed_attempts = 0;
cred.lockout_until = 0;

return save_user_credential(username, &cred);
}

/**
* @brief 强制重新创建用户凭据
*/
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;
TS_LOGI(TAG, "Creating/resetting user '%s'", username);

return write_user_password_credential(username, default_pwd, false);
}

/**
* @brief 初始化用户(如果不存在则创建默认密码)
*/
Expand Down Expand Up @@ -279,29 +294,27 @@ esp_err_t ts_auth_change_password(const char *username, const char *old_password
return ret;
}

/* 加载凭据 */
user_credential_t cred;
ret = load_user_credential(username, &cred);
if (ret != ESP_OK) return ret;

/* 生成新 salt */
esp_fill_random(cred.salt, SALT_LEN);

/* 计算新密码哈希 */
ret = compute_password_hash(cred.salt, new_password, cred.hash);
if (ret != ESP_OK) return ret;

cred.password_changed = true;
cred.failed_attempts = 0;

ret = save_user_credential(username, &cred);
ret = write_user_password_credential(username, new_password, true);
if (ret == ESP_OK) {
TS_LOGI(TAG, "Password changed for user %s", username);
}

return ret;
}

/**
* @brief Root 管理功能:设置 admin 密码
*/
esp_err_t ts_auth_set_admin_password(const char *new_password)
{
esp_err_t ret = write_user_password_credential("admin", new_password, true);
if (ret == ESP_OK) {
TS_LOGI(TAG, "Password set for admin by root");
}

return ret;
}

/**
* @brief 检查用户是否已修改初始密码
*/
Expand Down Expand Up @@ -399,20 +412,7 @@ esp_err_t ts_auth_reset_password(const char *username)
return ESP_ERR_NOT_FOUND;
}

user_credential_t cred;

/* 生成新 salt */
esp_fill_random(cred.salt, SALT_LEN);

/* 使用默认密码 */
esp_err_t ret = compute_password_hash(cred.salt, default_pwd, cred.hash);
if (ret != ESP_OK) return ret;

cred.password_changed = false;
cred.failed_attempts = 0;
cred.lockout_until = 0;

ret = save_user_credential(username, &cred);
esp_err_t ret = write_user_password_credential(username, default_pwd, false);
if (ret == ESP_OK) {
TS_LOGI(TAG, "Password reset to default for user %s", username);
}
Expand Down
Loading
Loading