/* SPDX-License-Identifier: GPL-3.0-only */ /* Physical UART0 role-based user administration. */ #include "user_console.h" #include #include #include #include "admin_command_gate.h" #include "admin_ssh_console.h" #include "console_input.h" #include "esp_console.h" #include "mbedtls/base64.h" #include "secure_random.h" #include "ssh_transport.h" #include "user_database.h" #include "web_security.h" #include "web_serial_transport.h" #define USER_CONSOLE_KEY_LINE_CAPACITY 256U /* `user` commands are serialized by the administration gate. */ static user_database_snapshot_t s_user_snapshot; static void print_usage(void) { printf("Usage:\n"); printf(" user status|list\n"); printf(" user show \n"); printf(" user bootstrap [--generate]\n"); printf(" user recover --force\n"); printf(" user add [--generate]\n"); printf(" user delete --force\n"); printf(" user role --force\n"); printf(" user password [--generate]\n"); printf(" user key add \n"); printf(" user key add \n"); printf(" user key delete <0..2> --force\n"); printf(" user key clear --force\n"); } static void revoke_user_network_sessions(const char *username) { size_t username_length = strlen(username); esp_err_t web_error = web_serial_transport_revoke_user( (const uint8_t *)username, username_length); esp_err_t ssh_error = ssh_transport_revoke_user( (const uint8_t *)username, username_length); if (web_error != ESP_OK && web_error != ESP_ERR_INVALID_STATE) { printf("Warning: WebSocket revocation failed: %s\n", esp_err_to_name(web_error)); } if (ssh_error != ESP_OK && ssh_error != ESP_ERR_INVALID_STATE) { printf("Warning: SSH revocation failed: %s\n", esp_err_to_name(ssh_error)); } } 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) { esp_err_t error = user_database_get_snapshot(&s_user_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)s_user_snapshot.generation, (unsigned int)s_user_snapshot.user_count, USER_DATABASE_MAX_USERS, (unsigned int)s_user_snapshot.admin_count, s_user_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 = &s_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 (!s_user_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; } revoke_user_network_sessions("admin"); printf("Administrator account bootstrapped. Role-aware HTTPS and SSH authentication is active.\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; } revoke_user_network_sessions(username); 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; } revoke_user_network_sessions(username); printf("Password changed; affected network sessions are now stale and will be revoked.\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_parts(const char *username, const uint8_t *type, size_t type_length, const uint8_t *encoded, size_t encoded_length) { 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(type, type_length, blob, blob_length)) { printf("Unsupported or malformed key; use ssh-ed25519 or ecdsa-sha2-nistp256.\n"); secure_wipe(blob, sizeof(blob)); return 1; } uint8_t key_index = 0U; esp_err_t error = user_database_add_ssh_key( (const uint8_t *)username, strlen(username), type, type_length, blob, blob_length, &key_index); secure_wipe(blob, sizeof(blob)); if (error != ESP_OK) { if (error == USER_DATABASE_ERR_DUPLICATE_SSH_KEY) { printf("Could not add SSH key: that public key is already assigned to this account.\n"); } else if (error == ESP_ERR_NO_MEM) { printf("Could not add SSH key: the account already has %u keys.\n", USER_DATABASE_MAX_SSH_KEYS_PER_USER); } else { printf("Could not add SSH key: %s\n", esp_err_to_name(error)); } return 1; } revoke_user_network_sessions(username); printf("SSH public key added at index %u. Public-key login is active.\n", (unsigned int)key_index); return 0; } 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); int result = add_key_parts(username, line, type_length, encoded, encoded_length); secure_wipe(line, sizeof(line)); return result; } static int command_user_inner(int argc, char **argv) { bool remote = admin_ssh_console_dispatch_is_remote(); const user_principal_t *principal = admin_ssh_console_dispatch_principal(); 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) { if (remote) { printf("User database recovery is restricted to physical UART0.\n"); return 1; } 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; } if (remote) { printf("Administrator bootstrap is restricted to physical UART0.\n"); 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; } revoke_user_network_sessions(argv[2]); 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; } revoke_user_network_sessions(argv[2]); 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; } if (remote && generated && principal != NULL && strlen(argv[2]) == principal->username_length && memcmp(argv[2], principal->username, principal->username_length) == 0) { printf("Remote generated-password changes for the current admin are disabled; use UART0.\n"); 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], "add") == 0) { return add_key_parts(argv[3], (const uint8_t *)argv[4], strlen(argv[4]), (const uint8_t *)argv[5], strlen(argv[5])); } 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; } revoke_user_network_sessions(argv[3]); 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; } revoke_user_network_sessions(argv[3]); printf("SSH keys cleared for '%s'.\n", argv[3]); return 0; } print_usage(); return 1; } static int command_user(int argc, char **argv) { esp_err_t error = admin_command_gate_take(); if (error != ESP_OK) { printf("Administrative command gate unavailable: %s\n", esp_err_to_name(error)); return 1; } int result = command_user_inner(argc, argv); admin_command_gate_give(); return result; } 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); }