#!/usr/bin/env bash
# SPDX-License-Identifier: LGPL-2.1-or-later
set -eux
set -o pipefail

# TODO:
#   - /proc/cmdline parsing
#   - expect + interactive auth?

# We set up an encrypted /var partition which should get mounted automatically
# on boot
mountpoint /var

systemctl --state=failed --no-legend --no-pager | tee /failed
if [[ -s /failed ]]; then
    echo >&2 "Found units in failed state"
    exit 1
fi

at_exit() {
    set +e

    mountpoint -q /proc/cmdline && umount /proc/cmdline
    rm -f /etc/crypttab
    [[ -e /tmp/crypttab.bak ]] && cp -fv /tmp/crypttab.bak /etc/crypttab
    [[ -n "${STORE_LOOP:-}" ]] && losetup -d "$STORE_LOOP"
    [[ -n "${WORKDIR:-}" ]] && rm -rf "$WORKDIR"

    systemctl daemon-reload
}

trap at_exit EXIT

cryptsetup_start_and_check() {
    local expect_fail=0
    local umount_header_and_key=0
    local ec volume unit

    if [[ "${1:?}" == "-f" ]]; then
        expect_fail=1
        shift
    fi

    if [[ "${1:?}" == "-u" ]]; then
        umount_header_and_key=1
        shift
    fi

    for volume in "$@"; do
        unit="systemd-cryptsetup@$volume.service"

        # The unit existence check should always pass
        [[ "$(systemctl show -P LoadState "$unit")" == loaded ]]
        systemctl list-unit-files "$unit"

        systemctl start "$unit" && ec=0 || ec=$?
        if [[ "$expect_fail" -ne 0 ]]; then
            if [[ "$ec" -eq 0 ]]; then
                echo >&2 "Unexpected pass when starting $unit"
                return 1
            fi

            return 0
        fi

        if [[ "$ec" -ne 0 ]]; then
            echo >&2 "Unexpected fail when starting $unit"
            return 1
        fi

        if [[ "$umount_header_and_key" -ne 0 ]]; then
            umount "$TMPFS_DETACHED_KEYFILE"
            umount "$TMPFS_DETACHED_HEADER"
            udevadm settle --timeout=60
        fi

        systemctl status "$unit"
        test -e "/dev/mapper/$volume"
        systemctl stop "$unit"
        test ! -e "/dev/mapper/$volume"
    done

    return 0
}

# Note: some stuff (especially TPM-related) is already tested by TEST-70-TPM2,
#       so focus more on other areas instead

# Use a common workdir to make the cleanup easier
WORKDIR="$(mktemp -d)"

# Prepare a couple of LUKS2-encrypted disk images
#
# 1) Image with an empty password
IMAGE_EMPTY="$WORKDIR/empty.img)"
IMAGE_EMPTY_KEYFILE="$WORKDIR/empty.keyfile"
IMAGE_EMPTY_KEYFILE_ERASE="$WORKDIR/empty-erase.keyfile"
IMAGE_EMPTY_KEYFILE_ERASE_FAIL="$WORKDIR/empty-erase-fail.keyfile)"
truncate -s 32M "$IMAGE_EMPTY"
echo -n passphrase >"$IMAGE_EMPTY_KEYFILE"
chmod 0600 "$IMAGE_EMPTY_KEYFILE"
cryptsetup luksFormat --batch-mode \
                      --pbkdf pbkdf2 \
                      --pbkdf-force-iterations 1000 \
                      --use-urandom \
                      "$IMAGE_EMPTY" "$IMAGE_EMPTY_KEYFILE"
PASSWORD=passphrase NEWPASSWORD="" systemd-cryptenroll --password "$IMAGE_EMPTY"
# Duplicate the key file to test keyfile-erase as well
cp -v "$IMAGE_EMPTY_KEYFILE" "$IMAGE_EMPTY_KEYFILE_ERASE"
# The key should get erased even on a failed attempt, so test that too
cp -v "$IMAGE_EMPTY_KEYFILE" "$IMAGE_EMPTY_KEYFILE_ERASE_FAIL"

