#!/usr/bin/ash
# shellcheck shell=dash

# Runtime hook to unlock LUKS volumes with help of the Mandos client
# (https://www.recompile.se/mandos). To be used instead of the
# "encrypt" hook. Replaces Debian's initramfs-tools keyscript approach
# with an explicit pipe into cryptsetup.

# Usage: replace "encrypt" with "mandos" in the HOOKS array of
# /etc/mkinitcpio.conf. The kernel command line stays as before
# ("cryptdevice=<dev>:<name>").
#
# Kernel command line additions (all optional):
#   mandos=off                  skip the Mandos attempt; cryptsetup will
#                               fail and the unlock is retried until the
#                               user types the password interactively
#                               (plugin-runner falls back to a console
#                               prompt)
#   mandos=connect:ADDR:PORT    contact the Mandos server directly at
#                               ADDR:PORT without resorting to zeroconf
#                               discovery (ADDR may be an IPv6 address,
#                               which may itself contain colons)
#
# The in-image plugin-runner configuration lives at
# /etc/mandos/plugin-runner.conf and is generated by the "install"
# part of this hook at initramfs build time.

mandos_get_password() {
    local mandosclientopts=
    if [ -n "${connect}" ]; then
        mandosclientopts="--options-for=mandos-client:--connect=${connect}"
    fi
    /usr/lib/mandos/plugin-runner \
        --config-file=/etc/mandos/plugin-runner.conf \
        --plugin-dir=/usr/lib/mandos/plugins.d \
        --plugin-helper-dir=/usr/lib/mandos/plugin-helpers \
        ${mandosclientopts} \
        >"${mandospwfile}"
}

run_hook() {
    local param connect override_all resolved old_ifs
    local mandospwfile=/tmp/.mandos-password
    quiet="${quiet:-}"
    rootdelay="${rootdelay:-}"
    connect=
    override_all=

    # parse the "mandos=" kernel option, if present
    old_ifs="${IFS}"
    for param in $(cat /proc/cmdline); do
        case "${param}" in
            mandos=*)
                IFS="${old_ifs},"
                for mparam in ${param#mandos=}; do
                    case "${mparam}" in
                        off)
                            override_all=yes
                            ;;
                        connect|connect:)
                            connect=
                            ;;
                        connect:*)
                            connect="${mparam#connect:}"
                            ;;
                        *)
                            echo "Warning: Mandos: unrecognized mandos= kernel parameter '${mparam}'"
                            ;;
                    esac
                done
                IFS="${old_ifs}"
                ;;
        esac
    done
    unset param

    if [ "${override_all}" = "yes" ]; then
        echo "Mandos is disabled (mandos=off). Please enter the password manually."
        return 1
    fi

    # Mandos' side of the network setup is done by the mandos-client
    # plugin itself: it needs IPv6 (it uses IPv6 link-local addressing
    # for zeroconf) and of course the network driver modules.
    modprobe -q ipv6 >/dev/null 2>&1

    if [ -z "${cryptdevice}" ]; then
        echo "Mandos: No cryptdevice= kernel parameter found, doing nothing."
        return 0
    fi

    # enforce a writable tempdir for gpgme (the plugins run as root
    # in the initramfs and use $HOME and $TMPDIR)
    mkdir -p /tmp
    chmod 1777 /tmp
    mkdir -p "${HOME:-/tmp}/.gnupg"
    chmod 0700 "${HOME:-/tmp}/.gnupg" 2>/dev/null
    export TMPDIR=/tmp
    export HOME=/tmp
    mkdir -p /tmp/.gnupg
    chmod 0700 /tmp/.gnupg

    # keep the password file readable only by root
    umask 077

    # cryptdevice can contain ':' which needs to be escaped.
    # shellcheck disable=SC2162
    IFS=: read cryptdev cryptname cryptoptions <<EOF
${cryptdevice}
EOF
    IFS="${old_ifs}"

    if [ -b "/dev/mapper/${cryptname}" ]; then
        echo "Device ${cryptname} already exists, not doing any crypt setup."
        return 0
    fi

    set -f
    IFS=,
    local cryptargs=""
    for cryptopt in ${cryptoptions}; do
        case "${cryptopt}" in
            allow-discards|discard)
                cryptargs="${cryptargs} --allow-discards"
                ;;
            no-read-workqueue|perf-no_read_workqueue)
                cryptargs="${cryptargs} --perf-no_read_workqueue"
                ;;
            no-write-workqueue|perf-no_write_workqueue)
                cryptargs="${cryptargs} --perf-no_write_workqueue"
                ;;
            sector-size=*)
                cryptargs="${cryptargs} --sector-size ${cryptopt#*=}"
                ;;
            *)
                echo "Encryption option '${cryptopt}' not known, ignoring." >&2
                ;;
        esac
    done
    set +f
    IFS="${old_ifs}"

    if resolved=$(resolve_device "${cryptdev}" "${rootdelay}" 2>/dev/null); then
        LUKSDEVICE="${resolved}"
    else
        LUKSDEVICE="${cryptdev}"
    fi

    echo "A password is required to access the ${cryptname} volume."
    echo "We will try to get it from the Mandos server first; if that"
    echo "does not work you will be asked for it on the console."

    # Retry as often as it takes: if the server is not reachable, the
    # plugin-runner itself falls back to prompting the password on the
    # console. If that password is (or was) wrong, cryptsetup fails
    # and we simply ask again. Loop until the volume is unlocked.
    while [ ! -e "/dev/mapper/${cryptname}" ]; do
        rm -f "${mandospwfile}"
        mandos_get_password && [ -s "${mandospwfile}" ] || {
            echo "Mandos: could not obtain a password, retrying..."
            sleep 2
            continue
        }
        if [ "${quiet}" = "y" ]; then
            cryptsetup open --type luks --key-file="${mandospwfile}" \
                "${LUKSDEVICE}" "${cryptname}" ${cryptargs} >/dev/null
        else
            cryptsetup open --type luks --key-file="${mandospwfile}" \
                "${LUKSDEVICE}" "${cryptname}" ${cryptargs}
        fi || echo "Mandos: unlock of ${cryptname} failed, retrying..."
    done
    rm -f "${mandospwfile}"
    unset mandospwfile
}

# vim: set ft=sh ts=4 sw=4 et:
