Files
ESP32_Serial_Swiss_Army_Knife/src/user_database.c
T

1275 lines
46 KiB
C

/* SPDX-License-Identifier: GPL-3.0-only */
/* Bounded persistent role-based user and SSH authorized-key database. */
#include "user_database.h"
#include <stddef.h>
#include <string.h>
#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;
}