#!/bin/bash

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

## To enable debug log, run:
## sudo touch /etc/pam-info-debug
##
## Debug log if enabled can be found in file:
## /root/pam-info-debug.txt

true "$0: START PHASE 1"

if test -f /etc/pam-info-debug || test -f /usr/local/etc/pam-info-debug ; then
   set -x
   exec 5>&1 1>> ~/pam-info-debug.txt
   exec 6>&2 2>> ~/pam-info-debug.txt
fi

true "$0: START PHASE 2"

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

error_handler() {
   exit_code="$?"
   printf '%s\n' "\
$0: ERROR: Unexpected error.
BASH_COMMAND: '$BASH_COMMAND'
exit_code: '$exit_code'
ERROR: Please report this bug." >&2
   exit 1
}

trap error_handler ERR

if ! printf '%s\n' "" | wc -l >/dev/null ; then
  printf '%s\n' "\
$0: ERROR: command 'wc' test failed! Do not ignore this!

'wc' can core dump. Example:
zsh: illegal hardware instruction (core dumped) wc -l
https://github.com/rspamd/rspamd/issues/5137" >&2
  exit 1
fi

command -v str_replace &>/dev/null

## Named constants.
pam_faillock_state_dir="/var/lib/security-misc/faillock"

[[ -v PAM_USER ]] || PAM_USER=""
[[ -v SUDO_USER ]] || SUDO_USER=""

## Debugging.
who_ami="$(whoami)"
true "$0: who_ami: $who_ami"
true "$0: PAM_USER: $PAM_USER"
true "$0: SUDO_USER: $SUDO_USER"

if [ "$PAM_USER" = "" ]; then
   true "$0: ERROR: Environment variable PAM_USER is unset!"
   exit 0
fi

## PAM_USER is set by the PAM stack and is generally trusted.
## Sanity check: reject control characters and values starting with '-'
## to prevent option injection into commands like faillock.
if [[ "$PAM_USER" =~ [[:cntrl:]] ]] || [[ "$PAM_USER" == -* ]]; then
  true "$0: ERROR: PAM_USER contains suspicious characters: '$(printf '%q' "$PAM_USER")'"
  exit 0
fi

grep_result="$(grep -- "accessfile=/etc/security/access-security-misc.conf" /etc/pam.d/common-account 2>/dev/null)" || true

## Check if grep matched something.
if [ ! "$grep_result" = "" ]; then
   ## Yes, grep matched.

   ## Check if not out commented.
   if ! printf '%s\n' "$grep_result" | grep --quiet -- "#" ; then
      ## Not out commented indeed.

      ## https://forums.whonix.org/t/etc-security-hardening-console-lockdown/8592

      console_allowed=""
      if id --name --groups --zero -- "$PAM_USER" | grep --quiet --null-data --line-regexp --fixed-strings -- "console"; then
         console_allowed=true
      fi
      if id --name --groups --zero -- "$PAM_USER" | grep --quiet --null-data --line-regexp --fixed-strings -- "console-unrestricted"; then
         console_allowed=true
      fi

      if [ ! "$console_allowed" = "true" ]; then
         printf '%s\n' "\
$0: ERROR: PAM_USER: '$PAM_USER' is not a member of group 'console'
To unlock, run the following command as superuser:
(If you still have a sudo/root shell somewhere.)

adduser $PAM_USER console

However, possibly unlock procedure is required.
First boot into recovery mode at grub boot menu and then run above command.
See also:
https://www.kicksecure.com/wiki/root#console
" >&2
         exit 0
      fi
   fi
fi

if [ "$PAM_USER" = 'sysmaint' ]; then
   sysmaint_passwd_info="$(passwd --status sysmaint 2>/dev/null)" || true
   sysmaint_lock_info="$(cut -d' ' -f2 <<< "${sysmaint_passwd_info}")"
   if [ "${sysmaint_lock_info}" = 'L' ]; then
      printf '%s\n' "$0: ERROR: Reboot and choose 'PERSISTENT Mode - SYSMAINT Session' for system maintenance. See https://www.kicksecure.com/wiki/Sysmaint" >&2
   fi
fi

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

if [ "$PAM_USER" != 'sysmaint' ]; then
   if [[ "${kernel_cmdline}" =~ 'boot-role=sysmaint' ]]; then
      printf '%s\n' "$0: WARNING: Use account 'sysmaint' for system maintenance. See https://www.kicksecure.com/wiki/Sysmaint" >&2
   fi
fi

## https://forums.whonix.org/t/how-strong-do-linux-user-account-passwords-have-to-be-when-using-full-disk-encryption-fde-too/7698

## Does not work (yet) for login, pam_securetty runs before and aborts.
## Also this should only run for login since securetty covers only login.
# if [ "$PAM_USER" = "root" ]; then
#    if [ -f /etc/securetty ]; then
#       grep_result="$(grep -- "^[^#]" /etc/securetty)"
#       if [ "$grep_result" = "" ]; then
#          printf '%s\n' "\
# $0: ERROR: Root login is disabled.
# ERROR: This is because file '/etc/securetty' is empty.
# See also:
# https://www.kicksecure.com/wiki/root#login
# " >&2
#          exit 0
#       fi
#    fi
# fi

