#!/bin/bash
# Shell functions for most initialization scripts
. /etc/ash_functions

# Print <hidden> or <empty> depending on whether $1 is empty.  Useful to mask an
# optional password parameter.
mask_param() {
	if [ -z "$1" ]; then
		echo "<empty>"
	else
		echo "<hidden>"
	fi
}

# Pipe input to this to sink it to the debug log, with a name prefix.
# If the input is empty, no output is produced, so actual output is
# readily visible in logs.
#
# For example:
# ls /boot/vmlinux* | SINK_LOG "/boot kernels"
SINK_LOG() {
	local name="$1"
	local line haveblank
	# If the input doesn't end with a line break, read won't give us the
	# last (unterminated) line.  Add a line break with echo to ensure we
	# don't lose any input.  Buffer up to one blank line so we can avoid
	# emitting a final (or only) blank line.
	(
		cat
		echo
	) | while IFS= read -r line; do
		[[ -n "$haveblank" ]] && DEBUG "$name: " # Emit buffered blank line
		if [[ -z "$line" ]]; then
			haveblank=y
		else
			haveblank=
			LOG "$name: $line"
		fi
	done
}

# Trace a command with DEBUG, then execute it.  Trace failed exit status, stdout
# and stderr, etc.
#
# DO_WITH_DEBUG is designed so it can be dropped in to most command invocations
# without side effects - it adds visibility without actually affecting the
# execution of the script.  Exit statuses, stdout, and stderr are traced, but
# they are still returned/written to the caller.
#
# A password parameter can be masked by passing --mask-position N before the
# command to execute, the debug trace will just indicate whether the password
# was empty or nonempty (which is important when use of a password is optional).
# N=0 is the name of the command to be executed, N=1 is its first parameter,
# etc.
#
# DO_WITH_DEBUG() can be added in most places where a command is executed to
# add visibility in the debug log.  For example:
#
# [DO_WITH_DEBUG] mount "$BLOCK" "$MOUNTPOINT"
#   ^-- adding DO_WITH_DEBUG will show the block device, mountpoint, and whether
#   the mount fails
#
# [DO_WITH_DEBUG --mask-position 7] tpmr seal "$KEY" "$IDX" "$pcrs" "$pcrf" "$size" "$PASSWORD"
#   ^-- trace the resulting invocation, but mask the password in the log
#
# if ! [DO_WITH_DEBUG] umount "$MOUNTPOINT"; then [...]
#   ^-- it can be used when the exit status is checked, like the condition of `if`
#
# hotp_token_info="$([DO_WITH_DEBUG] hotp_verification info)"
#   ^-- output of hotp_verification info becomes visible in debug log while
#   still being captured by script
#
# [DO_WITH_DEBUG] umount "$MOUNTPOINT" &>/dev/null || true
#   ^-- if the command's stdout/stderr/failure are ignored, this still works the
#   same way with DO_WITH_DEBUG
DO_WITH_DEBUG() {
	local exit_status=0
	local cmd_output
	if [[ "$1" == "--mask-position" ]]; then
		local mask_position="$2"
		shift
		shift
		local show_args=("$@")
		show_args[$mask_position]="$(mask_param "${show_args[$mask_position]}")"
		DEBUG "${show_args[@]}"
	else
		DEBUG "$@"
	fi

	# Execute the command and capture the exit status. Tee stdout/stderr to
	# debug sinks, so they're visible but still can be used by the caller
	#
	# This is tricky when set -e / set -o pipefail may or may not be in
	# effect.
	# - Putting the command in an `if` ensures set -e won't terminate us,
	#   and also does not overwrite $? (like `|| true` would).
	# - We capture PIPESTATUS[0] whether the command succeeds or fails,
	#   since we don't know whether the pipeline status will be that of the
	#   command or 'tee' (depends on set -o pipefail).
	if ! "$@" 2> >(tee /dev/stderr | SINK_LOG "$1 stderr") | tee >(SINK_LOG "$1 stdout"); then
		exit_status="${PIPESTATUS[0]}"
	else
		exit_status="${PIPESTATUS[0]}"
	fi
	if [[ "$exit_status" -ne 0 ]]; then
		# Trace unsuccessful exit status, but only at DEBUG because this
		# may be expected.  Include the command name in case the command
		# also invoked a DO_WITH_DEBUG (it could be a script).
		DEBUG "$1: exited with status $exit_status"
	fi
	# If the command was (probably) not found, trace PATH in case it
	# prevented the command from being found
	if [[ "$exit_status" -eq 127 ]]; then
		DEBUG "$1: PATH=$PATH"
	fi

	return "$exit_status"
}