# 2) Image with a detached header and a key file offset + size
IMAGE_DETACHED="$WORKDIR/detached.img"
IMAGE_DETACHED_KEYFILE="$WORKDIR/detached.keyfile"
IMAGE_DETACHED_KEYFILE2="$WORKDIR/detached.keyfile2"
IMAGE_DETACHED_HEADER="$WORKDIR/detached.header"
truncate -s 32M "$IMAGE_DETACHED"
dd if=/dev/urandom of="$IMAGE_DETACHED_KEYFILE" count=64 bs=1
dd if=/dev/urandom of="$IMAGE_DETACHED_KEYFILE2" count=32 bs=1
chmod 0600 "$IMAGE_DETACHED_KEYFILE" "$IMAGE_DETACHED_KEYFILE2"
cryptsetup luksFormat --batch-mode \
                      --pbkdf pbkdf2 \
                      --pbkdf-force-iterations 1000 \
                      --use-urandom \
                      --header "$IMAGE_DETACHED_HEADER" \
                      --keyfile-offset 32 \
                      --keyfile-size 16 \
                      "$IMAGE_DETACHED" "$IMAGE_DETACHED_KEYFILE"
# Also, add a second key file to key slot 8
# Note: --key-slot= behaves as --new-key-slot= when used alone for backwards compatibility
cryptsetup luksAddKey --batch-mode \
                      --header "$IMAGE_DETACHED_HEADER" \
                      --key-file "$IMAGE_DETACHED_KEYFILE" \
                      --keyfile-offset 32 \
                      --keyfile-size 16 \
                      --key-slot 8 \
                      "$IMAGE_DETACHED" "$IMAGE_DETACHED_KEYFILE2"

# Prepare a couple of dummy devices we'll store a copy of the detached header
# and one of the keys on to test if systemd-cryptsetup correctly mounts them
# when necessary
STORE_IMAGE="$WORKDIR/store.img"
truncate -s 64M "$STORE_IMAGE"
STORE_LOOP="$(losetup --show --find --partscan "$STORE_IMAGE")"
udevadm lock --timeout=30 --device "$STORE_LOOP" sfdisk "$STORE_LOOP" <<EOF
label: gpt
type=0FC63DAF-8483-4772-8E79-3D69D8477DE4 name=header_store size=32M
type=0FC63DAF-8483-4772-8E79-3D69D8477DE4 name=keyfile_store
EOF
udevadm settle --timeout=60
mkdir -p /mnt
udevadm lock --timeout=30 --device "/dev/disk/by-partlabel/header_store" mkfs.ext4 -L header_store "/dev/disk/by-partlabel/header_store"
mount "/dev/disk/by-partlabel/header_store" /mnt
cp "$IMAGE_DETACHED_HEADER" /mnt/header
umount /mnt
udevadm lock --timeout=30 --device "/dev/disk/by-partlabel/keyfile_store" mkfs.ext4 -L keyfile_store "/dev/disk/by-partlabel/keyfile_store"
mount "/dev/disk/by-partlabel/keyfile_store" /mnt
cp "$IMAGE_DETACHED_KEYFILE2" /mnt/keyfile
umount /mnt

# Also copy the key and header on a tmpfs that we will umount after unlocking
TMPFS_DETACHED_KEYFILE="$(mktemp -d)"
TMPFS_DETACHED_HEADER="$(mktemp -d)"
mount -t tmpfs -o size=32M tmpfs "$TMPFS_DETACHED_KEYFILE"
mount -t tmpfs -o size=32M tmpfs "$TMPFS_DETACHED_HEADER"
cp "$IMAGE_DETACHED_KEYFILE" "$TMPFS_DETACHED_KEYFILE/keyfile"
cp "$IMAGE_DETACHED_HEADER" "$TMPFS_DETACHED_HEADER/header"

udevadm settle --timeout=60

# Prepare our test crypttab
[[ -e /etc/crypttab ]] && cp -fv /etc/crypttab /tmp/crypttab.bak
cat >/etc/crypttab <<EOF
# headless should translate to headless=1
empty_key            $IMAGE_EMPTY    $IMAGE_EMPTY_KEYFILE            headless,x-systemd.device-timeout=1m
empty_key_erase      $IMAGE_EMPTY    $IMAGE_EMPTY_KEYFILE_ERASE      headless=1,keyfile-erase=1
empty_key_erase_fail $IMAGE_EMPTY    $IMAGE_EMPTY_KEYFILE_ERASE_FAIL headless=1,keyfile-erase=1,keyfile-offset=4
# Empty passphrase without try-empty-password(=yes) shouldn't work
empty_fail0          $IMAGE_EMPTY    -                               headless=1
empty_fail1          $IMAGE_EMPTY    -                               headless=1,try-empty-password=0
empty0               $IMAGE_EMPTY    -                               headless=1,try-empty-password
empty1               $IMAGE_EMPTY    -                               headless=1,try-empty-password=1
# This one expects the key to be under /{etc,run}/cryptsetup-keys.d/empty_nokey.key
empty_nokey          $IMAGE_EMPTY    -                               headless=1
empty_pkcs11_auto    $IMAGE_EMPTY    -                               headless=1,pkcs11-uri=auto

