#!/bin/sh

# List of required .so files
REQUIRED_MODULES="
/usr/lib/security/pam_faildelay.so
/usr/lib/security/pam_faillock.so
/usr/lib/security/pam_unix.so
/usr/lib/security/pam_deny.so
/usr/lib/security/pam_permit.so
/usr/lib/security/pam_passwdqc.so
"

MFA_APP="google-authenticator"
MFA_LIB="/usr/lib/security/pam_google_authenticator.so"
MFA_DIR="/etc/security"
MFA_SECRET_FILE="${MFA_DIR}/mfa_secret"
MFA_QR_FILE="${MFA_DIR}/mfa_qr"

log() {
	echo "$*" | logger -t sshd.init -p info
}

log_err() {
	echo "$*" | logger -t sshd.init -p err
}

check_required_modules() {
	for mod in $REQUIRED_MODULES; do
		if [ ! -f "$mod" ]; then
			log_err "ERROR: Cannot setup security policy, missing PAM module: $mod"
			return 1
		fi
	done

	return 0
}

write_line() {
	local filepath="$1"
	local line="$2"

	echo "$line" >> "$filepath"
}

compare_and_replace() {
	local src dst
	src="$1"
	dst="$2"

	if [ ! -f "$dst" ] || ! cmp -s "$src" "$dst"; then
		cp "$src" "$dst"
		log "Updated $dst"
	fi
}

setup_mfa() {
	mkdir -p "$MFA_DIR"

	if [ -f "${MFA_SECRET_FILE}" ] && [ -f "${MFA_QR_FILE}" ]; then
		log "Not updating MFA files, they already exist"
		return
	fi

	touch "$MFA_SECRET_FILE"
	touch "$MFA_QR_FILE"
	chmod 600 "$MFA_SECRET_FILE"
	chmod 600 "$MFA_QR_FILE"

	local cmd="$MFA_APP -f -s $MFA_SECRET_FILE -C -t -d -r 3 -R 30 -w 3 -Q UTF8 -e 3"

	log "Running MFA app: $cmd"
	# if the google-authenticator senses that the output is not going to a terminal
	# then it does not print out the QR, so simple redirection does not work
	# we have to run the command inside a pseudo-terminal using script command
	# and then redirect that output to a file
	cmd="script -q -c \"$cmd\" $MFA_QR_FILE > /dev/null"
	eval "$cmd"
}

update_auth() {
	# Write /etc/pam.d/sshd-auth
	local tmp_file pam_file
	tmp_file="/tmp/sshd-auth"
	pam_file="/etc/pam.d/sshd-auth"

	local auth_enabled="${1}"
	local enabled="${2}"
	local mfa_enabled="${3}"

	local faildelay="$(uci -q get users.authentication_policy.fail_delay || echo 3)"
	local faillock_lockout_time="$(uci -q get users.authentication_policy.faillock_lockout_time || echo 300)"
	local faillock_attempts="$(uci -q get users.authentication_policy.faillock_attempts || echo 6)"

	# Convert seconds to microseconds for pam_faildelay
	local faildelay_usec=$((faildelay * 1000000))

	rm -f "$tmp_file"
	touch "$tmp_file"

	if [ "${auth_enabled}" -eq 1 ] && [ "${enabled}" -eq 1 ]; then
		write_line "$tmp_file" "auth	optional	pam_faildelay.so delay=$faildelay_usec"
		write_line "$tmp_file" "auth	required	pam_faillock.so preauth deny=$faillock_attempts even_deny_root unlock_time=$faillock_lockout_time"
	fi

	write_line "$tmp_file" "auth	[success=1 default=bad]	pam_unix.so nullok_secure"
	write_line "$tmp_file" "auth	[default=die]	pam_faillock.so authfail audit deny=$faillock_attempts even_deny_root unlock_time=$faillock_lockout_time"

	if [ "$mfa_enabled" -eq 1 ]; then
		write_line "$tmp_file" "auth	[success=1 default=bad]	pam_google_authenticator.so secret=$MFA_SECRET_FILE"
		write_line "$tmp_file" "auth	[default=die]	pam_faillock.so authfail audit deny=$faillock_attempts even_deny_root unlock_time=$faillock_lockout_time"
	fi

	write_line "$tmp_file" "auth	sufficient	pam_faillock.so authsucc audit deny=$faillock_attempts even_deny_root unlock_time=$faillock_lockout_time"

	write_line "$tmp_file" "auth	requisite	pam_deny.so"

	compare_and_replace "$tmp_file" "$pam_file"
}

