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

FDE_DEFAULT_AUTHORIZED_POLICY="authorized-policy"

# FIXME: this needs work for boot loaders other than grub
# Maybe we should introduce a bootloader_stop_event() function.
FDE_STOP_EVENT="grub-file=grub.cfg"

##################################################################
# Check whether a TPM is present and working reasonably well
##################################################################
function tpm_present_and_working {

    # Try to fail more gracefully when there's no TPM (esp on platforms
    # that do not support TPM devices at all).
    if [ ! -d /sys/class/tpm ]; then
	fde_trace "There do not seem to be any TPM devices."
    fi

    if ! pcr-oracle self-test; then
	fde_trace "This system does not have a TPM2 chip. Full disk encryption with TPM protection not available"
	return 1
    fi

    return 0
}

function tpm_seal_key {

    secret=$1
    sealed_secret=$2

    echo "Sealing secret against PCR policy covering $FDE_SEAL_PCR_LIST" >&2
    pcr-oracle --input "$secret" --output "$sealed_secret" \
			--key-format tpm2.0 \
			--algorithm "$FDE_SEAL_PCR_BANK" \
			--from eventlog \
			--stop-event "$FDE_STOP_EVENT" \
			--after \
			seal-secret \
			"$FDE_SEAL_PCR_LIST"
}

function tpm_test {

    key_size=$1

    secret=$(fde_make_tempfile secret)
    dd if=/dev/zero of=$secret bs=$key_size count=1 status=none >&2

    secret=$(fde_make_tempfile secret)
    sealed_secret=$(fde_make_tempfile sealed_secret)
    recovered=$(fde_make_tempfile recovered)
    result=1

    dd if=/dev/zero of=$secret bs=$key_size count=1 status=none >&2

    fde_trace "Testing TPM seal/unseal"
    pcr-oracle \
	--algorithm "$FDE_SEAL_PCR_BANK" \
        --input "$secret" \
        --output "$sealed_secret" \
        --from current \
        seal-secret "$FDE_SEAL_PCR_LIST"

    pcr-oracle \
	--algorithm "$FDE_SEAL_PCR_BANK" \
        --input "$sealed_secret" \
        --output "$recovered" \
        unseal-secret "$FDE_SEAL_PCR_LIST"

    if ! cmp "$secret" "$recovered"; then
        fde_trace "BAD: Unable to recover original secret"
        fde_trace "TPM seal/unseal does not seem to work; please take me to a parallel universe"
    else
        fde_trace "TPM seal/unseal works"
	result=0
    fi

    return $result
}


function tpm_seal_secret {

    secret="$1"
    sealed_secret="$2"
    authorized_policy="$3"

    # If we are expected to use an authorized policy, seal the secret
    # against that, using pcr-oracle rather than the tpm2 tools
    if [ -n "$authorized_policy" ]; then
	pcr-oracle --authorized-policy "$authorized_policy" \
			--key-format tpm2.0 \
			--input $secret \
			--output $sealed_secret \
			seal-secret
	return $?
    fi

    # The sealed key that grub expects is just the concatenation of
    # TPM2B_PUBLIC and a TPM2B containing the private key portion
    # This may not work with systemd-boot. If systemd-boot requires
    # public/private in separate files, we should probably add
    # a subcommand "split-secret" to pcr-oracle.
    if ! tpm_seal_key $secret $sealed_secret >&2; then
	rm -f $sealed_secret
	# FIXME: this should be an error dialog.
	# Let's hope the user has set a recovery password
	echo "Failed to seal LUKS encryption key" >&2
	return 1
    fi
}

##################################################################
# Authorized policy support
##################################################################
function tpm_set_authorized_policy_paths {

    policy_name="$1"

    # Note, caller is expected to set FDE_CONFIG_DIR
    # (usually to $rootdir/etc/fde).
    declare -g FDE_AP_CONFIG_DIR="$FDE_CONFIG_DIR/$policy_name"
    declare -g FDE_AP_SECRET_KEY="$FDE_AP_CONFIG_DIR/secret-key.pem"
    declare -g FDE_AP_AUTHPOLICY="$FDE_AP_CONFIG_DIR/authorized-policy.tpm"
    declare -g FDE_AP_PUBLIC_KEY="$FDE_AP_CONFIG_DIR/public-key.tpm"
    declare -g FDE_AP_SEALED_SECRET="$FDE_AP_CONFIG_DIR/sealed.tpm"

    mkdir -p -m 755 "$FDE_AP_CONFIG_DIR"
}

function tpm_create_authorized_policy {

    secret_key="$1"
    output_policy="$2"
    public_key="$3"

    # Generate the private key if it does not exist
    extra_opts=
    if [ ! -f "$secret_key" ]; then
	extra_opts="--rsa-generate-key"
    fi

    pcr-oracle $extra_opts \
        --private-key "$secret_key" \
        --authorized-policy $output_policy \
	--algorithm $FDE_SEAL_PCR_BANK \
        create-authorized-policy $FDE_SEAL_PCR_LIST
    if [ $? -ne 0 ]; then
	return 1
    fi

    # Store the public key in a format suitable for feeding it to the TPM
    if [ -n "$public_key" ]; then
	pcr-oracle \
		--private-key "$secret_key" \
		--public-key "$public_key" \
		store-public-key
	if [ $? -ne 0 ]; then
	    return 1
	fi
    fi
}

function tpm_authorize {

    private_key_file="$1"
    sealed_key_file="$2"
    signed_key_file="$3"

    pcr-oracle \
		--key-format tpm2.0 \
		--algorithm "$FDE_SEAL_PCR_BANK" \
                --private-key "$private_key_file" \
                --from eventlog \
		--stop-event "$FDE_STOP_EVENT" \
		--after \
		--input "$sealed_key_file" \
                --output "$signed_key_file" \
                sign "$FDE_SEAL_PCR_LIST"
}