detached             $IMAGE_DETACHED $IMAGE_DETACHED_KEYFILE         headless=1,header=$IMAGE_DETACHED_HEADER,keyfile-offset=32,keyfile-size=16
detached_store0      $IMAGE_DETACHED $IMAGE_DETACHED_KEYFILE         headless=1,header=/header:LABEL=header_store,keyfile-offset=32,keyfile-size=16
detached_store1      $IMAGE_DETACHED /keyfile:LABEL=keyfile_store    headless=1,header=$IMAGE_DETACHED_HEADER
detached_store2      $IMAGE_DETACHED /keyfile:LABEL=keyfile_store    headless=1,header=/header:LABEL=header_store
detached_fail0       $IMAGE_DETACHED $IMAGE_DETACHED_KEYFILE         headless=1,header=$IMAGE_DETACHED_HEADER,keyfile-offset=32
detached_fail1       $IMAGE_DETACHED $IMAGE_DETACHED_KEYFILE         headless=1,header=$IMAGE_DETACHED_HEADER
detached_fail2       $IMAGE_DETACHED $IMAGE_DETACHED_KEYFILE         headless=1
detached_fail3       $IMAGE_DETACHED $IMAGE_DETACHED_KEYFILE         headless=1,header=$IMAGE_DETACHED_HEADER,keyfile-offset=16,keyfile-size=16
detached_fail4       $IMAGE_DETACHED $IMAGE_DETACHED_KEYFILE         headless=1,header=$IMAGE_DETACHED_HEADER,keyfile-offset=32,keyfile-size=8
detached_slot0       $IMAGE_DETACHED $IMAGE_DETACHED_KEYFILE2        headless=1,header=$IMAGE_DETACHED_HEADER
detached_slot1       $IMAGE_DETACHED $IMAGE_DETACHED_KEYFILE2        headless=1,header=$IMAGE_DETACHED_HEADER,key-slot=8
detached_slot_fail   $IMAGE_DETACHED $IMAGE_DETACHED_KEYFILE2        headless=1,header=$IMAGE_DETACHED_HEADER,key-slot=0
detached_nofail      $IMAGE_DETACHED $TMPFS_DETACHED_KEYFILE/keyfile headless=1,header=$TMPFS_DETACHED_HEADER/header,keyfile-offset=32,keyfile-size=16,nofail
EOF

# Temporarily drop luks.name=/luks.uuid= from the kernel command line, as it makes
# systemd-cryptsetup-generator ignore mounts from /etc/crypttab that are not also
# specified on the kernel command line
sed -r 's/luks.(name|uuid)=[^[:space:]+]//' /proc/cmdline >/tmp/cmdline.tmp
mount --bind /tmp/cmdline.tmp /proc/cmdline
# Run the systemd-cryptsetup-generator once explicitly, to collect coverage,
# as during daemon-reload we run generators in a sandbox
mkdir -p /tmp/systemd-cryptsetup-generator.out
/usr/lib/systemd/system-generators/systemd-cryptsetup-generator /tmp/systemd-cryptsetup-generator.out/
systemctl daemon-reload
systemctl list-unit-files "systemd-cryptsetup@*"

cryptsetup_start_and_check empty_key
test -e "$IMAGE_EMPTY_KEYFILE_ERASE"
cryptsetup_start_and_check empty_key_erase
test ! -e "$IMAGE_EMPTY_KEYFILE_ERASE"
test -e "$IMAGE_EMPTY_KEYFILE_ERASE_FAIL"
cryptsetup_start_and_check -f empty_key_erase_fail
test ! -e "$IMAGE_EMPTY_KEYFILE_ERASE_FAIL"
cryptsetup_start_and_check -f empty_fail{0..1}
cryptsetup_start_and_check empty{0..1}
# First, check if we correctly fail without any key
cryptsetup_start_and_check -f empty_nokey
# And now provide the key via /{etc,run}/cryptsetup-keys.d/
mkdir -p /run/cryptsetup-keys.d
cp "$IMAGE_EMPTY_KEYFILE" /run/cryptsetup-keys.d/empty_nokey.key
cryptsetup_start_and_check empty_nokey