# Trace the current script and function.
TRACE_FUNC() {
	# Index [1] for BASH_SOURCE and FUNCNAME give us the caller location.
	# FUNCNAME is 'main' if called from a script outside any function.
	# BASH_LINENO is offset by 1, it provides the line that the
	# corresponding FUNCNAME was _called from_, so BASH_LINENO[0] is the
	# location of the caller.
	TRACE "${BASH_SOURCE[1]}(${BASH_LINENO[0]}): ${FUNCNAME[1]}"
}

# Show the entire current call stack in debug output - useful if a catastrophic
# error or something very unexpected occurs, like totally invalid parameters.
DEBUG_STACK() {
	local FRAMES
	FRAMES="${#FUNCNAME[@]}"
	DEBUG "call stack: ($((FRAMES - 1)) frames)"
	# Don't print DEBUG_STACK itself, start from 1
	for i in $(seq 1 "$((FRAMES - 1))"); do
		DEBUG "- $((i - 1)) - ${BASH_SOURCE[$i]}(${BASH_LINENO[$((i - 1))]}): ${FUNCNAME[$i]}"
	done
}

pcrs() {
	if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then
		tpm2 pcrread sha256
	elif [ "$CONFIG_TPM" = "y" ]; then
		head -8 /sys/class/tpm/tpm0/pcrs
	fi
}

confirm_totp() {
	TRACE_FUNC
	prompt="$1"
	last_half=X
	unset totp_confirm

	while true; do

		# update the TOTP code every thirty seconds
		date=$(date "+%Y-%m-%d %H:%M:%S")
		seconds=$(date "+%s")
		half=$(expr \( $seconds % 60 \) / 30)
		if [ "$CONFIG_TPM" != "y" ]; then
			TOTP="NO TPM"
		elif [ "$half" != "$last_half" ]; then
			last_half=$half
			TOTP=$(unseal-totp) ||
				recovery "TOTP code generation failed"
		fi

		echo -n "$date $TOTP: "

		# read the first character, non-blocking
		read \
			-t 1 \
			-n 1 \
			-s \
			-p "$prompt" \
			totp_confirm &&
			break

		# nothing typed, redraw the line
		echo -ne '\r'
	done

	# clean up with a newline
	echo
}

reseal_tpm_disk_decryption_key() {
	TRACE_FUNC
	#For robustness, exit early if LUKS TPM Disk Unlock Key is prohibited in board configs
	if [ "$CONFIG_TPM_DISK_UNLOCK_KEY" == "n" ]; then
		DEBUG "LUKS TPM Disk Unlock Key is prohibited in board configs"
		return
	else
		DEBUG "LUKS TPM Disk Unlock Key is allowed in board configs. Continuing"
	fi

	if ! grep -q /boot /proc/mounts; then
		mount -o ro /boot ||
			recovery "Unable to mount /boot"
	fi

	if [ -s /boot/kexec_key_devices.txt ] || [ -s /boot/kexec_key_lvm.txt ]; then
		warn "LUKS TPM sealed Disk Unlock Key secret needs to be resealed alongside TOTP/HOTP secret"
		echo "Resealing LUKS TPM Disk Unlock Key to be unsealed by LUKS TPM Disk Unlock Key passphrase"
		while ! kexec-seal-key /boot; do
			warn "Recovery Disk Encryption key passphrase/TPM Owner Password may be invalid. Please try again"
		done
		warn "LUKS header hash changed under /boot/kexec_luks_hdr_hash.txt"
		echo "Updating checksums and signing all files under /boot/kexec.sig"
		while ! update_checksums; do
			warn "Checksums were not signed. Preceding errors should explain possible causes"
		done
		warn "Rebooting in 3 seconds to enable booting default boot option"
		sleep 3
		reboot
	else
		DEBUG "No TPM disk decryption key to reseal"
	fi
}

# Enable USB storage (if not already enabled), and wait for storage devices to
# be detected.  If USB storage was already enabled, no wait occurs, this would
# have happened already when USB storage was enabled.
enable_usb_storage() {
	TRACE_FUNC
	if ! lsmod | grep -q usb_storage; then
		timeout=0
		echo "Scanning for USB storage devices..."
		insmod /lib/modules/usb-storage.ko >/dev/null 2>&1 ||
			die "usb_storage: module load failed"
		while [[ $(list_usb_storage | wc -l) -eq 0 ]]; do
			[[ $timeout -ge 8 ]] && break
			sleep 1
			timeout=$(($timeout + 1))
		done
	fi
}

