271 lines
8.6 KiB
Bash
271 lines
8.6 KiB
Bash
#!/usr/bin/env bash
|
|
#
|
|
# face-unlock: face unlock for Plasma, GNOME, Hyprland and Niri
|
|
#
|
|
# Look at the screen and it unlocks, the way a phone does. The lock screen,
|
|
# sudo in a terminal and the admin password prompts can all take a face
|
|
# instead of a password, with a check that it is a face and not a photo of one.
|
|
#
|
|
# This is the front end: the menu and the commands. The camera, the face data
|
|
# and the decision are the daemon's (face-unlockd, running as root),
|
|
# the lock screen and the bubble at the top of the screen are the agent's
|
|
# (face-unlock-agent, in the session), and sudo and polkit reach the
|
|
# daemon through a PAM module. See the man page for how the pieces fit.
|
|
#
|
|
# Copyright (C) 2026 Felitendo
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
set -uo pipefail
|
|
|
|
FU_SELF="$(readlink -f "${BASH_SOURCE[0]}")"
|
|
|
|
# Run as root (through sudo from the menu), only what was installed counts.
|
|
# An environment that points the libraries somewhere else is ignored then.
|
|
if [[ $EUID -eq 0 ]]; then
|
|
unset FU_LIBDIR FU_LIBEXECDIR FU_LOCALEDIR FU_PAMDIR FU_CTL FU_AGENT FU_PAM_MODULE FU_SYSCONFIG \
|
|
FU_PAM_ETC_DIR FU_PAM_VENDOR_DIRS FU_OLD_SYSDIR FU_OLD_STATEDIR FU_STATEDIR FU_SYSTEMD_ETC
|
|
fi
|
|
|
|
FU_LIBDIR="${FU_LIBDIR:-@LIBDIR@}"
|
|
|
|
for _mod in common config daemon pam system migrate menu; do
|
|
# shellcheck source=/dev/null
|
|
if ! source "$FU_LIBDIR/$_mod.sh"; then
|
|
printf 'face-unlock: cannot load %s/%s.sh\n' "$FU_LIBDIR" "$_mod" >&2
|
|
exit 14
|
|
fi
|
|
done
|
|
unset _mod
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Commands
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# The daemon is started by its socket. Enabling the socket is the one thing
|
|
# that needs root once per machine.
|
|
fu_ensure_service() {
|
|
fu_status_load && return 0
|
|
|
|
if ! fu_socket_enabled; then
|
|
fu_say " $(fu_msg "Face unlock's service has to be switched on once for this computer. That needs your password.")"
|
|
fu_root socket-enable || return 1
|
|
sleep 0.3
|
|
fi
|
|
fu_status_load && return 0
|
|
|
|
fu_bad "$(fu_reason_text unreachable)"
|
|
fu_note "$(fu_msg "Check it with: systemctl status %s" "$FU_UNIT_SOCKET")"
|
|
return 1
|
|
}
|
|
|
|
fu_do_setup() {
|
|
local name="${1:-}" rc
|
|
|
|
fu_ensure_service || return 1
|
|
|
|
if [[ -z ${WAYLAND_DISPLAY:-} && -z ${DISPLAY:-} ]]; then
|
|
fu_bad "$(fu_msg "Setting up a face needs the camera picture on screen. Run this inside your desktop session.")"
|
|
return 1
|
|
fi
|
|
|
|
fu_say " $(fu_msg "The setup window is open. Follow it there.")"
|
|
if [[ -n $name ]]; then
|
|
"$FU_AGENT" --enroll --name "$name" > /dev/null 2>&1
|
|
else
|
|
"$FU_AGENT" --enroll > /dev/null 2>&1
|
|
fi
|
|
rc=$?
|
|
|
|
if (( rc == 0 )); then
|
|
fu_ok "$(fu_msg "The face is set up.")"
|
|
fu_config_load
|
|
if [[ $CFG_ENABLED != yes ]]; then
|
|
fu_note "$(fu_msg "Face unlock is still off. Turn it on with [1] or \`%s enable\`." "$FU_NAME")"
|
|
fi
|
|
return 0
|
|
fi
|
|
fu_note "$(fu_msg "No face was added.")"
|
|
return 1
|
|
}
|
|
|
|
fu_do_enable() {
|
|
fu_ensure_service || return 1
|
|
|
|
fu_faces_load
|
|
if (( ${#FU_FACE_IDS[@]} == 0 )); then
|
|
fu_say " $(fu_msg "First, set up your face.")"
|
|
fu_do_setup || return 1
|
|
fi
|
|
|
|
fu_config_set Enabled yes || { fu_bad "$(fu_msg "Could not save the setting.")"; return 1; }
|
|
fu_config_load
|
|
|
|
if fu_agent_available; then
|
|
fu_agent_enable || fu_bad "$(fu_msg "Could not start the lock screen agent.")"
|
|
fu_agent_autostarts || fu_agent_hint
|
|
else
|
|
fu_note "$(fu_msg "No systemd user session, so the lock screen agent was not started.")"
|
|
fi
|
|
|
|
# sudo and admin prompts: on unless turned off in the settings.
|
|
if [[ $CFG_SUDO == yes ]] && fu_pam_available sudo && ! fu_pam_enabled sudo; then
|
|
fu_root pam-enable sudo
|
|
fi
|
|
if [[ $CFG_POLKIT == yes ]] && fu_pam_available polkit-1 && ! fu_pam_enabled polkit-1; then
|
|
fu_root pam-enable polkit-1
|
|
fi
|
|
# The lock screens of Hyprland, Niri and the like only open themselves:
|
|
# the face goes into their password check. On unless turned off.
|
|
if fu_pam_lockers_here && [[ $CFG_LOCKERS == yes ]] && ! fu_pam_lockers_enabled; then
|
|
fu_root pam-enable lockscreens
|
|
fi
|
|
|
|
case "$FU_DESKTOP" in
|
|
gnome)
|
|
fu_gnome_extension_enable
|
|
case $? in
|
|
1) fu_bad "$(fu_msg "Could not switch on the GNOME extension that shows the bubble.")" ;;
|
|
2) fu_note "$(fu_msg "Log out and back in once: then GNOME shows the bubble too.")" ;;
|
|
esac
|
|
;;
|
|
esac
|
|
# Where the lock screen is a program of its own: face-unlock's, with the
|
|
# bubble, or that program with a line of text. Asked once. Without
|
|
# face-unlock's own (older Qt) there is nothing to ask.
|
|
if [[ $FU_DESKTOP != plasma && $FU_DESKTOP != gnome && -z $(fu_config_get LockScreenStyle) ]]; then
|
|
if ! fu_own_lock_here; then
|
|
fu_lock_style_set yours
|
|
elif [[ -n ${WAYLAND_DISPLAY:-} ]]; then
|
|
fu_say " $(fu_msg "Pick your lock screen in the window.")"
|
|
fu_lock_choose || fu_note "$(fu_msg "No lock screen picked. You can do it later under Settings.")"
|
|
fi
|
|
fi
|
|
if fu_pam_lockers_here && fu_pam_lockers_enabled && [[ $(fu_config_get LockScreenStyle) != own ]]; then
|
|
fu_note "$(fu_msg "The lock screen (%s) takes your face too. If it does not scan when you come back, press Enter on the empty password field." "$(fu_pam_lockers_list)")"
|
|
fi
|
|
|
|
fu_ok "$(fu_msg "Face unlock is on. Lock the screen and look at it to try.")"
|
|
if [[ $CFG_SUDO != yes && $CFG_POLKIT != yes ]]; then
|
|
fu_note "$(fu_msg "It can do sudo and admin prompts too. See Settings.")"
|
|
fi
|
|
}
|
|
|
|
fu_do_disable() {
|
|
fu_config_set Enabled no || { fu_bad "$(fu_msg "Could not save the setting.")"; return 1; }
|
|
fu_agent_available && fu_agent_disable
|
|
[[ $FU_DESKTOP == gnome ]] && fu_gnome_extension_disable
|
|
|
|
local service
|
|
for service in "${FU_PAM_SERVICES[@]}" "${FU_PAM_LOCKERS[@]}"; do
|
|
if fu_pam_enabled "$service"; then
|
|
fu_root pam-disable "$service" || true
|
|
fi
|
|
done
|
|
|
|
fu_ok "$(fu_msg "Face unlock is off. Your faces are kept; delete them under Faces.")"
|
|
}
|
|
|
|
fu_do_status() {
|
|
fu_head " $FU_PRETTY"
|
|
fu_ui_status
|
|
printf '\n'
|
|
}
|
|
|
|
fu_do_faces() {
|
|
local i state
|
|
fu_status_load || { fu_bad "$(fu_reason_text unreachable)"; return 1; }
|
|
fu_faces_load
|
|
if (( ${#FU_FACE_IDS[@]} == 0 )); then
|
|
fu_note "$(fu_msg "No face is set up yet.")"
|
|
return 0
|
|
fi
|
|
for i in "${!FU_FACE_IDS[@]}"; do
|
|
if [[ ${FU_FACE_ON[i]} == true ]]; then state="$(fu_msg "on")"; else state="$(fu_msg "off")"; fi
|
|
printf ' %s %-24s %-4s %s\n' "${FU_FACE_IDS[i]}" "${FU_FACE_NAMES[i]}" "$state" \
|
|
"$(fu_msg "%s samples, %s learned" "${FU_FACE_SAMPLES[i]}" "${FU_FACE_LEARNED[i]}")"
|
|
done
|
|
}
|
|
|
|
fu_do_remove() {
|
|
local id="${1:-}" line
|
|
[[ -n $id ]] || { fu_bad "$(fu_msg "Which face? See \`%s faces\` for the ids." "$FU_NAME")"; return 1; }
|
|
line="$(fu_ctl remove "$id" | tail -n1)"
|
|
_fu_fields "$line"
|
|
if [[ ${FU_F[ok]:-} == true ]]; then
|
|
fu_ok "$(fu_msg "Deleted.")"
|
|
else
|
|
fu_bad "$(fu_reason_text "${FU_F[reason]:-unreachable}")"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
fu_do_help() {
|
|
cat <<- EOF
|
|
$FU_PRETTY $FU_VERSION
|
|
|
|
$(fu_msg "Usage: face-unlock [command]")
|
|
|
|
$(fu_msg "Commands:")
|
|
enable $(fu_msg "Turn face unlock on")
|
|
disable $(fu_msg "Turn it off (faces are kept)")
|
|
setup [NAME] $(fu_msg "Add a face")
|
|
faces $(fu_msg "List the faces")
|
|
remove ID $(fu_msg "Delete a face")
|
|
test $(fu_msg "Look at the camera and see what it sees")
|
|
lock $(fu_msg "Lock the screen")
|
|
status $(fu_msg "Show what is on")
|
|
-h, --help $(fu_msg "Show this help")
|
|
-V, --version $(fu_msg "Show the version")
|
|
|
|
$(fu_msg "Without a command an interactive menu is shown.")
|
|
EOF
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Dispatch
|
|
# ---------------------------------------------------------------------------
|
|
|
|
main() {
|
|
local cmd="${1:-}"
|
|
[[ $# -gt 0 ]] && shift
|
|
|
|
case "$cmd" in
|
|
--root) fu_root_verb "$@"; return ;;
|
|
esac
|
|
|
|
# Face data and settings are per user; root has neither a lock screen
|
|
# nor a face of its own to set up.
|
|
if [[ $EUID -eq 0 && -z ${FU_ALLOW_ROOT:-} ]]; then
|
|
fu_bad "$(fu_msg "Run this as your own user, not as root. It asks for your password when it needs to.")"
|
|
exit 2
|
|
fi
|
|
|
|
case "$cmd" in
|
|
''|enable|disable|setup|add|enroll|faces|list|remove|delete|test|try) fu_migrate ;;
|
|
esac
|
|
|
|
case "$cmd" in
|
|
enable) fu_do_enable ;;
|
|
disable) fu_do_disable ;;
|
|
setup|add|enroll) fu_do_setup "$@" ;;
|
|
faces|list) fu_do_faces ;;
|
|
remove|delete) fu_do_remove "$@" ;;
|
|
test|try) fu_ensure_service && fu_test ;;
|
|
status) fu_do_status ;;
|
|
lock) exec "$FU_AGENT" --lock ;;
|
|
|
|
-h|--help|help) fu_do_help ;;
|
|
-V|--version) printf '%s %s\n' "$FU_NAME" "$FU_VERSION" ;;
|
|
|
|
'') fu_ui_menu ;;
|
|
*)
|
|
fu_bad "$(fu_msg "Unknown command: %s" "$cmd")"
|
|
printf '\n'
|
|
fu_do_help
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|