## under account "user"
## /usr/sbin/faillock -u user
## faillock: Error opening /var/log/tallylog for update: Permission denied
## /usr/sbin/faillock: Authentication error
##
## xscreensaver runs under account "user", therefore pam_faillock cannot function.
## xscreensaver has its own failed login counter.
##
## https://askubuntu.com/questions/983183/how-lock-the-unlock-screen-after-wrong-password-attempts
##
## https://web.archive.org/web/20200919221439/https://www.whonix.org/pipermail/whonix-devel/2019-September/001439.html
##
## Checking exit code to avoid breaking when read-only disk boot but
## without ro-mode-init or grub-live being used.
##
## end-of-options ("--") unsupported by faillock.
if ! pam_faillock_output="$(faillock --dir "$pam_faillock_state_dir" --user "$PAM_USER")" ; then
   true "$0: faillock non-zero exit code."
   exit 0
fi

if [ "$pam_faillock_output" = "" ]; then
   true "$0: no failed login"
   exit 0
fi

## example pam_faillock_output (stdout):
## user:
## When                Type  Source                                           Valid
## 2021-08-10 16:26:33 RHOST                                                      V
## 2021-08-10 16:26:54 RHOST                                                      V

## example pam_faillock_output (stderr):
## faillock: No user name supplied.
## Usage: faillock [--dir /path/to/tally-directory] [--user username] [--reset]

## Get first line.
#pam_faillock_output_first_line="$(printf '%s\n' "$pam_faillock_output" | head --lines=1)"
while read -t 10 -r pam_faillock_output_first_line ; do
   break
done <<< "$pam_faillock_output"

true "pam_faillock_output_first_line: '$pam_faillock_output_first_line'"
## example pam_faillock_output_first_line:
## user:

user_name="$(printf '%s\n' "$pam_faillock_output_first_line" | str_replace ":" "")"
## example user_name:
## user
## root

if [ "$PAM_USER" != "$user_name" ]; then
   printf '%s\n' "\
$0: ERROR: Variable 'PAM_USER' '$PAM_USER' does not match variable 'user_name' '$user_name'.
ERROR: Please report this bug.
" >&2
   exit 1
fi

pam_faillock_output_count="$(printf '%s\n' "$pam_faillock_output" | wc -l)"
## example pam_faillock_output_count:
## 2
## example pam_faillock_output_count:
## 4

if [[ "$pam_faillock_output_count" == *[!0-9]* ]]; then
   printf '%s\n' "\
$0: ERROR: Variable 'pam_faillock_output_count' is not numeric. pam_faillock_output_count: '$pam_faillock_output_count'
ERROR: Please report this bug.
" >&2
   exit 0
fi

## Do not count the first two informational textual output lines (starting with "user:" and "When") if present,
failed_login_counter=$(( pam_faillock_output_count - 2 ))

## example failed_login_counter:
## 2

## Ensuring failed_login_counter is not set to a negative value.
## https://github.com/Kicksecure/security-misc/pull/305
if [ "$failed_login_counter" -lt "0" ]; then
   true "$0: WARNING: Failed login counter is negative. Resetting to 0."
   failed_login_counter=0
fi

if [ "$failed_login_counter" = "0" ]; then
   true "$0: INFO: Failed login counter is 0, ok."
   exit 0
fi

## pam_faillock default if it cannot be determined below.
deny=3

if test -f /etc/security/faillock.conf ; then
   deny_line=$(grep --invert-match "#" -- /etc/security/faillock.conf | grep -- "deny =") || true
   deny="$(printf '%s\n' "$deny_line" | str_replace "=" "" | str_replace "deny" "" | str_replace " " "")"
   ## Example:
   #deny=50
fi

if [[ "$deny" == *[!0-9]* ]]; then
   printf '%s\n' "\
$0: ERROR: Variable 'deny' is not numeric. deny: '$deny'
ERROR: Please report this bug.
" >&2
   exit 0
fi

remaining_attempts="$(( deny - failed_login_counter ))"

if [ "$remaining_attempts" -le "0" ]; then
   printf '%s\n' "\
$0: ERROR: Login blocked after $failed_login_counter attempts.
To unlock, run the following command as superuser:
(If you still have a sudo/root shell somewhere.)

faillock --dir $pam_faillock_state_dir --reset --user $PAM_USER

However, most likely unlock procedure is required.
First boot into recovery mode at grub boot menu and then run above command.
See also:
https://www.kicksecure.com/wiki/root#unlock
" >&2
   exit 0
fi

printf '%s\n' "\
$0: WARNING: $failed_login_counter failed login attempts for account '$user_name'.
Login will be blocked after $deny attempts.
You have $remaining_attempts more attempts before unlock procedure is required.
" >&2

if [ "$PAM_SERVICE" = "su" ]; then
   printf '%s\n' "\
$0: NOTE: Type the password. When entering the password, no password feedback (no asterisk (\"*\") symbol) will be shown.
" >&2
fi

true "$0: END"

exit 0