device_has_partitions() {
	local DEVICE="$1"
	# fdisk normally says "doesn't contain a valid partition table" for
	# devices that lack a partition table - except for FAT32.
	#
	# FAT32 devices have a volume boot record that looks enough like an MBR
	# to satisfy fdisk.  In that case, fdisk prints a partition table header
	# but no partitions.
	#
	# This check covers that: [ $(fdisk -l "$b" | wc -l) -eq 5 ]
	# In both cases the output is 5 lines: 3 about device info, 1 empty line
	# and the 5th will be the table header or the invalid message.
	local DISK_DATA=$(fdisk -l "$DEVICE")
	if echo "$DISK_DATA" | grep -q "doesn't contain a valid partition table" ||
		[ "$(echo "$DISK_DATA" | wc -l)" -eq 5 ]; then
		# No partition table
		return 1
	fi
	# There is a partition table
	return 0
}

list_usb_storage() {
	TRACE_FUNC
	# List all USB storage devices, including partitions unless we received argument stating we want drives only
	# The output is a list of device names, one per line.

	if [ "$1" = "disks" ]; then
		DEBUG "Listing USB storage devices (disks only) since list_usb_storage was called with 'disks' argument"
	else
		DEBUG "Listing USB storage devices (including partitions)"
	fi

	stat -c %N /sys/block/sd* 2>/dev/null | grep usb |
		cut -f1 -d ' ' |
		sed "s/[']//g" |
		while read b; do
			# Ignore devices of size 0, such as empty SD card
			# readers on laptops attached via USB.
			if [ "$(cat "$b/size")" -gt 0 ]; then
				DEBUG "USB storage device of size greater then 0: $b"
				echo "$b"
			fi
		done |
		sed "s|/sys/block|/dev|" |
		while read b; do
			# If the device has a partition table, ignore it and
			# include the partitions instead - even if the kernel
			# hasn't detected the partitions yet.  Such a device is
			# never usable directly, and this allows the "wait for
			# disks" loop in mount-usb to correctly wait for the
			# partitions.
			if ! device_has_partitions "$b"; then
				# No partition table, include this device
				DEBUG "USB storage device without partition table: $b"
				echo "$b"
			#Bypass the check for partitions if we want only disks
			elif [ "$1" = "disks" ]; then
				# disks only were requested, so we don't list partitions
				DEBUG "USB storage device with partition table: $b"
				DEBUG "We asked for disks only, so we don't want to list partitions"
				echo "$b"
			else
				# Has a partition table, include partitions
				DEBUG "USB storage device with partition table: $b"
				ls -1 "$b"* | awk 'NR!=1 {print $0}'
			fi
		done
}

# Prompt for a TPM Owner Password if it is not already cached in /tmp/secret/tpm_owner_password.
# Sets tpm_owner_password variable reused in flow, and cache file used until recovery shell is accessed.
# Tools should optionally accept a TPM password on the command line, since some flows need
# it multiple times and only one prompt is ideal.
prompt_tpm_owner_password() {
	TRACE_FUNC

	if [ -s /tmp/secret/tpm_owner_password ]; then
		DEBUG "/tmp/secret/tpm_owner_password already cached in file. Reusing"
		tpm_owner_password=$(cat /tmp/secret/tpm_owner_password)
		return 0
	fi

	read -s -p "TPM Owner Password: " tpm_owner_password
	echo # new line after password prompt

	# Cache the password externally to be reused by who needs it
	DEBUG "Caching TPM Owner Password to /tmp/secret/tpm_owner_password"
	mkdir -p /tmp/secret || die "Unable to create /tmp/secret"
	echo -n "$tpm_owner_password" >/tmp/secret/tpm_owner_password || die "Unable to cache TPM owner_password under /tmp/secret/tpm_owner_password"
}

# Prompt for a new TPM Owner Password when resetting the TPM.
# Returned in tpm_owner_passpword and cached under /tpm/secret/tpm_owner_password
# The password must be 1-32 characters and must be entered twice,
# the script will loop until this is met.
prompt_new_owner_password() {
	TRACE_FUNC
	local tpm_owner_password2
	tpm_owner_password=1
	tpm_owner_password2=2
	while [ "$tpm_owner_password" != "$tpm_owner_password2" ] || [ "${#tpm_owner_password}" -gt 32 ] || [ -z "$tpm_owner_password" ]; do
		read -s -p "New TPM Owner Password (2 words suggested, 1-32 characters max): " tpm_owner_password
		echo

		read -s -p "Repeat chosen TPM Owner Password: " tpm_owner_password2
		echo

		if [ "$tpm_owner_password" != "$tpm_owner_password2" ]; then
			echo "Passphrases entered do not match. Try again!"
			echo
		fi
	done

	# Cache the password externally to be reused by who needs it
	DEBUG "Caching TPM Owner Password to /tmp/secret/tpm_owner_password"
	mkdir -p /tmp/secret || die "Unable to create /tmp/secret"
	echo -n "$tpm_owner_password" >/tmp/secret/tpm_owner_password || die "Unable to cache TPM password under /tmp/secret"
}

