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

. tang-common-test-functions

error() {
    echo "${1}" >&2
    exit 1
}

skip_test() {
    echo "${1}" >&2
    exit 77
}

# We require cryptsetup >= 2.0.4 to fully support LUKSv2.
# Support is determined at build time.
luks2_supported() {
    return @OLD_CRYPTSETUP@
}

# We require cryptsetup >= 2.6.0 to fully support LUKSv2 addkey/open by token ID
# Support is determined at build time.
luks2_existing_token_id_supported() {
    return @OLD_CRYPTSETUP_EXISTING_TOKEN_ID@
}

# Creates a new LUKS1 or LUKS2 device to be used.
new_device() {
    local LUKS="${1}"
    local DEV="${2}"
    local PASS="${3}"

    # Some builders fail if the cryptsetup steps are not ran as root, so let's
    # skip the test now if not running as root.
    if [ "$(id -u)" != 0 ]; then
        skip_test "WARNING: You must be root to run this test; test skipped."
    fi

    # Using a default password, if none has been provided.
    if [ -z "${PASS}" ]; then
        PASS="${DEFAULT_PASS}"
    fi

    local DEV_CACHED="${TMP}/${LUKS}.cached"

    # Let's reuse an existing device, if there is one.
    if [ -f "${DEV_CACHED}" ]; then
        echo "Reusing cached ${LUKS} device..."
        cp -f "${DEV_CACHED}" "${DEV}"
        return 0
    fi

    fallocate -l64M "${DEV}"
    cryptsetup luksFormat --type "${LUKS}" --pbkdf pbkdf2 \
        --pbkdf-force-iterations 1000 --key-size 512 --batch-mode \
        --force-password "${DEV}" <<< "${PASS}"
    # Caching the just-formatted device for possible reuse.
    cp -f "${DEV}" "${DEV_CACHED}"
}

# Creates a new LUKS1 or LUKS2 device with a specific hash algorithm.
new_device_hash() {
    local LUKS="${1}"
    local DEV="${2}"
    local HASH="${3}"
    local PASS="${4}"

    # Some builders fail if the cryptsetup steps are not ran as root, so let's
    # skip the test now if not running as root.
    if [ "$(id -u)" != 0 ]; then
        skip_test "WARNING: You must be root to run this test; test skipped."
    fi

    # Using a default password, if none has been provided.
    if [ -z "${PASS}" ]; then
        PASS="${DEFAULT_PASS}"
    fi

    fallocate -l64M "${DEV}" 2>/dev/null \
        || dd if=/dev/zero of="${DEV}" bs=1M count=64 status=none
    cryptsetup luksFormat --type "${LUKS}" --hash "${HASH}" --pbkdf pbkdf2 \
        --pbkdf-force-iterations 1000 --key-size 512 --batch-mode \
        --force-password "${DEV}" <<< "${PASS}"
}

# Creates a new LUKS1 or LUKS2 device to be used, using a keyfile.
new_device_keyfile() {
    local LUKS="${1}"
    local DEV="${2}"
    local KEYFILE="${3}"

    # Some builders fail if the cryptsetup steps are not ran as root, so let's
    # skip the test now if not running as root.
    if [ "$(id -u)" != 0 ]; then
        skip_test "WARNING: You must be root to run this test; test skipped."
    fi

    if [[ -z "${KEYFILE}" ]] || [[ ! -f "${KEYFILE}" ]]; then
        error "Invalid keyfile (${KEYFILE})."
    fi

    fallocate -l64M "${DEV}"
    cryptsetup luksFormat --type "${LUKS}" --pbkdf pbkdf2 \
        --pbkdf-force-iterations 1000 --key-size 512 --batch-mode \
        "${DEV}" "${KEYFILE}"
}

pin_cfg_equal() {
    # Let's remove the single quotes from the pin configuration.
    local cfg1="${1//\'/}"
    local cfg2="${2//\'/}"

    # Now we sort and present them in compact form.
    local sorted_cfg1 sorted_cfg2
    sorted_cfg1="$(jq --compact-output --sort-keys . < <(echo -n "${cfg1}"))"
    sorted_cfg2="$(jq --compact-output --sort-keys . < <(echo -n "${cfg2}"))"

    # And we finally compare.
    if [ "${sorted_cfg1}" = "${sorted_cfg2}" ]; then
        return 0
    fi
    return 1
}

