How to load LUKS passphrase from USB, falling back to keyboard?

Andrew picture Andrew · Oct 31, 2013 · Viewed 17.6k times · Source

I want to set up a headless Linux (Debian Wheezy) PC with whole disk encryption, with the ability to unlock the disk either with a USB drive, or by entering a passphrase by keyboard. My starting point is a fresh install using the basic whole disk encryption option in the Debian Installer, which manages everything besides /boot as a LUKS-encrypted logical volume group and gives me the keyboard option. I will describe my current solution in an answer, in hopes that it will be useful and that others can improve on it.

Here are some of the issues I had:

  • Setting up a passphrase and putting it on the USB drive.

  • Loading the USB modules in time.

  • Waiting for the USB drive to recognized by Linux before trying to read from it.

  • Identifying the correct USB drive (not some other drive that happens to be inserted).

  • Writing a "keyscript" to pull a passphrase off the USB drive.

  • Ensuring that the fall-back to keyboard kicks in in all USB failure cases.

I will accept an answer with significant improvements and upvote answers that offer contributions.

Answer

Andrew picture Andrew · Oct 31, 2013

A lot of my solution is derived from the post, Using A USB Key For The LUKS Passphrase.

  1. Create a random passphrase:

    dd if=/dev/urandom bs=1 count=256 > passphrase
    
  2. Insert a USB drive. dmesg output will show the device name; assume /dev/sdd. Figure out its size:

    blockdev --getsize64 /dev/sdd
    
  3. I decided to install the passphrase at the end of the raw device, figuring it might survive any accidental use of the USB drive.

    dd if=passphrase of=/dev/sdd bs=1 seek=<size-256>
    
  4. Add the passphrase to the LUKS volume:

    cryptsetup luksAddKey /dev/sda5 passphrase
    

    This does not affect the existing hand-entered passphrase from the installer. The passphrase file can be deleted:

    rm passphrase
    
  5. Find a unique name for the USB stick, so we can identify it when present:

    ls -l /dev/disk/by-id | grep -w sdd
    

    You should see one symlink. I will call it /dev/disk/by-id/<ID>.

  6. Edit /etc/crypttab. You should see a line like:

    sdc5_crypt UUID=b9570e0f-3bd3-40b0-801f-ee20ac460207 none luks
    

    Modify it to:

    sdc5_crypt UUID=b9570e0f-3bd3-40b0-801f-ee20ac460207 /dev/disk/by-id/<ID> luks,keyscript=/bin/passphrase-from-usb
    
  7. The keyscript referred to above will need to read the passphrase from the USB device. However, it needs to do more than that. To understand how it is used, check /usr/share/initramfs-tools/scripts/local-top/cryptroot, the script that runs at boot time to unlock the root device. Note when a keyscript is set, it is simply run and the output piped to luksOpen with no other checking. There is no way to signal an error (USB drive not present) or fall back to keyboard input. If the passphrase fails, the keyscript is run again in a loop, up to some number of times; however we are not told which iteration we are on. Also, we have no control over when the keyscript is run, so we can't be sure Linux has recognized the USB drive.

    I addressed this with some hacks:

    1. Poll on the USB drive and wait 3 seconds for it to appear. This works for me, but I would love to know a better way.

    2. Create a dummy file /passphrase-from-usb-tried on first run to indicate that we have been run at least once.

    3. If we have been run at least once, or the USB drive cannot be found, run the askpass program used by cryptroot for keyboard input.

    The final script:

    #!/bin/sh
    
    set -e
    
    if ! [ -e /passphrase-from-usb-tried ]; then
        touch /passphrase-from-usb-tried
        if ! [ -e "$CRYPTTAB_KEY" ]; then
            echo "Waiting for USB stick to be recognized..." >&2
            sleep 3
        fi
        if [ -e "$CRYPTTAB_KEY" ]; then
            echo "Unlocking the disk $CRYPTTAB_SOURCE ($CRYPTTAB_NAME) from USB key" >&2
            dd if="$CRYPTTAB_KEY" bs=1 skip=129498880 count=256 2>/dev/null
            exit
        else
            echo "Can't find $CRYPTTAB_KEY; USB stick not present?" >&2
        fi
    fi
    
    /lib/cryptsetup/askpass "Unlocking the disk $CRYPTTAB_SOURCE ($CRYPTTAB_NAME)\nEnter passphrase: "
    

    Finally, we need to ensure that this script is available in the initramfs. Create /etc/initramfs-tools/hooks/passphrase-from-usb containing:

    #!/bin/sh
    
    PREREQ=""
    
    prereqs() {
            echo "$PREREQ"
    }
    
    case "$1" in
            prereqs)
                    prereqs
                    exit 0
            ;;
    esac
    
    . "${CONFDIR}/initramfs.conf"
    . /usr/share/initramfs-tools/hook-functions
    
    copy_exec /bin/passphrase-from-usb /bin
    
  8. The USB drivers were not present in my initramfs. (It appears they are by default in later versions of Debian.) I had to add them by adding to /etc/initramfs-tools/modules:

    uhci_hcd
    ehci_hcd
    usb_storage
    
  9. When all is done, update the initramfs:

    update-initramfs -u