#!/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/>.
#

# 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 CLEVIS_UUID="cb6e8904-81ff-40da-a84a-07ab9ab5715e"
        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}$" \
                    | head -n 1 | 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}"
}

# findexe() finds an executable.
findexe() {
    while read -r -d: path; do
        [ -f "${path}/${1}" ] && [ -x "${path}/${1}" ] && \
          echo "${path}/${1}" && return 0
    done <<< "${PATH}:"
    return 1
}

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

    local slots
    if cryptsetup isLuks --type luks1 "${DEV}"; then
        readarray -t slots < <(cryptsetup luksDump "${DEV}" \
            | sed -rn 's|^Key Slot ([0-7]): ENABLED$|\1|p')
    elif cryptsetup isLuks --type luks2 "${DEV}"; then
        readarray -t slots < <(cryptsetup luksDump "${DEV}" \
            | sed -rn 's|^\s+([0-9]+): luks2$|\1|p')
    else
        echo "${DEV} is not a supported LUKS device!" >&2
        return 1
    fi
    echo "${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
    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}"
        ;;
    sss)
        local threshold
        threshold=$(jose fmt -j- -Og t -o- <<< "${content}")
        clevis_luks_process_sss_pin "${content}" "${threshold}"
        ;;
    *)
        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_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
        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}" ]; 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_is_key_valid() checks whether the given key is valid to unlock
# the given device.
clevis_luks_is_key_valid() {
    local DEV="${1}"
    local KEY="${2}"

    if ! cryptsetup open --test-passphrase "${DEV}" \
                         --key-file <(echo -n "${KEY}") 2>/dev/null; then
        return 1
    fi
    return 0
}

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

    [ -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="$(clevis decrypt < <(echo -n "${jwe}") 2>/dev/null)" \
                       || [ -z "${passphrase}" ]; then
        return 1
    fi

    if ! clevis_luks_is_key_valid "${DEV}" "${passphrase}"; then
        return 1
    fi
    echo -n "${passphrase}"
    return 0
}

# 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}"
    [ -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}") \
                  || [ -z "${pt}" ]; then
            continue
        fi
        echo -n "${pt}"
        return 0
    done
    return 1
}

# Generate a key with the same entropy as the LUKS master key of a given
# device.
generate_key() {
    local DEV="${1}"

    if [ -z "${DEV}" ]; then
        echo "Please, specify a device." >&2
        return 1
    fi

    local dump
    local filter
    dump=$(cryptsetup luksDump "${DEV}")
    if cryptsetup isLuks --type luks1 "${DEV}"; then
        filter=$(sed -rn 's|MK bits:[ \t]*([0-9]+)|\1|p' <<< "${dump}")
    elif cryptsetup isLuks --type luks2 "${DEV}"; then
        filter=$(sed -rn 's|^\s+Key:\s+([0-9]+) bits\s*$|\1|p' <<< "${dump}")
    else
        echo "${DEV} is not a supported LUKS device!" >&2
        return 1
    fi
    local bits
    bits=$(sort -n <<< "${filter}" | tail -n 1)
    pwmake "${bits}"
}

# 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 OVERWRITE="${4}"

    ! luksmeta test -d "${DEV}" && return 1

    local UUID="cb6e8904-81ff-40da-a84a-07ab9ab5715e"
    if luksmeta load -d "${DEV}" -s "${SLOT}" -u "${UUID}" >/dev/null 2>/dev/null; then
        [ -z "${OVERWRITE}" ] && return 1
        if ! luksmeta wipe -f -d "${DEV}" -s "${SLOT}" -u "${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 "${UUID}"; then
        echo "Error saving metadata to LUKSMeta slot ${SLOT} from ${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 JWE="${3}"
    local OVERWRITE="${4}"

    local dump token
    dump="$(cryptsetup luksDump "${DEV}")"
    if ! token="$(grep -E -B1 "^\s+Keyslot:\s+${SLOT}$" <<< "${dump}" \
            | 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 "${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

    local metadata
    metadata=$(printf '{"type":"clevis","keyslots":["%s"],"jwe":%s}' \
               "${SLOT}" "$(jose jwe fmt -i- <<< "${JWE}")")
    if ! cryptsetup token import "${DEV}" <<< "${metadata}"; 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 JWE="${3}"
    local OVERWRITE="${4}"

    if cryptsetup isLuks --type luks1 "${DEV}"; then
        ! clevis_luks1_save_slot "${DEV}" "${SLOT}" "${JWE}" "${OVERWRITE}" \
            && return 1
    elif cryptsetup isLuks --type luks2 "${DEV}"; then
        ! clevis_luks2_save_slot "${DEV}" "${SLOT}" "${JWE}" "${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

    local slots slt uuid jwe fname
    readarray -t slots < <(cryptsetup luksDump "${DEV}" \
                           | sed -rn 's|^Key Slot ([0-7]): ENABLED$|\1|p')

    for slt in "${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

    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=$(< "${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

    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 DEV="${1}"
    local TMP="${2}"

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

    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_is_luks_device_by_uuid_open() checks whether the LUKS device with
# given UUID is open.
clevis_is_luks_device_by_uuid_open() {
    local LUKS_UUID="${1}"
    [ -z "${LUKS_UUID}" ] && return 1
    test -b /dev/disk/by-id/dm-uuid-*"${LUKS_UUID//-/}"*
}
