#!/bin/bash -e
# vim: set tabstop=8 shiftwidth=4 softtabstop=4 expandtab smarttab colorcolumn=80:
#
# Copyright (c) 2019 Red Hat, Inc.
# 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_UUID="cb6e8904-81ff-40da-a84a-07ab9ab5715e"

# Length, in bytes, used for password generated for LUKS key
# This value corresponds to an entropy of 256 bits if the password
# was generated by pwmake or similar tool
JOSE_PASSWORD_LENGTH=40

enable_debugging() {
    # Automatically enable debugging if in initramfs phase and rd.debug
    if [ -e /usr/lib/dracut-lib.sh ]; then
        local bashopts=$-
        # Because dracut is loosely written, disable hardening options temporarily
        [[ $bashopts != *u* ]] || set +u
        [[ $bashopts != *e* ]] || set +e
        . /usr/lib/dracut-lib.sh
        [[ $bashopts != *u* ]] || set -u
        [[ $bashopts != *e* ]] || set -e
    fi
}

enable_debugging

# valid_slot() will check whether a given slot is possibly valid, i.e., if it
# is a numeric value within the specified range.
valid_slot() {
    local SLT="${1}"
    local MAX_SLOTS="${2}"
    case "${SLT}" in
        ''|*[!0-9]*)
            return 1
            ;;
        *)
            # We got an integer, now let's make sure it is within the
            # supported range.
            if [ "${SLT}" -ge "${MAX_SLOTS}" ]; then
                return 1
            fi
            ;;
    esac
}

# clevis_luks_read_slot() will read a particular slot of a given device, which
# should be either LUKS1 or LUKS2. Returns 1 in case of failure; 0 in case of
# success.
clevis_luks_read_slot() {
    local DEV="${1}"
    local SLT="${2}"

    if [ -z "${DEV}" ] || [ -z "${SLT}" ]; then
        echo "Need both a device and a slot as arguments." >&2
        return 1
    fi

    local DATA_CODED=''
    local MAX_LUKS1_SLOTS=8
    local MAX_LUKS2_SLOTS=32
    if cryptsetup isLuks --type luks1 "${DEV}"; then
        if ! valid_slot "${SLT}" "${MAX_LUKS1_SLOTS}"; then
            echo "Please, provide a valid key slot number; 0-7 for LUKS1" >&2
            return 1
        fi

        if ! luksmeta test -d "${DEV}"; then
            echo "The ${DEV} device is not valid!" >&2
            return 1
        fi

        local uuid
        # Pattern from luksmeta: active slot uuid.
        read -r _ _ uuid <<< "$(luksmeta show -d "${DEV}" | grep "^${SLT} *")"

        if [ "${uuid}" != ${CLEVIS_UUID}"" ]; then
            echo "Not a clevis slot!" >&2
            return 1
        fi

        if ! DATA_CODED="$(luksmeta load -d "${DEV}" -s "${SLT}")"; then
            echo "Cannot load data from ${DEV} slot:${SLT}!" >&2
            return 1
        fi
    elif cryptsetup isLuks --type luks2 "${DEV}"; then
        if ! valid_slot "${SLT}" "${MAX_LUKS2_SLOTS}"; then
            echo "Please, provide a valid key slot number; 0-31 for LUKS2" >&2
            return 1
        fi

        local token_id
        token_id=$(cryptsetup luksDump "${DEV}" \
                    | grep -E -B1 "^\s+Keyslot:\s+${SLT}$" \
                    | sed -n 1p | sed -rn 's|^\s+([0-9]+): clevis|\1|p')
        if [ -z "${token_id}" ]; then
            echo "Cannot load data from ${DEV} slot:${SLT}. No token found!" >&2
            return 1
        fi

        local token
        token=$(cryptsetup token export --token-id "${token_id}" "${DEV}")
        DATA_CODED=$(jose fmt -j- -Og jwe -o- <<< "${token}" \
                     | jose jwe fmt -i- -c)

        if [ -z "${DATA_CODED}" ]; then
            echo "Cannot load data from ${DEV} slot:${SLT}!" >&2
            return 1
        fi
    else
        echo "${DEV} is not a supported LUKS device!" >&2
        return 1
    fi
    echo "${DATA_CODED}"
}

# clevis_luks_used_slots() will return the list of used slots for a given LUKS
# device.
clevis_luks_used_slots() {
    local DEV="${1:-}"
    [ -z "${DEV}" ] && return 1

    local used_slots
    if cryptsetup isLuks --type luks1 "${DEV}"; then
        if ! used_slots=$(cryptsetup luksDump "${DEV}" 2>/dev/null \
                          | sed -rn 's|^Key Slot ([0-7]): ENABLED$|\1|p'); then
            return 1
        fi
    elif cryptsetup isLuks --type luks2 "${DEV}"; then
        if ! used_slots=$(cryptsetup luksDump "${DEV}" 2>/dev/null \
                          | sed -rn 's|^\s+([0-9]+): luks2$|\1|p'); then
            return 1
        fi
    else
        echo "${DEV} is not a supported LUKS device!" >&2
        return 1
    fi
    echo "${used_slots}"
}

# clevis_luks_decode_jwe() will decode a given JWE.
clevis_luks_decode_jwe() {
    local jwe="${1}"

    local coded
    read -r -d . coded <<< "${jwe}"
    jose b64 dec -i- <<< "${coded}"
}

# clevis_luks_print_pin_config() will print the config of a given pin; i.e.
# for tang it will display the associated url address, and for tpm2, the
# properties in place, like the hash, for instance.
clevis_luks_print_pin_config() {
    local P="${1}"
    local decoded="${2}"

    local content
    if ! content="$(jose fmt -j- -g clevis -g "${P}" -o- <<< "${decoded}")" \
                    || [ -z "${content}" ]; then
        return 1
    fi

    local pin=
    case "${P}" in
    pkcs11)
        local uri
        uri="$(jose fmt -j- -g uri -u- <<< "${content}")"
        mechanism="$(jose fmt -j- -g mechanism -u- <<< "${content}")"
        if [ -z "${mechanism}" ]; then
            pin=$(printf '{"uri":"%s"}' "${uri}")
        else
            pin=$(printf '{"uri":"%s", "mechanism":"%s"}' "${uri}" "${mechanism}")
        fi
        printf "pkcs11 '%s'" "${pin}"
        ;;
    sss)
        local threshold
        threshold=$(jose fmt -j- -Og t -o- <<< "${content}")
        clevis_luks_process_sss_pin "${content}" "${threshold}"
        ;;
    tang)
        local url
        url="$(jose fmt -j- -g url -u- <<< "${content}")"
        pin=$(printf '{"url":"%s"}' "${url}")
        printf "tang '%s'" "${pin}"
        ;;
    tpm2)
        # Valid properties for tpm2 pin are the following:
        # hash, key, pcr_bank, pcr_ids, pcr_digest.
        local key
        local value
        for key in 'hash' 'key' 'pcr_bank' 'pcr_ids' 'pcr_digest'; do
            if value=$(jose fmt -j- -g "${key}" -u- <<< "${content}"); then
                pin=$(printf '%s,"%s":"%s"' "${pin}" "${key}" "${value}")
            fi
        done
        # Remove possible leading comma.
        pin=${pin/#,/}
        printf "tpm2 '{%s}'" "${pin}"
        ;;
    *)
        printf "unknown pin '%s'" "${P}"
        ;;
    esac
}

# clevis_luks_decode_pin_config() will receive a JWE and extract a pin config
# from it.
clevis_luks_decode_pin_config() {
    local jwe="${1}"

    local decoded
    if ! decoded=$(clevis_luks_decode_jwe "${jwe}"); then
        return 1
    fi

    local P
    if ! P=$(jose fmt -j- -Og clevis -g pin -u- <<< "${decoded}"); then
        return 1
    fi

    clevis_luks_print_pin_config "${P}" "${decoded}"
}

# clevis_luks_join_sss_cfg() will receive a list of configurations for a given
# pin and returns it as list, in the format PIN [cfg1, cfg2, ..., cfgN].
clevis_luks_join_sss_cfg() {
    local pin="${1}"
    local cfg="${2}"
    cfg=$(echo "${cfg}" | tr -d "'" | sed -e 's/^,//')
    printf '"%s":[%s]' "${pin}" "${cfg}"
}

# clevis_luks_process_sss_pin() will receive a JWE with information on the sss
# pin config, and also its associated threshold, and will extract the info.
clevis_luks_process_sss_pin() {
    local jwe="${1}"
    local threshold="${2}"

    local sss_pkcs11
    local sss_tang
    local sss_tpm2
    local sss
    local pin_cfg
    local pin
    local cfg

    local coded
    for coded in $(jose fmt -j- -Og jwe -Af- <<< "${jwe}"| tr -d '"'); do
        if ! pin_cfg="$(clevis_luks_decode_pin_config "${coded}")"; then
            continue
        fi
        read -r pin cfg <<< "${pin_cfg}"
        case "${pin}" in
        pkcs11)
            sss_pkcs11="${sss_pkcs11},${cfg}"
            ;;
        tang)
            sss_tang="${sss_tang},${cfg}"
            ;;
        tpm2)
            sss_tpm2="${sss_tpm2},${cfg}"
            ;;
        sss)
            sss=$(echo "${cfg}" | tr -d "'")
            ;;
        esac
    done

    cfg=
    if [ -n "${sss_tang}" ]; then
        cfg=$(clevis_luks_join_sss_cfg "tang" "${sss_tang}")
    fi

    if [ -n "${sss_tpm2}" ]; then
        cfg="${cfg},"$(clevis_luks_join_sss_cfg "tpm2" "${sss_tpm2}")
    fi

    if [ -n "${sss_pkcs11}" ]; then
        cfg="${cfg},"$(clevis_luks_join_sss_cfg "pkcs11" "${sss_pkcs11}")
    fi

    if [ -n "${sss}" ]; then
        cfg=$(printf '%s,"sss":%s' "${cfg}" "${sss}")
    fi

    # Remove possible leading comma.
    cfg=${cfg/#,/}
    pin=$(printf '{"t":%d,"pins":{%s}}' "${threshold}" "${cfg}")
    printf "sss '%s'" "${pin}"
}

# clevis_luks_read_pins_from_slot() will receive a given device and slot and
# will then output its associated policy configuration.
clevis_luks_read_pins_from_slot() {
    local DEV="${1}"
    local SLOT="${2}"

    local jwe
    if ! jwe=$(clevis_luks_read_slot "${DEV}" "${SLOT}" 2>/dev/null); then
        return 1
    fi

    local cfg
    if ! cfg="$(clevis_luks_decode_pin_config "${jwe}")"; then
        return 1
    fi
    printf "%s: %s\n" "${SLOT}" "${cfg}"
}

# clevis_luks_check_valid_key_or_keyfile() receives a devices and either a
# passphrase or keyfile and then checks whether it is able to unlock the
# device wih the received passphrase/keyfile.
clevis_luks_check_valid_key_or_keyfile() {
    local DEV="${1}"
    local KEY="${2:-}"
    local KEYFILE="${3:-}"
    local SLT="${4:-}"
    local EXISTING_TOKEN_ID="${5:-}"

    [ -z "${DEV}" ] && return 1

    local extra_args
    extra_args="$([ -n "${SLT}" ] && printf -- '--key-slot %s' "${SLT}")"

    # We have an empty key here.
    if [ -z "${EXISTING_TOKEN_ID}" ] && [ -z "${KEYFILE}" ] \
         && [ -z "${KEY}" ]; then
        echo | cryptsetup open --force-password --test-passphrase "${DEV}" \
               ${extra_args}
        return
    fi

    if [ -n "${KEYFILE}" ]; then
        cryptsetup open --test-passphrase "${DEV}" --key-file "${KEYFILE}" \
                   ${extra_args}
        return
    fi
    if [ -n "${EXISTING_TOKEN_ID}" ]; then
        cryptsetup open --test-passphrase "${DEV}" --token-id "${EXISTING_TOKEN_ID}" \
                   ${extra_args}
        return
    fi

    printf '%s' "${KEY}" | cryptsetup open --test-passphrase "${DEV}" \
                                      ${extra_args}
}

# clevis_luks_unlock_device_by_slot() does the unlock of the device and slot
# passed as parameters and returns the decoded passphrase.
clevis_luks_unlock_device_by_slot() {
    local DEV="${1}"
    local SLT="${2}"
    local SKIP_CHECK="${3}"

    [ -z "${DEV}" ] && return 1
    [ -z "${SLT}" ] && return 1

    local jwe passphrase
    if ! jwe="$(clevis_luks_read_slot "${DEV}" "${SLT}" 2>/dev/null)" \
                || [ -z "${jwe}" ]; then
        return 1
    fi

    if ! passphrase="$(printf '%s' "${jwe}" | clevis decrypt)" \
                       || [ -z "${passphrase}" ]; then
        return 1
    fi
    if [ -z "${SKIP_CHECK}" ]; then
        clevis_luks_check_valid_key_or_keyfile "${DEV}" "${passphrase}" || return 1
    fi
    printf '%s' "${passphrase}"
}

# clevis_luks_unlock_device() does the unlock of the device passed as
# parameter and returns the decoded passphrase.
clevis_luks_unlock_device() {
    local DEV="${1}"
    local SKIP_CHECK="YES"

    [ -z "${DEV}" ] && return 1

    local used_slots
    if ! used_slots=$(clevis_luks_used_slots "${DEV}") \
                      || [ -z "${used_slots}" ]; then
        return 1
    fi

    local slt pt
    for slt in ${used_slots}; do
        if ! pt=$(clevis_luks_unlock_device_by_slot "${DEV}" "${slt}" "${SKIP_CHECK}") \
                  || [ -z "${pt}" ]; then
             continue
        fi
        printf '%s' "${pt}"
        return 0
    done
    return 1
}

