#!/bin/sh
#
# cgkey - cryptgroup key
# decrypt multiple cryptdisks with the same passphrase on Debian GNU/Linux
#
# This script prompts for a passphrase for cryptsetup for a given cryptgroup and 
# stores it in a keyring using Linux' key retention facility. Further requests 
# for the same cryptgroup return the stored passphrase without asking the user 
# again.
#
# Installation:
#
# - install keyutils
# - install this script to /bin/cgkey
# - if this script should also be used for the root volume, install the
#   provided cryptgroup hook to /etc/initramfs-tools/hooks/. The hook adds
#   /bin/cgkey and /sbin/keyctl to the initrd.
#
# Setup:
#
# Cryptgroups are defined in /etc/crypttab. For each volume that is part
# of a cryptgroup, add the option `keyscript=/bin/cgkey'. The <key file>
# parameter is used as the cryptgroup name. If `:' is appended to the
# cryptgroup name, the passphrase is requested from the user and stored
# in the key ring. Otherwise the passphrase is read from the key ring and
# printed to stdout. In this case, if the passphrase is not found in the
# key ring, it is requested from the user but not stored. Example crypttab:
#
# <crypt0> <clear0> groupkey: luks,keyscript=/bin/cgkey
# <crypt1> <clear1> groupkey  luks,keyscript=/bin/cgkey
# <crypt2> <clear2> groupkey  luks,keyscript=/bin/cgkey
#
# This would prompt for `groupkey' when trying to decrypt <crypt0> and store
# the entered passphrase in the key ring. For <crypt1> and <crypt2> the
# stored passphrase is then returned from the key ring and not requested again.
# If the passphrase is incorrect for <crypt0> it will be requested again. If
# the passphrase is incorrect for <crypt1> or <crypt2>, it will not be
# requested again, and the setup fails.
#
# This way, multiple users can share common volumes and also have their user
# specific volumes. For instance, Alice's passphrase decrypts <crypt0> and 
# <crypt1> and Bob's <crypt0> and <crypt2>. The setup fails for the other
# user's volume without asking for that passphrase.
#
# Problems:
#
# - only works on Linux, because the kernel's key retention facility is used
# - the passphrase is stored and can be retrieved by root. This is different
#   from retrieving the key for a decrypted volume, because that key is only
#   used for that one volume but the passphrase might be used for multiple
#   volumes on multiple systems by the user. 
# - to minimise the risk of passphrase exposure, an expiry time is set
#   on the stored passphrase, which should be as short as possible. But it 
#   also needs to be long enough to satisfy all requests, which happen
#   at multiple times during the boot process (very early for encrypted root
#   and later on for other volumes). There is no `correct' value.
# - the passphrase is piped between programmes and could end up in unsecure
#   memory and be written to swap
#
# Harald Braumann <harry@unheit.net>
#

PATH=/sbin:/bin:/usr/sbin:/usr/bin

set -eu

NAME="cgkey"

# common prefix for all keys stored in the kernel key ring
KEYPREFIX="cryptdisk-"
# key's expiry time
KEYEXPIRY="240"

KEYCTL="/bin/keyctl"
ASKPASS="/lib/cryptsetup/askpass"

KEYNAME="${1:-}"

die() {
    if [ -n "$*" ]; then
        echo "${NAME}: $*" >&2
    fi
    exit 1
}

[ -n "${KEYNAME}" ] || die "no key name specified"
[ -x "${KEYCTL}" ]  || die "${KEYCTL} not found"
[ -x "${ASKPASS}" ] || die "${ASKPASS} not found"

# check if keyname ends in `:' 
TMP="${KEYNAME%:}"
if [ "x${KEYNAME}" != "x${TMP}" ]; then
    PERSIST=1
    KEYNAME="${TMP}"
fi


# keyname for keyctl
K="${KEYPREFIX}${KEYNAME}"

if [ -n "${PERSIST:-}" ]; then

    # ask for this key and store it

   KN=`${ASKPASS} "Enter disk passphrase for ${KEYNAME}: " | \
       ${KEYCTL} padd user "${K}" @u` || die

   if ! ${KEYCTL} timeout "${KN}" "${KEYEXPIRY}"; then
       ${KEYCTL} revoke "${KN}" || true
       die "error setting key's expiry time"
   fi

   ${KEYCTL} pipe "$KN" || die

elif KN=`${KEYCTL} search @u user "${K}" 2>/dev/null`; then

    # key already entered => pipe it

    ${KEYCTL} pipe "${KN}" || die

else

    # key not found

    ${ASKPASS} "Enter disk passphrase for ${KEYNAME}: " || die

fi

exit 0
