#!/bin/bash
#
#   Copyright (C) 2022, 2023 SUSE LLC
#
#   This program is free software; you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 2 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program; if not, write to the Free Software
#   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#
#   Written by Olaf Kirch <okir@suse.com>

. /etc/sysconfig/fde-tools

. /usr/share/fde/util
. /usr/share/fde/luks
. /usr/share/fde/tpm
. /usr/share/fde/uefi
. /usr/share/fde/grub2

# dialog functions that use the firstboot plumbing
. /usr/share/fde/ui/dialog

if [ -f /etc/default/grub ]; then
    . /etc/default/grub
fi

##################################################################
# Values and locations used by KIWI
##################################################################
KIWI_ROOT_KEYFILE=/root/.root.keyfile
KIWI_REENCRYPTION_KEYFILE=/run/.kiwi_reencrypt.keyfile

##################################################################
# Aliases are not expanded in non-interactive mode.
# Set the bootloader specific functions here as aliases
##################################################################

function bootloader_enable_fde_without_tpm {
    grub_enable_fde_without_tpm "$@"
}

function bootloader_get_fde_password {
    grub_get_fde_password "$@"
}

function bootloader_get_keyslots {
    grub_get_keyslots "$@"
}

function bootloader_platform_parameters {
   grub_platform_parameters
}

function bootloader_rsa_sizes {
    grub_rsa_sizes
}

function bootloader_stop_event {
    grub_stop_event
}

##################################################################
# FDE Firstboot functions
##################################################################

function fde_protect_notpm {

    local luks_dev=$1

    bootloader_enable_fde_without_tpm

    return $?
}

