#
#   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>

##################################################################
# Note: some of the functiions below expect the caller to provide
# functions for user interaction:
#
# request_password MESSAGE
#	Prompt the user for a password, using the first argument
#	as prompt message.
#	On success, it sets the global variable result_password
#	and returns 0.
#	If the user cancelled the dialog, it returns 1.
#
# request_new_password MESSAGE
#	Prompt the user for a new password, using the first argument
#	as prompt message. Have the user confirm the password to
#	avoid typos.
#	On success, it sets the global variable result_password
#	and returns 0.
#	If the user cancelled the dialog, it returns 1.
#
# display_gauge MESSAGE
#	Display a gauge meter like "dialog --gauge" does. This
#	function receives integer values from 0 to 100 on standard
#	input and should render a progress bar with the provided
#	message above or below the bar.
##################################################################

##################################################################
# Locate the device corresponding to a mount point
##################################################################
function luks_device_for_path {

    path="$1"

    df --output=source "$path" | grep /dev/
}

##################################################################
# Get the dm name for the given partition
##################################################################
function luks_dm_name_for_device {

    dev="$1"

    if [ "${dev%/*}" = "/dev/mapper" ]; then
	name="${dev##*/}"
	echo $name
	return 0
    fi

    devnum=$(stat -Lc "%t:%T" "$dev" 2>/dev/null)
    test -n "$devnum" || return 1

    for name in $(ls /dev/mapper); do
	dmdev="/dev/mapper/$name"
	test -b "$dmdev" || continue

	dmdevnum=$(stat -Lc "%t:%T" "$dmdev" 2>/dev/null)
	if [ "$devnum" = "$dmdevnum" ]; then
	    echo "$name"
	    return 0
	fi
    done

    return 1
}

