Files
Claude 8672c15469 Migrate Password Protection & Lockout menu; add missing ask_time helper; bump to v2.2.0
Fifth menu migrated onto lib/menu.sh + lib/config.sh: menus/lockout.sh
covers enable/disable, changing the password, inactivity timeout, daily
lock time, and boot password. The password is SHA-256 hashed before
it's ever assigned to LOCKOUT_PASSWORD (matching main.js's comparison
logic) - verified by test that the stored value is the correct hash and
never plaintext.

Rewrote the legacy configure_password_protection's linear "ask
everything, confirm save at the end" wizard as the same immediate-save
pattern used by every other migrated menu: each action (change
password, change timeout, toggle boot password, ...) is a complete,
standalone change, consistent with Sites/Display/Timezone/Hidden PIN.
LOCKOUT_ACTIVE_START/END are deliberately left untouched - per the
Readme they're inert leftover fields the app ignores, so lib/config.sh
just carries whatever is already in config.json through unchanged.

Testing this menu surfaced a real gap before it ever shipped: lib/menu.sh
never had ask_time/validate_time at all (only validate_integer/ask_integer,
ask_url, etc were ported when the framework was first built) - "set a
daily lock time" would have failed for every single user with
"ask_time: command not found". Ported both from the legacy script.

Also promoted the ON/OFF toggle-label helper (previously private to
menus/display.sh as display_onoff) to a shared onoff() in lib/menu.sh,
since menus/lockout.sh needed the same thing and menu files should only
ever depend on lib/, never on each other.

Bumped SCRIPT_VERSION to 2.2.0 with matching changelog entries in the
script header and Readme, and updated "Modular Management" to list the
new menu and drop Password Protection & Lockout from the "not yet
migrated" list.

Verified:
- Full regression: re-ran the Sites, Display, Timezone/PIN scratch-config
  suites after every change in this round (the onoff refactor, and again
  after adding ask_time) - all still clean.
- New scratch-config test for lockout.sh: enable (password+timeout+daily
  lock+boot toggle), independently recomputed the expected SHA-256 hash
  and confirmed it matches config.json exactly, change password, change
  timeout, clear daily lock, toggle boot password, disable (confirmed
  every field clears), and that the menu builder's options correctly
  differ between the enabled and disabled states.
- End-to-end: ran the real install.sh as a genuine non-root, non-"kiosk"
  user - Lockout menu -> enable protection with a real password entered
  via the masked prompt -> set 20m timeout, 23:00 daily lock, boot
  password on -> confirmed the menu redraws with the new state -> clean
  exit (code 0). Checked the resulting config.json and file permissions
  on disk.
2026-08-18 16:48:30 +00:00

188 lines
6.3 KiB
Bash

