Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 26 additions & 21 deletions snow_first_setup/gtk/install-disk.ui
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,15 @@
<object class="AdwToolbarView">
<property name="content">
<object class="AdwStatusPage" id="status_page">
<property name="icon-name">computer-symbolic</property>
<property name="title" translatable="yes">Select Installation Disk</property>
<property name="description" translatable="yes">Choose the physical disk where the system will be installed. All data on the selected disk will be erased.</property>
<property name="description" translatable="yes">All data on the selected disk will be erased.</property>
<child>
<object class="AdwClamp">
<property name="maximum-size">720</property>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">24</property>
<property name="margin-top">12</property>
<property name="spacing">12</property>
<child>
<object class="AdwPreferencesGroup" id="disks_group">
<property name="title" translatable="yes">Available Disks</property>
Expand All @@ -26,11 +24,10 @@

<child>
<object class="AdwPreferencesGroup">
<property name="title" translatable="yes">Filesystem</property>
<property name="title" translatable="yes">Options</property>
<child>
<object class="AdwComboRow" id="fs_combo">
<property name="title" translatable="yes">Filesystem Type</property>
<property name="subtitle" translatable="yes">Select the filesystem for the installation</property>
<property name="title" translatable="yes">Filesystem</property>
<property name="model">
<object class="GtkStringList">
<items>
Expand All @@ -41,28 +38,36 @@
</property>
</object>
</child>
</object>
</child>

<child>
<object class="AdwPreferencesGroup" id="fde_group">
<property name="title" translatable="yes">Encryption</property>
<child>
<object class="AdwActionRow" id="fde_row">
<property name="title" translatable="yes">(Broken) Full Disk Encryption (LUKS)</property>
<child type="suffix">
<object class="GtkCheckButton" id="fde_checkbox">
<property name="valign">center</property>
</object>
</child>
<object class="AdwSwitchRow" id="fde_row">
<property name="title" translatable="yes">Full Disk Encryption</property>
<signal name="notify::active" handler="__on_fde_toggled" swapped="no"/>
</object>
</child>
<child>
<object class="AdwPasswordEntryRow" id="passphrase_entry">
<property name="title" translatable="yes">Passphrase</property>
<property name="visible">false</property>
</object>
</child>
<child>
<object class="AdwPasswordEntryRow" id="passphrase_confirm_entry">
<property name="title" translatable="yes">Confirm Passphrase</property>
<property name="visible">false</property>
</object>
</child>
<child>
<object class="AdwSwitchRow" id="tpm_row">
<property name="title" translatable="yes">TPM Auto-unlock</property>
<property name="visible">false</property>
</object>
</child>
</object>
</child>

<child>
<object class="GtkLabel" id="no_disks_label">
<property name="label" translatable="yes">No removable disks were found. If you expect a disk to appear, ensure it is connected and try again.</property>
<property name="label" translatable="yes">No disks found. Ensure your disk is connected and try again.</property>
<property name="visible">false</property>
<property name="wrap">true</property>
<property name="wrap-mode">word-char</property>
Expand Down
36 changes: 32 additions & 4 deletions snow_first_setup/scripts/install-to-disk
Original file line number Diff line number Diff line change
Expand Up @@ -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 <image> <filesystem> <device> [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 <image> <filesystem> <device> [fde]"}'
if [ -z "${1:-}" ] || [ -z "${2:-}" ] || [ -z "${3:-}" ]; then
echo '{"type":"error","message":"Missing arguments. Usage: install-to-disk <image> <filesystem> <device> [fde] [passphrase] [tpm2]"}'
exit 5
fi

Expand All @@ -23,13 +31,27 @@ IMAGE="$1"
FILESYSTEM="$2"
DEVICE="$3"
FDE="${4:-false}"
PASSPHRASE="${5:-}"
TPM2="${6:-false}"

# Validate FDE parameter
if [ "$FDE" != "true" ] && [ "$FDE" != "false" ]; then
echo '{"type":"error","message":"FDE parameter must be '"'true'"' or '"'false'"', got: '"$FDE"'"}'
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"}'
Expand All @@ -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
Expand Down
6 changes: 5 additions & 1 deletion snow_first_setup/views/install_confirm.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ def set_page_active(self):
self.fs_label.set_text(_("<none>"))
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"))

Expand Down
156 changes: 143 additions & 13 deletions snow_first_setup/views/install_disk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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):
Expand Down Expand Up @@ -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()
15 changes: 12 additions & 3 deletions snow_first_setup/views/install_progress.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,16 +148,25 @@ 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."))
return

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(
Expand Down
Loading