/* SPDX-License-Identifier: GPL-3.0-only */ /* Bounded persistent role-based user and SSH authorized-key database. */ #include "user_database.h" #include #include #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" #include "mbedtls/ecp.h" #include "mbedtls/pkcs5.h" #include "mbedtls/sha256.h" #include "nvs.h" #include "nvs_flash.h" #include "secure_random.h" #define USER_DATABASE_SCHEMA_VERSION 1U #define USER_DATABASE_PASSWORD_SALT_LENGTH 16U #define USER_DATABASE_PASSWORD_HASH_LENGTH 32U #define USER_DATABASE_PBKDF2_ITERATIONS 50000U #define USER_DATABASE_PBKDF2_MIN_ITERATIONS 50000U #define USER_DATABASE_PBKDF2_MAX_ITERATIONS 1000000U static const uint8_t s_generated_alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; static const uint8_t s_admin_username[] = "admin"; static const uint8_t s_ed25519_type[] = "ssh-ed25519"; static const uint8_t s_ecdsa_type[] = "ecdsa-sha2-nistp256"; static const uint8_t s_ecdsa_curve[] = "nistp256"; static const uint8_t s_dummy_password[] = "invalid-pass"; typedef struct { uint8_t active; uint8_t type_length; uint16_t blob_length; uint8_t type[USER_DATABASE_SSH_KEY_TYPE_CAPACITY]; uint8_t blob[USER_DATABASE_SSH_KEY_BLOB_CAPACITY]; uint8_t fingerprint[USER_DATABASE_SHA256_LENGTH]; } stored_key_t; typedef struct { uint8_t active; uint8_t role; uint8_t username_length; uint8_t key_count; uint32_t user_id; uint32_t auth_generation; uint32_t password_iterations; uint8_t username[USER_DATABASE_USERNAME_CAPACITY]; uint8_t password_salt[USER_DATABASE_PASSWORD_SALT_LENGTH]; uint8_t password_hash[USER_DATABASE_PASSWORD_HASH_LENGTH]; stored_key_t keys[USER_DATABASE_MAX_SSH_KEYS_PER_USER]; } stored_user_t; typedef struct { uint32_t version; uint32_t size; uint32_t generation; uint8_t admin_bootstrapped; uint8_t user_count; uint8_t admin_count; uint8_t reserved; stored_user_t users[USER_DATABASE_MAX_USERS]; } stored_database_t; static SemaphoreHandle_t s_mutex; static stored_database_t s_database; static stored_database_t s_candidate; static bool s_initialized; static uint8_t s_dummy_salt[USER_DATABASE_PASSWORD_SALT_LENGTH]; static uint8_t s_dummy_hash[USER_DATABASE_PASSWORD_HASH_LENGTH]; static esp_err_t initialize_dummy_verifier(void) { esp_err_t error = secure_random_fill(s_dummy_salt, sizeof(s_dummy_salt)); if (error == ESP_OK) { error = secure_random_fill(s_dummy_hash, sizeof(s_dummy_hash)); } return error; } static void discard_candidate(void) { secure_wipe(&s_candidate, sizeof(s_candidate)); } static bool constant_time_equal(const uint8_t *left, const uint8_t *right, size_t length) { uint8_t difference = 0U; for (size_t index = 0U; index < length; ++index) { difference |= left[index] ^ right[index]; } return difference == 0U; } static bool all_zero(const void *data, size_t length) { const uint8_t *bytes = data; uint8_t aggregate = 0U; for (size_t index = 0U; index < length; ++index) { aggregate |= bytes[index]; } return aggregate == 0U; } bool user_database_username_valid(const uint8_t *username, size_t length) { if (username == NULL || length == 0U || length > USER_DATABASE_USERNAME_CAPACITY || username[0] < 'a' || username[0] > 'z') { return false; } for (size_t index = 1U; index < length; ++index) { uint8_t value = username[index]; if (!((value >= 'a' && value <= 'z') || (value >= '0' && value <= '9') || value == '_' || value == '-')) { return false; } } return true; } bool user_database_password_valid(const uint8_t *password, size_t length) { if (password == NULL || length < USER_DATABASE_PASSWORD_MIN_LENGTH || length > USER_DATABASE_PASSWORD_CAPACITY) { return false; } for (size_t index = 0U; index < length; ++index) { if (password[index] < 0x20U || password[index] > 0x7eU) { return false; } } return true; } static bool read_ssh_string(const uint8_t *blob, size_t blob_length, size_t *offset, const uint8_t **value, size_t *value_length) { if (*offset > blob_length || blob_length - *offset < 4U) { return false; } size_t position = *offset; uint32_t length = ((uint32_t)blob[position] << 24U) | ((uint32_t)blob[position + 1U] << 16U) | ((uint32_t)blob[position + 2U] << 8U) | blob[position + 3U]; position += 4U; if (length > blob_length - position) { return false; } *value = blob + position; *value_length = length; *offset = position + length; return true; } bool user_database_key_valid(const uint8_t *key_type, size_t key_type_length, const uint8_t *key_blob, size_t key_blob_length) { if (key_type == NULL || key_blob == NULL || key_type_length == 0U || key_type_length > USER_DATABASE_SSH_KEY_TYPE_CAPACITY || key_blob_length == 0U || key_blob_length > USER_DATABASE_SSH_KEY_BLOB_CAPACITY) { return false; } size_t offset = 0U; const uint8_t *embedded_type = NULL; size_t embedded_type_length = 0U; if (!read_ssh_string(key_blob, key_blob_length, &offset, &embedded_type, &embedded_type_length) || embedded_type_length != key_type_length || memcmp(embedded_type, key_type, key_type_length) != 0) { return false; } if (key_type_length == sizeof(s_ed25519_type) - 1U && memcmp(key_type, s_ed25519_type, sizeof(s_ed25519_type) - 1U) == 0) { const uint8_t *public_key = NULL; size_t public_key_length = 0U; return read_ssh_string(key_blob, key_blob_length, &offset, &public_key, &public_key_length) && public_key_length == 32U && offset == key_blob_length; } if (key_type_length == sizeof(s_ecdsa_type) - 1U && memcmp(key_type, s_ecdsa_type, sizeof(s_ecdsa_type) - 1U) == 0) { const uint8_t *curve = NULL; const uint8_t *point = NULL; size_t curve_length = 0U; size_t point_length = 0U; bool structure_valid = read_ssh_string(key_blob, key_blob_length, &offset, &curve, &curve_length) && curve_length == sizeof(s_ecdsa_curve) - 1U && memcmp(curve, s_ecdsa_curve, sizeof(s_ecdsa_curve) - 1U) == 0 && read_ssh_string(key_blob, key_blob_length, &offset, &point, &point_length) && point_length == 65U && point[0] == 0x04U && offset == key_blob_length; if (!structure_valid) { return false; } mbedtls_ecp_group group; mbedtls_ecp_point public_point; mbedtls_ecp_group_init(&group); mbedtls_ecp_point_init(&public_point); int result = mbedtls_ecp_group_load(&group, MBEDTLS_ECP_DP_SECP256R1); if (result == 0) { result = mbedtls_ecp_point_read_binary(&group, &public_point, point, point_length); } if (result == 0) { result = mbedtls_ecp_check_pubkey(&group, &public_point); } mbedtls_ecp_point_free(&public_point); mbedtls_ecp_group_free(&group); return result == 0; } return false; } const char *user_role_to_string(user_role_t role) { return role == USER_ROLE_ADMIN ? "admin" : role == USER_ROLE_USER ? "user" : "unknown"; } bool user_role_parse(const char *text, user_role_t *role) { if (text == NULL || role == NULL) { return false; } if (strcmp(text, "user") == 0) { *role = USER_ROLE_USER; return true; } if (strcmp(text, "admin") == 0) { *role = USER_ROLE_ADMIN; return true; } return false; } static esp_err_t derive_password(const uint8_t *password, size_t password_length, const uint8_t salt[USER_DATABASE_PASSWORD_SALT_LENGTH], uint32_t iterations, uint8_t output[USER_DATABASE_PASSWORD_HASH_LENGTH]) { int result = mbedtls_pkcs5_pbkdf2_hmac_ext( MBEDTLS_MD_SHA256, password, password_length, salt, USER_DATABASE_PASSWORD_SALT_LENGTH, iterations, USER_DATABASE_PASSWORD_HASH_LENGTH, output); return result == 0 ? ESP_OK : ESP_FAIL; } static esp_err_t set_record_password(stored_user_t *user, const uint8_t *password, size_t password_length) { if (!user_database_password_valid(password, password_length)) { return ESP_ERR_INVALID_ARG; } uint8_t salt[USER_DATABASE_PASSWORD_SALT_LENGTH] = {0}; uint8_t hash[USER_DATABASE_PASSWORD_HASH_LENGTH] = {0}; esp_err_t error = secure_random_fill(salt, sizeof(salt)); if (error == ESP_OK) { error = derive_password(password, password_length, salt, USER_DATABASE_PBKDF2_ITERATIONS, hash); } if (error == ESP_OK) { memcpy(user->password_salt, salt, sizeof(salt)); memcpy(user->password_hash, hash, sizeof(hash)); user->password_iterations = USER_DATABASE_PBKDF2_ITERATIONS; } secure_wipe(salt, sizeof(salt)); secure_wipe(hash, sizeof(hash)); return error; } static esp_err_t generate_password(user_database_generated_password_t *generated) { if (generated == NULL) { return ESP_ERR_INVALID_ARG; } memset(generated, 0, sizeof(*generated)); uint8_t random[USER_DATABASE_GENERATED_PASSWORD_LENGTH] = {0}; esp_err_t error = secure_random_fill(random, sizeof(random)); if (error == ESP_OK) { _Static_assert(sizeof(s_generated_alphabet) - 1U == 64U, "generated password alphabet must contain 64 symbols"); for (size_t index = 0U; index < sizeof(random); ++index) { generated->password[index] = s_generated_alphabet[random[index] & 0x3fU]; } generated->password_length = sizeof(random); } secure_wipe(random, sizeof(random)); return error; } static int find_user(const stored_database_t *database, const uint8_t *username, size_t username_length) { for (size_t index = 0U; index < USER_DATABASE_MAX_USERS; ++index) { const stored_user_t *user = &database->users[index]; if (user->active != 0U && user->username_length == username_length && memcmp(user->username, username, username_length) == 0) { return (int)index; } } return -1; } static int find_free_user(const stored_database_t *database) { for (size_t index = 0U; index < USER_DATABASE_MAX_USERS; ++index) { if (database->users[index].active == 0U) { return (int)index; } } return -1; } static bool stored_keys_equal(const stored_key_t *left, const stored_key_t *right) { return left->active != 0U && right->active != 0U && left->type_length == right->type_length && left->blob_length == right->blob_length && memcmp(left->type, right->type, left->type_length) == 0 && constant_time_equal(left->blob, right->blob, left->blob_length); } static esp_err_t validate_database(const stored_database_t *database) { if (database->version != USER_DATABASE_SCHEMA_VERSION || database->size != sizeof(*database) || database->generation == 0U || database->admin_bootstrapped > 1U || database->reserved != 0U) { return ESP_ERR_INVALID_VERSION; } uint8_t users = 0U; uint8_t admins = 0U; for (size_t index = 0U; index < USER_DATABASE_MAX_USERS; ++index) { const stored_user_t *user = &database->users[index]; if (user->active == 0U) { if (!all_zero(user, sizeof(*user))) { return ESP_ERR_INVALID_RESPONSE; } continue; } ++users; if (user->active != 1U || (user->role != USER_ROLE_USER && user->role != USER_ROLE_ADMIN) || !user_database_username_valid(user->username, user->username_length) || user->user_id == 0U || user->auth_generation == 0U || user->password_iterations < USER_DATABASE_PBKDF2_MIN_ITERATIONS || user->password_iterations > USER_DATABASE_PBKDF2_MAX_ITERATIONS || user->key_count > USER_DATABASE_MAX_SSH_KEYS_PER_USER || !all_zero(user->username + user->username_length, sizeof(user->username) - user->username_length)) { return ESP_ERR_INVALID_RESPONSE; } if (user->role == USER_ROLE_ADMIN) { ++admins; } for (size_t other = index + 1U; other < USER_DATABASE_MAX_USERS; ++other) { const stored_user_t *candidate = &database->users[other]; if (candidate->active != 0U && (candidate->user_id == user->user_id || (candidate->username_length == user->username_length && memcmp(candidate->username, user->username, user->username_length) == 0))) { return ESP_ERR_INVALID_RESPONSE; } } uint8_t keys = 0U; for (size_t key_index = 0U; key_index < USER_DATABASE_MAX_SSH_KEYS_PER_USER; ++key_index) { const stored_key_t *key = &user->keys[key_index]; if (key->active == 0U) { if (!all_zero(key, sizeof(*key))) { return ESP_ERR_INVALID_RESPONSE; } continue; } ++keys; if (key->active != 1U || !user_database_key_valid(key->type, key->type_length, key->blob, key->blob_length) || !all_zero(key->type + key->type_length, sizeof(key->type) - key->type_length) || !all_zero(key->blob + key->blob_length, sizeof(key->blob) - key->blob_length)) { return ESP_ERR_INVALID_RESPONSE; } for (size_t prior_user = 0U; prior_user <= index; ++prior_user) { size_t prior_key_limit = prior_user == index ? key_index : USER_DATABASE_MAX_SSH_KEYS_PER_USER; for (size_t prior_key = 0U; prior_key < prior_key_limit; ++prior_key) { if (stored_keys_equal(key, &database->users[prior_user].keys[prior_key])) { return ESP_ERR_INVALID_RESPONSE; } } } uint8_t fingerprint[USER_DATABASE_SHA256_LENGTH]; if (mbedtls_sha256(key->blob, key->blob_length, fingerprint, 0) != 0 || !constant_time_equal(fingerprint, key->fingerprint, sizeof(fingerprint))) { secure_wipe(fingerprint, sizeof(fingerprint)); return ESP_ERR_INVALID_RESPONSE; } secure_wipe(fingerprint, sizeof(fingerprint)); } if (keys != user->key_count) { return ESP_ERR_INVALID_RESPONSE; } } if (users != database->user_count || admins != database->admin_count || (database->admin_bootstrapped != 0U) != (admins > 0U)) { return ESP_ERR_INVALID_RESPONSE; } return ESP_OK; } static void recount(stored_database_t *database) { database->user_count = 0U; database->admin_count = 0U; for (size_t index = 0U; index < USER_DATABASE_MAX_USERS; ++index) { if (database->users[index].active != 0U) { ++database->user_count; if (database->users[index].role == USER_ROLE_ADMIN) { ++database->admin_count; } } } } static esp_err_t next_generation(uint32_t *generation) { if (*generation == UINT32_MAX) { return ESP_ERR_INVALID_STATE; } ++*generation; return ESP_OK; } static esp_err_t commit_candidate_locked(void) { recount(&s_candidate); esp_err_t error = next_generation(&s_candidate.generation); if (error == ESP_OK) { error = validate_database(&s_candidate); } if (error != ESP_OK) { discard_candidate(); return error; } nvs_handle_t handle; error = nvs_open(USER_DATABASE_NVS_NAMESPACE, NVS_READWRITE, &handle); if (error == ESP_OK) { error = nvs_set_blob(handle, USER_DATABASE_NVS_BLOB_KEY, &s_candidate, sizeof(s_candidate)); if (error == ESP_OK) { error = nvs_commit(handle); } nvs_close(handle); } if (error == ESP_OK) { secure_wipe(&s_database, sizeof(s_database)); s_database = s_candidate; } discard_candidate(); return error; } static esp_err_t initialize_user(stored_user_t *user, const uint8_t *username, size_t username_length, user_role_t role, const uint8_t *password, size_t password_length) { memset(user, 0, sizeof(*user)); if (!user_database_username_valid(username, username_length) || (role != USER_ROLE_USER && role != USER_ROLE_ADMIN)) { return ESP_ERR_INVALID_ARG; } uint32_t user_id = 0U; esp_err_t error = secure_random_fill(&user_id, sizeof(user_id)); if (error != ESP_OK) { return error; } if (user_id == 0U) { user_id = 1U; } user->active = 1U; user->role = (uint8_t)role; user->username_length = (uint8_t)username_length; user->user_id = user_id; user->auth_generation = 1U; memcpy(user->username, username, username_length); error = set_record_password(user, password, password_length); if (error != ESP_OK) { secure_wipe(user, sizeof(*user)); } return error; } static bool legacy_credentials_valid(const user_database_legacy_credentials_t *legacy) { return legacy != NULL && legacy->username != NULL && legacy->password != NULL && user_database_username_valid(legacy->username, legacy->username_length) && user_database_password_valid(legacy->password, legacy->password_length); } static esp_err_t synchronize_legacy_locked( const user_database_legacy_credentials_t *legacy, bool *synchronized) { *synchronized = false; if (s_database.admin_bootstrapped != 0U) { return ESP_OK; } int index = find_user(&s_database, legacy->username, legacy->username_length); if (index < 0) { return ESP_OK; } const stored_user_t *stored = &s_database.users[index]; uint8_t derived[USER_DATABASE_PASSWORD_HASH_LENGTH] = {0}; esp_err_t error = derive_password(legacy->password, legacy->password_length, stored->password_salt, stored->password_iterations, derived); bool already_current = error == ESP_OK && constant_time_equal(derived, stored->password_hash, sizeof(derived)); secure_wipe(derived, sizeof(derived)); if (error != ESP_OK || already_current) { *synchronized = already_current; return error; } s_candidate = s_database; stored_user_t *candidate_user = &s_candidate.users[index]; error = set_record_password(candidate_user, legacy->password, legacy->password_length); if (error == ESP_OK) { error = next_generation(&candidate_user->auth_generation); } if (error == ESP_OK) { error = commit_candidate_locked(); } else { discard_candidate(); } *synchronized = error == ESP_OK; return error; } esp_err_t user_database_init(const user_database_legacy_credentials_t *legacy, user_database_load_result_t *load_result) { if (load_result == NULL || s_mutex != NULL) { return ESP_ERR_INVALID_ARG; } *load_result = USER_DATABASE_LOAD_EMPTY; s_mutex = xSemaphoreCreateMutex(); if (s_mutex == NULL) { return ESP_ERR_NO_MEM; } esp_err_t error = nvs_flash_init(); if (error != ESP_OK) { vSemaphoreDelete(s_mutex); s_mutex = NULL; return error; } bool storage_missing = false; nvs_handle_t handle; error = nvs_open(USER_DATABASE_NVS_NAMESPACE, NVS_READONLY, &handle); if (error == ESP_OK) { size_t size = 0U; error = nvs_get_blob(handle, USER_DATABASE_NVS_BLOB_KEY, NULL, &size); if (error == ESP_ERR_NVS_NOT_FOUND) { storage_missing = true; error = ESP_OK; } else if (error == ESP_OK && size == sizeof(s_database)) { error = nvs_get_blob(handle, USER_DATABASE_NVS_BLOB_KEY, &s_database, &size); } else if (error == ESP_OK) { error = ESP_ERR_INVALID_VERSION; } nvs_close(handle); if (error == ESP_OK && !storage_missing) { error = validate_database(&s_database); } if (error == ESP_OK && !storage_missing && legacy_credentials_valid(legacy)) { bool synchronized = false; error = synchronize_legacy_locked(legacy, &synchronized); } if (error == ESP_OK && !storage_missing) { error = initialize_dummy_verifier(); if (error == ESP_OK) { s_initialized = true; *load_result = USER_DATABASE_LOAD_STORED; return ESP_OK; } } if (error != ESP_OK) { goto init_failed; } } else if (error == ESP_ERR_NVS_NOT_FOUND) { storage_missing = true; } else { goto init_failed; } if (!storage_missing || !legacy_credentials_valid(legacy)) { error = ESP_ERR_INVALID_STATE; goto init_failed; } memset(&s_database, 0, sizeof(s_database)); s_database.version = USER_DATABASE_SCHEMA_VERSION; s_database.size = sizeof(s_database); s_database.generation = 1U; error = initialize_user(&s_database.users[0], legacy->username, legacy->username_length, USER_ROLE_USER, legacy->password, legacy->password_length); if (error != ESP_OK) { goto init_failed; } *load_result = USER_DATABASE_LOAD_MIGRATED_LEGACY; recount(&s_database); s_candidate = s_database; error = commit_candidate_locked(); if (error != ESP_OK) { goto init_failed; } error = initialize_dummy_verifier(); if (error != ESP_OK) { goto init_failed; } s_initialized = true; return ESP_OK; init_failed: secure_wipe(&s_database, sizeof(s_database)); discard_candidate(); secure_wipe(s_dummy_salt, sizeof(s_dummy_salt)); secure_wipe(s_dummy_hash, sizeof(s_dummy_hash)); vSemaphoreDelete(s_mutex); s_mutex = NULL; return error; } esp_err_t user_database_sync_legacy_credentials( const user_database_legacy_credentials_t *legacy, bool *synchronized) { if (synchronized == NULL || !legacy_credentials_valid(legacy)) { return ESP_ERR_INVALID_ARG; } *synchronized = false; if (!s_initialized || s_mutex == NULL) { return ESP_ERR_INVALID_STATE; } xSemaphoreTake(s_mutex, portMAX_DELAY); esp_err_t error = synchronize_legacy_locked(legacy, synchronized); xSemaphoreGive(s_mutex); return error; } esp_err_t user_database_recover_from_legacy( const user_database_legacy_credentials_t *legacy) { if (!legacy_credentials_valid(legacy)) { return ESP_ERR_INVALID_ARG; } if (s_initialized || s_mutex != NULL) { return ESP_ERR_INVALID_STATE; } s_mutex = xSemaphoreCreateMutex(); if (s_mutex == NULL) { return ESP_ERR_NO_MEM; } esp_err_t error = nvs_flash_init(); if (error != ESP_OK) { goto recovery_failed; } memset(&s_database, 0, sizeof(s_database)); memset(&s_candidate, 0, sizeof(s_candidate)); s_candidate.version = USER_DATABASE_SCHEMA_VERSION; s_candidate.size = sizeof(s_candidate); s_candidate.generation = 1U; error = initialize_user(&s_candidate.users[0], legacy->username, legacy->username_length, USER_ROLE_USER, legacy->password, legacy->password_length); if (error == ESP_OK) { error = commit_candidate_locked(); } else { discard_candidate(); } if (error == ESP_OK) { error = initialize_dummy_verifier(); } if (error == ESP_OK) { s_initialized = true; return ESP_OK; } recovery_failed: secure_wipe(&s_database, sizeof(s_database)); discard_candidate(); secure_wipe(s_dummy_salt, sizeof(s_dummy_salt)); secure_wipe(s_dummy_hash, sizeof(s_dummy_hash)); vSemaphoreDelete(s_mutex); s_mutex = NULL; return error; } esp_err_t user_database_get_snapshot(user_database_snapshot_t *snapshot) { if (snapshot == NULL) { return ESP_ERR_INVALID_ARG; } if (!s_initialized || s_mutex == NULL) { return ESP_ERR_INVALID_STATE; } memset(snapshot, 0, sizeof(*snapshot)); xSemaphoreTake(s_mutex, portMAX_DELAY); snapshot->initialized = true; snapshot->admin_bootstrapped = s_database.admin_bootstrapped != 0U; snapshot->generation = s_database.generation; snapshot->user_count = s_database.user_count; snapshot->admin_count = s_database.admin_count; for (size_t index = 0U; index < USER_DATABASE_MAX_USERS; ++index) { const stored_user_t *stored = &s_database.users[index]; user_database_user_snapshot_t *user = &snapshot->users[index]; if (stored->active == 0U) { continue; } user->active = true; user->user_id = stored->user_id; user->auth_generation = stored->auth_generation; user->role = (user_role_t)stored->role; user->username_length = stored->username_length; memcpy(user->username, stored->username, stored->username_length); user->public_key_count = stored->key_count; for (size_t key_index = 0U; key_index < USER_DATABASE_MAX_SSH_KEYS_PER_USER; ++key_index) { const stored_key_t *stored_key = &stored->keys[key_index]; user_database_key_snapshot_t *key = &user->public_keys[key_index]; if (stored_key->active == 0U) { continue; } key->active = true; key->index = (uint8_t)key_index; key->key_type_length = stored_key->type_length; memcpy(key->key_type, stored_key->type, stored_key->type_length); memcpy(key->sha256_fingerprint, stored_key->fingerprint, sizeof(key->sha256_fingerprint)); } } xSemaphoreGive(s_mutex); return ESP_OK; } static void fill_principal(const stored_user_t *user, user_auth_method_t method, user_principal_t *principal) { memset(principal, 0, sizeof(*principal)); principal->user_id = user->user_id; principal->auth_generation = user->auth_generation; principal->role = (user_role_t)user->role; principal->method = method; principal->username_length = user->username_length; memcpy(principal->username, user->username, user->username_length); } esp_err_t user_database_authenticate_password( const uint8_t *username, size_t username_length, const uint8_t *password, size_t password_length, user_principal_t *principal, bool *authenticated) { if (principal == NULL || authenticated == NULL || (username == NULL && username_length != 0U) || (password == NULL && password_length != 0U)) { return ESP_ERR_INVALID_ARG; } *authenticated = false; memset(principal, 0, sizeof(*principal)); if (!s_initialized || s_mutex == NULL) { return ESP_ERR_INVALID_STATE; } bool password_valid = user_database_password_valid(password, password_length); bool credentials_valid = user_database_username_valid(username, username_length) && password_valid; const uint8_t *kdf_password = password_valid ? password : s_dummy_password; size_t kdf_password_length = password_valid ? password_length : sizeof(s_dummy_password) - 1U; uint32_t user_id = 0U; uint32_t auth_generation = 0U; uint32_t iterations = USER_DATABASE_PBKDF2_ITERATIONS; user_role_t role = USER_ROLE_USER; uint8_t salt[USER_DATABASE_PASSWORD_SALT_LENGTH] = {0}; uint8_t expected_hash[USER_DATABASE_PASSWORD_HASH_LENGTH] = {0}; uint8_t derived_hash[USER_DATABASE_PASSWORD_HASH_LENGTH] = {0}; bool known_user = false; xSemaphoreTake(s_mutex, portMAX_DELAY); int index = credentials_valid ? find_user(&s_database, username, username_length) : -1; if (index >= 0) { const stored_user_t *user = &s_database.users[index]; known_user = true; user_id = user->user_id; auth_generation = user->auth_generation; role = (user_role_t)user->role; iterations = user->password_iterations; memcpy(salt, user->password_salt, sizeof(salt)); memcpy(expected_hash, user->password_hash, sizeof(expected_hash)); } else { memcpy(salt, s_dummy_salt, sizeof(salt)); memcpy(expected_hash, s_dummy_hash, sizeof(expected_hash)); } xSemaphoreGive(s_mutex); esp_err_t error = derive_password(kdf_password, kdf_password_length, salt, iterations, derived_hash); bool matched = error == ESP_OK && constant_time_equal(derived_hash, expected_hash, sizeof(derived_hash)); if (matched && known_user) { xSemaphoreTake(s_mutex, portMAX_DELAY); index = find_user(&s_database, username, username_length); if (index >= 0) { const stored_user_t *user = &s_database.users[index]; if (user->user_id == user_id && user->auth_generation == auth_generation && user->role == role) { fill_principal(user, USER_AUTH_METHOD_PASSWORD, principal); *authenticated = true; } } xSemaphoreGive(s_mutex); } secure_wipe(salt, sizeof(salt)); secure_wipe(expected_hash, sizeof(expected_hash)); secure_wipe(derived_hash, sizeof(derived_hash)); return error; } esp_err_t user_database_authorize_ssh_public_key( const uint8_t *username, size_t username_length, const uint8_t *key_type, size_t key_type_length, const uint8_t *key_blob, size_t key_blob_length, user_principal_t *principal, bool *authorized) { if (principal == NULL || authorized == NULL) { return ESP_ERR_INVALID_ARG; } *authorized = false; memset(principal, 0, sizeof(*principal)); if (!s_initialized || s_mutex == NULL) { return ESP_ERR_INVALID_STATE; } if (!user_database_username_valid(username, username_length) || !user_database_key_valid(key_type, key_type_length, key_blob, key_blob_length)) { return ESP_OK; } xSemaphoreTake(s_mutex, portMAX_DELAY); int index = find_user(&s_database, username, username_length); if (index >= 0) { const stored_user_t *user = &s_database.users[index]; for (size_t key_index = 0U; key_index < USER_DATABASE_MAX_SSH_KEYS_PER_USER; ++key_index) { const stored_key_t *key = &user->keys[key_index]; if (key->active != 0U && key->type_length == key_type_length && key->blob_length == key_blob_length && memcmp(key->type, key_type, key_type_length) == 0 && constant_time_equal(key->blob, key_blob, key_blob_length)) { fill_principal(user, USER_AUTH_METHOD_SSH_PUBLIC_KEY, principal); *authorized = true; break; } } } xSemaphoreGive(s_mutex); return ESP_OK; } esp_err_t user_database_principal_is_current(const user_principal_t *principal, bool *current) { if (principal == NULL || current == NULL) { return ESP_ERR_INVALID_ARG; } *current = false; if (!s_initialized || s_mutex == NULL) { return ESP_ERR_INVALID_STATE; } xSemaphoreTake(s_mutex, portMAX_DELAY); int index = find_user(&s_database, (const uint8_t *)principal->username, principal->username_length); if (index >= 0) { const stored_user_t *user = &s_database.users[index]; *current = user->user_id == principal->user_id && user->auth_generation == principal->auth_generation && user->role == principal->role; } xSemaphoreGive(s_mutex); return ESP_OK; } static esp_err_t create_locked(const uint8_t *username, size_t username_length, user_role_t role, const uint8_t *password, size_t password_length) { if (find_user(&s_database, username, username_length) >= 0) { return ESP_ERR_INVALID_STATE; } int free_index = find_free_user(&s_database); if (free_index < 0) { return ESP_ERR_NO_MEM; } s_candidate = s_database; esp_err_t error = initialize_user(&s_candidate.users[free_index], username, username_length, role, password, password_length); if (error == ESP_OK && role == USER_ROLE_ADMIN) { s_candidate.admin_bootstrapped = 1U; } if (error == ESP_OK) { return commit_candidate_locked(); } discard_candidate(); return error; } esp_err_t user_database_create(const uint8_t *username, size_t username_length, user_role_t role, const uint8_t *password, size_t password_length) { if (!s_initialized || s_mutex == NULL) { return ESP_ERR_INVALID_STATE; } if (!user_database_username_valid(username, username_length) || !user_database_password_valid(password, password_length) || (role != USER_ROLE_USER && role != USER_ROLE_ADMIN)) { return ESP_ERR_INVALID_ARG; } xSemaphoreTake(s_mutex, portMAX_DELAY); esp_err_t error = create_locked(username, username_length, role, password, password_length); xSemaphoreGive(s_mutex); return error; } esp_err_t user_database_create_generated( const uint8_t *username, size_t username_length, user_role_t role, user_database_generated_password_t *generated_password) { esp_err_t error = generate_password(generated_password); if (error == ESP_OK) { error = user_database_create(username, username_length, role, generated_password->password, generated_password->password_length); } if (error != ESP_OK && generated_password != NULL) { secure_wipe(generated_password, sizeof(*generated_password)); } return error; } esp_err_t user_database_bootstrap_admin(const uint8_t *password, size_t password_length) { if (!s_initialized || s_mutex == NULL || !user_database_password_valid(password, password_length)) { return ESP_ERR_INVALID_ARG; } xSemaphoreTake(s_mutex, portMAX_DELAY); if (s_database.admin_bootstrapped != 0U) { xSemaphoreGive(s_mutex); return ESP_ERR_INVALID_STATE; } int index = find_user(&s_database, s_admin_username, sizeof(s_admin_username) - 1U); esp_err_t error; if (index < 0) { error = create_locked(s_admin_username, sizeof(s_admin_username) - 1U, USER_ROLE_ADMIN, password, password_length); } else { s_candidate = s_database; stored_user_t *user = &s_candidate.users[index]; error = set_record_password(user, password, password_length); if (error == ESP_OK) { user->role = USER_ROLE_ADMIN; error = next_generation(&user->auth_generation); } if (error == ESP_OK) { s_candidate.admin_bootstrapped = 1U; error = commit_candidate_locked(); } else { discard_candidate(); } } xSemaphoreGive(s_mutex); return error; } esp_err_t user_database_bootstrap_admin_generated( user_database_generated_password_t *generated_password) { esp_err_t error = generate_password(generated_password); if (error == ESP_OK) { error = user_database_bootstrap_admin(generated_password->password, generated_password->password_length); } if (error != ESP_OK && generated_password != NULL) { secure_wipe(generated_password, sizeof(*generated_password)); } return error; } static esp_err_t mutate_user_begin(const uint8_t *username, size_t username_length, int *index) { if (!user_database_username_valid(username, username_length)) { return ESP_ERR_INVALID_ARG; } *index = find_user(&s_database, username, username_length); if (*index < 0) { return ESP_ERR_NOT_FOUND; } s_candidate = s_database; return ESP_OK; } esp_err_t user_database_delete(const uint8_t *username, size_t username_length) { if (!s_initialized || s_mutex == NULL) { return ESP_ERR_INVALID_STATE; } xSemaphoreTake(s_mutex, portMAX_DELAY); int index; esp_err_t error = mutate_user_begin(username, username_length, &index); if (error == ESP_OK) { const stored_user_t *user = &s_database.users[index]; bool protected_migrated_admin = s_database.admin_bootstrapped == 0U && user->username_length == sizeof(s_admin_username) - 1U && memcmp(user->username, s_admin_username, sizeof(s_admin_username) - 1U) == 0; if (protected_migrated_admin || (user->role == USER_ROLE_ADMIN && s_database.admin_count <= 1U)) { error = ESP_ERR_INVALID_STATE; discard_candidate(); } else { secure_wipe(&s_candidate.users[index], sizeof(s_candidate.users[index])); error = commit_candidate_locked(); } } xSemaphoreGive(s_mutex); return error; } esp_err_t user_database_set_role(const uint8_t *username, size_t username_length, user_role_t role) { if (!s_initialized || s_mutex == NULL || (role != USER_ROLE_USER && role != USER_ROLE_ADMIN)) { return ESP_ERR_INVALID_ARG; } xSemaphoreTake(s_mutex, portMAX_DELAY); int index; esp_err_t error = mutate_user_begin(username, username_length, &index); if (error == ESP_OK) { stored_user_t *user = &s_candidate.users[index]; if (user->role == role) { error = ESP_OK; discard_candidate(); } else if (user->role == USER_ROLE_ADMIN && s_database.admin_count <= 1U) { error = ESP_ERR_INVALID_STATE; discard_candidate(); } else { user->role = (uint8_t)role; error = next_generation(&user->auth_generation); if (error == ESP_OK && role == USER_ROLE_ADMIN) { s_candidate.admin_bootstrapped = 1U; } if (error == ESP_OK) { error = commit_candidate_locked(); } else { discard_candidate(); } } } xSemaphoreGive(s_mutex); return error; } esp_err_t user_database_set_password(const uint8_t *username, size_t username_length, const uint8_t *password, size_t password_length) { if (!s_initialized || s_mutex == NULL || !user_database_password_valid(password, password_length)) { return ESP_ERR_INVALID_ARG; } xSemaphoreTake(s_mutex, portMAX_DELAY); int index; esp_err_t error = mutate_user_begin(username, username_length, &index); if (error == ESP_OK) { stored_user_t *user = &s_candidate.users[index]; error = set_record_password(user, password, password_length); if (error == ESP_OK) { error = next_generation(&user->auth_generation); } if (error == ESP_OK) { error = commit_candidate_locked(); } else { discard_candidate(); } } xSemaphoreGive(s_mutex); return error; } esp_err_t user_database_generate_password( const uint8_t *username, size_t username_length, user_database_generated_password_t *generated_password) { esp_err_t error = generate_password(generated_password); if (error == ESP_OK) { error = user_database_set_password(username, username_length, generated_password->password, generated_password->password_length); } if (error != ESP_OK && generated_password != NULL) { secure_wipe(generated_password, sizeof(*generated_password)); } return error; } esp_err_t user_database_add_ssh_key( const uint8_t *username, size_t username_length, const uint8_t *key_type, size_t key_type_length, const uint8_t *key_blob, size_t key_blob_length, uint8_t *key_index) { if (!s_initialized || s_mutex == NULL || key_index == NULL || !user_database_key_valid(key_type, key_type_length, key_blob, key_blob_length)) { return ESP_ERR_INVALID_ARG; } xSemaphoreTake(s_mutex, portMAX_DELAY); int user_index; esp_err_t error = mutate_user_begin(username, username_length, &user_index); if (error == ESP_OK) { stored_user_t *user = &s_candidate.users[user_index]; int free_index = -1; for (size_t candidate_user_index = 0U; candidate_user_index < USER_DATABASE_MAX_USERS; ++candidate_user_index) { stored_user_t *candidate_user = &s_candidate.users[candidate_user_index]; if (candidate_user->active == 0U) { continue; } for (size_t index = 0U; index < USER_DATABASE_MAX_SSH_KEYS_PER_USER; ++index) { stored_key_t *key = &candidate_user->keys[index]; if (candidate_user_index == (size_t)user_index && key->active == 0U && free_index < 0) { free_index = (int)index; } if (key->active != 0U && key->type_length == key_type_length && key->blob_length == key_blob_length && memcmp(key->type, key_type, key_type_length) == 0 && constant_time_equal(key->blob, key_blob, key_blob_length)) { error = ESP_ERR_INVALID_STATE; break; } } if (error != ESP_OK) { break; } } if (error == ESP_OK && free_index < 0) { error = ESP_ERR_NO_MEM; } if (error == ESP_OK) { stored_key_t *key = &user->keys[free_index]; memset(key, 0, sizeof(*key)); key->active = 1U; key->type_length = (uint8_t)key_type_length; key->blob_length = (uint16_t)key_blob_length; memcpy(key->type, key_type, key_type_length); memcpy(key->blob, key_blob, key_blob_length); if (mbedtls_sha256(key_blob, key_blob_length, key->fingerprint, 0) != 0) { error = ESP_FAIL; } else { ++user->key_count; error = next_generation(&user->auth_generation); } if (error == ESP_OK) { *key_index = (uint8_t)free_index; error = commit_candidate_locked(); } } if (error != ESP_OK) { discard_candidate(); } } xSemaphoreGive(s_mutex); return error; } esp_err_t user_database_remove_ssh_key(const uint8_t *username, size_t username_length, uint8_t key_index) { if (!s_initialized || s_mutex == NULL || key_index >= USER_DATABASE_MAX_SSH_KEYS_PER_USER) { return ESP_ERR_INVALID_ARG; } xSemaphoreTake(s_mutex, portMAX_DELAY); int user_index; esp_err_t error = mutate_user_begin(username, username_length, &user_index); if (error == ESP_OK) { stored_user_t *user = &s_candidate.users[user_index]; if (user->keys[key_index].active == 0U) { error = ESP_ERR_NOT_FOUND; discard_candidate(); } else { secure_wipe(&user->keys[key_index], sizeof(user->keys[key_index])); --user->key_count; error = next_generation(&user->auth_generation); if (error == ESP_OK) { error = commit_candidate_locked(); } else { discard_candidate(); } } } xSemaphoreGive(s_mutex); return error; } esp_err_t user_database_clear_ssh_keys(const uint8_t *username, size_t username_length) { if (!s_initialized || s_mutex == NULL) { return ESP_ERR_INVALID_STATE; } xSemaphoreTake(s_mutex, portMAX_DELAY); int user_index; esp_err_t error = mutate_user_begin(username, username_length, &user_index); if (error == ESP_OK) { stored_user_t *user = &s_candidate.users[user_index]; if (user->key_count == 0U) { error = ESP_OK; discard_candidate(); } else { secure_wipe(user->keys, sizeof(user->keys)); user->key_count = 0U; error = next_generation(&user->auth_generation); if (error == ESP_OK) { error = commit_candidate_locked(); } else { discard_candidate(); } } } xSemaphoreGive(s_mutex); return error; }