#!/usr/bin/bash -e
# vim: set tabstop=8 shiftwidth=4 softtabstop=4 expandtab smarttab colorcolumn=80:
#
# Copyright (c) 2016 Red Hat, Inc.
# Author: Harald Hoyer <harald@redhat.com>
# Author: Nathaniel McCallum <npmccallum@redhat.com>
#
# 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/>.
#

SUMMARY="Binds a LUKS device using the specified policy"
UUID=cb6e8904-81ff-40da-a84a-07ab9ab5715e

function usage() {
    echo >&2
    echo "Usage: clevis luks bind [-f] [-s SLT] [-k KEY] -d DEV PIN CFG" >&2
    echo >&2
    echo "$SUMMARY": >&2
    echo >&2
    echo "  -f      Do not prompt for LUKSMeta initialization" >&2
    echo >&2
    echo "  -d DEV  The LUKS device on which to perform binding" >&2
    echo >&2
    echo "  -s SLT  The LUKS slot to use" >&2
    echo >&2
    echo "  -k KEY  Non-interactively read LUKS password from KEY file" >&2
    echo "  -k -    Non-interactively read LUKS password from standard input" >&2
    echo >&2
    exit 1
}

if [ $# -eq 1 -a "$1" == "--summary" ]; then
    echo "$SUMMARY"
    exit 0
fi

while getopts ":hfd:s:k:" o; do
    case "$o" in
    f) FRC=-f;;
    d) export DEV=$OPTARG;;
    s) SLT=$OPTARG;;
    k) KEY=$OPTARG;;
    *) usage;;
    esac
done

if [ -z "$DEV" ]; then
    echo "Did not specify a device!" >&2
    usage
fi

if ! cryptsetup isLuks "$DEV"; then
    echo "$DEV is not a LUKS device!" >&2
    exit 1
fi

if ! PIN=${@:$((OPTIND++)):1} || [ -z "$PIN" ]; then
    echo "Did not specify a pin!" >&2
    usage
fi

if ! CFG=${@:$((OPTIND++)):1} || [ -z "$CFG" ]; then
    echo "Did not specify a pin config!" >&2
    usage
fi

if cryptsetup isLuks --type luks1 "$DEV"; then
   # The first free slot, as per cryptsetup. In connection to bug #70, we may
    # have to wipe out the LUKSMeta slot priot to adding the new key.
    first_free_cs_slot=$(cryptsetup luksDump "${DEV}" \
                         | sed -rn 's|^Key Slot ([0-7]): DISABLED$|\1|p' \
                         | head -n 1)
    if [ -z "${first_free_cs_slot}" ]; then
        echo "There are no more free slots in ${DEV}!" >&2
        exit 1
    fi
fi

if [ -n "$KEY" ]; then
    if [ "$KEY" == "-" ]; then
        if cryptsetup isLuks --type luks1 "$DEV"; then
            if ! luksmeta test -d $DEV && [ -z "$FRC" ]; then
                echo "Cannot use '-k-' without '-f' unless already initialized!" >&2
                usage
            fi
        fi
    elif ! [ -f "$KEY" ]; then
        echo "Key file '$KEY' not found!" >&2
        exit 1
    fi
fi

# Generate a key with the same entropy as the LUKS Master Key
dump=`cryptsetup luksDump $DEV`
if cryptsetup isLuks --type luks1 "$DEV"; then
    filt=`sed -rn 's|MK bits:[ \t]*([0-9]+)|\1|p' <<< "$dump"`
else
    filt=`sed -rn 's|^\s+Key:\s+([0-9]+) bits\s*$|\1|p' <<< "$dump"`
fi
bits=`sort -n <<<"$filt" | tail -n 1`
export key=`pwmake $bits`

# Encrypt the new key
jwe=`echo -n "$key" | clevis encrypt "$PIN" "$CFG"`

# If necessary, initialize the LUKS volume
if cryptsetup isLuks --type luks1 "$DEV" && ! luksmeta test -d "$DEV"; then
    luksmeta init -d "$DEV" $FRC
