diff --git a/.gitignore b/.gitignore index 7d63841..0ae4b65 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,7 @@ build/ .DS_Store .system_generated/ .config + +# Python bytecode +__pycache__/ +*.pyc diff --git a/include/security/auth.h b/include/security/auth.h index 240f5d5..b228e6c 100644 --- a/include/security/auth.h +++ b/include/security/auth.h @@ -31,4 +31,9 @@ int auth_set_current_user(const char* username); size_t auth_get_user_count(void); const user_account_t* auth_get_user_by_index(size_t index); +/* Interactive boot-time login: prompts for a username and password on the + * console and blocks until a valid pair is entered, then sets the current + * user. Default account is SUB / SUB. */ +void console_login(void); + #endif // _SECURITY_AUTH_H diff --git a/kernel/main.c b/kernel/main.c index 44802b5..bdd363c 100644 --- a/kernel/main.c +++ b/kernel/main.c @@ -275,20 +275,25 @@ static void subos_modular_core_boot(void) { bool emergency_text = init_has_param("nogui") || init_has_param("text") || init_has_param("emergency") || init_has_param("single"); + // End the splash and return the screen to a text console so the login + // prompt is visible before any session (graphical or TTY) begins. + boot_logo_splash_end(); + fbcon_enable(true); + + // Gate the session behind an interactive login. This blocks until a valid + // username/password is entered; the default account is SUB / SUB. + console_login(); + if (!emergency_text) { - printk(KERN_INFO "INIT: Booting directly into SUB-OS Graphical Desktop Environment...\n"); - boot_logo_splash_end(); + printk(KERN_INFO "INIT: Starting SUB-OS Graphical Desktop Environment...\n"); gui_start_desktop(); - printk(KERN_INFO "INIT: GUI session closed. Entering Emergency Kernel TTY Mode.\n"); + printk(KERN_INFO "INIT: GUI session closed. Entering Kernel TTY Mode.\n"); + // The desktop owned the framebuffer; return it to the text console. + fbcon_enable(true); } else { printk(KERN_INFO "INIT: Emergency text/kernel TTY mode requested via boot parameters.\n"); - boot_logo_splash_end(); } - // The screen returns to the console before handing over to userland, whether - // the GUI session just closed or a text boot was requested. - fbcon_enable(true); - // Hand control to userland: /sbin/init is a separate static ELF that runs // in ring 3 with its own page tables and talks back only through INT 0x80. exec_run_builtin_init(); diff --git a/scripts/kconfig/menuconfig.py b/scripts/kconfig/menuconfig.py index b4e382b..b72b78b 100755 --- a/scripts/kconfig/menuconfig.py +++ b/scripts/kconfig/menuconfig.py @@ -407,56 +407,68 @@ def run_dialog_gui(): continue items = submenu["items"] - item_opts = [] + is_radio = any(i.get("type") == "radio" for i in items) - for item in items: - itype = item.get("type") - if itype == "bool": + item_opts = [] + if is_radio: + # Single-choice group (architecture, optimization). Use a --menu, + # not a --radiolist: in a radiolist the arrow keys only move the + # highlight and is required to move the (*) dot, so a user + # who highlights another architecture and presses confirms + # the ORIGINAL choice -- the selection appears to do nothing. With + # --menu, selects the highlighted row directly. + default_item = None + for item in items: + item_opts.extend([item["id"], item["label"]]) + group = item.get("group") + cur = cfg.get("CONFIG_ARCH") if group == "arch" else ( + cfg.get("CONFIG_OPTIMIZATION") if group == "opt" else None) + if cur is not None and cur == item["val"]: + default_item = item["id"] + + cmd = ["dialog", "--clear", "--backtitle", backtitle, + "--title", submenu["title"], "--help-button"] + if default_item: + cmd += ["--default-item", default_item] + cmd += ["--menu", + "Highlight a choice with the arrow keys and press to select it.", + "20", "78", "10"] + item_opts + else: + for item in items: status = "ON" if cfg.get(item["id"], False) else "OFF" item_opts.extend([item["id"], item["label"], status]) - elif itype == "radio": - group = item.get("group") - if group == "arch": - status = "ON" if cfg.get("CONFIG_ARCH") == item["val"] else "OFF" - elif group == "opt": - status = "ON" if cfg.get("CONFIG_OPTIMIZATION") == item["val"] else "OFF" - else: - status = "OFF" - item_opts.extend([item["id"], item["label"], status]) - is_radiolist = any(i.get("type") == "radio" for i in items) - dialog_type = "--radiolist" if is_radiolist else "--checklist" - - cmd = [ - "dialog", - "--clear", - "--backtitle", backtitle, - "--title", submenu["title"], - "--help-button", - "--help-status", - dialog_type, - "Use to toggle features [*], to confirm, for info.", - "20", "78", "10" - ] + item_opts + cmd = ["dialog", "--clear", "--backtitle", backtitle, + "--title", submenu["title"], "--help-button", "--help-status", + "--checklist", + "Use to toggle features [*], to confirm, for info.", + "20", "78", "10"] + item_opts res = subprocess.run(cmd, stderr=subprocess.PIPE, text=True) ret = res.returncode out = res.stderr.strip() if ret == 0: # OK / Confirm - _apply_selection(cfg, items, [t.strip('"') for t in out.split()], is_radiolist) + if is_radio: + # --menu returns just the highlighted tag. + _apply_selection(cfg, items, [out.strip('"')], True) + else: + _apply_selection(cfg, items, [t.strip('"') for t in out.split()], False) current_menu_id = "main" elif ret == 1 or ret == 255: # Cancel or Esc current_menu_id = "main" elif ret == 2: # Help button - # With --help-status the payload is "HELP ". - # Re-apply the on-tags first so the toggles made before pressing - # survive the round trip, then show the focused item's help. - tokens = _help_tokens(out) - focus = tokens[0] if tokens else None - _apply_selection(cfg, items, tokens[1:], is_radiolist) + if is_radio: + # --menu help payload is just "HELP ". + focus = _help_tag(out) + else: + # --help-status payload is "HELP "; + # re-apply the on-tags so toggles made before survive. + tokens = _help_tokens(out) + focus = tokens[0] if tokens else None + _apply_selection(cfg, items, tokens[1:], False) focused = next((i for i in items if i["id"] == focus), None) if focused: help_title = f"{focused['label']} - Help" @@ -467,8 +479,10 @@ def run_dialog_gui(): subprocess.run(["dialog", "--backtitle", backtitle, "--title", help_title, "--msgbox", help_text, "16", "72"]) - save_config(cfg) - print(f"*** Configuration written to {CONFIG_FILE} and {AUTOCONF_HEADER} ***") + # Saving is handled inside the loop: the button writes immediately, + # and asks first and writes only on "Yes". Saving again here would + # override an explicit "No", so just report the resulting state. + print(f"*** Kernel configuration ready: {CONFIG_FILE} (ARCH={cfg.get('CONFIG_ARCH', 'x86_64')}) ***") # ----------------------------------------------------------------------------- # Pure Python Curses GUI Renderer (Pixel-Perfect Linux lxdialog Replica) diff --git a/security/auth.c b/security/auth.c index d13ace9..c749d6e 100644 --- a/security/auth.c +++ b/security/auth.c @@ -2,6 +2,7 @@ #include #include #include +#include static user_account_t user_table[MAX_USERS]; static size_t user_count = 0; @@ -123,3 +124,76 @@ const user_account_t* auth_get_user_by_index(size_t index) { if (index >= MAX_USERS || !user_table[index].in_use) return NULL; return &user_table[index]; } + +/* Read a line from the PS/2 keyboard into buf. When echo is false the typed + * characters are masked with '*' (password entry). Blocks until Enter. */ +static void login_read_line(char* buf, size_t max, bool echo) { + size_t len = 0; + while (true) { + uint16_t key = keyboard_get_key(); + if (key & KEY_SPECIAL_FLAG) { + continue; // ignore arrows / function keys at the login prompt + } + char c = (char)(key & 0xFF); + + if (c == '\n' || c == '\r') { + printk("\n"); + break; + } + if (c == '\b' || (unsigned char)c == 0x7F || c == 0x08) { + if (len > 0) { + len--; + printk("\b \b"); // erase the last glyph on screen + } + continue; + } + if ((unsigned char)c >= 32 && (unsigned char)c <= 126 && len < max - 1) { + buf[len++] = c; + if (echo) { + printk("%c", c); + } else { + printk("*"); + } + } + } + buf[len] = '\0'; +} + +void console_login(void) { + char username[32]; + char password[64]; + + printk("\n" ANSI_BRIGHT_CYAN + "=================================================================\n" + " SUB-OS Secure Login -- authenticate to start your session\n" + "=================================================================\n" + ANSI_RESET); + printk(ANSI_YELLOW " Default credentials: username " ANSI_BRIGHT_GREEN "SUB" + ANSI_YELLOW ", password " ANSI_BRIGHT_GREEN "SUB" ANSI_RESET "\n\n"); + + while (true) { + printk(ANSI_BRIGHT_GREEN "sub-os " ANSI_RESET "login: "); + login_read_line(username, sizeof(username), true); + if (username[0] == '\0') { + continue; // empty username, re-prompt + } + + printk("Password: "); + login_read_line(password, sizeof(password), false); + + bool ok = auth_verify_password(username, password); + + // Scrub the password from the stack regardless of the outcome. + memset(password, 0, sizeof(password)); + + if (ok) { + auth_set_current_user(username); + printk(ANSI_BRIGHT_GREEN + "\nLogin successful. Welcome to SUB-OS, %s!\n\n" ANSI_RESET, + username); + return; + } + + printk(ANSI_BRIGHT_RED "\nLogin incorrect. Please try again.\n\n" ANSI_RESET); + } +}