#!/bin/bash

## Copyright (C) 2025 - 2025 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## See the file COPYING for copying conditions.

set -o errexit
set -o nounset
set -o errtrace
set -o pipefail

source /usr/libexec/helper-scripts/package_installed_check.sh

if [ -z "${PAM_USER:-}" ]; then
  true "$0: ERROR: Environment variable 'PAM_USER' is unset!"
  ## 'exit 0' here to let the appropriate PAM module handle this.
  exit 0
fi
if [ -z "${PAM_SERVICE:-}" ]; then
  true "$0: ERROR: Environment variable 'PAM_SERVICE' is unset!"
  ## 'exit 0' here to let the appropriate PAM module handle this.
  exit 0
fi

if ! pkg_installed 'user-sysmaint-split' ; then
  true "$0: INFO: user-sysmaint-split not installed. Proceeding, ok."
  exit 0
fi

kernel_cmdline=''
if [ -r /proc/cmdline ]; then
  kernel_cmdline="$(cat /proc/cmdline)"
elif [ -r /proc/1/cmdline ]; then
  kernel_cmdline="$(cat /proc/1/cmdline)"
fi

if [[ "$kernel_cmdline" =~ 'boot-role=sysmaint' ]]; then
  true "INFO: session type: sysmaint session"
  if [ "$PAM_USER" != 'sysmaint' ]; then
    printf '%s\n' "ERROR: Rejecting non-sysmaint account '$PAM_USER' in sysmaint session!"
    exit 1
  fi
  true 'INFO: Running in sysmaint session and authenticating as sysmaint account, allowing authentication to proceed.'
  exit 0
fi

true "INFO: session type: user session"

if [ "$PAM_USER" = 'sysmaint' ]; then
  printf '%s\n' 'ERROR: Rejecting sysmaint account in user session!'
  exit 1
fi

## Threat model:
## https://www.kicksecure.com/wiki/Dev/Strong_Linux_User_Account_Isolation#Block_Unsafe_Logins

login_service_list=( 'login' 'greetd' 'sshd' 'swaylock' )
for login_service in "${login_service_list[@]}"; do
  if [ "$PAM_SERVICE" = "$login_service" ]; then
    true "INFO: Login service '$PAM_SERVICE' is considered safe, allowing authentication to proceed."
    exit 0
  fi
done

true "INFO: Login service '$PAM_SERVICE' is potentially unsafe, checking if account is sensitive and passwordless."

if ! user_list_str="$(/usr/libexec/helper-scripts/get-user-list)"; then
  printf '%s\n' 'ERROR: Failed to get user list!'
  exit 1
fi
readarray -t user_list <<< "$user_list_str"
if [ "${#user_list[@]}" = '0' ] || [ -z "${user_list[0]}" ]; then
  printf '%s\n' 'ERROR: No user accounts found!'
  exit 1
fi

## Minor race condition here, quick deletion of users during this process
## could result in user_list and passwd_status_list becoming misaligned. This
## attack would require root privileges to execute though, so this is likely
## not a concern. We do this before checking if $PAM_USER is in the list of
## interactive users to keep the race window as short as possible.
##
## NOTE: PAM modules may run as non-root in some instances (such as when used
## by Swaylock).
if [ "$(id -u)" = '0' ]; then
  passwd_status_list_cmd=(
    '/usr/libexec/helper-scripts/get-password-status-list'
  )
else
  passwd_status_list_cmd=( 'leaprun' 'get-password-status-list' )
fi
if ! password_status_list_str="$("${passwd_status_list_cmd[@]}")"; then
  printf '%s\n' 'ERROR: Failed to get password status list!'
  exit 1
fi
readarray -t passwd_status_list <<< "$password_status_list_str"
if [ "${#passwd_status_list[@]}" = '0' ] \
  || [ -z "${passwd_status_list[0]}" ] \
  || (( ${#passwd_status_list[@]} != ${#user_list[@]} )); then
  printf '%s\n' 'ERROR: Unexpected number of password status entries!'
  exit 1
fi

interactive_user_idx='-1'
for user_idx in "${!user_list[@]}"; do
  if [ "${user_list[user_idx]}" = "$PAM_USER" ]; then
    interactive_user_idx="$user_idx"
    break
  fi
done
if [ "$interactive_user_idx" = '-1' ]; then
  ## This isn't a user account we care about (it's not an interactive
  ## account), therefore allow authentication to proceed.
  true "INFO: Account '$PAM_USER' is not an interactive account, allowing authentication to proceed."
  exit 0
fi

IFS=' ' read -r -a user_gid_list < <(id --groups -- "$PAM_USER")
sensitive_group_list=( 'sudo' 'root' 'sysmaint' )
is_user_sensitive='false'

for sensitive_group in "${sensitive_group_list[@]}"; do
  sensitive_gid="$(accountctl "$sensitive_group" get-entry group gid)"
  for user_gid in "${user_gid_list[@]}"; do
    if [ "$sensitive_gid" = "$user_gid" ]; then
      is_user_sensitive='true'
      break
    fi
  done
  if [ "$is_user_sensitive" = 'true' ]; then
    break
  fi
done

if [ "$is_user_sensitive" = 'true' ]; then
  if [ "${passwd_status_list[interactive_user_idx]}" = 'Absent' ]; then
    ## User account is sensitive and passwordless, deny authentication
    printf '%s\n' "ERROR: Rejecting passwordless sensitive account '$PAM_USER'!"
    exit 1
  else
    true "INFO: Account '$PAM_USER' is sensitive but protected, allowing authentication to proceed."
    exit 0
  fi
fi

true "INFO: Account '$PAM_USER' is not sensitive, allowing authentication to proceed."
exit 0