# clevis_map_device() tries to map the device received as a parameter to a
# block device. As per crypttab(5), we support /path/to/encrypted/blockdev
# or UUID=<uuid>.
clevis_map_device() {
    local CDEV="${1}"

    if [[ "${CDEV}" == UUID=* ]]; then
        CDEV=/dev/disk/by-uuid/${CDEV#UUID=}
    fi

    if [[ "${CDEV}" == /* ]] && [ -b "${CDEV}" ]; then
        echo "${CDEV}"
    else
      # Invalid crypttab entry.
      return 1
    fi
}

# clevis_is_luks_device_by_uuid_open() checks whether the LUKS device whose
# UUID was passed as a parameter is already open.
clevis_is_luks_device_by_uuid_open() {
    local dev_luks_uuid="${1}"
    [ -z "${dev_luks_uuid}" ] && return 1
    dev_luks_uuid="$(echo "${dev_luks_uuid}" | sed -e 's/-//g')"
    test -b /dev/disk/by-id/dm-uuid-*"${dev_luks_uuid}"*
}

# clevis_devices_to_unlock() returns a list of devices to be unlocked, as per
# the info from crypttab.
clevis_devices_to_unlock() {
    local list_open_devices="${1:-}"
    [ ! -r /etc/crypttab ] && return 1

    local dev clevis_devices crypt_device dev_uuid bindings
    clevis_devices=

    # Build list of devices to unlock.
    while read -r _volname_ crypt_device _; do
        # skip empty lines and lines which begin with the '#' char, per
        # crypttab(5)
        case $_volname_ in
            ''|\#*) continue ;;
        esac
        if ! dev=$(clevis_map_device "${crypt_device}") \
                   || [ -z "${dev}" ]; then
            # Unable to get the device - maybe it's not available, e.g. a
            # device on a volume group that has not been activated yet.
            # Add it to the list anyway, since it's a pending device.
            clevis_devices="${clevis_devices} ${crypt_device}"
            continue
        fi

        # Check if this device has clevis bindings.
        if ! bindings="$(clevis luks list -d "${dev}" 2>/dev/null)" \
                         || [ -z "${bindings}" ]; then
            continue
        fi

        if [ -z "${list_open_devices}" ]; then
            # Check if this device is already open.
            dev_uuid="$(cryptsetup luksUUID "${dev}")"
            if clevis_is_luks_device_by_uuid_open "${dev_uuid}"; then
                continue
            fi
        fi

        clevis_devices="${clevis_devices} ${dev}"
    done < /etc/crypttab
    echo "${clevis_devices}" | sed -e 's/^ //'
}

# clevis_luks1_save_slot() works with LUKS1 devices and it saves a given JWE
# to a specific device and slot. The last parameter indicates whether we
# should overwrite existing metadata.
clevis_luks1_save_slot() {
    local DEV="${1}"
    local SLOT="${2}"
    local JWE="${3}"
    local SHOULD_OVERWRITE="${4:-}"

    luksmeta test -d "${DEV}" || return 1

    if luksmeta load -d "${DEV}" -s "${SLOT}" -u "${CLEVIS_UUID}" \
                        >/dev/null 2>/dev/null; then
        [ -z "${SHOULD_OVERWRITE}" ] && return 1
        if ! luksmeta wipe -f -d "${DEV}" -s "${SLOT}" \
                           -u "${CLEVIS_UUID}"; then
            echo "Error wiping slot ${SLOT} from ${DEV}" >&2
            return 1
        fi
    fi

    if ! echo -n "${JWE}" | luksmeta save -d "${DEV}" -s "${SLOT}" \
                                          -u "${CLEVIS_UUID}"; then
        echo "Error saving metadata to LUKSMeta slot ${SLOT} from ${DEV}" >&2
        return 1
    fi

    if ! luksmeta test -d "${DEV}" 2>/dev/null >/dev/null ; then
        echo "Error detected after saving metadata to LUKSMeta slot ${SLOT}, device ${DEV}" >&2
        return 1
    fi

    return 0
}

# clevis_luks2_save_slot() works with LUKS2 devices and it saves a given JWE
# to a specific device and slot. The last parameter indicates whether we
# should overwrite existing metadata.
clevis_luks2_save_slot() {
    local DEV="${1}"
    local SLOT="${2}"
    local TKN_ID="${3}"
    local JWE="${4}"
    local SHOULD_OVERWRITE="${5:-}"

    # Sanitize clevis LUKS2 tokens. Remove "orphan" clevis tokens, i.e.,
    # tokens that are not linked to any key slots.
    local token array_len
    for token in $(cryptsetup luksDump "${DEV}" \
                   | sed -rn 's|^\s+([0-9]+): clevis|\1|p'); do
        # Let's check the length of the "keyslots" array. If zero, it means
        # no key slots are linked, which is a problem.
        if ! array_len=$(cryptsetup token export --token-id \
                         "${token}" "${DEV}" \
                         | jose fmt --json=- --get keyslots --array --length \
                         --output=-) || [ "${array_len}" -eq 0 ]; then
            # Remove bad token.
            cryptsetup token remove --token-id "${token}" "${DEV}"
        fi
    done


    if ! token="$(cryptsetup luksDump "${DEV}" \
                  | grep -E -B1 "^\s+Keyslot:\s+${SLOT}$" \
                  | sed -rn 's|^\s+([0-9]+): clevis|\1|p')"; then
        echo "Error trying to read token from LUKS2 device ${DEV}, slot ${SLOT}" >&2
        return 1
    fi

    if [ -n "${token}" ]; then
        [ -z "${SHOULD_OVERWRITE}" ] && return 1
        if ! cryptsetup token remove --token-id "${token}" "${DEV}"; then
            echo "Error while removing token ${token} from LUKS2 device ${DEV}" >&2
            return 1
        fi
    fi

    if [ -n "${SHOULD_OVERWRITE}" ] && [ -n "${TKN_ID}" ]; then
        cryptsetup token remove --token-id "${TKN_ID}" "${DEV}" 2>/dev/null || :
    fi

    local metadata
    metadata=$(printf '{"type":"clevis","keyslots":["%s"],"jwe":%s}' \
               "${SLOT}" "$(jose jwe fmt --input="${JWE}")")
    if ! printf '%s' "${metadata}" | cryptsetup token import \
                $([ -n "${TKN_ID}" ] && printf -- '--token-id %s' "${TKN_ID}") \
                "${DEV}"; then
        echo "Error saving metadata to LUKS2 header in device ${DEV}" >&2
        return 1
    fi
    return 0
}

# clevis_luks_save_slot() saves a given JWE to a LUKS device+slot. It can also
# overwrite existing metadata.
clevis_luks_save_slot() {
    local DEV="${1}"
    local SLOT="${2}"
    local TKN_ID="${3}"
    local JWE="${4}"
    local SHOULD_OVERWRITE="${5:-}"

    if cryptsetup isLuks --type luks1 "${DEV}"; then
        clevis_luks1_save_slot "${DEV}" "${SLOT}" "${JWE}" \
                               "${SHOULD_OVERWRITE}" || return 1
    elif cryptsetup isLuks --type luks2 "${DEV}"; then
        clevis_luks2_save_slot "${DEV}" "${SLOT}" "${TKN_ID}" "${JWE}" \
                               "${SHOULD_OVERWRITE}" || return 1
    else
        return 1
    fi
    return 0
}

# clevis_luks1_backup_dev() backups the LUKSMeta slots from a LUKS device,
# which can be restored with clevis_luks1_restore_dev().
clevis_luks1_backup_dev() {
    local DEV="${1}"
    local TMP="${2}"

    [ -z "${DEV}" ] && return 1
    [ -z "${TMP}" ] && return 1

    luksmeta test -d "${DEV}" || return 0
    touch "${TMP}/initialized"

    local used_slots slt uuid jwe fname
    if ! used_slots=$(clevis_luks_used_slots "${DEV}") \
                      || [ -z "${used_slots}" ]; then
        return 1
    fi

    for slt in ${used_slots}; do
        if ! uuid=$(luksmeta show -d "${DEV}" -s "${slt}") \
                    || [ -z "${uuid}" ]; then
            continue
        fi
        if ! jwe=$(luksmeta load -d "${DEV}" -s "${slt}") \
                   || [ -z "${jwe}" ]; then
            continue
        fi

        fname=$(printf "slot_%s_%s" "${slt}" "${uuid}")
        printf "%s" "${jwe}" > "${TMP}/${fname}"
    done
    return 0
}

# clevis_luks1_restore_dev() takes care of restoring the LUKSMeta slots from
# a LUKS device that was backup'ed by clevis_luks1_backup_dev().
clevis_luks1_restore_dev() {
    local DEV="${1}"
    local TMP="${2}"

    [ -z "${DEV}" ] && return 1
    [ -z "${TMP}" ] && return 1

    [ -e "${TMP}/initialized" ] || return 0
    luksmeta test -d "${DEV}" || luksmeta init -f -d "${DEV}"

    local slt uuid jwe fname
    for fname in "${TMP}"/slot_*; do
        [ -f "${fname}" ] || break
        if ! slt=$(echo "${fname}" | cut -d '_' -f 2) \
                   || [ -z "${slt}" ]; then
            continue
        fi
        if ! uuid=$(echo "${fname}" | cut -d '_' -f 3) \
                    || [ -z "${uuid}" ]; then
            continue
        fi
        if ! jwe=$(cat "${fname}") || [ -z "${jwe}" ]; then
            continue
        fi
        if ! clevis_luks1_save_slot "${DEV}" "${slt}" \
                                    "${jwe}" "overwrite"; then
            echo "Error restoring LUKSmeta slot ${slt} from ${DEV}" >&2
            return 1
        fi
    done
    return 0
}

# clevis_luks_backup_dev() backups a particular LUKS device, which can then
# be restored with clevis_luks_restore_dev().
clevis_luks_backup_dev() {
    local DEV="${1}"
    local TMP="${2}"

    [ -z "${DEV}" ] && return 1
    [ -z "${TMP}" ] && return 1

    printf '%s' "${DEV}" > "${TMP}/device"

    printf '%s' "${DEV}" > "${TMP}/device"

    local HDR
    HDR="${TMP}/$(basename "${DEV}").header"
    if ! cryptsetup luksHeaderBackup "${DEV}" --batch-mode \
            --header-backup-file "${HDR}"; then
        echo "Error backing up LUKS header from ${DEV}" >&2
        return 1
    fi

    # If LUKS1, we need to manually back up (and later restore) the
    # LUKSmeta slots. For LUKS2, simply saving the header also saves
    # the associated tokens.
    if cryptsetup isLuks --type luks1 "${DEV}"; then
        if ! clevis_luks1_backup_dev "${DEV}" "${TMP}"; then
            return 1
        fi
    fi
    return 0
}

# clevis_luks_restore_dev() restores a given device that was backup'ed by
# clevis_luks_backup_dev().
clevis_luks_restore_dev() {
    local TMP="${1}"

    [ -z "${TMP}" ] && return 1
    [ -r "${TMP}"/device ] || return 1

    local DEV
    DEV="$(cat "${TMP}"/device)"

    local HDR
    HDR="${TMP}/$(basename "${DEV}").header"
    if [ ! -e "${HDR}" ]; then
        echo "LUKS header backup does not exist" >&2
        return 1
    fi

    if ! cryptsetup luksHeaderRestore "${DEV}" --batch-mode \
            --header-backup-file "${HDR}"; then
        echo "Error restoring LUKS header from ${DEV}" >&2
        return 1
    fi

    # If LUKS1, we need to manually back up (and later restore) the
    # LUKSmeta slots. For LUKS2, simply saving the header also saves
    # the associated tokens.
    if cryptsetup isLuks --type luks1 "${DEV}"; then
        if ! clevis_luks1_restore_dev "${DEV}" "${TMP}"; then
            return 1
        fi
    fi
    return 0
}

# clevis_luks_get_existing_key() may try to recover a valid password from
# existing bindings and additionally prompt the user for the passphrase.
clevis_luks_get_existing_key() {
    local DEV="${1}"
    local PROMPT="${2}"
    local RECOVER="${3:-}"

    [ -z "${DEV}" ] && return 1

    local pt
    if [ -n "${RECOVER}" ] && pt="$(clevis_luks_unlock_device "${DEV}")" \
                           && [ -n "${pt}" ]; then
        printf '%s' "${pt}"
        return 0
    fi

    # Let's prompt the user for the password.
    IFS= read -r -s -p "${PROMPT}" pt; echo >&2

    # Check if key is valid.
    clevis_luks_check_valid_key_or_keyfile "${DEV}" "${pt}" || return 1
    printf '%s' "${pt}"
}

# clevis_luks_luksmeta_sync_fix() makes sure LUKSmeta slots are sync'ed with
# cryptsetup, in order to prevent issues when saving clevis metadata.
clevis_luks_luksmeta_sync_fix() {
    local DEV="${DEV}"
    [ -z "${DEV}" ] && return 1

    # This applies only to LUKS1 devices.
    cryptsetup isLuks --type luks1 "${DEV}" || return 0

    # No issues if the LUKSmeta metadata is not initialized.
    luksmeta test -d "${DEV}" || return 0

    local first_free_slot
    if ! first_free_slot=$(clevis_luks_first_free_slot "${DEV}") \
                           || [ -z "${first_free_slot}" ]; then
        echo "There are possibly no free slots in ${DEV}" >&2
        return 1
    fi

    # 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 using
    # the LUKSMeta slot.
    local lmeta_slot lmeta_status lmeta_uuid
    lmeta_slot="$(luksmeta show -d "${DEV}" | grep "^${first_free_slot}")"
    # 1   active cb6e8904-81ff-40da-a84a-07ab9ab5715e
    # 2 inactive cb6e8904-81ff-40da-a84a-07ab9ab5715e
    lmeta_status="$(echo "${lmeta_slot}" | awk '{print $2}')"
    [ "${lmeta_status}" != 'inactive' ] && return 0

    lmeta_uuid="$(echo "${lmeta_slot}" | awk '{print $3}')"
    [ "${lmeta_uuid}" != "${CLEVIS_UUID}" ] && return 0

    luksmeta wipe -f -d "${DEV}" -s "${first_free_slot}"
}

# clevis_luks_get_hash() returns the hash algorithm used by a LUKS device.
clevis_luks_get_hash() {
    local DEV="${1}"
    [ -z "${DEV}" ] && return 1

    local luks_type hash
    luks_type=$(clevis_luks_type "${DEV}") || return 1

    case "${luks_type}" in
        luks1) hash=$(cryptsetup luksDump "${DEV}" 2>/dev/null \
                      | awk '/^Hash spec:[ \t]+/{print $3; exit}');;
        luks2) hash=$(cryptsetup luksDump "${DEV}" 2>/dev/null \
                      | awk '/^[[:space:]]+[0-9]+: luks/{in_slot=1}
                             in_slot && /Hash:/{print $2; exit}');;
        *) return 1;;
    esac

    # Validate: only allow alphanumeric characters, hyphens and underscores.
    case "${hash}" in
        *[!a-zA-Z0-9_-]*|"") return 1;;
    esac

    printf '%s\n' "${hash}"
}

# clevis_luks_add_key() adds a new key to a key slot.
clevis_luks_add_key() {
    local DEV="${1}"
    local SLT="${2}"
    local NEWKEY="${3}"
    local KEY="${4}"
    local KEYFILE="${5:-}"
    local EXISTING_TOKEN_ID="${6:-}"

    [ -z "${DEV}" ] && return 1
    [ -z "${NEWKEY}" ] && return 1

    local extra_args='' input
    input="$(printf '%s\n%s' "${KEY}" "${NEWKEY}")"
    if [ -n "${KEYFILE}" ]; then
        extra_args="$(printf -- '--key-file %s' "${KEYFILE}")"
        input="$(printf '%s' "${NEWKEY}")"
    fi
    if [ -n "${EXISTING_TOKEN_ID}" ]; then
        extra_args="$(printf -- '--token-id %s' "${EXISTING_TOKEN_ID}")"
        input="$(printf '%s' "${NEWKEY}")"
    fi
    local pbkdf_args="--pbkdf pbkdf2 --pbkdf-force-iterations 1000"

    local hash
    hash=$(clevis_luks_get_hash "${DEV}")
    if [ -n "${hash}" ]; then
        pbkdf_args="${pbkdf_args} --hash ${hash}"
    fi

    printf '%s' "${input}" | cryptsetup luksAddKey --force-password --batch-mode \
                                         --key-slot "${SLT}" \
                                         "${DEV}" \
                                         ${pbkdf_args} \
                                         ${extra_args}
}

# clevis_luks_update_key() will update a key slot with a new key.
clevis_luks_update_key() {
    local DEV="${1}"
    local SLT="${2}"
    local NEWKEY="${3}"
    local KEY="${4}"
    local KEYFILE="${5:-}"
    local EXISTING_TOKEN_ID="${6:-}"

    [ -z "${DEV}" ] && return 1
    [ -z "${NEWKEY}" ] && return 1

    # 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.
    local in_place
    clevis_luks_check_valid_key_or_keyfile "${DEV}" \
                                           "${KEY}" "${KEYFILE}" \
                                           "${SLT}" "${EXISTING_TOKEN_ID}" 2>/dev/null \
                                           && in_place=true

    local input extra_args=
    input="$(printf '%s\n%s' "${KEY}" "${NEWKEY}")"
    if [ -n "${KEYFILE}" ]; then
        extra_args="$(printf -- '--key-file %s --force-password' "${KEYFILE}")"
        input="$(printf '%s' "${NEWKEY}")"
    fi
    if [ -n "${EXISTING_TOKEN_ID}" ]; then
        extra_args="$(printf -- '--token-id %s --force-password' "${EXISTING_TOKEN_ID}")"
        input="$(printf '%s' "${NEWKEY}")"
    fi

    local pbkdf_args="--pbkdf pbkdf2 --pbkdf-force-iterations 1000"

    local hash
    hash=$(clevis_luks_get_hash "${DEV}")
    if [ -n "${hash}" ]; then
        pbkdf_args="${pbkdf_args} --hash ${hash}"
    fi

    if [ -n "${in_place}" ]; then
        printf '%s' "${input}" | cryptsetup luksChangeKey "${DEV}" \
                                            --key-slot "${SLT}" \
                                            --batch-mode \
                                            ${pbkdf_args} \
                                            ${extra_args}
        return
    fi

    if ! printf '%s' "${input}" | cryptsetup luksKillSlot "${DEV}" \
                                                          "${SLT}" \
                                                          ${extra_args}; then
        echo "Error wiping slot ${SLT} from ${DEV}" >&2
        return 1
    fi
    clevis_luks_add_key "${DEV}" "${SLT}" "${NEWKEY}" "${KEY}" "${KEYFILE}"
}

# clevis_luks_save_key_to_slot() will save a new key to a slot. It can either
# add a new key to a slot or updating an already used slot.
clevis_luks_save_key_to_slot() {
    local DEV="${1}"
    local SLT="${2}"
    local NEWKEY="${3}"
    local KEY="${4}"
    local KEYFILE="${5:-}"
    local OVERWRITE="${6:-}"
    local EXISTING_TOKEN_ID="${7:-}"

    [ -z "${DEV}" ] && return 1
    [ -z "${SLT}" ] && return 1
    [ -z "${NEWKEY}" ] && return 1

    # Make sure LUKSmeta slots are in sync with cryptsetup, to avoid the
    # problem reported in github issue #70. Applies to LUKS1 only.
    clevis_luks_luksmeta_sync_fix "${DEV}"

    # Let's check if we are adding a new key or updating an existing one.
    local update
    update="$(clevis_luks_used_slots "${DEV}" | grep "^${SLT}$")"
    if [ -n "${update}" ]; then
        # Replace an existing key.
        [ -n "${OVERWRITE}" ] || return 1

        clevis_luks_update_key "${DEV}" "${SLT}" \
                               "${NEWKEY}" "${KEY}" "${KEYFILE}" "${EXISTING_TOKEN_ID}"
        return
    fi

    # Add a new key.
    clevis_luks_add_key "${DEV}" "${SLT}" \
                        "${NEWKEY}" "${KEY}" "${KEYFILE}" "${EXISTING_TOKEN_ID}"
}

# clevis_luks_generate_key() generates a new key for use with clevis.
clevis_luks_generate_key() {
    local input
    input=$(printf '{"kty":"oct","bytes":%s}' "${JOSE_PASSWORD_LENGTH}")
    jose jwk gen --input="${input}" --output=- | \
        jose fmt --json=- --object --get k --unquote=-
}

# clevis_luks_token_id_by_slot() returns the token ID linked to a
# particular LUKS2 key slot.
clevis_luks_token_id_by_slot() {
    local DEV="${1}"
    local SLT="${2}"

    [ -z "${DEV}" ] && return 1
    [ -z "${SLT}" ] && return 1

    cryptsetup isLuks --type luks1 "${DEV}" && echo && return
    local tkn_id
    tkn_id="$(cryptsetup luksDump "${DEV}" \
              | grep -E -B1 "^\s+Keyslot:\s+${SLT}$" \
              | sed -rn 's|^\s+([0-9]+): clevis|\1|p')"

    printf '%s' "${tkn_id}"
}

# clevis_luks_cleanup() removes the temporary directory used to store the data
# relevant to device backup and restore.
clevis_luks_cleanup() {
    [ -z "${CLEVIS_TMP_DIR}" ] && return 0
    [ -d "${CLEVIS_TMP_DIR}" ] || return 0

    if ! rm -rf "${CLEVIS_TMP_DIR}"; then
        echo "Deleting temporary files failed!" >&2
        echo "You may need to clean up '${CLEVIS_TMP_DIR}'" >&2
        exit 1
    fi
    unset CLEVIS_TMP_DIR
}

# clevis_luks_first_free_slot() returns the first key slot that is available
# in a LUKS device.
clevis_luks_first_free_slot() {
    local DEV="${1}"
    [ -z "${DEV}" ] && return 1
    local first_free_slot
    if cryptsetup isLuks --type luks1 "${DEV}"; then
        first_free_slot=$(cryptsetup luksDump "${DEV}" \
                          | sed -rn 's|^Key Slot ([0-7]): DISABLED$|\1|p' \
                          | sed -n 1p)
    elif cryptsetup isLuks --type luks2 "${DEV}"; then
        local used_slots slt
        used_slots="$(clevis_luks_used_slots "${DEV}")"
        for slt in {0..31}; do
            if ! echo "${used_slots}" | grep -q "^${slt}$"; then
                first_free_slot="${slt}"
                break
            fi
        done
    else
        echo "Unsupported device ${DEV}" >&2
        return 1
    fi
    echo "${first_free_slot}"
}

# clevis_luks_do_bind() creates or updates a particular binding.
clevis_luks_do_bind() {
    local DEV="${1}"
    local SLT="${2}"
    local TKN_ID="${3}"
    local PIN="${4}"
    local CFG="${5}"
    local YES="${6:-}"
    local OVERWRITE="${7:-}"
    local KEY="${8:-}"
    local KEYFILE="${9:-}"
    local EXISTING_TOKEN_ID="${10:-}"

    [ -z "${DEV}" ] && return 1
    [ -z "${PIN}" ] && return 1
    [ -z "${CFG}" ] && return 1

    if ! clevis_luks_check_valid_key_or_keyfile "${DEV}" \
                                                "${KEY}" \
                                                "${KEYFILE}" \
                                                "" \
                                                "${EXISTING_TOKEN_ID}" \
                    && ! KEY="$(clevis_luks_get_existing_key "${DEV}" \
                                "Enter existing LUKS password: " \
                                "recover")"; then
        return 1
    fi

    local newkey jwe
    if ! newkey="$(clevis_luks_generate_key)" || [ -z "${newkey}" ]; then
        echo "Unable to generate a new key" >&2
        return 1
    fi

    # Encrypt the new key.
    if ! jwe="$(printf '%s' "${newkey}" | clevis encrypt "${PIN}" "${CFG}" \
                ${YES})" || [ -z "${jwe}" ]; then
        echo "Unable to perform encryption with PIN '${PIN}' and config '${CFG}'" >&2
        return 1
    fi

    # We can proceed to binding, after backing up the LUKS header and
    # metadata.
    local CLEVIS_TMP_DIR
    mkdir -p "${TMPDIR:-/tmp}"
    if ! CLEVIS_TMP_DIR="$(mktemp -d)" || [ -z "${CLEVIS_TMP_DIR}" ]; then
        echo "Unable to create a a temporary dir for device backup/restore" >&2
        return 1
    fi
    export CLEVIS_TMP_DIR
    trap 'clevis_luks_cleanup' EXIT

    # Backup LUKS header.
    if ! clevis_luks_backup_dev "${DEV}" "${CLEVIS_TMP_DIR}"; then
        echo "Unable to back up LUKS header from ${DEV}" >&2
        return 1
    fi

    if [ -z "${SLT}" ] && ! SLT=$(clevis_luks_first_free_slot "${DEV}") \
                                  || [ -z "${SLT}" ]; then
        echo "Unable to find a free slot in ${DEV}" >&2
        return 1
    fi

    [ -z "${TKN_ID}" ] && ! TKN_ID="$(clevis_luks_token_id_by_slot "${DEV}" \
                                      "${SLT}")" && return 1

    if ! clevis_luks_save_key_to_slot "${DEV}" "${SLT}" \
                                      "${newkey}" "${KEY}" "${KEYFILE}" \
                                      "${OVERWRITE}" "${EXISTING_TOKEN_ID}"; then
        echo "Unable to save/update key slot; operation cancelled" >&2
        clevis_luks_restore_dev "${CLEVIS_TMP_DIR}" || :
        rm -rf "${CLEVIS_TMP_DIR}"
        return 1
    fi

    if ! clevis_luks_save_slot "${DEV}" "${SLT}" "${TKN_ID}" \
                               "${jwe}" "${OVERWRITE}"; then
        echo "Unable to update metadata; operation cancelled" >&2
        clevis_luks_restore_dev "${CLEVIS_TMP_DIR}" || :
        rm -rf "${CLEVIS_TMP_DIR}"
        return 1
    fi

    clevis_luks_cleanup
    trap - EXIT
    return 0
}

# clevis_luks_luks2_supported() indicates whether we support LUKS2 devices.
# Support is determined at build time.
function clevis_luks_luks2_supported() {
    # We require cryptsetup >= 2.0.4 to fully support LUKSv2.
    return @OLD_CRYPTSETUP@
}

# clevis_luks_luks2_existing_token_id_supported() indicates whether
# cryptsetup allows token id for passphrase providing
function clevis_luks_luks2_existing_token_id_supported() {
    # We require cryptsetup >= 2.6.0 to fully support LUKSv2 addkey/open by token ID
    return @OLD_CRYPTSETUP_EXISTING_TOKEN_ID@
}

# clevis_luks_type() returns the LUKS type of a device, e.g. "luks1".
clevis_luks_type() {
    local DEV="${1}"
    [ -z "${DEV}" ] && return 1
    local luks_type
    if cryptsetup isLuks --type luks1 "${DEV}"; then
        luks_type="luks1"
    elif cryptsetup isLuks --type luks2 "${DEV}"; then
        clevis_luks_luks2_supported "${DEV}" || return 1
        luks_type="luks2"
    else
        return 1
    fi
    echo "${luks_type}"
}