#!/bin/bash
################################################################################
# menus/lockout.sh - "Password Protection & Lockout" menu.
#
# Fifth menu migrated. Back to config.json (like Display), but with a
# sensitive field: the lockout password is SHA-256 hashed before it's
# ever written to disk (matching the Electron app's comparison logic in
# main.js) - LOCKOUT_PASSWORD must never hold plaintext.
#
# Unlike the legacy configure_password_protection wizard (walk through
# every question once, then one final "save these changes? y/n"), this
# follows the same immediate-save pattern as every other migrated menu:
# each action is a complete, standalone change. Re-running "Enable" to
# change your mind is just as easy as the old "discard changes" path,
# and there's no separate confirm-at-the-end step to forget.
#
# LOCKOUT_ACTIVE_START/LOCKOUT_ACTIVE_END are intentionally never touched
# here - the app doesn't act on them (see Readme "Configuration Files"),
# so lib/config.sh just carries whatever is already in config.json
# through unchanged.
#
# Depends on: lib/menu.sh, lib/config.sh being sourced first.
################################################################################
lockout_status() {
if [[ "$ENABLE_PASSWORD_PROTECTION" == "true" ]]; then
echo "Password protection: ENABLED"
echo "Inactivity lockout: ${LOCKOUT_TIMEOUT} minutes$( [[ "$LOCKOUT_TIMEOUT" == "0" ]] && echo " (disabled - boot/wake only)")"
if [[ -n "$LOCKOUT_AT_TIME" ]]; then
echo "Daily lock time: $LOCKOUT_AT_TIME"
else
echo "Daily lock time: not set"
fi
echo "Password on boot: $(onoff "$REQUIRE_PASSWORD_ON_BOOT")"
else
echo "Password protection: disabled"
fi
}
lockout_menu_builder() {
if [[ "$ENABLE_PASSWORD_PROTECTION" == "true" ]]; then
MENU_LABELS=(
"Change lockout password"
"Change inactivity lockout timeout (currently: ${LOCKOUT_TIMEOUT}m)"
"Set/clear daily lock time (currently: ${LOCKOUT_AT_TIME:-not set})"
"Toggle require password on boot (currently: $(onoff "$REQUIRE_PASSWORD_ON_BOOT"))"
"Disable password protection"
)
MENU_HANDLERS=(
action_change_password
action_change_timeout
action_change_daily_lock
action_toggle_boot_password
action_disable_protection
)
else
MENU_LABELS=("Enable password protection")
MENU_HANDLERS=(action_enable_protection)
fi
}
lockout_menu() {
load_existing_config
run_menu "PASSWORD PROTECTION & LOCKOUT" lockout_menu_builder lockout_status
}
################################################################################
# Shared helpers
################################################################################
# Prompts for a new password twice, hashes it, and assigns to
# LOCKOUT_PASSWORD. Returns 1 (without saving) if the user gives up.
prompt_and_hash_password() {
local pass1 pass2
while true; do
read -r -s -p "Enter password: " pass1
echo
read -r -s -p "Confirm password: " pass2
echo
if [[ -z "$pass1" ]]; then
echo "❌ Password cannot be empty"
continue
fi
if [[ "$pass1" != "$pass2" ]]; then
echo "❌ Passwords don't match, try again"
continue
fi
LOCKOUT_PASSWORD=$(echo -n "$pass1" | sha256sum | cut -d' ' -f1)
return 0
done
}
################################################################################
# Actions
################################################################################
action_enable_protection() {
echo
echo "Add password protection with automatic lockout:"
echo " • Blank screen after an inactivity period"
echo " • Password required to unlock"
echo " • Password required after display schedule wake-up"
echo " • Optional: lock at a specific time daily"
echo " • Optional: require password on system boot"
echo
echo "Set lockout password:"
prompt_and_hash_password
echo
echo "Session lockout time (minutes of inactivity)."
echo "Enter 0 to only require a password after display wake or boot."
LOCKOUT_TIMEOUT=$(ask_integer "Lockout timeout in minutes" "30" 0 1440)
echo
if ask_yes_no "Lock automatically at a specific time each day?" "n"; then
LOCKOUT_AT_TIME=$(ask_time "Time to lock (24-hour HH:MM)" "17:00")
else
LOCKOUT_AT_TIME=""
fi
echo
if ask_yes_no "Require password on system boot/power on?" "y"; then
REQUIRE_PASSWORD_ON_BOOT="true"
else
REQUIRE_PASSWORD_ON_BOOT="false"
fi
ENABLE_PASSWORD_PROTECTION="true"
log_success "Password protection enabled (lockout: ${LOCKOUT_TIMEOUT}m)"
save_config
}
action_disable_protection() {
ENABLE_PASSWORD_PROTECTION="false"
LOCKOUT_PASSWORD=""
LOCKOUT_TIMEOUT=0
LOCKOUT_AT_TIME=""
REQUIRE_PASSWORD_ON_BOOT="false"
log_success "Password protection disabled"
save_config
}
action_change_password() {
echo
prompt_and_hash_password
log_success "Password updated"
save_config
}
action_change_timeout() {
echo
echo "Session lockout time (minutes of inactivity)."
echo "Enter 0 to only require a password after display wake or boot."
LOCKOUT_TIMEOUT=$(ask_integer "Lockout timeout in minutes" "$LOCKOUT_TIMEOUT" 0 1440)
log_success "Lockout timeout: ${LOCKOUT_TIMEOUT}m"
save_config
}
action_change_daily_lock() {
echo
local default_prompt
[[ -n "$LOCKOUT_AT_TIME" ]] && default_prompt="y" || default_prompt="n"
if ask_yes_no "Lock automatically at a specific time each day?" "$default_prompt"; then
LOCKOUT_AT_TIME=$(ask_time "Time to lock (24-hour HH:MM)" "${LOCKOUT_AT_TIME:-17:00}")
log_success "Will lock at ${LOCKOUT_AT_TIME} daily"
else
LOCKOUT_AT_TIME=""
log_success "Daily lock time cleared"
fi
save_config
}
action_toggle_boot_password() {
if [[ "$REQUIRE_PASSWORD_ON_BOOT" == "true" ]]; then
REQUIRE_PASSWORD_ON_BOOT="false"
log_warning "Password on boot disabled"
else
REQUIRE_PASSWORD_ON_BOOT="true"
log_success "Password on boot enabled"
fi
save_config
}