#!/bin/bash
#
# Copyright (c) 2017 Red Hat, Inc.
# Copyright (c) 2017 Shawn Rose
# Copyright (c) 2017 Guilhem Moulin
#
# Author: Harald Hoyer <harald@redhat.com>
# Author: Nathaniel McCallum <npmccallum@redhat.com>
# Author: Shawn Rose <shawnandrewrose@gmail.com>
# Author: Guilhem Moulin <guilhem@guilhem.org>
#
# 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 3 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, see <http://www.gnu.org/licenses/>.
#

case $1 in
prereqs) exit 0 ;;
esac

# Return fifo path or nothing if not found
get_pid_fifo_path() {
    local pid="$1"
    for fd in /proc/$pid/fd/*; do
        if [ -e "$fd" ]; then
            if [[ $(readlink -f "${fd}") == *"/cryptsetup/passfifo" ]]; then
                readlink -f "${fd}"
                return 0
            fi
        fi
    done
    return 1
}

# Gets the luks device to be unlocked and used pins
get_pid_device_pins() {
    local pid="$1"
    local CRYPTTAB_SOURCE

    CRYPTTAB_SOURCE=$(tr '\0' '\n' 2>/dev/null </proc/${pid}/environ | \
        grep '^CRYPTTAB_SOURCE=' | cut -d= -f2)

    # Wrong process, no CRYPTTAB_SOURCE, return error
    [ -n "$CRYPTTAB_SOURCE" ] || return 1

    [ -b "$CRYPTTAB_SOURCE" ] || return 0

    local cache="/var/cache/clevis-disks/${CRYPTTAB_SOURCE//\//_}"
    if [ ! -f "$cache" ]; then
        local pins
        if ! pins=$(clevis_luks_read_used_pins "$CRYPTTAB_SOURCE"); then
            return 1
        fi

        echo "${CRYPTTAB_SOURCE}:${pins}" > "$cache"
    fi

    cat "$cache"
    return 0
}

# Print colon-separated password-asking info like device, pins and fifo
# path for unlocking with password
get_askpass_info() {
    local cryptkeyscript=$1
    local psinfo pf dev_pins
    psinfo=$(ps) # Doing this so I don't end up matching myself
    echo "$psinfo" | awk "/$cryptkeyscript/ { print \$1 }" | {
        while read -r pid; do
            if pf=$(get_pid_fifo_path "${pid}") && dev_pins=$(get_pid_device_pins "${pid}"); then
                if [[ $pf != "" && $dev_pins != "" ]]; then
                    # Output only in case of clevis device
                    echo "${dev_pins}:${pf}"
                fi
                # Return that we found valid process
                return 0
            fi
        done
        return 1
    }
}

# Try to decrypt the password to fifo file
luks_decrypt() {
    local CRYPTTAB_SOURCE=$1
    local PASSFIFO=$2
    local pt

    if pt=$(clevis_luks_unlock_device "${CRYPTTAB_SOURCE}"); then
        echo -n "${pt}" >"${PASSFIFO}"
        return 0
    else
        return 1
    fi
}

# Wait for askpass, and then try and decrypt immediately. Just in case
# there are multiple devices that need decrypting, this will loop
# infinitely (The local-bottom script will kill this after decryption)
clevisloop() {
    # Set the path how we want it (Probably not all needed)
    PATH="/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/bin"

    local cryptkeyscript
    local askpass_info
    local sleep_time
    local CRYPTTAB_SOURCE
    local OLD_CRYPTTAB_SOURCE=""
    local netcfg_attempted=0
    local tpm1cfg_attempted=0
    local pins
    local PASSFIFO

    if [ -x /bin/plymouth ] && plymouth --ping; then
        cryptkeyscript='plymouth ask-for-password'
    else
        # This has to be escaped for awk
        cryptkeyscript='\/lib\/cryptsetup\/askpass'
    fi

    while true; do
        # Re-get the askpass PID in case there are multiple encrypted devices
        CRYPTTAB_SOURCE=""
        sleep_time=.1
        until [ -n "$CRYPTTAB_SOURCE" ] && [ -p "$PASSFIFO" ]; do
            sleep $sleep_time
            if askpass_info=$(get_askpass_info "$cryptkeyscript"); then
                # Workaround for initramfs-tools checking the script as sh-compatible
                IFS=':' read -r CRYPTTAB_SOURCE pins PASSFIFO <<EOM
${askpass_info}
EOM
                # Ask process is running, variables might be empty for non-clevis device
                sleep_time=.5
            else
                # No valid process found
                sleep_time=.1
            fi
        done

        # Make the source has changed if needed
        [ "$CRYPTTAB_SOURCE" = "$OLD_CRYPTTAB_SOURCE" ] && continue
        OLD_CRYPTTAB_SOURCE="$CRYPTTAB_SOURCE"

        if [[ " $pins " == *" tang "* ]] && [ $netcfg_attempted -eq 0 ]; then
            netcfg_attempted=1
            do_configure_networking
        fi
        if [[ " $pins " == *" tpm1 "* ]] && [ $tpm1cfg_attempted -eq 0 ]; then
            tpm1cfg_attempted=1
            do_configure_tpm1
        fi

        if luks_decrypt "${CRYPTTAB_SOURCE}" "${PASSFIFO}"; then
            echo "Unlocked ${CRYPTTAB_SOURCE} with clevis"

            # Now that the current device has its password, let's sleep a
            # bit. This gives cryptsetup time to actually decrypt the
            # device and prompt for the next password if needed.
            sleep .5
        else
            # Unable to unlock now, retry later
            OLD_CRYPTTAB_SOURCE=""
            sleep 5
        fi
    done
}

. /scripts/functions
. clevis-luks-common-functions

# This is a copy  of 'all_netbootable_devices/all_non_enslaved_devices' for
# platforms that might not provide it.
clevis_all_netbootable_devices() {
    for device in /sys/class/net/*; do
        if [ ! -e "$device/flags" ]; then
            continue
        fi

        loop=$(($(cat "$device/flags") & 0x8 && 1 || 0))
        bc=$(($(cat "$device/flags") & 0x2 && 1 || 0))
        ptp=$(($(cat "$device/flags") & 0x10 && 1 || 0))

        # Skip any device that is a loopback
        if [ $loop = 1 ]; then
            continue
        fi

        # Skip any device that isn't a broadcast
        # or point-to-point.
        if [ $bc = 0 ] && [ $ptp = 0 ]; then
            continue
        fi

        # Skip any enslaved device (has "master" link
        # attribute on it)
        device=$(basename "$device")
        ip -o link show "$device" | grep -q -w master && continue
        if [ -z "$DEVICE" ]; then
            DEVICE="$device"
        else
            DEVICE="$DEVICE $device"
        fi
    done
    echo "$DEVICE"
}

get_specified_device() {
    local dev="$(echo $IP | awk -F: '{ print $6 }')"
    [ -z "$dev" ] || echo $dev
}

# Workaround configure_networking() not waiting long enough for an interface
# to appear. This code can be removed once that has been fixed in all the
# distro releases we care about.
wait_for_device() {
    local device
    local ret=0

    device=$(get_specified_device)

    if [ -n "$device" ]; then
        log_begin_msg "clevis: Waiting for interface ${device} to become available"
        local netdev_wait=0
        while [ $netdev_wait -lt 10 ]; do
            if [ -e "/sys/class/net/${device}" ]; then
                break
            fi
            netdev_wait=$((netdev_wait + 1))
            sleep 1
        done
        if [ ! -e "/sys/class/net/${device}" ]; then
            log_failure_msg "clevis: Interface ${device} did not appear in time"
            ret=1
        fi
        log_end_msg
    fi

    wait_for_udev 10

    return $ret
}

do_configure_networking() {
    # Make sure networking is set up: if booting via nfs, it already is
    if [ "$boot" != nfs ] && wait_for_device; then
        clevis_net_cnt=$(clevis_all_netbootable_devices | tr ' ' '\n' | wc -l)
        if [ -z "$IP" ] && [ "$clevis_net_cnt" -gt 1 ]; then
            echo ""
            echo "clevis: Warning: multiple network interfaces available but no ip= parameter provided."
        fi
        configure_networking

        # Add DNS servers from configure_networking to /etc/resolv.conf
        if [ ! -e /etc/resolv.conf ]; then
            touch /etc/resolv.conf
            for intf in /run/net-*.conf; do
                . "${intf}"
                if [ ! -z "${IPV4DNS0}" ] && [ "${IPV4DNS0}" != "0.0.0.0" ]; then
                    echo nameserver "${IPV4DNS0}" >> /etc/resolv.conf
                fi
                if [ ! -z "${IPV4DNS1}" ] && [ "${IPV4DNS1}" != "0.0.0.0" ]; then
                    echo nameserver "${IPV4DNS1}" >> /etc/resolv.conf
                fi
                if [ ! -z "${IPV6DNS0}" ]; then
                    echo nameserver "${IPV6DNS0}" >> /etc/resolv.conf
                fi
            done
        fi
    fi
}

do_configure_tpm1() {
    local tcsd_output=

    [ -x /usr/bin/clevis-decrypt-tpm1 ] && [ -f /usr/libexec/clevis-luks-tpm1-functions ] || return

    . /usr/libexec/clevis-luks-tpm1-functions

    log_begin_msg "clevis: Starting TCSD daemon"

    wait_for_udev 10

    # shellcheck disable=SC2034 # setting default value
    TCSD_NO_PRIVILEGE_DROP=0
    [ -f /conf/conf.d/clevis ] && . /conf/conf.d/clevis

    if ! tcsd_output=$(start_tcsd 2>&1); then
        if [ -n "$tcsd_output" ]; then
            log_failure_msg "failed to start TCSD: $tcsd_output"
        else
            log_failure_msg "failed to start TCSD"
        fi
    fi

    log_end_msg
}

mkdir -p /var/cache/clevis-disks
chmod 0700 /var/cache/clevis-disks

clevisloop &
echo $! >/run/clevis.pid