function fde_setup_encrypted {

    root_dev=$1
    luks_keyfile="$2"
    luks_recovery_pass="$3"

    with_pass=false
    with_tpm=false
    with_ccid=false

    # Set when the user asked for a recovery password but we could not install
    # it. The key slots have to be left alone in that case.
    pass_failed=false

    is_reencrypted=false

    for method in $FDE_PROTECTION; do
	case $method in
	pass) with_pass=true;;
	tpm)  with_tpm=true;;
	ccid) with_ccid=true;;
	esac
    done

    if ! [[ "$root_dev" =~ /dev/mapper/.* ]]; then
	display_errorbox "root device $root_dev does not look like a dm device"
	return 1
    fi

    luks_name=$(expr "$root_dev" : ".*/\(.*\)")
    luks_dev=$(luks_get_underlying_device "$luks_name")
    if [ -z "$luks_dev" ]; then
	display_errorbox "Unable to determine underlying LUKS device for $root_dev"
	return 1
    fi

    if $with_tpm && ! tpm_test $FDE_KEY_SIZE_BYTES; then
	display_errorbox "TPM key sealing and unsealing does not seem to work"
	return 1
    fi

    # bsc#1244323 lsinitrd may not be able to deal with the symlink properly.
    # To avoid the potential error, always use the real path to the initrd.
    sys_initrd="`readlink -f /boot/initrd`"

    # KIWI may save sha256sum of the LUKS header in initrd before reencrypting
    # the root partition. If the checksum differs from the one of the current
    # LUKS header, the root partition is already reencryted.
    luks_hdr_sum_kiwi="`lsinitrd --file root/.luks.header ${sys_initrd}`"
    if [ "${luks_hdr_sum_kiwi}" != "" ]; then
	cryptsetup luksHeaderBackup ${luks_dev} --header-backup-file /root/.luks.header
	luks_hdr_sum_cur="`sha256sum /root/.luks.header | cut -f1 -d' '`"
	rm -f /root/.luks.header

	if [ "${luks_hdr_sum_cur}" != "${luks_hdr_sum_kiwi}" ]; then
	    is_reencrypted=true
	fi
    fi

    luks_current_password="${luks_recovery_pass}"

    # The installer/imager may have created a secondary slot that is protected
    # by a random key, stored in ${luks_keyfile}. That key is a far better
    # credential than the built-in recovery password: we can check that it is
    # valid, whereas the recovery password is only a guess, and in FIPS mode
    # PBKDF2 refuses passphrases shorter than 8 characters, so a short built-in
    # password fails to open any key slot at all.
    # Do not drop the slot here: it is the only credential we are sure about,
    # and both the recovery path below and fde_setup_unencrypted still need it.
    luks_imager_keyfile=""
    if [ -n "$luks_keyfile" ] && luks_verify_password "${luks_dev}" "${luks_keyfile}"; then
	luks_imager_keyfile="${luks_keyfile}"
    fi

    # Change the built-in recovery password to the one provided by the user
    # FIXME: only do this if the password is well-known, eg when dealing with a VM image
    # shipped by us.
    if $with_pass; then
	if luks_change_password "${luks_dev}" "${luks_current_password}" "${luks_imager_keyfile}"; then
	    luks_current_password="${result_password}"
	else
	    display_errorbox "Failed to change recovery password."
	    with_pass=false
	    pass_failed=true
	fi
    fi

    # Write the current password to a file for the later operations
    pass_keyfile=$(luks_write_password pass "${luks_current_password}")

    # Now that the recovery password is in place, drop every key slot we no
    # longer need: the imager's random key, and the slot that still holds the
    # built-in recovery password when luks_change_password authenticated with
    # the imager key rather than with that password. TPM protected slots are
    # preserved.
    # "cryptsetup reencrypt" doesn't really deal all that well with a LUKS
    # header that has more than one valid key slot, so this has to happen
    # before luks_reencrypt below.
    # Leave the header alone if we failed to install the recovery password:
    # dropping slots at that point would only throw away the last credential
    # the recovery path below can still use.
    drop_imager_keyfile=false
    if $with_pass; then
	if ! luks_drop_other_keyslots "${luks_dev}" "${pass_keyfile}"; then
	    display_errorbox "Failed to remove initial random key"
	    return 1
	fi
	drop_imager_keyfile=true
    elif ! $pass_failed && [ -n "$luks_imager_keyfile" ]; then
	# No recovery password was requested, so there is no obsolete slot to
	# collect either; just drop the imager's random key as we always did.
	if ! luks_drop_key "${luks_dev}" "${luks_imager_keyfile}"; then
	    display_errorbox "Failed to remove initial random key"
	    return 1
	fi
	drop_imager_keyfile=true
    fi

    if $drop_imager_keyfile && [ -n "$luks_keyfile" ]; then
	rm -f "${luks_keyfile}"

	# Remove the dracut conf for the key file
	rm -f /etc/dracut.conf.d/99-luks-boot.conf

	# Replace the key file path in /etc/crypttab with
	# "/.virtual-root.key" to avoid errors when unmounting the LUKS
	# partition (bsc#1218181). The path must not be on / itself, which is
	# read-only on SL Micro.
	sed -i "s,${luks_keyfile},/.virtual-root.key,g" /etc/crypttab

	luks_keyfile=""
    fi

    # Reencrypt with the new password
    # FIXME: only do this if the LUKS master key is well-known, eg when dealing with
    # a VM image.
    if [ "$is_reencrypted" = "false" ]; then
        luks_reencrypt "${luks_dev}" "${pass_keyfile}"
    fi

    if $with_tpm; then
	if ! fdectl regenerate-key --passfile "${pass_keyfile}"; then
	    display_errorbox "Failed to protect encrypted volume with TPM"
	    with_tpm=false
	else
	    systemctl enable fde-tpm-enroll.service
	fi
    else
	# Update grub.cfg to attempt a cryptomount and ask the user for the
	# password
	if ! fde_protect_notpm "${luks_dev}"; then
	    display_errorbox "Failed to protect encrypted volume with password"
	    with_pass=false
	fi
    fi

    # Remove the password file
    rm -f ${pass_keyfile}

    # Update initrd and /boot/grub2/grub.cfg
    if test -d "/boot/writable"; then
	transactional-update initrd grub.cfg
	transactional-update apply
    else
	dracut -f
	grub2-mkconfig -o /boot/grub2/grub.cfg
    fi

    if $with_pass || $with_tpm || $with_ccid; then
	: all is well
    else
	return 1
    fi

}