compare_luks_header() {
    DEV1="${1}"
    DEV2="${2}"
    TMP="${3}"

    cryptsetup luksHeaderBackup "${DEV1}" \
               --header-backup-file "${TMP}"/check-header1
    cryptsetup luksHeaderBackup "${DEV2}" \
               --header-backup-file "${TMP}"/check-header2

    local cs1 cs2
    cs1=$(cksum "${TMP}"/check-header1 | cut -d' ' -f 1)
    cs2=$(cksum "${TMP}"/check-header2 | cut -d' ' -f 1)
    rm -f "${TMP}"/check-header{1,2}

    if [ "${cs1}" == "${cs2}" ]; then
        return 0
    fi
    return 1
}

used_luks1_metadata_slots() {
    DEV="${1}"
    if ! luksmeta test -d "${DEV}"; then
        echo ""
        return 0
    fi

    local clevis_uuid="cb6e8904-81ff-40da-a84a-07ab9ab5715e"
    luksmeta show -d "${DEV}" \
        | sed -rn "s|^([0-9]+)\s+active\s+${clevis_uuid}$|\1|p" \
        | tr '\n' ' ' | sed 's/ $//'
}

used_luks2_metadata_slots() {
    DEV="${1}"
    cryptsetup luksDump "${DEV}" \
        | grep -E -A1 "^\s+[0-9]+:\s+clevis$" \
        | sed -rn 's|^\s+Keyslot:\s+([0-9]+)$|\1|p' | sort -n \
        | tr '\n' ' ' | sed 's/ $//'
}

used_luks2_metadata_tokens() {
    DEV="${1}"
    cryptsetup luksDump "${DEV}" \
        | grep -E -B1 "^\s+Keyslot:\s+[0-9]+$" \
        | sed -rn 's|^\s+([0-9]+): clevis|\1|p' \
        | tr '\n' ' ' | sed 's/ $//'
}

compare_luks1_metadata() {
    DEV1="${1}"
    DEV2="${2}"

    # If both are non-initialized, metadata is the same.
    ! luksmeta test -d "${DEV1}" && ! luksmeta test -d "${DEV2}" && return 0
    # Otherwise, metadata differ.
    ! luksmeta test -d "${DEV1}" && return 1
    ! luksmeta test -d "${DEV2}" && return 1

    local slt1 slt2
    slt1=$(used_luks1_metadata_slots "${DEV1}")
    slt2=$(used_luks1_metadata_slots "${DEV2}")

    if [ "${slt1}" != "${slt2}" ]; then
        echo "used slots did not match ($slt1) ($slt2)" >&2
        return 1
    fi

    local slt md1 md2
    for slt in ${slt1}; do
        md1="$(luksmeta load -d "${DEV}" -s "${slt}")"
        md2="$(luksmeta load -d "${DEV2}" -s "${slt}")"
        if [ "${md1}" != "${md2}" ]; then
            echo "metadata in slot ${slt} did not match" >&2
            return 1
        fi
    done
    return 0
}

compare_luks2_metadata() {
    DEV1="${1}"
    DEV2="${2}"

    local slt1 slt2
    slt1=$(used_luks2_metadata_slots "${DEV1}")
    slt2=$(used_luks2_metadata_slots "${DEV2}")

    if [ "${slt1}" != "${slt2}" ]; then
        echo "used slots did not match ($slt1) ($slt2)" >&2
        return 1
    fi

    local tkn1 tkn2
    tkn1=$(used_luks2_metadata_tokens "${DEV1}")
    tkn2=$(used_luks2_metadata_tokens "${DEV2}")

    if [ "${tkn1}" != "${tkn2}" ]; then
        echo "used tokens did not match ($tkn1) ($tkn2)" >&2
        return 1
    fi

    local tkn md1 md2
    for tkn in ${tkn1}; do
        md1="$(cryptsetup token export --token-id "${tkn}" "${DEV1}")"
        md2="$(cryptsetup token export --token-id "${tkn}" "${DEV2}")"
        if [ "${md1}" != "${md2}" ]; then
            echo "metadata in token ${tkn} did not match" >&2
            return 1
        fi
    done
    return 0
}

new_passphrase() {
    jose jwk gen --input='{"kty":"oct","bytes":8}' --output=- \
        | jose fmt --json=- --object --get k --unquote=-
}

export DEFAULT_PASS='   just-some-   test-password-here 1.+?~!@#$%^&*();:'"'"'"[]{}_=/`\   '
