#!/usr/bin/bash
# vim: set tabstop=8 shiftwidth=4 softtabstop=4 expandtab smarttab colorcolumn=80:
#
# Copyright (c) 2018 Red Hat, Inc.
# Author: Radovan Sroka <rsroka@redhat.com>
# Author: Sergio Correia <scorreia@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/>.
#

. clevis-luks-common-functions

SUMMARY="Regenerate LUKS metadata"

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

function usage_and_exit () {
    exec >&2
    echo "Usage: clevis luks regen -d DEV -s SLOT"
    echo
    echo "$SUMMARY"
    echo
    echo "  -d DEV  The LUKS device on which to perform rebinding"
    echo
    echo "  -s SLT  The LUKS slot to use"
    echo
    exit "${1}"
}

on_exit() {
    if [ ! -d "${TMP}" ] || ! rm -rf "${TMP}"; then
        echo "Delete temporary files failed!" >&2
        echo "You need to clean up: ${TMP}" >&2
        exit 1
    fi
}

while getopts ":hfd:s:" o; do
    case "$o" in
    d) DEV="$OPTARG";;
    h) usage_and_exit 0;;
    s) SLT="$OPTARG";;
    *) usage_and_exit 1;;
    esac
done

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

if [ -z "$SLT" ]; then
    echo "Did not specify a slot!" >&2
    exit 1
fi

### ----------------------------------------------------------------------
if ! pin_cfg=$(clevis luks list -d "${DEV}" -s "${SLT}" 2>/dev/null); then
    echo "Error obtaining current configuration of device ${DEV}:${SLT}" >&2
    exit 1
fi

PIN=$(echo "${pin_cfg}" | awk '{ print $2 }')
CFG=$(echo "${pin_cfg}" | awk '{ print $3 }' | tr -d "'")

echo "Regenerating with:"
echo "PIN: $PIN"
echo "CONFIG: $CFG"

trap 'echo "Ignoring CONTROL-C!"' INT TERM

# Get the existing key.
if ! existing_key=$(clevis luks pass -d "${DEV}" -s "${SLT}" 2>/dev/null); then
    # We failed to obtain the passphrase for the slot -- perhaps
    # it was rotated? --, so let's request user input.
    read -r -s -p "Enter existing LUKS password: " existing_key; echo
fi

# Check if the key is valid.
if ! cryptsetup open --test-passphrase "${DEV}" <<< "${existing_key}"; then
    exit 1
fi

# Check if we can do the update in-place, i.e., if the key we got is the one
# for the slot we are interested in.
in_place=
if cryptsetup open --test-passphrase --key-slot "${SLT}" "${DEV}" \
        <<< "${existing_key}"; then
    in_place=true
fi

# Create new key.
if ! new_passphrase=$(generate_key "${DEV}"); then
    echo "Error generating new key for device ${DEV}" >&2
    exit 1
fi

# Reencrypt the new password.
if ! jwe=$(clevis encrypt "${PIN}" "${CFG}" <<< "${new_passphrase}"); then
    echo "Error using pin '${PIN}' with config '${CFG}'" >&2
    exit 1
fi

# Updating the metadata and the actual passphrase are destructive operations,
# hence we will do a backup of the LUKS header and restore it later in case
# we have issues performing these operations.
if ! TMP="$(mktemp -d)"; then
    echo "Creating a temporary dir for device backup/restore failed!" >&2
    exit 1
fi
trap 'on_exit' EXIT

# Backup LUKS header.
if ! clevis_luks_backup_dev "${DEV}" "${TMP}"; then
    echo "Error while trying to back up LUKS header from ${DEV}" >&2
    exit 1
fi

restore_device() {
    local DEV="${1}"
    local TMP="${2}"

    if ! clevis_luks_restore_dev "${DEV}" "${TMP}"; then
        echo "Error while trying to restore LUKS header from ${DEV}." >&2
    else
        echo "LUKS header restored successfully." >&2
    fi
}

# Update the key slot with the new key. If we have the key for this slot,
# the change happens in-place. Otherwise, we kill the slot and re-add it.
if [ -n "${in_place}" ]; then
    if ! cryptsetup luksChangeKey "${DEV}" --key-slot "${SLT}" \
            <(echo -n "${new_passphrase}") <<< "${existing_key}"; then
        echo "Error updating LUKS passphrase in ${DEV}:${SLT}" >&2
        restore_device "${DEV}" "${TMP}"
        exit 1
    fi
else
    if ! cryptsetup luksKillSlot --batch-mode "${DEV}" "${SLT}"; then
        echo "Error wiping slot ${SLT} from ${DEV}" >&2
        restore_device "${DEV}" "${TMP}"
        exit 1
    fi

    if ! echo -n "${new_passphrase}" \
            | cryptsetup luksAddKey --key-slot "${SLT}" \
                         --key-file <(echo -n "${existing_key}") "${DEV}"; then
        echo "Error updating LUKS passphrase in ${DEV}:${SLT}." >&2
        restore_device "${DEV}" "${TMP}"
        exit 1
    fi
fi

# Update the metadata.
if ! clevis_luks_save_slot "${DEV}" "${SLT}" "${jwe}" "overwrite"; then
    echo "Error updating metadata in ${DEV}:${SLT}" >&2
    restore_device "${DEV}" "${TMP}"
    exit 1
fi

# Now make sure that we can unlock this device after the change.
# If we can't, undo the changes.
if ! cryptsetup open --test-passphrase --key-slot "${SLT}" "${DEV}" 2>/dev/null \
        <<< $(clevis luks pass -d "${DEV}" -s "${SLT}" 2>/dev/null); then
    echo "Invalid configuration detected after rebinding. Reverting changes."
    restore_device "${DEV}" "${TMP}"
    exit 1
fi

echo "Keys were succesfully rotated."