check_tpm_counter() {
	TRACE_FUNC

	LABEL=${2:-3135106223}
	tpm_password="$3"
	# if the /boot.hashes file already exists, read the TPM counter ID
	# from it.
	if [ -r "$1" ]; then
		TPM_COUNTER=$(grep counter- "$1" | cut -d- -f2)
	else
		warn "$1 does not exist; creating new TPM counter"
		tpmr counter_create \
			-pwdc '' \
			-la $LABEL |
			tee /tmp/counter ||
			die "Unable to create TPM counter"
		TPM_COUNTER=$(cut -d: -f1 </tmp/counter)
	fi

	if [ -z "$TPM_COUNTER" ]; then
		die "$1: TPM Counter not found?"
	fi
}

read_tpm_counter() {
	TRACE_FUNC
	tpmr counter_read -ix "$1" | tee "/tmp/counter-$1" ||
		die "Counter read failed"
}

increment_tpm_counter() {
	TRACE_FUNC
	tpmr counter_increment -ix "$1" -pwdc '' |
		tee /tmp/counter-$1 ||
		die "TPM counter increment failed for rollback prevention. Please reset the TPM"
}

check_config() {
	TRACE_FUNC
	if [ ! -d /tmp/kexec ]; then
		mkdir /tmp/kexec ||
			die 'Failed to make kexec tmp dir'
	else
		rm -rf /tmp/kexec/* ||
			die 'Failed to empty kexec tmp dir'
	fi

	if [ ! -r $1/kexec.sig -a "$CONFIG_BASIC" != "y" ]; then
		return
	fi

	if [ $(find $1/kexec*.txt | wc -l) -eq 0 ]; then
		return
	fi

	if [ "$2" != "force" ]; then
		if ! sha256sum $(find $1/kexec*.txt) | gpgv $1/kexec.sig -; then
			die 'Invalid signature on kexec boot params'
		fi
	fi

	echo "+++ Found verified kexec boot params"
	cp $1/kexec*.txt /tmp/kexec ||
		die "Failed to copy kexec boot params to tmp"
}

# Replace a file in a ROM (add it if the file does not exist)
replace_rom_file() {
	ROM="$1"
	ROM_FILE="$2"
	NEW_FILE="$3"

	if (cbfs.sh -o "$ROM" -l | grep -q "$ROM_FILE"); then
		cbfs.sh -o "$ROM" -d "$ROM_FILE"
	fi
	cbfs.sh -o "$ROM" -a "$ROM_FILE" -f "$NEW_FILE"
}

replace_config() {
	TRACE_FUNC
	CONFIG_FILE=$1
	CONFIG_OPTION=$2
	NEW_SETTING=$3

	touch $CONFIG_FILE
	# first pull out the existing option from the global config and place in a tmp file
	awk "gsub(\"^export ${CONFIG_OPTION}=.*\",\"export ${CONFIG_OPTION}=\\\"${NEW_SETTING}\\\"\")" /tmp/config >${CONFIG_FILE}.tmp
	awk "gsub(\"^${CONFIG_OPTION}=.*\",\"${CONFIG_OPTION}=\\\"${NEW_SETTING}\\\"\")" /tmp/config >>${CONFIG_FILE}.tmp

	# then copy any remaining settings from the existing config file, minus the option you changed
	grep -v "^export ${CONFIG_OPTION}=" ${CONFIG_FILE} | grep -v "^${CONFIG_OPTION}=" >>${CONFIG_FILE}.tmp || true
	sort ${CONFIG_FILE}.tmp | uniq >${CONFIG_FILE}
	rm -f ${CONFIG_FILE}.tmp
}

# Generate a secret for TPM-less HOTP by reading the ROM.  Output is the
# sha256sum of the ROM (binary, not printable), which can be truncated to the
# supported secret length.
secret_from_rom_hash() {
	local ROM_IMAGE="/tmp/coreboot-notpm.rom"

	echo -e "\nTPM not detected; measuring ROM directly\n" 1>&2

	# Read the ROM if we haven't read it yet
	if [ ! -f "${ROM_IMAGE}" ]; then
		flash.sh -r "${ROM_IMAGE}" >/dev/null 2>&1 || return 1
	fi

	sha256sum "${ROM_IMAGE}" | cut -f1 -d ' ' | fromhex_plain
}

update_checksums() {
	TRACE_FUNC
	# ensure /boot mounted
	if ! grep -q /boot /proc/mounts; then
		mount -o ro /boot ||
			recovery "Unable to mount /boot"
	fi

	# remount RW
	mount -o rw,remount /boot

	# sign and auto-roll config counter
	extparam=
	if [ "$CONFIG_TPM" = "y" ]; then
		if [ "$CONFIG_IGNORE_ROLLBACK" != "y" ]; then
			extparam=-r
		fi
	fi
	if ! kexec-sign-config -p /boot -u $extparam; then
		rv=1
	else
		rv=0
	fi

	# switch back to ro mode
	mount -o ro,remount /boot

	return $rv
}

print_tree() {
	TRACE_FUNC
	find ./ ! -path './kexec*' -print0 | sort -z
}

# Escape zero-delimited standard input to safely display it to the user in e.g.
# `whiptail`, `less`, `echo`, `cat`. Doesn't produce shell-escaped output.
# Most printable characters are passed verbatim (exception: \).
# These escapes are used to replace their corresponding characters: #n#r#t#v#b
# Other characters are rendered as hexadecimal escapes.
# escape_zero [prefix] [escape character]
# prefix: \0 in the input will result in \n[prefix]
# escape character: character to use for escapes (default: #); \ may be interpreted by `whiptail`
escape_zero() {
	local prefix="$1"
	local echar="${2:-#}"
	local todo=""
	local echar_hex="$(echo -n "$echar" | xxd -p -c1)"
	[ ${#echar_hex} -eq 2 ] || die "Invalid escape character $echar passed to escape_zero(). Programming error?!"

	echo -e -n "$prefix"
	xxd -p -c1 | tr -d '\n' |
		{
			while IFS= read -r -n2 -d ''; do
				if [ -n "$todo" ]; then
					#REPLY == "  " is EOF
					[[ "$REPLY" == "  " ]] && echo '' || echo -e -n "$todo"
					todo=""
				fi

				case "$REPLY" in
				00)
					todo="\n$prefix"
					;;
				08)
					echo -n "${echar}b"
					;;
				09)
					echo -n "${echar}t"
					;;
				0a)
					echo -n "${echar}n"
					;;
				0b)
					echo -n "${echar}v"
					;;
				0d)
					echo -n "${echar}r"
					;;
				"$echar_hex")
					echo -n "$echar$echar"
					;;
				#interpreted characters:
				2[0-9a-f] | 3[0-9a-f] | 4[0-9a-f] | 5[0-9abd-f] | 6[0-9a-f] | 7[0-9a-e])
					echo -e -n '\x'"$REPLY"
					;;
				# All others are escaped
				*)
					echo -n "${echar}x$REPLY"
					;;
				esac
			done
		}
}

# Currently heads doesn't support signing file names with certain characters
# due to https://bugs.busybox.net/show_bug.cgi?id=14226. Also, certain characters
# may be intepreted by `whiptail`, `less` et al (e.g. \n, \b, ...).
assert_signable() {
	TRACE_FUNC
	# ensure /boot mounted
	detect_boot_device

	find /boot -print0 >/tmp/signable.ref
	local del='\001-\037\134\177-\377'
	LC_ALL=C tr -d "$del" </tmp/signable.ref >/tmp/signable.del || die "Failed to execute tr."
	if ! cmp -s "/tmp/signable.ref" "/tmp/signable.del" &>/dev/null; then
		local user_out="/tmp/hash_output_mismatches"
		local add="Please investigate!"
		[ -f "$user_out" ] && add="Please investigate the following relative paths to /boot (where # are sanitized invalid characters):"$'\n'"$(cat "$user_out")"
		recovery "Some /boot file names contain characters that are currently not supported by heads: $del"$'\n'"$add"
	fi
	rm -f /tmp/signable.*
}

verify_checksums() {
	TRACE_FUNC
	local boot_dir="$1"
	local gui="${2:-y}"

	(
		set +e -o pipefail
		local ret=0
		cd "$boot_dir" || ret=1
		sha256sum -c "$TMP_HASH_FILE" >/tmp/hash_output || ret=1

		# also make sure that the file & directory structure didn't change
		# (sha256sum won't detect added files)
		print_tree >/tmp/tree_output || ret=1
		if ! cmp -s "$TMP_TREE_FILE" /tmp/tree_output &>/dev/null; then
			ret=1
			[[ "$gui" != "y" ]] && exit "$ret"
			# produce a diff that can safely be presented to the user
			# this is relatively hard as file names may e.g. contain backslashes etc.,
			# which are interpreted by whiptail, less, ...
			escape_zero "(new) " <"$TMP_TREE_FILE" >"${TMP_TREE_FILE}.user"
			escape_zero "(new) " </tmp/tree_output >/tmp/tree_output.user
			diff "${TMP_TREE_FILE}.user" /tmp/tree_output.user | grep -E '^\+\(new\).*$' | sed -r 's/^\+\(new\)/(new)/g' >>/tmp/hash_output
			rm -f "${TMP_TREE_FILE}.user"
			rm -f /tmp/tree_output.user
		fi
		exit $ret
	)
	return $?
}

# Check if a device is an LVM2 PV, and if so print the VG name
find_lvm_vg_name() {
	TRACE_FUNC
	local DEVICE VG
	DEVICE="$1"

	mkdir -p /tmp/root-hashes-gui
	if ! lvm pvs "$DEVICE" >/tmp/root-hashes-gui/lvm_vg 2>/dev/null; then
		# It's not an LVM PV
		return 1
	fi

	VG="$(tail -n +2 /tmp/root-hashes-gui/lvm_vg | awk '{print $2}')"
	if [ -z "$VG" ]; then
		DEBUG "Could not find LVM2 VG from lvm pvs output:"
		DEBUG "$(cat /tmp/root-hashes-gui/lvm_vg)"
		return 1
	fi

	echo "$VG"
}

# If a block device is a partition, check if it is a bios-grub partition on a
# GPT-partitioned disk.
is_gpt_bios_grub() {
	TRACE_FUNC

	local PART_DEV="$1" DEVICE NUMBER

	# Figure out the partitioned device containing this device (if there is
	# one) from /sys/class/block.
	local DEVICE_MATCHES=("/sys/class/block/"*"/$(basename "$PART_DEV")")

	DEVICE="$(echo "${DEVICE_MATCHES[0]}" | cut -d/ -f5)"
	if [ "${#DEVICE_MATCHES[@]}" -ne 1 ] || [ "$DEVICE" = "*" ]; then
		return 0
	fi

	# Extract the partition number
	if ! [[ $(basename "$PART_DEV") =~ ([0-9]+)$ ]]; then
		return 0 # Can't figure out the partition number
	fi

	NUMBER="${BASH_REMATCH[1]}"

	# Now we know the device and partition number, get the type.  This is
	# specific to GPT disks, MBR disks are shown differently by fdisk.
	TRACE "$PART_DEV is partition $NUMBER of $DEVICE"
	if [ "$(fdisk -l "/dev/$DEVICE" | awk '$1 == '"$NUMBER"' {print $5}')" == grub ]; then
		return 0
	fi
	return 1
}

# Test if a block device could be used as /boot - we can mount it and it
# contains /boot/grub* files.  (Here, the block device could be a partition or
# an unpartitioned device.)
#
# If the device is a partition, its type is also checked.  Some common types
# that we definitely can't mount this way are excluded to silence spurious exFAT
# errors.
#
# Any existing /boot is unmounted.  If the device is a reasonable boot device,
# it's left mounted on /boot.
mount_possible_boot_device() {
	TRACE_FUNC

	local BOOT_DEV="$1"
	local PARTITION_TYPE

	# Unmount anything on /boot.  Ignore failure since there might not be
	# anything.  If there is something mounted and we cannot unmount it for
	# some reason, mount will fail, which is handled.
	umount /boot 2>/dev/null || true

	# Skip bios-grub partitions on GPT disks, LUKS partitions, and LVM PVs,
	# we can't mount these as /boot.
	if is_gpt_bios_grub "$BOOT_DEV" || cryptsetup isLuks "$BOOT_DEV" ||
		find_lvm_vg_name "$BOOT_DEV" >/dev/null; then
		TRACE "$BOOT_DEV is not a mountable partition for /boot"
		return 1
	fi

	# Get the size of BOOT_DEV in 512-byte sectors
	sectors=$(blockdev --getsz "$BOOT_DEV")

	# Check if the partition is small (less than 2MB, which is 4096 sectors)
	if [ "$sectors" -lt 4096 ]; then
		TRACE_FUNC
		DEBUG "Partition $BOOT_DEV is very small, likely BIOS boot. Skipping mount."
		return 1
	else
		TRACE_FUNC
		DEBUG "Try mounting $BOOT_DEV as /boot"
		if mount -o ro "$BOOT_DEV" /boot >/dev/null 2>&1; then
			if ls -d /boot/grub* >/dev/null 2>&1; then
				# This device is a reasonable boot device
				return 0
			fi
			umount /boot || true
		fi
	fi

	return 1
}

# detect and set /boot device
# mount /boot if successful
detect_boot_device() {
	TRACE_FUNC
	local devname
	# unmount /boot to be safe
	cd / && umount /boot 2>/dev/null

	# check $CONFIG_BOOT_DEV if set/valid
	if [ -e "$CONFIG_BOOT_DEV" ] && mount_possible_boot_device "$CONFIG_BOOT_DEV"; then
		# CONFIG_BOOT_DEV is valid device and contains an installed OS
		return 0
	fi

	# generate list of possible boot devices
	fdisk -l | grep "Disk /dev/" | cut -f2 -d " " | cut -f1 -d ":" >/tmp/disklist

	# Check each possible boot device
	for i in $(cat /tmp/disklist); do
		# If the device has partitions, check the partitions instead
		if device_has_partitions "$i"; then
			devname="$(basename "$i")"
			partitions=("/sys/class/block/$devname/$devname"?*)
		else
			partitions=("$i") # Use the device itself
		fi
		for partition in "${partitions[@]}"; do
			partition_dev=/dev/"$(basename "$partition")"
			# No sense trying something we already tried above
			if [ "$partition_dev" = "$CONFIG_BOOT_DEV" ]; then
				continue
			fi
			# If this is a reasonable boot device, select it and finish
			if mount_possible_boot_device "$partition_dev"; then
				CONFIG_BOOT_DEV="$partition_dev"
				return 0
			fi
		done
	done

	# no valid boot device found
	echo "Unable to locate /boot files on any mounted disk"
	return 1
}

scan_boot_options() {
	TRACE_FUNC
	local bootdir config option_file
	bootdir="$1"
	config="$2"
	option_file="$3"

	if [ -r $option_file ]; then rm $option_file; fi
	for i in $(find $bootdir -name "$config"); do
		DO_WITH_DEBUG kexec-parse-boot "$bootdir" "$i" >>$option_file
	done
	# FC29/30+ may use BLS format grub config files
	# https://fedoraproject.org/wiki/Changes/BootLoaderSpecByDefault
	# only parse these if $option_file is still empty
	if [ ! -s $option_file ] && [ -d "$bootdir/loader/entries" ]; then
		for i in $(find $bootdir -name "$config"); do
			kexec-parse-bls "$bootdir" "$i" "$bootdir/loader/entries" >>$option_file
		done
	fi
}

calc() {
	awk "BEGIN { print "$*" }"
}

# truncate a file to a size only if it is longer (busybox truncate lacks '<' and
# always sets the file size)
truncate_max_bytes() {
	local bytes="$1"
	local file="$2"
	if [ "$(stat -c %s "$file")" -gt "$bytes" ]; then
		truncate -s "$bytes" "$file"
	fi
}

# Busybox xxd -p pads the last line with spaces to 60 columns, which not only
# trips up many scripts, it's very difficult to diagnose by looking at the
# output.  Delete line breaks and spaces to really get plain hex output.
tohex_plain() {
	xxd -p | tr -d '\n '
}

# Busybox xxd -p -r silently truncates lines longer than 60 hex chars.
# Shorter lines are OK, spaces are OK, and even splitting a byte across lines is
# allowed, so just fold the text to maximum 60 column lines.
# Note that also unlike GNU xxd, non-hex chars in input corrupt the output (GNU
# xxd ignores them).
fromhex_plain() {
	fold -w 60 | xxd -p -r
}

print_battery_health() {
	if [ -d /sys/class/power_supply/BAT* ]; then
		battery_health=$(calc $(cat /sys/class/power_supply/BAT*/charge_full)/$(cat /sys/class/power_supply/BAT*/charge_full_design)*100 | awk -F "." {'print $1'})
		echo "$battery_health"
	fi
}

