From f2c3a039da2c9398375561a8fe52b77d54ca5e97 Mon Sep 17 00:00:00 2001
From: Harald Hoyer <harald@redhat.com>
Date: Thu, 18 Jul 2013 11:53:08 +0200
Subject: [PATCH] crypt: add support for keyfiles in the initramfs

https://bugzilla.redhat.com/show_bug.cgi?id=886194
---
 modules.d/90crypt/crypt-lib.sh     | 222 +++++++++++++++++++++++++++++++++++++
 modules.d/90crypt/cryptroot-ask.sh | 121 ++++++++++++++------
 modules.d/90crypt/install          |   4 +
 3 files changed, 315 insertions(+), 32 deletions(-)
 create mode 100644 modules.d/90crypt/crypt-lib.sh

diff --git a/modules.d/90crypt/crypt-lib.sh b/modules.d/90crypt/crypt-lib.sh
new file mode 100644
index 0000000..69f14d0
--- /dev/null
+++ b/modules.d/90crypt/crypt-lib.sh
@@ -0,0 +1,222 @@
+#!/bin/sh
+# -*- mode: shell-script; indent-tabs-mode: nil; sh-basic-offset: 4; -*-
+# ex: ts=8 sw=4 sts=4 et filetype=sh
+
+command -v getarg >/dev/null || . /lib/dracut-lib.sh
+
+# ask_for_password
+#
+# Wraps around plymouth ask-for-password and adds fallback to tty password ask
+# if plymouth is not present.
+#
+# --cmd command
+#   Command to execute. Required.
+# --prompt prompt
+#   Password prompt. Note that function already adds ':' at the end.
+#   Recommended.
+# --tries n
+#   How many times repeat command on its failure.  Default is 3.
+# --ply-[cmd|prompt|tries]
+#   Command/prompt/tries specific for plymouth password ask only.
+# --tty-[cmd|prompt|tries]
+#   Command/prompt/tries specific for tty password ask only.
+# --tty-echo-off
+#   Turn off input echo before tty command is executed and turn on after.
+#   It's useful when password is read from stdin.
+ask_for_password() {
+    local cmd; local prompt; local tries=3
+    local ply_cmd; local ply_prompt; local ply_tries=3
+    local tty_cmd; local tty_prompt; local tty_tries=3
+    local ret
+
+    while [ $# -gt 0 ]; do
+        case "$1" in
+            --cmd) ply_cmd="$2"; tty_cmd="$2" shift;;
+            --ply-cmd) ply_cmd="$2"; shift;;
+            --tty-cmd) tty_cmd="$2"; shift;;
+            --prompt) ply_prompt="$2"; tty_prompt="$2" shift;;
+            --ply-prompt) ply_prompt="$2"; shift;;
+            --tty-prompt) tty_prompt="$2"; shift;;
+            --tries) ply_tries="$2"; tty_tries="$2"; shift;;
+            --ply-tries) ply_tries="$2"; shift;;
+            --tty-tries) tty_tries="$2"; shift;;
+            --tty-echo-off) tty_echo_off=yes;;
+        esac
+        shift
+    done
+
+    { flock -s 9;
+        # Prompt for password with plymouth, if installed and running.
+        if [ -x /bin/plymouth ] && /bin/plymouth --ping; then
+            /bin/plymouth ask-for-password \
+                --prompt "$ply_prompt" --number-of-tries=$ply_tries \
+                --command="$ply_cmd"
+            ret=$?
+        else
+            if [ "$tty_echo_off" = yes ]; then
+                stty_orig="$(stty -g)"
+                stty -echo
+            fi
+
+            local i=1
+            while [ $i -le $tty_tries ]; do
+                [ -n "$tty_prompt" ] && \
+                    printf "$tty_prompt [$i/$tty_tries]:" >&2
+                eval "$tty_cmd" && ret=0 && break
+                ret=$?
+                i=$(($i+1))
+                [ -n "$tty_prompt" ] && printf '\n' >&2
+            done
+
+            [ "$tty_echo_off" = yes ] && stty $stty_orig
+        fi
+    } 9>/.console_lock
+
+    [ $ret -ne 0 ] && echo "Wrong password" >&2
+    return $ret
+}
+
+# Try to mount specified device (by path, by UUID or by label) and check
+# the path with 'test'.
+#
+# example:
+# test_dev -f LABEL="nice label" /some/file1
+test_dev() {
+    local test_op=$1; local dev="$2"; local f="$3"
+    local ret=1; local mount_point=$(mkuniqdir /mnt testdev)
+    local path
+
+    [ -n "$dev" -a -n "$*" ] || return 1
+    [ -d "$mount_point" ] || die 'Mount point does not exist!'
+
+    if mount -r "$dev" "$mount_point" >/dev/null 2>&1; then
+        test $test_op "${mount_point}/${f}"
+        ret=$?
+        umount "$mount_point"
+    fi
+
+    rmdir "$mount_point"
+
+    return $ret
+}
+
+# Get kernel name for given device.  Device may be the name too (then the same
+# is returned), a symlink (full path), UUID (prefixed with "UUID=") or label
+# (prefixed with "LABEL=").  If just a beginning of the UUID is specified or
+# even an empty, function prints all device names which UUIDs match - every in
+# single line.
+#
+# NOTICE: The name starts with "/dev/".
+#
+# Example:
+#   devnames UUID=123
+# May print:
+#   /dev/dm-1
+#   /dev/sdb1
+#   /dev/sdf3
+devnames() {
+    local dev="$1"; local d; local names
+
+    case "$dev" in
+    UUID=*)
+        dev="$(foreach_uuid_until '! blkid -U $___' "${dev#UUID=}")" \
+            && return 255
+        [ -z "$dev" ] && return 255
+        ;;
+    LABEL=*) dev="$(blkid -L "${dev#LABEL=}")" || return 255 ;;
+    /dev/?*) ;;
+    *) return 255 ;;
+    esac
+
+    for d in $dev; do
+        names="$names
+$(readlink -e -q "$d")" || return 255
+    done
+
+    echo "${names#
+}"
+}
+
+# match_dev devpattern dev
+#
+# Returns true if 'dev' matches 'devpattern'.  Both 'devpattern' and 'dev' are
+# expanded to kernel names and then compared.  If name of 'dev' is on list of
+# names of devices matching 'devpattern', the test is positive.  'dev' and
+# 'devpattern' may be anything which function 'devnames' recognizes.
+#
+# If 'devpattern' is empty or '*' then function just returns true.
+#
+# Example:
+#   match_dev UUID=123 /dev/dm-1
+# Returns true if /dev/dm-1 UUID starts with "123".
+match_dev() {
+    [ -z "$1" -o "$1" = '*' ] && return 0
+    local devlist; local dev
+
+    devlist="$(devnames "$1")" || return 255
+    dev="$(devnames "$2")" || return 255
+
+    strstr "
+$devlist
+" "
+$dev
+"
+}
+
+# getkey keysfile for_dev
+#
+# Reads file <keysfile> produced by probe-keydev and looks for first line to
+# which device <for_dev> matches.  The successful result is printed in format
+# "<keydev>:<keypath>".  When nothing found, just false is returned.
+#
+# Example:
+#   getkey /tmp/luks.keys /dev/sdb1
+# May print:
+#   /dev/sdc1:/keys/some.key
+getkey() {
+    local keys_file="$1"; local for_dev="$2"
+    local luks_dev; local key_dev; local key_path
+
+    [ -z "$keys_file" -o -z "$for_dev" ] && die 'getkey: wrong usage!'
+    [ -f "$keys_file" ] || return 1
+
+    local IFS=:
+    while read luks_dev key_dev key_path; do
+        if match_dev "$luks_dev" "$for_dev"; then
+            echo "${key_dev}:${key_path}"
+            return 0
+        fi
+    done < "$keys_file"
+
+    return 1
+}
+
+# readkey keypath keydev device
+#
+# Mounts <keydev>, reads key from file <keypath>, optionally processes it (e.g.
+# if encrypted with GPG) and prints to standard output which is supposed to be
+# read by cryptsetup.  <device> is just passed to helper function for
+# informational purpose.
+readkey() {
+    local keypath="$1"
+    local keydev="$2"
+    local device="$3"
+
+    local mntp=$(mkuniqdir /mnt keydev)
+    mount -r "$keydev" "$mntp" || die 'Mounting rem. dev. failed!'
+
+    case "${keypath##*.}" in
+        gpg)
+            if [ -f /lib/dracut-crypt-gpg-lib.sh ]; then
+                . /lib/dracut-crypt-gpg-lib.sh
+                gpg_decrypt "$mntp" "$keypath" "$keydev" "$device"
+            else
+                die "No GPG support to decrypt '$keypath' on '$keydev'."
+            fi
+            ;;
+        *) cat "$mntp/$keypath" ;;
+    esac
+
+    umount "$mntp"
+    rmdir "$mntp"
+}
diff --git a/modules.d/90crypt/cryptroot-ask.sh b/modules.d/90crypt/cryptroot-ask.sh
index a6a9af1..6a95dc2 100755
--- a/modules.d/90crypt/cryptroot-ask.sh
+++ b/modules.d/90crypt/cryptroot-ask.sh
@@ -1,7 +1,12 @@
 #!/bin/sh
