#!/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_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}"
            fi
        fi
    done
}

# Print the PID of the askpass process and fifo path with a file descriptor opened to
get_askpass_pid() {
    psinfo=$(ps) # Doing this so I don't end up matching myself
    echo "$psinfo" | awk "/$cryptkeyscript/ { print \$1 }" | while read -r pid; do
        pf=$(get_fifo_path "${pid}")
        if [[ $pf != "" ]]; then
            echo "${pid} ${pf}"
            break
        fi
    done
}

luks1_decrypt() {
    local CRYPTTAB_SOURCE=$1
    local PASSFIFO=$2
    UUID=cb6e8904-81ff-40da-a84a-07ab9ab5715e
    luksmeta show -d "$CRYPTTAB_SOURCE" | while read -r slot state uuid; do
        [ "$state" == "active" ] || continue
        [ "$uuid" == "$UUID" ] || continue

        lml=$(luksmeta load -d "${CRYPTTAB_SOURCE}" -s "${slot}" -u "${UUID}")
        [ $? -eq 0 ] || continue

        decrypted=$(echo -n "${lml}" | clevis decrypt 2>/dev/null)
        [ $? -eq 0 ] || continue

        # Fail safe
        [ "$decrypted" != "" ] || continue

        echo -n "${decrypted}" >"$PASSFIFO"
        return 0
    done

    return 1
}

luks2_decrypt() {
    local CRYPTTAB_SOURCE=$1
    local PASSFIFO=$2
    cryptsetup luksDump "$CRYPTTAB_SOURCE" | sed -rn 's|^\s+([0-9]+): clevis|\1|p' | while read -r id; do
        # jose jwe fmt -c outputs extra \n, so clean it up
        cte=$(cryptsetup token export --token-id "$id" "$CRYPTTAB_SOURCE")
        [ $? -eq 0 ] || continue

        josefmt=$(echo "${cte}" | jose fmt -j- -Og jwe -o-)
        [ $? -eq 0 ] || continue

        josejwe=$(echo "${josefmt}" | jose jwe fmt -i- -c)
        [ $? -eq 0 ] || continue

        jwe=$(echo "${josejwe}" | tr -d '\n')
        [ $? -eq 0 ] || continue

        decrypted=$(echo -n "${jwe}" | clevis decrypt 2>/dev/null)
        [ $? -eq 0 ] || continue

        # Fail safe
        [ "$decrypted" != "" ] || continue

        echo -n "${decrypted}" >"$PASSFIFO"
        return 0
    done

    return 1
}

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

    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

    OLD_CRYPTTAB_SOURCE=""

    while true; do

        until [ "$pid" ] && [ -p "$PASSFIFO" ]; do
            sleep .1
            pid_fifo=$(get_askpass_pid)
            pid=$(echo "${pid_fifo}" | cut -d' ' -f1)
            PASSFIFO=$(echo "${pid_fifo}" | cut -d' ' -f2-)
        done

        # Import CRYPTTAB_SOURCE from the askpass process.
        local "$(tr '\0' '\n' < /proc/${pid}/environ | \
	    grep '^CRYPTTAB_SOURCE=')"

        # Make sure that CRYPTTAB_SOURCE is actually a block device
        [ ! -b "$CRYPTTAB_SOURCE" ] && continue

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

        if cryptsetup isLuks --type luks1 "$CRYPTTAB_SOURCE"; then
            # If the device is not initialized, sliently skip it.
            luksmeta test -d "$CRYPTTAB_SOURCE" || continue

            if luks1_decrypt "${CRYPTTAB_SOURCE}" "${PASSFIFO}"; then
                echo "Unlocked ${CRYPTTAB_SOURCE} with clevis"
            else
                OLD_CRYPTTAB_SOURCE=""
                sleep 5
            fi
        elif cryptsetup isLuks --type luks2 "$CRYPTTAB_SOURCE"; then
            if luks2_decrypt "${CRYPTTAB_SOURCE}" "${PASSFIFO}"; then
                echo "Unlocked ${CRYPTTAB_SOURCE} with clevis"
            else
                OLD_CRYPTTAB_SOURCE=""
                sleep 5
            fi
        fi
        # 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
    done
}

. /scripts/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
        DEVICE="$DEVICE $device"
    done
    echo "$DEVICE"
}

# Check if network is up before trying to configure it.
eth_check() {
    for device in $(clevis_all_netbootable_devices); do
        ip link set dev "$device" up
        sleep 1
        ETH_HAS_CARRIER=$(cat /sys/class/net/"$device"/carrier)
        if [ "$ETH_HAS_CARRIER" = '1' ]; then
            return 0
        fi
    done
    return 1
}
if eth_check; then
    # Make sure networking is set up: if booting via nfs, it already is
    # Doesn't seem to work when added to clevisloop for some reason
    [ "$boot" = nfs ] || configure_networking
fi

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