build_pam_passwdqc_line() {
	local base="password requisite pam_passwdqc.so"
	local k v line

	for line in $(uci show users.passwdqc 2>/dev/null); do
		case "$line" in
			users.passwdqc=*) continue ;;
			users.passwdqc.enabled=*) continue ;;
		esac

		k="${line%%=*}"
		k="${k#users.passwdqc.}"
		v="${line#*=}"
		v="${v%\'}"
		v="${v#\'}"
		base="$base $k=$v"
	done

	base="$base match=0"

	echo "$base"
}

# NOTE:
# for some reason setting min 8 makes passwdqc accept minimum 12 letter password with this configuration
# if we set it to 12 then we need atleast 16 characters and so on
# passphrase = 0 means no space separated words
# passphrase = N means the number of words required for a passphrase or 0 to disable the support for user-chosen passphrases.
# rest can be figured out from passwdqc man page
update_password() {
	local tmp_file pam_file enabled line
	tmp_file="/tmp/sshd-password"
	pam_file="/etc/pam.d/sshd-password"

	local auth_enabled="${1}"

	rm -f "$tmp_file"
	touch "$tmp_file"

	# Check if section exists
	if uci -q get users.passwdqc >/dev/null 2>&1; then
		# if enabled is not present it is assumed to be 0
		enabled=$(uci -q get users.passwdqc.enabled || echo "0")
		if [ "${auth_enabled}" -eq 1 ] && [ "${enabled}" -eq 1 ]; then
			line="$(build_pam_passwdqc_line)"
			write_line "$tmp_file" "$line"
		fi
	fi

	write_line "$tmp_file" "password	[success=1 default=ignore]	pam_unix.so sha512"
	write_line "$tmp_file" ""
	write_line "$tmp_file" "password	requisite			pam_deny.so"
	write_line "$tmp_file" "password	required			pam_permit.so"

	compare_and_replace "$tmp_file" "$pam_file"
}

update_account() {
	# Write /etc/pam.d/sshd-account
	local tmp_file pam_file
	tmp_file="/tmp/sshd-account"
	pam_file="/etc/pam.d/sshd-account"

	local auth_enabled="${1}"
	local enabled="${2}"

	rm -f "$tmp_file"
	touch "$tmp_file"

	if [ "${auth_enabled}" -eq 1 ] && [ "${enabled}" -eq 1 ]; then
		write_line "$tmp_file" "account 	required       		pam_faillock.so"
	fi

	write_line "$tmp_file" "account		[success=1 new_authtok_reqd=done default=ignore]	pam_unix.so"
	write_line "$tmp_file" ""
	write_line "$tmp_file" "account		requisite		pam_deny.so"
	write_line "$tmp_file" "account		required		pam_permit.so"

	compare_and_replace "$tmp_file" "$pam_file"
}

handle_security_policy() {
	local auth_enabled enabled
	local use_mfa mfa_enabled

	# Read UCI values
	auth_enabled="$(uci -q get users.users.auth_policy_enable || echo 0)"
	enabled="$(uci -q get users.authentication_policy.enabled || echo 0)"
	use_mfa="$(uci -q get sshd.@sshd[0].UseMFA || echo 0)"

	# if any .so files are missing, then we cannot setup security
	if ! check_required_modules; then
		return
	fi

	# Detect and enable MFA only if requested and module exists
	if which "$MFA_APP" > /dev/null 2>&1 && [ "$use_mfa" = "1" ] && [ -f "$MFA_LIB" ]; then
		mfa_enabled=1
		setup_mfa
	else
		rm -rf "${MFA_QR_FILE}"
		rm -rf "${MFA_SECRET_FILE}"
		mfa_enabled=0
	fi

	update_auth "${auth_enabled}" "${enabled}" "${mfa_enabled}"
	update_account "${auth_enabled}" "${enabled}"
	update_password "${auth_enabled}"
}
