Lock screen: password or 4-8 digit PIN, optional separate boot code

The unlock code and the boot lock code can each be a password or a
4-8 digit PIN. A PIN gets an on-screen number pad, so the lock works on
a touch-only kiosk. The boot lock can use the unlock code or its own.
These are the kiosk's own codes, set in Core Settings -> Password
Protection & Lockout or on the Web UI's Lockout page, never the admin
(sudo) password.

The lock screen moves from an inline data: URL to lockscreen.html. The
code is now hashed and compared in the main process, which also slows
down guessing: after every 5 wrong codes in a row, input is refused for
30s, doubling up to 5 minutes. Existing configs behave as before:
missing fields mean a password, and the same code at boot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm
This commit is contained in:
Claude
2026-10-10 15:26:59 +00:00
parent 600f5174d8
commit f215c64085
10 changed files with 554 additions and 116 deletions
+109 -24
View File
@@ -2,6 +2,11 @@
################################################################################
# menus/lockout.sh - "Password Protection & Lockout" menu.
#
# The unlock code and the (optional, separate) boot code are each either
# a password or a 4-8 digit PIN - the kiosk's own codes, independent of
# the admin account's sudo password. A PIN gets an on-screen number pad
# on the lock screen, so it works on a touch-only kiosk.
#
# 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
@@ -31,7 +36,11 @@ lockout_status() {
else
echo "Daily lock time: not set"
fi
echo "Password on boot: $(onoff "$REQUIRE_PASSWORD_ON_BOOT")"
echo "Unlock code: $(lockout_code_label "$LOCKOUT_CODE_TYPE")"
echo "Lock on boot: $(onoff "$REQUIRE_PASSWORD_ON_BOOT")"
if [[ "$REQUIRE_PASSWORD_ON_BOOT" == "true" ]]; then
echo "Boot code: $(lockout_boot_code_label)"
fi
else
echo "Password protection: disabled"
fi
@@ -40,10 +49,11 @@ lockout_status() {
lockout_menu_builder() {
if [[ "$ENABLE_PASSWORD_PROTECTION" == "true" ]]; then
MENU_LABELS=(
"Change lockout password"
"Change unlock password/PIN (currently: $(lockout_code_label "$LOCKOUT_CODE_TYPE"))"
"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"))"
"Toggle lock on boot (currently: $(onoff "$REQUIRE_PASSWORD_ON_BOOT"))"
"Set boot password/PIN (currently: $(lockout_boot_code_label))"
"Disable password protection"
)
MENU_HANDLERS=(
@@ -51,6 +61,7 @@ lockout_menu_builder() {
action_change_timeout
action_change_daily_lock
action_toggle_boot_password
action_set_boot_code
action_disable_protection
)
else
@@ -68,31 +79,74 @@ lockout_menu() {
# 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
# "PIN" / "password" for menus and status lines.
lockout_code_label() {
[[ "$1" == "pin" ]] && echo "PIN" || echo "password"
}
lockout_boot_code_label() {
if [[ -z "$BOOT_PASSWORD" ]]; then
echo "same as unlock"
else
echo "separate $(lockout_code_label "$BOOT_CODE_TYPE")"
fi
}
# True if $1 is a valid kiosk PIN: 4-8 digits.
lockout_valid_pin() {
[[ "$1" =~ ^[0-9]{4,8}$ ]]
}
# prompt_code WHAT - asks whether WHAT (e.g. "unlock") is a password or a
# 4-8 digit PIN, then for the code itself twice. Sets CODE_TYPE ("password"
# or "pin") and CODE_HASH (SHA-256 hex, matching the Electron app's check
# in main.js - never plaintext). These are the kiosk's own codes, not
# any Linux account's password.
prompt_code() {
local what="$1" choice
echo "How should the $what code be entered?"
echo " 1) Password (letters, numbers, anything - needs a keyboard)"
echo " 2) PIN (4-8 digits - on-screen number pad, works on a touch screen)"
while true; do
read -r -s -p "Enter password: " pass1
echo
read -r -s -p "Confirm password: " pass2
echo
read -r -p "Choose [1-2]: " choice
case "$choice" in
1) CODE_TYPE="password"; break ;;
2) CODE_TYPE="pin"; break ;;
*) echo "❌ Enter 1 or 2" ;;
esac
done
local label pass1 pass2
label=$(lockout_code_label "$CODE_TYPE")
while true; do
read -r -s -p "Enter $what $label: " pass1
echo
if [[ -z "$pass1" ]]; then
echo "❌ Password cannot be empty"
echo "❌ The $label cannot be empty"
continue
fi
if [[ "$CODE_TYPE" == "pin" ]] && ! lockout_valid_pin "$pass1"; then
echo "❌ A PIN is 4-8 digits (0-9 only)"
continue
fi
read -r -s -p "Confirm $what $label: " pass2
echo
if [[ "$pass1" != "$pass2" ]]; then
echo "❌ Passwords don't match, try again"
echo "❌ They don't match, try again"
continue
fi
LOCKOUT_PASSWORD=$(echo -n "$pass1" | sha256sum | cut -d' ' -f1)
CODE_HASH=$(echo -n "$pass1" | sha256sum | cut -d' ' -f1)
return 0
done
}
# Prompts for the unlock code into LOCKOUT_PASSWORD/LOCKOUT_CODE_TYPE.
prompt_and_hash_password() {
prompt_code "unlock"
LOCKOUT_CODE_TYPE="$CODE_TYPE"
LOCKOUT_PASSWORD="$CODE_HASH"
}
################################################################################
# Actions
################################################################################
@@ -101,13 +155,14 @@ 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 " • Password or PIN required to unlock"
echo " • Required after display schedule wake-up"
echo " • Optional: lock at a specific time daily"
echo " • Optional: require password on system boot"
echo " • Optional: locked on system boot, with the same or its own code"
echo
echo "These are the kiosk's own codes - not your admin (sudo) password."
echo
echo "Set lockout password:"
prompt_and_hash_password
echo
@@ -123,8 +178,16 @@ action_enable_protection() {
fi
echo
if ask_yes_no "Require password on system boot/power on?" "y"; then
BOOT_PASSWORD=""
BOOT_CODE_TYPE="password"
if ask_yes_no "Lock the kiosk on system boot/power on?" "y"; then
REQUIRE_PASSWORD_ON_BOOT="true"
echo
if ask_yes_no "Use a different password/PIN at boot than for unlocking?" "n"; then
prompt_code "boot"
BOOT_CODE_TYPE="$CODE_TYPE"
BOOT_PASSWORD="$CODE_HASH"
fi
else
REQUIRE_PASSWORD_ON_BOOT="false"
fi
@@ -140,6 +203,9 @@ action_disable_protection() {
LOCKOUT_TIMEOUT=0
LOCKOUT_AT_TIME=""
REQUIRE_PASSWORD_ON_BOOT="false"
LOCKOUT_CODE_TYPE="password"
BOOT_PASSWORD=""
BOOT_CODE_TYPE="password"
log_success "Password protection disabled"
save_config
}
@@ -147,7 +213,7 @@ action_disable_protection() {
action_change_password() {
echo
prompt_and_hash_password
log_success "Password updated"
log_success "Unlock $(lockout_code_label "$LOCKOUT_CODE_TYPE") updated"
save_config
}
@@ -178,10 +244,29 @@ action_change_daily_lock() {
action_toggle_boot_password() {
if [[ "$REQUIRE_PASSWORD_ON_BOOT" == "true" ]]; then
REQUIRE_PASSWORD_ON_BOOT="false"
log_warning "Password on boot disabled"
log_warning "Lock on boot disabled"
else
REQUIRE_PASSWORD_ON_BOOT="true"
log_success "Password on boot enabled"
log_success "Lock on boot enabled (boot code: $(lockout_boot_code_label))"
fi
save_config
}
action_set_boot_code() {
echo
echo "The boot lock screen can use its own password/PIN, or the unlock one."
echo "Currently: $(lockout_boot_code_label)"
echo
if ask_yes_no "Use a separate password/PIN at boot?" "y"; then
prompt_code "boot"
BOOT_CODE_TYPE="$CODE_TYPE"
BOOT_PASSWORD="$CODE_HASH"
REQUIRE_PASSWORD_ON_BOOT="true"
log_success "Boot $(lockout_code_label "$BOOT_CODE_TYPE") set (lock on boot: on)"
else
BOOT_PASSWORD=""
BOOT_CODE_TYPE="password"
log_success "Boot uses the unlock $(lockout_code_label "$LOCKOUT_CODE_TYPE")"
fi
save_config
}