+# -*- mode: shell-script; indent-tabs-mode: nil; sh-basic-offset: 4; -*-
+# ex: ts=8 sw=4 sts=4 et filetype=sh
+
+PATH=/usr/sbin:/usr/bin:/sbin:/bin
+NEWROOT=${NEWROOT:-"/sysroot"}
 
 # do not ask, if we already have root
-[ -f /sysroot/proc ] && exit 0
+[ -f $NEWROOT/proc ] && exit 0
 
 # check if destination already exists
 [ -b /dev/mapper/$2 ] && exit 0
@@ -12,11 +17,14 @@
 # load dm_crypt if it is not already loaded
 [ -d /sys/module/dm_crypt ] || modprobe dm_crypt
 
-. /lib/dracut-lib.sh
+. /lib/dracut-crypt-lib.sh
 
 # default luksname - luks-UUID
 luksname=$2
 
+# fallback to passphrase
+ask_passphrase=1
+
 # if device name is /dev/dm-X, convert to /dev/mapper/name
 if [ "${1##/dev/dm-}" != "$1" ]; then
     device="/dev/mapper/$(dmsetup info -c --noheadings -o name "$1")"
@@ -24,42 +32,91 @@ else
     device="$1"
 fi
 
-if [ -f /etc/crypttab ] && ! getargs rd_NO_CRYPTTAB; then
-    while read name dev rest; do
-	# ignore blank lines and comments
-	if [ -z "$name" -o "${name#\#}" != "$name" ]; then
-	    continue
-	fi
-
-	# UUID used in crypttab
-	if [ "${dev%%=*}" = "UUID" ]; then
-	    if [ "luks-${dev##UUID=}" = "$2" ]; then
-		luksname="$name"
-		break
-	    fi
-	
-	# path used in crypttab
-	else
-	    cdev=$(readlink -f $dev)
-	    mdev=$(readlink -f $device)
-	    if [ "$cdev" = "$mdev" ]; then
-		luksname="$name"
-		break
-	    fi
-	fi
+# TODO: improve to support what cmdline does
+if [ -f /etc/crypttab ] && getargbool 1 rd.luks.crypttab -n rd_NO_CRYPTTAB; then
+    while read name dev luksfile rest; do
+        # ignore blank lines and comments
+        if [ -z "$name" -o "${name#\#}" != "$name" ]; then
+            continue
+        fi
+
+        # UUID used in crypttab
+        if [ "${dev%%=*}" = "UUID" ]; then
+            if [ "luks-${dev##UUID=}" = "$2" ]; then
+                luksname="$name"
+                break
+            fi
+
+        # path used in crypttab
+        else
+            cdev=$(readlink -f $dev)
+            mdev=$(readlink -f $device)
+            if [ "$cdev" = "$mdev" ]; then
+                luksname="$name"
+                break
+            fi
+        fi
     done < /etc/crypttab
     unset name dev rest
 fi
 
