diff --git a/snow_first_setup/gtk/install-disk.ui b/snow_first_setup/gtk/install-disk.ui index 3ae261a..d16c16d 100644 --- a/snow_first_setup/gtk/install-disk.ui +++ b/snow_first_setup/gtk/install-disk.ui @@ -7,17 +7,15 @@ - computer-symbolic Select Installation Disk - Choose the physical disk where the system will be installed. All data on the selected disk will be erased. + All data on the selected disk will be erased. 720 vertical - 24 - 12 + 12 Available Disks @@ -26,11 +24,10 @@ - Filesystem + Options - Filesystem Type - Select the filesystem for the installation + Filesystem @@ -41,20 +38,28 @@ - - - - - - Encryption - - (Broken) Full Disk Encryption (LUKS) - - - center - - + + Full Disk Encryption + + + + + + Passphrase + false + + + + + Confirm Passphrase + false + + + + + TPM Auto-unlock + false @@ -62,7 +67,7 @@ - No removable disks were found. If you expect a disk to appear, ensure it is connected and try again. + No disks found. Ensure your disk is connected and try again. false true word-char diff --git a/snow_first_setup/scripts/install-to-disk b/snow_first_setup/scripts/install-to-disk index eb7a5e7..4238f8a 100755 --- a/snow_first_setup/scripts/install-to-disk +++ b/snow_first_setup/scripts/install-to-disk @@ -3,9 +3,17 @@ set -euo pipefail # Install script using nbc (SNOW bootc installer) # Outputs JSON Lines for streaming progress to the installer GUI +# +# Usage: install-to-disk [fde] [passphrase] [tpm2] +# image - container image reference +# filesystem - filesystem type (btrfs, ext4) +# device - block device path +# fde - "true" or "false" for full disk encryption (default: false) +# passphrase - encryption passphrase (required if fde=true) +# tpm2 - "true" or "false" for TPM2 auto-unlock (default: false) -if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then - echo '{"type":"error","message":"Missing arguments. Usage: install-to-disk [fde]"}' +if [ -z "${1:-}" ] || [ -z "${2:-}" ] || [ -z "${3:-}" ]; then + echo '{"type":"error","message":"Missing arguments. Usage: install-to-disk [fde] [passphrase] [tpm2]"}' exit 5 fi @@ -23,6 +31,8 @@ IMAGE="$1" FILESYSTEM="$2" DEVICE="$3" FDE="${4:-false}" +PASSPHRASE="${5:-}" +TPM2="${6:-false}" # Validate FDE parameter if [ "$FDE" != "true" ] && [ "$FDE" != "false" ]; then @@ -30,6 +40,18 @@ if [ "$FDE" != "true" ] && [ "$FDE" != "false" ]; then exit 7 fi +# Validate TPM2 parameter +if [ "$TPM2" != "true" ] && [ "$TPM2" != "false" ]; then + echo '{"type":"error","message":"TPM2 parameter must be '"'true'"' or '"'false'"', got: '"$TPM2"'"}' + exit 8 +fi + +# If FDE is enabled, require a passphrase +if [ "$FDE" == "true" ] && [ -z "$PASSPHRASE" ]; then + echo '{"type":"error","message":"FDE is enabled but no passphrase provided"}' + exit 10 +fi + # Check that nbc is available if ! command -v nbc &> /dev/null; then echo '{"type":"error","message":"nbc command not found, cannot proceed with installation"}' @@ -45,9 +67,15 @@ NBC_ARGS=( "--json" ) -# Add FDE flag if enabled +# Add FDE options if enabled if [ "$FDE" == "true" ]; then - NBC_ARGS+=("--fde") + NBC_ARGS+=("--encrypt") + NBC_ARGS+=("--password" "$PASSPHRASE") + + # Add TPM2 auto-unlock if requested + if [ "$TPM2" == "true" ]; then + NBC_ARGS+=("--tpm2") + fi fi # Run nbc install with JSON output streaming directly to stdout diff --git a/snow_first_setup/views/install_confirm.py b/snow_first_setup/views/install_confirm.py index c210b5e..20a77ed 100644 --- a/snow_first_setup/views/install_confirm.py +++ b/snow_first_setup/views/install_confirm.py @@ -63,7 +63,11 @@ def set_page_active(self): self.fs_label.set_text(_("")) if getattr(self, 'fde_label', None) is not None: if fde_enabled: - self.fde_label.set_text(_("Enabled")) + tpm_enabled = getattr(self.__window, "install_tpm_enabled", False) + if tpm_enabled: + self.fde_label.set_text(_("Enabled (TPM auto-unlock)")) + else: + self.fde_label.set_text(_("Enabled")) else: self.fde_label.set_text(_("Disabled")) diff --git a/snow_first_setup/views/install_disk.py b/snow_first_setup/views/install_disk.py index 4fe3061..f0a374c 100644 --- a/snow_first_setup/views/install_disk.py +++ b/snow_first_setup/views/install_disk.py @@ -19,26 +19,74 @@ class VanillaInstallDisk(Adw.Bin): disks_group = Gtk.Template.Child() no_disks_label = Gtk.Template.Child() fs_combo = Gtk.Template.Child() - fde_checkbox = Gtk.Template.Child() + fde_row = Gtk.Template.Child() + passphrase_entry = Gtk.Template.Child() + passphrase_confirm_entry = Gtk.Template.Child() + tpm_row = Gtk.Template.Child() def __init__(self, window, **kwargs): super().__init__(**kwargs) self.__window = window self.__selected_device = None + # Persisted FDE settings + self.__fde_enabled = False + self.__fde_passphrase = "" + self.__fde_passphrase_confirm = "" + self.__tpm_enabled = False # store tuples of (action_row, radio_button) self.__rows = [] - # wire up FDE checkbox behavior (currently no extra inputs) + # wire up FDE switch behavior try: - self.fde_checkbox.connect("toggled", self.__on_fde_toggled) + self.fde_row.connect("notify::active", self.__on_fde_toggled) + except Exception: + pass + # wire up passphrase entry changes + try: + self.passphrase_entry.connect("changed", self.__on_passphrase_changed) + self.passphrase_confirm_entry.connect("changed", self.__on_passphrase_changed) + except Exception: + pass + # wire up TPM switch + try: + self.tpm_row.connect("notify::active", self.__on_tpm_toggled) except Exception: pass def set_page_active(self): # Refresh available disks each time the page becomes active GLib.idle_add(self.refresh_drives) + # Restore persisted FDE settings to UI + self.__restore_fde_settings() def set_page_inactive(self): - return + # Persist current FDE settings before leaving the page + self.__save_fde_settings() + + def __save_fde_settings(self): + """Save current FDE UI state to instance variables.""" + try: + self.__fde_enabled = self.fde_row.get_active() + self.__fde_passphrase = self.passphrase_entry.get_text() + self.__fde_passphrase_confirm = self.passphrase_confirm_entry.get_text() + self.__tpm_enabled = self.tpm_row.get_active() + except Exception: + pass + + def __restore_fde_settings(self): + """Restore persisted FDE settings to UI widgets.""" + try: + self.fde_row.set_active(self.__fde_enabled) + self.passphrase_entry.set_text(self.__fde_passphrase) + self.passphrase_confirm_entry.set_text(self.__fde_passphrase_confirm) + self.tpm_row.set_active(self.__tpm_enabled) + # Update visibility based on FDE state + self.passphrase_entry.set_visible(self.__fde_enabled) + self.passphrase_confirm_entry.set_visible(self.__fde_enabled) + self.tpm_row.set_visible(self.__fde_enabled) + if self.__fde_enabled: + self.__update_passphrase_validation_ui() + except Exception: + pass def finish(self): # Called when the user presses next. Ensure a device is selected and store it on the window. @@ -61,11 +109,15 @@ def finish(self): fs = "btrfs" self.__window.install_target_fs = fs - # Handle Full Disk Encryption selection (no passphrase inputs) - try: - self.__window.install_fde_enabled = bool(self.fde_checkbox.get_active()) - except Exception: - self.__window.install_fde_enabled = False + # Handle Full Disk Encryption selection - save and use persisted values + self.__save_fde_settings() + self.__window.install_fde_enabled = self.__fde_enabled + if self.__fde_enabled: + self.__window.install_fde_passphrase = self.__fde_passphrase + self.__window.install_tpm_enabled = self.__tpm_enabled + else: + self.__window.install_fde_passphrase = None + self.__window.install_tpm_enabled = False return True def refresh_drives(self): @@ -154,10 +206,88 @@ def __on_radio_toggled(self, radio, path): else: row.remove_css_class("selected") + self.__update_ready_state() + + def __validate_passphrase(self): + """Check if passphrase meets requirements: min 8 chars and both fields match.""" + passphrase = self.passphrase_entry.get_text() + confirm = self.passphrase_confirm_entry.get_text() + if len(passphrase) < 8: + return False + if passphrase != confirm: + return False + return True + + def __update_passphrase_validation_ui(self): + """Update visual validation state for passphrase fields.""" + passphrase = self.passphrase_entry.get_text() + confirm = self.passphrase_confirm_entry.get_text() + + # Validate passphrase field: must be at least 8 characters + if len(passphrase) == 0: + self.passphrase_entry.add_css_class("error") + elif len(passphrase) < 8: + self.passphrase_entry.add_css_class("error") + else: + self.passphrase_entry.remove_css_class("error") + + # Validate confirmation field: must match passphrase + if len(confirm) == 0 or confirm != passphrase: + self.passphrase_confirm_entry.add_css_class("error") + else: + self.passphrase_confirm_entry.remove_css_class("error") + + def __clear_passphrase_validation_ui(self): + """Clear validation error styling from passphrase fields.""" + try: + self.passphrase_entry.remove_css_class("error") + self.passphrase_confirm_entry.remove_css_class("error") + except Exception: + pass + + def __update_ready_state(self): + """Update the window ready state based on disk selection and FDE passphrase validity.""" + if not self.__selected_device: + self.__window.set_ready(False) + return + # If FDE is enabled, passphrase must be valid + try: + if self.fde_row.get_active(): + if not self.__validate_passphrase(): + self.__window.set_ready(False) + return + except Exception: + pass self.__window.set_ready(True) + def __on_passphrase_changed(self, entry): + """Called when either passphrase field changes.""" + self.__update_passphrase_validation_ui() + self.__save_fde_settings() + self.__update_ready_state() - def __on_fde_toggled(self, checkbox): - # No passphrase inputs to manage; optionally could trigger readiness update. - # Keep selection gating on disk radio buttons only. - return + def __on_fde_toggled(self, switch, pspec): + """Enable/disable passphrase fields and TPM row based on FDE selection.""" + fde_active = switch.get_active() + try: + # Show/hide encryption-related rows + self.passphrase_entry.set_visible(fde_active) + self.passphrase_confirm_entry.set_visible(fde_active) + self.tpm_row.set_visible(fde_active) + # If FDE is disabled, reset TPM and clear passphrases + if not fde_active: + self.tpm_row.set_active(False) + self.passphrase_entry.set_text("") + self.passphrase_confirm_entry.set_text("") + self.__clear_passphrase_validation_ui() + else: + # Show validation state when FDE is enabled + self.__update_passphrase_validation_ui() + except Exception: + pass + self.__save_fde_settings() + self.__update_ready_state() + + def __on_tpm_toggled(self, switch, pspec): + """Called when TPM auto-unlock is toggled.""" + self.__save_fde_settings() diff --git a/snow_first_setup/views/install_progress.py b/snow_first_setup/views/install_progress.py index 47dd05c..9370c9c 100644 --- a/snow_first_setup/views/install_progress.py +++ b/snow_first_setup/views/install_progress.py @@ -148,7 +148,9 @@ def __run_install(self): fs = getattr(self.__window, "install_target_fs", None) image = getattr(self.__window, "install_target_image", None) fde_enabled = getattr(self.__window, "install_fde_enabled", False) - print("[DEBUG] __run_install params:", device, fs, image, "fde_enabled:", fde_enabled) + fde_passphrase = getattr(self.__window, "install_fde_passphrase", None) or "" + tpm_enabled = getattr(self.__window, "install_tpm_enabled", False) + print("[DEBUG] __run_install params:", device, fs, image, "fde_enabled:", fde_enabled, "tpm_enabled:", tpm_enabled) if not device or not fs or not image: GLib.idle_add(self.__mark_finished, False, _("Missing installation parameters.")) @@ -156,8 +158,15 @@ def __run_install(self): GLib.idle_add(self.detail_label.set_text, _("Preparing installation…")) - # Build script arguments with FDE parameters - script_args = [image, fs, device, "true" if fde_enabled else "false"] + # Build script arguments: image, filesystem, device, fde, passphrase, tpm2 + script_args = [ + image, + fs, + device, + "true" if fde_enabled else "false", + fde_passphrase if fde_enabled else "", + "true" if (fde_enabled and tpm_enabled) else "false" + ] # Use streaming script runner to get real-time JSON updates success = backend.run_script_streaming(