#!/bin/bash
# clevis-s6-unlock -- open the Clevis-bound LUKS devices from /etc/crypttab.
#
# This is what clevis-luks-askpass would do on a systemd machine. That helper
# speaks the systemd password-agent protocol: it watches /run/systemd/ask-password
# for cryptsetup's questions and writes the answer back into the socket named in
# them. On an s6 system nobody ever asks those questions, so the helper waits
# forever. Calling "clevis luks unlock" directly is the same operation without
# the middleman.
#
# Usage: clevis-s6-unlock [name ...]
#        With no arguments, every crypttab entry that carries a Clevis binding
#        is tried. Names are crypttab's first column (the mapper name).
#
# Configuration: /etc/default/clevis-unlock
#   CLEVIS_UNLOCK_NAMES     space separated mapper names; empty = all bound ones
#   CLEVIS_UNLOCK_TRIES     attempts per device (default 20)
#   CLEVIS_UNLOCK_DELAY     seconds between attempts (default 3)
#   CLEVIS_UNLOCK_REQUIRED  yes: exit non-zero if a device stays closed
#                           (default yes -- a host running with half its storage
#                           missing is worse than one that stops here)
set -u

CLEVIS_UNLOCK_NAMES=""
CLEVIS_UNLOCK_TRIES=20
CLEVIS_UNLOCK_DELAY=3
CLEVIS_UNLOCK_REQUIRED="yes"
[ -r /etc/default/clevis-unlock ] && . /etc/default/clevis-unlock

[ $# -gt 0 ] && CLEVIS_UNLOCK_NAMES="$*"

log() { echo "clevis-unlock: $*"; }
err() { echo "clevis-unlock: $*" >&2; }

# The device for a crypttab entry. crypttab's second column is normally
# UUID=<luks uuid>, and /dev/disk/by-uuid does not exist without udevd -- the
# Tick hosts run mdevd, which populates /dev but not the by-uuid symlinks. So
# the UUID is compared against the block devices themselves. A plain /dev/sdX in
# crypttab is taken as it is.
resolve_device() {
  local spec="$1" want c n

  case "$spec" in
    UUID=*) want="${spec#UUID=}" ;;
    /dev/*) printf '%s\n' "$spec"; return 0 ;;
    *)      err "unsupported source specification '${spec}'"; return 1 ;;
  esac

  # Every block device the kernel knows, not a guessed list of names: a LUKS
  # container can sit on /dev/sdb, on a partition, on an md or loop device, or
  # on something a driver named itself. Device-mapper nodes are skipped -- an
  # unlocked volume is not where the header lives.
  for c in /sys/class/block/* /dev/* ; do
    case "$c" in
      /sys/*) n="/dev/${c##*/}" ;;
      *)      n="$c" ;;
    esac
    # Device-mapper nodes are skipped: an unlocked volume is not where the
    # header lives, and asking cryptsetup about it is pointless.
    case "${n##*/}" in dm-*) continue ;; esac
    [ -b "$n" ] || continue
    if [ "$(cryptsetup luksUUID "$n" 2>/dev/null)" = "$want" ]; then
      printf '%s\n' "$n"
      return 0
    fi
  done
  return 1
}

has_clevis_binding() {
  clevis luks list -d "$1" 2>/dev/null | grep -q .
}

rc=0
found_any=0

while read -r name source _rest; do
  case "$name" in ''|\#*) continue ;; esac
  if [ -n "${CLEVIS_UNLOCK_NAMES}" ]; then
    case " ${CLEVIS_UNLOCK_NAMES} " in *" ${name} "*) ;; *) continue ;; esac
  fi

  if [ -b "/dev/mapper/${name}" ]; then
    log "${name} is already open"
    found_any=1
    continue
  fi

  dev="$(resolve_device "$source")" || {
    err "${name}: no block device with ${source} -- skipped"
    [ "${CLEVIS_UNLOCK_REQUIRED}" = "yes" ] && rc=1
    continue
  }

  if ! has_clevis_binding "$dev"; then
    # Not an error: crypttab may well hold devices that are unlocked by hand or
    # by a key file. Only the bound ones are this unit's business.
    log "${name} (${dev}) has no Clevis binding -- not ours"
    continue
  fi

  found_any=1

  # The network does not arrive at the same moment as the device nodes, and the
  # Tang servers may themselves still be booting. Retrying is cheaper than
  # inventing an ordering that s6 cannot express here.
  i=1
  while : ; do
    if clevis luks unlock -d "$dev" -n "$name" 2>/dev/null; then
      log "${name} unlocked against Tang (${dev}, attempt ${i})"
      break
    fi
    if [ "$i" -ge "${CLEVIS_UNLOCK_TRIES}" ]; then
      err "${name}: no Tang reachable after ${i} attempts -- ${dev} stays closed"
      [ "${CLEVIS_UNLOCK_REQUIRED}" = "yes" ] && rc=1
      break
    fi
    i=$((i + 1))
    sleep "${CLEVIS_UNLOCK_DELAY}"
  done
done < /etc/crypttab

if [ "$found_any" = 0 ] && [ "$rc" = 0 ]; then
  log "no Clevis-bound device in /etc/crypttab -- nothing to do"
fi

exit "$rc"