function fde_setup_unencrypted {

    root_dev=$1
    luks_keyfile="$2"
    luks_recovery_pass="$3"

    if ! [[ "$root_dev" =~ /dev/mapper/.* ]]; then
	display_errorbox "root device $root_dev does not look like a dm device"
        return 1
    fi

    luks_name=$(expr "$root_dev" : ".*/\(.*\)")
    luks_dev=$(luks_get_underlying_device "$luks_name")
    if [ -z "$luks_dev" ]; then
        display_errorbox "Unable to determine underlying LUKS device for $root_dev"
	return 1
    fi

    # We may be called to recover from a failed fde_setup_encrypted, which
    # drops the imager key file once it is no longer needed. Fall back to the
    # recovery password in that case.
    if [ ! -s "${luks_keyfile}" ]; then
	luks_keyfile=$(luks_write_password recovery "${luks_recovery_pass}")
    fi

    if ! luks_decrypt "${luks_dev}" "${luks_keyfile}"; then
	rm -f "${luks_keyfile}"
	display_errorbox "Failed to decrypt ${luks_dev}"
	return 1
    fi

    rm -f "${luks_keyfile}"
    rm -f /etc/crypttab

    display_infobox "Re-creating initial ramdisk"
    if ! dracut --force >&2; then
	display_errorbox "Failed to rebuild initrd"
	return 1
    fi

    return 0
}


function __fde_valid_protections {

    pass_warn=true
    for tag in $*; do
        case $tag in
        pass) pass_warn=false ;;
        tpm) : ;;
        *)
	    display_errorbox "FDE key protection scheme $tag not yet implemented"
	    return 1;;
        esac
    done

    if $pass_warn; then
        display_errorbox "Pass phrase is mandatory"
        return 1
    fi

    return 0
}

##################################################################
# Check what protection method the system supports and let the
# user choose.
# returns false if the user declined device encryption
# The ccid thing is there just as a teaser for now :-)
##################################################################
function fde_choose_protection {

    declare -g FDE_PROTECTION
    declare -a options

    FDE_PROTECTION=""

    message="This system can be installed with an encrypted root and boot partition. Please choose the desired protection method(s) or press Cancel to install without encryption"
    options+=(pass 'Pass phrase' on)

    if ! tpm_present_and_working; then
	display_errorbox "This system does not seem to have a working TPM device."
    elif ! uefi_secure_boot_enabled; then
	display_errorbox "This system does not seem to use Secure Boot. Full disk encryption with TPM2 disabled"
    else
    	options+=(tpm 'Stored inside the TPM chip' on)
    fi

    # Disable the ccid option until we really implement it
    # options+=(ccid 'Stored inside a CCID capable token' off)

    while true; do
        d --title "Full Disk Encryption" --checklist \
			"$message" 12 74 4 \
			"${options[@]}"

	if [ $? -ne 0 ]; then
	    FDE_PROTECTION=""
	    return 1
	fi

	FDE_PROTECTION="$result"
	fde_trace "user selected protections: <$FDE_PROTECTION>"

	if __fde_valid_protections $FDE_PROTECTION; then
	    break
	fi
    done

    return 0
}

function fde_firstboot {

    # fde_setup_encrypted keeps these two up to date as it goes: it consumes
    # the imager key file once the recovery password is in place, and replaces
    # the built-in recovery password with the one chosen by the user. The
    # recovery path below has to use whatever is valid by then, not the
    # credentials we were originally called with.
    luks_keyfile="$2"
    luks_current_password="$3"

    if fde_choose_protection; then
	if fde_setup_encrypted "$@"; then
	    return 0
	fi

	display_errorbox "Failed to set up for full disk encryption. Trying to recover"
    fi

    fde_setup_unencrypted "$1" "${luks_keyfile}" "${luks_current_password}"
}

function fde_systemd_firstboot {

    display_infobox "Full Disk Encryption with TPM2 support"

    # Redirect the fde_trace messages from stderr to journald
    exec 2> >(systemd-cat -t fde-tools -p info)

    if [ -f "$KIWI_REENCRYPTION_KEYFILE" ]; then
        # Use the reencryption password from KIWI
        fde_root_passphrase="$(<${KIWI_REENCRYPTION_KEYFILE})"
    else
	# Try the default password
	fde_root_passphrase=$(bootloader_get_fde_password)
    fi

    if [ -z "$fde_root_passphrase" ]; then
	# HACK: we just know it's 1234 for the ALP kvm_encrypted image, so go with that
	fde_root_passphrase=1234
    fi

    if [ ! -s "$KIWI_ROOT_KEYFILE" ]; then
	KIWI_ROOT_KEYFILE=""
    fi

    fde_firstboot $(luks_device_for_path "/") "$KIWI_ROOT_KEYFILE" "$fde_root_passphrase"

    fde_clean_tempdir
}

function fde_post {

    :
}

function fde_cleanup {

    :
}
