Add role-based user database administration

This commit is contained in:
2026-08-30 00:56:18 +02:00
parent 0c7b763bec
commit cd235445c7
14 changed files with 2235 additions and 16 deletions
+457
View File
@@ -0,0 +1,457 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Physical UART0 role-based user administration. */
#include "user_console.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "console_input.h"
#include "esp_console.h"
#include "mbedtls/base64.h"
#include "secure_random.h"
#include "user_database.h"
#include "web_security.h"
#define USER_CONSOLE_KEY_LINE_CAPACITY 256U
static void print_usage(void)
{
printf("Usage:\n");
printf(" user status|list\n");
printf(" user show <username>\n");
printf(" user bootstrap [--generate]\n");
printf(" user recover --force\n");
printf(" user add <username> <user|admin> [--generate]\n");
printf(" user delete <username> --force\n");
printf(" user role <username> <user|admin> --force\n");
printf(" user password <username> [--generate]\n");
printf(" user key add <username>\n");
printf(" user key delete <username> <0..2> --force\n");
printf(" user key clear <username> --force\n");
}
static void print_fingerprint(const uint8_t fingerprint[USER_DATABASE_SHA256_LENGTH])
{
uint8_t encoded[48] = {0};
size_t length = 0U;
if (mbedtls_base64_encode(encoded, sizeof(encoded), &length,
fingerprint, USER_DATABASE_SHA256_LENGTH) != 0) {
printf("unavailable");
return;
}
while (length > 0U && encoded[length - 1U] == '=') {
--length;
}
printf("SHA256:%.*s", (int)length, (const char *)encoded);
secure_wipe(encoded, sizeof(encoded));
}
static void print_user(const user_database_user_snapshot_t *user)
{
printf("%.*s role=%s id=%lu generation=%lu keys=%u\n",
(int)user->username_length, user->username,
user_role_to_string(user->role),
(unsigned long)user->user_id,
(unsigned long)user->auth_generation,
(unsigned int)user->public_key_count);
for (size_t index = 0U; index < USER_DATABASE_MAX_SSH_KEYS_PER_USER; ++index) {
const user_database_key_snapshot_t *key = &user->public_keys[index];
if (!key->active) {
continue;
}
printf(" key %u %.*s ", (unsigned int)key->index,
(int)key->key_type_length, key->key_type);
print_fingerprint(key->sha256_fingerprint);
putchar('\n');
}
}
static int show_users(const char *selected)
{
user_database_snapshot_t snapshot;
esp_err_t error = user_database_get_snapshot(&snapshot);
if (error != ESP_OK) {
printf("User database unavailable: %s\n", esp_err_to_name(error));
return 1;
}
if (selected == NULL) {
printf("User database: generation=%lu users=%u/%u admins=%u bootstrapped=%s\n",
(unsigned long)snapshot.generation,
(unsigned int)snapshot.user_count,
USER_DATABASE_MAX_USERS,
(unsigned int)snapshot.admin_count,
snapshot.admin_bootstrapped ? "yes" : "no");
}
bool found = false;
for (size_t index = 0U; index < USER_DATABASE_MAX_USERS; ++index) {
const user_database_user_snapshot_t *user = &snapshot.users[index];
if (!user->active ||
(selected != NULL &&
(strlen(selected) != user->username_length ||
memcmp(selected, user->username, user->username_length) != 0))) {
continue;
}
print_user(user);
found = true;
}
if (selected != NULL && !found) {
printf("User '%s' not found.\n", selected);
return 1;
}
if (!snapshot.admin_bootstrapped) {
printf("Administrative network access is not bootstrapped; use 'user bootstrap'.\n");
}
return 0;
}
static esp_err_t read_password(uint8_t password[USER_DATABASE_PASSWORD_CAPACITY + 1U],
size_t *password_length)
{
uint8_t confirmation[USER_DATABASE_PASSWORD_CAPACITY + 1U] = {0};
size_t confirmation_length = 0U;
esp_err_t error = console_input_read_hidden(
"Password (12..64 printable characters, Ctrl-C cancels): ",
password, USER_DATABASE_PASSWORD_CAPACITY + 1U,
USER_DATABASE_PASSWORD_MIN_LENGTH, USER_DATABASE_PASSWORD_CAPACITY,
password_length);
if (error == ESP_OK) {
error = console_input_read_hidden(
"Repeat password: ", confirmation, sizeof(confirmation),
USER_DATABASE_PASSWORD_MIN_LENGTH, USER_DATABASE_PASSWORD_CAPACITY,
&confirmation_length);
}
if (error == ESP_OK &&
(*password_length != confirmation_length ||
memcmp(password, confirmation, *password_length) != 0)) {
printf("Passwords do not match.\n");
error = ESP_ERR_INVALID_RESPONSE;
}
secure_wipe(confirmation, sizeof(confirmation));
if (error != ESP_OK) {
secure_wipe(password, USER_DATABASE_PASSWORD_CAPACITY + 1U);
*password_length = 0U;
}
return error;
}
static void show_generated_password(const char *username,
user_database_generated_password_t *generated)
{
printf("Generated password for %s: %.*s\n",
username, (int)generated->password_length, generated->password);
printf("This password is shown once; store it securely.\n");
secure_wipe(generated, sizeof(*generated));
}
static int recover_database(void)
{
web_security_credentials_t credentials;
memset(&credentials, 0, sizeof(credentials));
esp_err_t error = web_security_show_credentials(&credentials);
if (error == ESP_OK) {
const user_database_legacy_credentials_t legacy = {
.username = (const uint8_t *)credentials.username,
.username_length = credentials.username_length,
.password = (const uint8_t *)credentials.password,
.password_length = credentials.password_length,
};
error = user_database_recover_from_legacy(&legacy);
}
secure_wipe(&credentials, sizeof(credentials));
if (error != ESP_OK) {
printf("Could not recover user database: %s\n", esp_err_to_name(error));
return 1;
}
printf("User database replaced from the current legacy network credential.\n");
printf("The imported account has role user; run 'user bootstrap' to establish an administrator.\n");
return 0;
}
static int bootstrap(bool generated)
{
esp_err_t error;
if (generated) {
user_database_generated_password_t password;
error = user_database_bootstrap_admin_generated(&password);
if (error == ESP_OK) {
show_generated_password("admin", &password);
}
} else {
uint8_t password[USER_DATABASE_PASSWORD_CAPACITY + 1U] = {0};
size_t password_length = 0U;
error = read_password(password, &password_length);
if (error == ESP_OK) {
error = user_database_bootstrap_admin(password, password_length);
}
secure_wipe(password, sizeof(password));
}
if (error != ESP_OK) {
printf("Could not bootstrap administrator: %s\n", esp_err_to_name(error));
return 1;
}
printf("Administrator account bootstrapped. Authentication integration follows in Phase 8B.\n");
return 0;
}
static int add_user(const char *username, const char *role_text, bool generated)
{
user_role_t role;
if (!user_database_username_valid((const uint8_t *)username, strlen(username)) ||
!user_role_parse(role_text, &role)) {
printf("Username must match [a-z][a-z0-9_-]{0,15}; role is user or admin.\n");
return 1;
}
esp_err_t error;
if (generated) {
user_database_generated_password_t password;
error = user_database_create_generated((const uint8_t *)username,
strlen(username), role, &password);
if (error == ESP_OK) {
show_generated_password(username, &password);
}
} else {
uint8_t password[USER_DATABASE_PASSWORD_CAPACITY + 1U] = {0};
size_t password_length = 0U;
error = read_password(password, &password_length);
if (error == ESP_OK) {
error = user_database_create((const uint8_t *)username, strlen(username),
role, password, password_length);
}
secure_wipe(password, sizeof(password));
}
if (error != ESP_OK) {
printf("Could not add user: %s\n", esp_err_to_name(error));
return 1;
}
printf("User '%s' added with role %s.\n", username, user_role_to_string(role));
return 0;
}
static int change_password(const char *username, bool generated)
{
esp_err_t error;
if (generated) {
user_database_generated_password_t password;
error = user_database_generate_password((const uint8_t *)username,
strlen(username), &password);
if (error == ESP_OK) {
show_generated_password(username, &password);
}
} else {
uint8_t password[USER_DATABASE_PASSWORD_CAPACITY + 1U] = {0};
size_t password_length = 0U;
error = read_password(password, &password_length);
if (error == ESP_OK) {
error = user_database_set_password((const uint8_t *)username,
strlen(username),
password, password_length);
}
secure_wipe(password, sizeof(password));
}
if (error != ESP_OK) {
printf("Could not change password: %s\n", esp_err_to_name(error));
return 1;
}
printf("Password changed; affected network sessions will be revoked in Phase 8B.\n");
return 0;
}
static bool parse_key_index(const char *text, uint8_t *index)
{
if (text == NULL || text[0] < '0' || text[0] > '9' || text[1] != '\0') {
return false;
}
uint8_t parsed = (uint8_t)(text[0] - '0');
if (parsed >= USER_DATABASE_MAX_SSH_KEYS_PER_USER) {
return false;
}
*index = parsed;
return true;
}
static bool key_delimiter(uint8_t value)
{
return value == ' ' || value == '\t';
}
static int add_key(const char *username)
{
uint8_t line[USER_CONSOLE_KEY_LINE_CAPACITY] = {0};
size_t line_length = 0U;
esp_err_t error = console_input_read_line(
"OpenSSH public key (type base64 [comment], Ctrl-C cancels): ",
line, sizeof(line), &line_length);
if (error != ESP_OK) {
secure_wipe(line, sizeof(line));
return 1;
}
uint8_t *space = NULL;
for (size_t index = 0U; index < line_length; ++index) {
if (key_delimiter(line[index])) {
space = &line[index];
break;
}
}
if (space == NULL) {
printf("Public key must contain a key type and Base64 blob.\n");
secure_wipe(line, sizeof(line));
return 1;
}
size_t type_length = (size_t)(space - line);
uint8_t *encoded = space + 1U;
size_t remaining = line_length - type_length - 1U;
while (remaining > 0U && key_delimiter(*encoded)) {
++encoded;
--remaining;
}
uint8_t *encoded_end = NULL;
for (size_t index = 0U; index < remaining; ++index) {
if (key_delimiter(encoded[index])) {
encoded_end = &encoded[index];
break;
}
}
size_t encoded_length = encoded_end == NULL
? remaining
: (size_t)(encoded_end - encoded);
uint8_t blob[USER_DATABASE_SSH_KEY_BLOB_CAPACITY] = {0};
size_t blob_length = 0U;
int decoded = mbedtls_base64_decode(blob, sizeof(blob), &blob_length,
encoded, encoded_length);
if (decoded != 0 ||
!user_database_key_valid(line, type_length, blob, blob_length)) {
printf("Unsupported or malformed key; use ssh-ed25519 or ecdsa-sha2-nistp256.\n");
secure_wipe(blob, sizeof(blob));
secure_wipe(line, sizeof(line));
return 1;
}
uint8_t key_index = 0U;
error = user_database_add_ssh_key((const uint8_t *)username, strlen(username),
line, type_length, blob, blob_length, &key_index);
secure_wipe(blob, sizeof(blob));
secure_wipe(line, sizeof(line));
if (error != ESP_OK) {
printf("Could not add SSH key: %s\n", esp_err_to_name(error));
return 1;
}
printf("SSH public key added at index %u. Key login is enabled in Phase 8B.\n",
(unsigned int)key_index);
return 0;
}
static int command_user(int argc, char **argv)
{
if (argc == 1 || (argc == 2 && strcmp(argv[1], "status") == 0) ||
(argc == 2 && strcmp(argv[1], "list") == 0)) {
return show_users(NULL);
}
if (argc == 3 && strcmp(argv[1], "show") == 0) {
return show_users(argv[2]);
}
if (argc == 3 && strcmp(argv[1], "recover") == 0 &&
strcmp(argv[2], "--force") == 0) {
return recover_database();
}
if ((argc == 2 || argc == 3) && strcmp(argv[1], "bootstrap") == 0) {
bool generated = argc == 3 && strcmp(argv[2], "--generate") == 0;
if (argc == 3 && !generated) {
print_usage();
return 1;
}
return bootstrap(generated);
}
if ((argc == 4 || argc == 5) && strcmp(argv[1], "add") == 0) {
bool generated = argc == 5 && strcmp(argv[4], "--generate") == 0;
if (argc == 5 && !generated) {
print_usage();
return 1;
}
return add_user(argv[2], argv[3], generated);
}
if (argc == 4 && strcmp(argv[1], "delete") == 0 &&
strcmp(argv[3], "--force") == 0) {
esp_err_t error = user_database_delete((const uint8_t *)argv[2], strlen(argv[2]));
if (error != ESP_OK) {
printf("Could not delete user (the migrated or final admin is protected): %s\n",
esp_err_to_name(error));
return 1;
}
printf("User '%s' deleted.\n", argv[2]);
return 0;
}
if (argc == 5 && strcmp(argv[1], "role") == 0 &&
strcmp(argv[4], "--force") == 0) {
user_role_t role;
if (!user_role_parse(argv[3], &role)) {
printf("Role must be user or admin.\n");
return 1;
}
esp_err_t error = user_database_set_role((const uint8_t *)argv[2],
strlen(argv[2]), role);
if (error != ESP_OK) {
printf("Could not change role (the final admin is protected): %s\n",
esp_err_to_name(error));
return 1;
}
printf("User '%s' role changed to %s.\n", argv[2], user_role_to_string(role));
return 0;
}
if ((argc == 3 || argc == 4) && strcmp(argv[1], "password") == 0) {
bool generated = argc == 4 && strcmp(argv[3], "--generate") == 0;
if (argc == 4 && !generated) {
print_usage();
return 1;
}
return change_password(argv[2], generated);
}
if (argc == 4 && strcmp(argv[1], "key") == 0 &&
strcmp(argv[2], "add") == 0) {
return add_key(argv[3]);
}
if (argc == 6 && strcmp(argv[1], "key") == 0 &&
strcmp(argv[2], "delete") == 0 && strcmp(argv[5], "--force") == 0) {
uint8_t index;
if (!parse_key_index(argv[4], &index)) {
printf("Key index must be 0..2.\n");
return 1;
}
esp_err_t error = user_database_remove_ssh_key(
(const uint8_t *)argv[3], strlen(argv[3]), index);
if (error != ESP_OK) {
printf("Could not delete SSH key: %s\n", esp_err_to_name(error));
return 1;
}
printf("SSH key %u deleted for '%s'.\n", (unsigned int)index, argv[3]);
return 0;
}
if (argc == 5 && strcmp(argv[1], "key") == 0 &&
strcmp(argv[2], "clear") == 0 && strcmp(argv[4], "--force") == 0) {
esp_err_t error = user_database_clear_ssh_keys(
(const uint8_t *)argv[3], strlen(argv[3]));
if (error != ESP_OK) {
printf("Could not clear SSH keys: %s\n", esp_err_to_name(error));
return 1;
}
printf("SSH keys cleared for '%s'.\n", argv[3]);
return 0;
}
print_usage();
return 1;
}
esp_err_t user_console_register_commands(void)
{
const esp_console_cmd_t command = {
.command = "user",
.help = "Manage bounded role-based users, passwords, and SSH public keys",
.hint = NULL,
.func = &command_user,
.argtable = NULL,
};
return esp_console_cmd_register(&command);
}