fi

# Get the existing key.
case "$KEY" in
"") read -r -s -p "Enter existing LUKS password: " existing_key; echo;;
 -) existing_key="$(/bin/cat)";;
 *) ! IFS= read -rd '' existing_key < "$KEY";;
esac

# Check if the key is valid.
if ! cryptsetup luksOpen --test-passphrase "${DEV}" \
        --key-file <(echo -n "${existing_key}"); then
    exit 1
fi

if cryptsetup isLuks --type luks1 "${DEV}"; then
    # In certain circumstances, we may have LUKSMeta slots "not in sync" with
    # cryptsetup, which means we will try to save LUKSMeta metadata over an
    # already used or partially used slot -- github issue #70.
    # If that is the case, let's wipe the LUKSMeta slot here prior to saving.
    if read -r _ state uuid < <(luksmeta show -d "${DEV}" \
            | grep "^${first_free_cs_slot} *"); then
        if [ "${state}" = "inactive" ] && [ "${uuid}" = "${UUID}" ]; then
            luksmeta wipe -f -d "${DEV}" -s "${first_free_cs_slot}"
        fi
    fi
fi

#Add the new key
if [ -n "$SLT" ]; then
    cryptsetup luksAddKey --key-slot "$SLT" --key-file \
        <(echo -n "$existing_key") "$DEV"
else
    if cryptsetup isLuks --type luks2 "${DEV}"; then
        readarray -t usedSlotsBeforeAddKey < <(cryptsetup luksDump "${DEV}" \
            | sed -rn 's|^\s+([0-9]+): luks2$|\1|p')
    else
        readarray -t usedSlotsBeforeAddKey < <(cryptsetup luksDump "${DEV}" \
            | sed -rn 's|^Key Slot ([0-7]): ENABLED$|\1|p')
    fi
    cryptsetup luksAddKey --key-file <(echo -n "${existing_key}") "$DEV"
fi < <(echo -n "${key}")
if [ $? -ne 0 ]; then
    echo "Error while adding new key to LUKS header!" >&2
    exit 1
fi

#Determine slot used by new key if a desired slot was not specified
if [ -z "$SLT" ]; then
    if cryptsetup isLuks --type luks2 "${DEV}"; then
        readarray -t usedSlotsAfterAddKey < <(cryptsetup luksDump "${DEV}" \
            | sed -rn 's|^\s+([0-9]+): luks2$|\1|p')
    else
        readarray -t usedSlotsAfterAddKey < <(cryptsetup luksDump "${DEV}" \
            | sed -rn 's|^Key Slot ([0-7]): ENABLED$|\1|p')
    fi
    for i in "${usedSlotsAfterAddKey[@]}"; do
        if [[ ! " ${usedSlotsBeforeAddKey[@]} " =~ " ${i} " ]]; then
            SLT=$i
            break
        fi
    done
fi

if [ -z "$SLT" ]; then
	echo "Error while adding new key to LUKS header! Key slot is undefined." >&2
	exit 1
fi

if cryptsetup isLuks --type luks1 "$DEV"; then
    if ! echo -n $jwe | luksmeta save -d "$DEV" -u "$UUID" -s $SLT 2>/dev/null; then
        echo "Error while saving Clevis metadata in LUKSMeta!" >&2
        cryptsetup luksRemoveKey "$DEV" <<<"$key"
        exit 1
    fi
else
    jwe=`jose jwe fmt -i- <<<"$jwe"` # Convert to JSON Serialization
    tok="{\"type\":\"clevis\",\"keyslots\":[\"$SLT\"],\"jwe\":$jwe}"

    if ! cryptsetup token import "$DEV" <<<"$tok" ; then
        echo "Error while saving Clevis metadata as a LUKS token!" >&2
        cryptsetup luksRemoveKey "$DEV" <<<"$key"
        exit 1
    fi
fi