if [[ -d /usr/lib/softhsm/tokens ]]; then
    # Test unlocking with a PKCS#11 token
    export SOFTHSM2_CONF="/etc/softhsm2.conf"

    PIN="1234" systemd-cryptenroll --pkcs11-token-uri="pkcs11:token=TestToken;object=RSATestKey" --unlock-key-file="$IMAGE_EMPTY_KEYFILE" "$IMAGE_EMPTY"
    cryptsetup_start_and_check empty_pkcs11_auto
    cryptsetup luksKillSlot -q "$IMAGE_EMPTY" 2
    cryptsetup token remove --token-id 0 "$IMAGE_EMPTY"

    PIN="1234" systemd-cryptenroll --pkcs11-token-uri="pkcs11:token=TestToken;object=RSATestKey;type=cert" --unlock-key-file="$IMAGE_EMPTY_KEYFILE" "$IMAGE_EMPTY"
    cryptsetup_start_and_check empty_pkcs11_auto
    cryptsetup luksKillSlot -q "$IMAGE_EMPTY" 2
    cryptsetup token remove --token-id 0 "$IMAGE_EMPTY"

    PIN="1234" systemd-cryptenroll --pkcs11-token-uri="pkcs11:token=TestToken;object=RSATestKey;type=public" --unlock-key-file="$IMAGE_EMPTY_KEYFILE" "$IMAGE_EMPTY"
    cryptsetup_start_and_check empty_pkcs11_auto
    cryptsetup luksKillSlot -q "$IMAGE_EMPTY" 2
    cryptsetup token remove --token-id 0 "$IMAGE_EMPTY"

    PIN="1234" systemd-cryptenroll --pkcs11-token-uri="pkcs11:token=TestToken;object=ECTestKey" --unlock-key-file="$IMAGE_EMPTY_KEYFILE" "$IMAGE_EMPTY"
    cryptsetup_start_and_check empty_pkcs11_auto
    cryptsetup luksKillSlot -q "$IMAGE_EMPTY" 2
    cryptsetup token remove --token-id 0 "$IMAGE_EMPTY"

    PIN="1234" systemd-cryptenroll --pkcs11-token-uri="pkcs11:token=TestToken;object=ECTestKey;type=cert" --unlock-key-file="$IMAGE_EMPTY_KEYFILE" "$IMAGE_EMPTY"
    cryptsetup_start_and_check empty_pkcs11_auto
    cryptsetup luksKillSlot -q "$IMAGE_EMPTY" 2
    cryptsetup token remove --token-id 0 "$IMAGE_EMPTY"

    PIN="1234" systemd-cryptenroll --pkcs11-token-uri="pkcs11:token=TestToken;object=ECTestKey;type=public" --unlock-key-file="$IMAGE_EMPTY_KEYFILE" "$IMAGE_EMPTY"
    cryptsetup_start_and_check empty_pkcs11_auto
    cryptsetup luksKillSlot -q "$IMAGE_EMPTY" 2
    cryptsetup token remove --token-id 0 "$IMAGE_EMPTY"

    # Verify that new RSA enrollments tag the LUKS2 token with the OAEP padding scheme. Old systemd
    # versions ignore this field and fall back to PKCS#1 v1.5, which causes the token to refuse to
    # decrypt the OAEP-wrapped blob, so this metadata is what makes graceful rejection on old systems
    # possible. The actual OAEP hash variant (SHA-1 vs SHA-256) is probed at enrollment time and
    # depends on what the token supports, but SoftHSM through at least 2.7.0 only accepts SHA-1
    PIN="1234" systemd-cryptenroll --pkcs11-token-uri="pkcs11:token=TestToken;object=RSATestKey" --unlock-key-file="$IMAGE_EMPTY_KEYFILE" "$IMAGE_EMPTY"
    cryptsetup luksDump --dump-json-metadata "$IMAGE_EMPTY" |
        jq -e '.tokens | with_entries(select(.value.type == "systemd-pkcs11"))[]["pkcs11-padding"] | select(. == "oaep-sha256" or . == "oaep-sha1")' >/dev/null

    # Forward-compat negative check: tampering with the padding tag must cause unlocking to fail
    # cleanly (the token rejects the OAEP blob when asked to do PKCS#1 v1.5, and vice versa).
    TOKEN_ID=$(cryptsetup luksDump --dump-json-metadata "$IMAGE_EMPTY" |
        jq -r '.tokens | to_entries[] | select(.value.type == "systemd-pkcs11") | .key' | head -n1)
    cryptsetup token export --token-id "$TOKEN_ID" "$IMAGE_EMPTY" |
        jq '. + {"pkcs11-padding": "pkcs1"}' >"$WORKDIR/tampered-token.json"
    cryptsetup token remove --token-id "$TOKEN_ID" "$IMAGE_EMPTY"
    cryptsetup token import --json-file="$WORKDIR/tampered-token.json" --token-id "$TOKEN_ID" "$IMAGE_EMPTY"
    cryptsetup_start_and_check -f empty_pkcs11_auto
    # Restore the correct token and confirm unlock works again.
    cryptsetup token remove --token-id "$TOKEN_ID" "$IMAGE_EMPTY"
    cryptsetup luksKillSlot -q "$IMAGE_EMPTY" 2

    PIN="1234" systemd-cryptenroll --pkcs11-token-uri="pkcs11:token=TestToken;object=RSATestKey" --unlock-key-file="$IMAGE_EMPTY_KEYFILE" "$IMAGE_EMPTY"
    cryptsetup_start_and_check empty_pkcs11_auto
    cryptsetup luksKillSlot -q "$IMAGE_EMPTY" 2
    cryptsetup token remove --token-id 0 "$IMAGE_EMPTY"

    # Backward-compat check: mock a legacy LUKS2 token wrapped with PKCS#1 v1.5 (no
    # 'pkcs11-padding' field) and verify the new code can still decrypt it
    SOFTHSM_MODULE=$(awk -F: '/^[[:space:]]*module[[:space:]]*:/ { gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit }' /usr/share/p11-kit/modules/softhsm2.module)
    [[ -n "$SOFTHSM_MODULE" ]] || { echo "Could not locate libsofthsm2.so via /usr/share/p11-kit/modules/softhsm2.module" >&2; exit 1; }
    # Read the RSA public key out of the matching certificate object on the token, generate random key and
    # wrap it
    pkcs11-tool --module "$SOFTHSM_MODULE" --token-label TestToken --pin 1234 \
                --read-object --type cert --label RSATestKey --output-file "$WORKDIR/rsa.der"
    openssl x509 -inform DER -in "$WORKDIR/rsa.der" -pubkey -noout >"$WORKDIR/rsa-pub.pem"
    openssl rand -out "$WORKDIR/decrypted.bin" 32
    openssl pkeyutl -encrypt -pubin -inkey "$WORKDIR/rsa-pub.pem" \
                    -pkeyopt rsa_padding_mode:pkcs1 \
                    -in "$WORKDIR/decrypted.bin" -out "$WORKDIR/encrypted.bin"
    base64 -w0 "$WORKDIR/decrypted.bin" >"$WORKDIR/passphrase"
    cryptsetup luksAddKey --batch-mode --key-file="$IMAGE_EMPTY_KEYFILE" "$IMAGE_EMPTY" "$WORKDIR/passphrase"
    LEGACY_SLOT=$(cryptsetup luksDump --dump-json-metadata "$IMAGE_EMPTY" |
        jq -r '.keyslots | keys | map(tonumber) | max')
    ENCRYPTED_B64=$(base64 -w0 "$WORKDIR/encrypted.bin")
    cat >"$WORKDIR/legacy-token.json" <<EOF
{
    "type": "systemd-pkcs11",
    "keyslots": ["$LEGACY_SLOT"],
    "pkcs11-uri": "pkcs11:token=TestToken;object=RSATestKey;type=private",
    "pkcs11-key": "$ENCRYPTED_B64"
}
EOF
    cryptsetup token import --json-file="$WORKDIR/legacy-token.json" "$IMAGE_EMPTY"
    cryptsetup_start_and_check empty_pkcs11_auto
    cryptsetup luksKillSlot -q "$IMAGE_EMPTY" "$LEGACY_SLOT"
    cryptsetup token remove --token-id 0 "$IMAGE_EMPTY"
fi

cryptsetup_start_and_check detached
cryptsetup_start_and_check detached_store{0..2}
cryptsetup_start_and_check -f detached_fail{0..4}
cryptsetup_start_and_check detached_slot{0..1}
cryptsetup_start_and_check -f detached_slot_fail
cryptsetup_start_and_check -u detached_nofail

systemd-cryptenroll --list-devices

touch /testok