-info "luksOpen $device $luksname"
-# flock against other interactive activities
-{ flock -s 9; 
-    echo -n "$device ($luksname) is password protected"
-    cryptsetup luksOpen -T1 $1 $luksname 
-} 9>/.console.lock
+#
+# Open LUKS device
+#
+
+info "luksOpen $device $luksname $luksfile"
+
+if [ -n "$luksfile" -a "$luksfile" != "none" -a -e "$luksfile" ]; then
+    if cryptsetup --key-file "$luksfile" luksOpen "$device" "$luksname"; then
+        ask_passphrase=0
+    fi
+else
+    while [ -n "$(getarg rd.luks.key)" ]; do
+        if tmp=$(getkey /tmp/luks.keys $device); then
+            keydev="${tmp%%:*}"
+            keypath="${tmp#*:}"
+        else
+            if [ $# -eq 3 ]; then
+                if [ $3 -eq 0 ]; then
+                    info "No key found for $device.  Fallback to passphrase mode."
+                    break
+                fi
+                info "No key found for $device.  Will try $3 time(s) more later."
+                set -- "$1" "$2" "$(($3 - 1))"
+            else
+                info "No key found for $device.  Will try later."
+            fi
+            initqueue --unique --onetime --settled \
+                --name cryptroot-ask-$luksname \
+                $(command -v cryptroot-ask) "$@"
+            exit 0
+        fi
+        unset tmp
+
+        info "Using '$keypath' on '$keydev'"
+        readkey "$keypath" "$keydev" "$device" \
+            | cryptsetup -d - luksOpen "$device" "$luksname"
+        unset keypath keydev
+        ask_passphrase=0
+        break
+    done
+fi
+
+if [ $ask_passphrase -ne 0 ]; then
+    luks_open="$(command -v cryptsetup) luksOpen"
+    ask_for_password --ply-tries 5 \
+        --ply-cmd "$luks_open -T1 $device $luksname" \
+        --ply-prompt "Password ($device)" \
+        --tty-tries 1 \
+        --tty-cmd "$luks_open -T5 $device $luksname"
+    unset luks_open
+fi
+
+unset device luksname luksfile
 
 # mark device as asked
 >> /tmp/cryptroot-asked-$2
 
+udevsettle
+
 exit 0
-# vim:ts=8:sw=4:sts=4:et
\ No newline at end of file
diff --git a/modules.d/90crypt/install b/modules.d/90crypt/install
index 7a7418d..fa25e34 100755
--- a/modules.d/90crypt/install
+++ b/modules.d/90crypt/install
@@ -1,7 +1,11 @@
 #!/bin/bash
 inst cryptsetup 
+inst rmdir
+inst readlink
+inst umount
 inst "$moddir"/cryptroot-ask.sh /sbin/cryptroot-ask
 inst_hook cmdline 30 "$moddir/parse-crypt.sh"
 inst_hook pre-pivot 30 "$moddir/crypt-cleanup.sh"
 inst_hook pre-pivot 31 "$moddir/crypt-cleanup.sh"
+inst "$moddir/crypt-lib.sh" "/lib/dracut-crypt-lib.sh"
 inst /etc/crypttab
-- 
1.8.3.1