print_battery_charge() {
	if [ -d /sys/class/power_supply/BAT* ]; then
		battery_charge=$(calc $(cat /sys/class/power_supply/BAT*/charge_now)/$(cat /sys/class/power_supply/BAT*/charge_full)*100 | awk -F "." {'print $1'})
		echo "$battery_charge"
	fi
}

generate_random_mac_address() {
	#Borrowed from https://stackoverflow.com/questions/42660218/bash-generate-random-mac-address-unicast
	hexdump -n 6 -ve '1/1 "%.2x "' /dev/urandom | awk -v a="2,6,a,e" -v r="$RANDOM" 'BEGIN{srand(r);}NR==1{split(a,b,",");r=int(rand()*4+1);printf "%s%s:%s:%s:%s:%s:%s\n",substr($1,0,1),b[r],$2,$3,$4,$5,$6}'
}

# Add a command to be invoked at exit.  (Note that trap EXIT replaces any
# existing handler.)  Commands are invoked in reverse order, so they can be used
# to clean up resources, etc.
# The parameters are all executed as-is and do _not_ require additional quoting
# (unlike trap).  E.g.:
# at_exit shred "$file" #<-- file is expanded when calling at_exit, no extra quoting needed
at_exit() {
	AT_EXIT_HANDLERS+=("$@") # Command and args
	AT_EXIT_HANDLERS+=("$#") # Number of elements in this command
}

# Array of all exit handler command arguments with lengths of each command at
# the end.  For example:
#   at_exit echo hello
#   at_exit echo a b c
# results in:
# AT_EXIT_HANDLERS=(echo hello 2 echo a b c 4)

AT_EXIT_HANDLERS=()
# Each handler is an array AT_EXIT_HANDLER_{i}
run_at_exit_handlers() {
	local cmd_pos cmd_len
	cmd_pos="${#AT_EXIT_HANDLERS[@]}"
	# Silence trace if there are no handlers, this is common and occurs a lot
	[ "$cmd_pos" -gt 0 ] && DEBUG "Running at_exit handlers"
	while [ "$cmd_pos" -gt 0 ]; do
		cmd_pos="$((cmd_pos - 1))"
		cmd_len="${AT_EXIT_HANDLERS[$cmd_pos]}"
		cmd_pos="$((cmd_pos - cmd_len))"
		"${AT_EXIT_HANDLERS[@]:$cmd_pos:$cmd_len}"
	done
}
trap run_at_exit_handlers EXIT

# Helper function to generate diceware passphrase
generate_passphrase() {
	usage_generate_passphrase() {
		echo "Usage: generate_passphrase --dictionary|-d <dictionary_file> [--number_words|-n <num_words>] [--max_length|-m <max_size>] [--lowercase|-l]"
		echo "Generates a passphrase using a Diceware dictionary."
		echo "  --dictionary|-d <dictionary_file>  Path to the Diceware dictionary file (defaults to /etc/diceware_dictionnaries/eff_short_wordlist_2_0.txt )."
		echo "  [--number_words|-n <num_words>]  Number of words in the passphrase (default: 3)."
		echo "  [--max_length|-m <max_size>]  Maximum size of the passphrase (default: 256)."
		echo "  [--lowercase|-l]  Use lowercase words (default: false)."
	}

	# Helper subfunction to get a word from the dictionary based on dice rolls
	get_word_from_dictionary() {
		local rolls="$1"
		local dictionary_file="$2"
		local word=""

		word=$(grep "^$rolls" "$dictionary_file" | awk -F ' ' '{print $2}')
		echo "$word"
	}

	# Helper subfunction to generate dice rolls
	generate_dice_rolls() {
		TRACE_FUNC
		local num_rolls="$1"
		local rolls=""
		local random_bytes

		# Read num_rolls bytes from /dev/random, fed by CPU RRAND in one go
		random_bytes=$(dd if=/dev/random bs=1 count="$num_rolls" 2>/dev/null | hexdump -e '1/1 "%u\n"')

		# Process each byte to generate a dice roll
		while read -r byte; do
			roll=$((byte % 6 + 1))
			rolls+=$roll
		done <<<"$random_bytes"
		echo "$rolls"
	}

	TRACE_FUNC
	local dictionary_file="/etc/diceware_dictionnaries/eff_short_wordlist_2_0.txt"
	local num_words=3
	local max_size=256
	local lowercase=false

	# Parse parameters
	while [[ "$#" -gt 0 ]]; do
		case "$1" in
		--dictionary | -d)
			dictionary_file="$2"
			shift
			;;
		--lowercase | -l)
			lowercase=true
			;;
		--number_words | -n)
			if ! [[ "$2" =~ ^[0-9]+$ ]] || [[ "$2" -le 0 ]]; then
				warn "Invalid number of words: $2"
				usage_generate_passphrase
				return 1
			fi
			num_words="$2"
			shift
			;;
		--max_length | -m)
			if ! [[ "$2" =~ ^[0-9]+$ ]] || [[ "$2" -le 0 ]]; then
				warn "Invalid maximum size: $2"
				usage_generate_passphrase
				return 1
			fi
			max_size="$2"
			shift
			;;
		*)
			warn "Unknown parameter: $1"
			usage_generate_passphrase
			return 1
			;;
		esac
		shift
	done

	# Validate dictionary file
	if [[ -z "$dictionary_file" || ! -f "$dictionary_file" ]]; then
		warn "Dictionary file not found or not provided: $dictionary_file"
		usage_generate_passphrase
		return 1
	fi

	local passphrase=""
	local word=""
	local key=""
	local digits=0

	# Read the number of digits from the first line of the dictionary file
	read -r key _ <"$dictionary_file"

	# Validate that the key is composed entirely of digits
	if ! [[ $key =~ ^[0-9]+$ ]]; then
		echo "Error: Dictionary is not compliant with EFF diceware dictionaries."
		echo "The first line of the dictionary should be in the format: <digits> <word>"
		echo "Example: 11111 word"
		exit 1
	fi

	digits=${#key} #Number of digits in dice rolls

	for ((i = 0; i < num_words; ++i)); do
		key=$(generate_dice_rolls "$digits")
		word=$(get_word_from_dictionary "$key" "$dictionary_file")
		if [[ "$lowercase" == "false" ]]; then
			word=${word^} # Capitalize the first letter
		fi
		passphrase+="$word "
		if [[ ${#passphrase} -gt $max_size ]]; then
			DEBUG "Passphrase exceeds max size: $max_size, removing last word"
			passphrase=${passphrase% *} # Remove the last word if it exceeds max_size
			break
		fi
	done

	#Remove passphrase trailing space from passphrase+="$word"
	passphrase=${passphrase% }
	echo "$passphrase"
	return 0
}