##################################################################
# Locate the underlying partition(s) of LUKS encrypted device
##################################################################
function luks_get_volume_for_fsdev {

    dev="$1"
    orig_dev="$1"

    if ! [[ "$dev" =~ /dev/mapper/* ]]; then
	dm_name=$(luks_dm_name_for_device "$dev")
	if [ -z "$dm_name" ]; then
	    fde_trace "$orig_dev does not seem to be a DM device"
	    return 1
	fi

	dev="/dev/mapper/$dm_name"
    fi

    # Trace back the block devices to locate the devices with
    # 'crypto_LUKS' file system type
    # - lsblk options
    #   -s: inverse dependencies
    #   -n: no header line
    #   -r: raw format
    #   -p: full device path
    #   -o: print only NAME and FSTYPE
    #
    # NOTE: A LVM device may contain multiple 'crypto_LUKS' devices.
    #       Use 'tac' to invert the order again since '-s' is used in 'lsblk'.
    luks_devices=$(lsblk -snrp -o NAME,FSTYPE ${dev} | grep crypto_LUKS | cut -d' ' -f 1 | tac)

    echo "${luks_devices}"
    return 0
}

function luks_get_underlying_device {

    local luks_name=$1

    local luks_dev=""
    for id in $(ls /sys/block); do
	test -f "/sys/block/$id/dm/name" || continue
	dm_name=$(cat "/sys/block/$id/dm/name")
	test "$dm_name" = "$luks_name" || continue

	slaves=$(ls /sys/block/$id/slaves)
	case "$slaves" in
	*" "*)
	    echo "Ambiguous number of slave devices for LUKS dm device">&2
	    return 1;;
	esac
	echo "/dev/$slaves"
	return 0
    done

    echo "Unable to find underlying LUKS block device for $luks_name" >&2
}

function __partlabel_to_dev {

    wanted="$1"
    lsblk -nPo NAME,PARTLABEL|while read _line; do
	eval declare -- $_line
	if [ "${PARTLABEL}" = "${wanted}" -a -n "${NAME}" ]; then
	    if ! [[ "${NAME}" =~ /dev/.* ]]; then
		NAME="/dev/${NAME}"
	    fi
	    echo "${NAME}"
	    break
	fi
    done
}

function partlabel_to_dev {

    for iter in 1 2 3; do
	dev=$(__partlabel_to_dev "$@")
	test -z "${dev}" || break
    done
    echo "${dev}"
}

##################################################################
# Write a passphrase to a file s.th. it can be used as
# --key-file argument.
# luksChangeKey requires two of these files, so we pass a string
# as first argument (eg newpass, oldpass)
##################################################################
function luks_write_password {

    filename=$(fde_make_tempfile "$1")
    shift

    echo -n "$*" >$filename
    echo $filename
}

##################################################################
# Drop an existing pass phrase from the LUKS header
##################################################################
function luks_drop_pass {

    local luks_dev=$1
    old_pass="$2"

    display_infobox "Dropping old recovery password (${luks_dev})"
    old_keyfile=$(luks_write_password oldpass "${old_pass}")
    if ! cryptsetup luksRemoveKey "${luks_dev}" ${old_keyfile}; then
	fde_trace "Warning: luksRemoveKey indicates failure"
	return 1
    fi

    rm -f ${old_keyfile}
    return 0
}

##################################################################
# Drop an existing key from the LUKS header
##################################################################
function luks_drop_key {

    local luks_dev=$1
    local luks_keyfile="$2"

    display_infobox "Dropping old LUKS key (${luks_dev})"
    if ! cryptsetup luksRemoveKey "${luks_dev}" ${luks_keyfile}; then
	fde_trace "Warning: luksRemoveKey indicates failure"
	return 1
    fi

    return 0
}

##################################################################
# Verify an existing password
##################################################################
function luks_verify_password {

    local luks_dev=$1
    local luks_keyfile="$2"

    display_infobox "Verifying LUKS recovery password (${luks_dev})"
    if ! cryptsetup open --test-passphrase --key-file "${luks_keyfile}" "${luks_dev}"; then
	fde_trace "Unable to open the device with the password"
	return 1
    fi

    return 0
}

##################################################################
# Change an existing password by files
##################################################################
function luks_set_password {
    local luks_dev="$1"
    local luks_keyfile="$2"
    local luks_new_keyfile="$3"

    display_infobox "Updating LUKS password (${luks_dev})"

    if ! cryptsetup --key-file "${luks_keyfile}" luksChangeKey --pbkdf "$FDE_LUKS_PBKDF" "${luks_dev}" ${luks_new_keyfile}; then
	# FIXME: dialog
	fde_trace "Warning: luksChangeKey indicates failure"
	return 1
    fi
}

##################################################################
# Change an existing password
# This function uses request_new_password to prompt the user for
# the new password.
##################################################################
function luks_change_password {

    local luks_dev=$1
    local luks_old_password="$2"

    if [ -z "$luks_old_password" ]; then
	request_password "Please enter old LUKS recovery password"
        if [ -z "$result_password" ]; then
            fde_trace "Unable to obtain old recovery password"
	    return 1
        fi
	luks_old_password="$result_password"
    fi

    request_new_password "Please enter new LUKS recovery password"
    if [ -z "$result_password" ]; then
        fde_trace "Unable to obtain new recovery password"
	return 1
    fi

    old_keyfile=$(luks_write_password oldpass "${luks_old_password}")
    new_keyfile=$(luks_write_password newpass "${result_password}")
    if ! luks_set_password "${luks_dev}" "${old_keyfile}" "${new_keyfile}"; then
	rm -f ${new_keyfile} ${old_keyfile}
	return 1
    fi

    rm -f ${new_keyfile} ${old_keyfile}
}

function luks_add_password {

    local luks_dev=$1
    local luks_keyfile="$2"
    local luks_new_password="$3"

    if [ -z "$luks_new_password" ]; then
	request_new_password "Please enter new LUKS recovery password"
	if [ -z "$result_password" ]; then
	    echo "Unable to obtain new recovery password" >&2
	    return 1
	fi
	luks_new_password="${result_password}"
    fi

    display_infobox "Updating LUKS password (${luks_dev})"

    new_keyfile=$(luks_write_password newpass "${luks_new_password}")
    if ! cryptsetup --key-file "${luks_keyfile}" luksAddKey --pbkdf "$FDE_LUKS_PBKDF" "${luks_dev}" ${new_keyfile}; then
	fde_trace "Warning: luksAddKey indicates failure"
	return 1
    fi

    rm -f ${new_keyfile}
}

function luks_add_key {

    local luks_dev="$1"
    local luks_keyfile="$2"
    local new_keyfile="$3"
    local luks_output
    local luks_keyslot

    if [ -z "$new_keyfile" ]; then
        echo "Unable to obtain new key" >&2
        return 1
    fi

    # Note: we try to reduce the cost of PBKDF to (almost) nothing.
    # There's no need in slowing down this operation for a
    # key that was random to begin with.
    luks_output=$(cryptsetup --verbose --key-file "${luks_keyfile}" luksAddKey \
		  --pbkdf "$FDE_LUKS_PBKDF" --pbkdf-force-iterations 1000 \
		  ${luks_dev} ${new_keyfile})
    if test $? -ne 0; then
	fde_trace "Warning: luksAddKey indicates failure"
        return 1
    fi

    luks_keyslot=$(sed -En 's/Key slot ([0-9]*) created./\1/p' <<< "${luks_output}")
    fdectl-grub-tpm2 add --key-slot ${luks_keyslot} ${luks_dev}
}

function luks_generate_random_key {
    local new_keyfile="$1"

    dd if=/dev/random bs=1 count=$FDE_KEY_SIZE_BYTES of=$new_keyfile status=none
}

function luks_add_random_key {

    local luks_dev="$1"
    local luks_keyfile="$2"
    local new_keyfile="$3"

    luks_generate_random_key ${new_keyfile}

    luks_add_key ${luks_dev} ${luks_keyfile} ${new_keyfile}
}

function luks_set_key {

    local luks_dev="$1"
    local luks_keyfile="$2"
    local new_keyfile="$3"

    # Note: we try to reduce the cost of PBKDF to (almost) nothing.
    # There's no need in slowing down this operation for a
    # key that was random to begin with.
    cryptsetup --key-file "${luks_keyfile}" luksChangeKey \
		--pbkdf "$FDE_LUKS_PBKDF" --pbkdf-force-iterations 1000 \
		$luks_dev $new_keyfile
}

function luks_set_random_key {
    local luks_dev="$1"
    local luks_keyfile="$2"

    new_keyfile=/dev/shm/new.keyfile
    luks_generate_random_key ${new_keyfile}

    luks_set_key ${luks_dev} ${luks_keyfile} ${new_keyfile}
    ret=$?

    cp $new_keyfile "${luks_keyfile}"
    rm -f $new_keyfile

    return $ret
}

function luks_reencrypt {

    local luks_dev="$1"
    local luks_keyfile="$2"

    # Online reencryption works with LUKS2 only. If we ever want to do FDE with luks1,
    # we need to perform reencryption during installation, after dd'ing the image to
    # disk and prior to mounting it.
    {
	cryptsetup reencrypt --key-file "$luks_keyfile" --progress-frequency 1 $luks_dev 2>&1|
	    sed -u 's/.* \([0-9]*\)[0-9.]*%.*/\1/'
	    echo 100
    } | display_gauge "Re-encrypting root file system on $luks_dev"
}

function luks_decrypt {

    luks_dev="$1"
    luks_keyfile="$2"

    # Online reencryption works with LUKS2 only. If we ever want to do FDE with luks1,
    # we need to perform reencryption during installation, after dd'ing the image to
    # disk and prior to mounting it.
    {
	cryptsetup reencrypt --decrypt --key-file "$luks_keyfile" --progress-frequency 1 $luks_dev 2>&1|
	    sed -u 's/.* \([0-9]*\)[0-9.]*%.*/\1/'
	    echo 100
    } | display_gauge "Decrypting LUKS device $luks_dev"
}

