Full Disk Encryption with USB master key



When I decided to go with full disk encryption on my machines, I had a pretty hard time figuring out exactly what to do. Not only were there tools and commands to learn, but there was quite a bit of design involved in the process. In this post, I will describe the setup I chose, and the scripts I use to make things a bit more convenient.

Partition Design

partition size label file system mount point notes
sda1 200MiB boot ext4 /boot
sda2 20GiB luks/ext4 /
sda3 218GiB luks/ext4 /home
sda4 6GiB Extended Partition
sda5 4096MiB recovery ext4 / Minimal Ubuntu for recovery
sda6 1900MiB luks/swap

Creating the partition layout

After deciding on a partition scheme, the next step was to create the partitions and format them. I created a 14.04 USB installer and booted the machine in the "Try Ubuntu" mode.

This part is easy. I used gparted to create the partition layout above. For the three encrypted partitions (marked luks/… above) I left them "unformatted".

Formatting the encrypted partitions

Instructions for the next step came mostly from the Arch wiki. The process involves first formatting the encrypted partitions with a Linux Unified Key Setup (LUKS) header. The luks header contains the AES master key as well as key information for up to eight passphrases (note, in this context a passphrase may be a file of arbitrary data) allowing access to that master key.

Initially, I chose to lock down the root and home partitions using a keyfile passphrase, and later added a password as a passphrase, so I will go through that process.

First, we need to create a keyfile for each partition. I used /dev/random to source random bytes (rather than /dev/urandom which is faster but pseudorandom). Knowing that /dev/random sources user interface event arrival times for randomness I made sure to do lots of typing while I was reading off a key. I chose to use a keysize of 512bytes. I initially accomplished this with

$ cat /dev/random > root.key

while watching in another terminal with ls -l until the file was over 512 bytes. Then I used

$ truncate -s 512 root.key

to reduce the size to 512. After doing this a few times I created a small script to provide saner feedback on how much data had been read from /dev/random

#!/usr/bin/python
# key_progress.py
import sys

max = 512
bytes = 0
while( bytes < max ):
  data = sys.stdin.read(1)
  bytes += len(data)
  fraction = bytes/float(max)
  digits = int(10*fraction)
  bars = '='*digits
  spaces = ' '*(10-digits)
  percent = 100*fraction
  text = "\r[%s%s] : %04.2f%%" % (bars,spaces,percent)
  sys.stdout.write(text)
  sys.stdout.flush()
sys.stdout.write('\n')

Usage is like

$ cat /dev/random | tee -a root.key | key_progress.py

Next, I set up the root partition. As described in the wiki, we create the luks header with

# cryptsetup -cipher=aes-xts-plain64 -key-file=~/root.key -key-size=512 -hash=sha51 -iter-time=5000 -use-random luksFormat /dev/sda2

Please research the different cipher options but at the time of this writing my research indicates that aes-xts is generally the best option for a general- use large block device (hard drive).

Note that one may omit the -key-file=/path/to/file flag to specify a password instead of a keyfile, but I chose to start with a keyfile and add a password later.

Once the block device has a proper luks header we can expose the encrypted block device as an unencrypted block device with the following command.

# cryptsetup luksOpen /dev/sda2 root

This will expose the unencrypted blocks of /dev/sda2 in /dev/mapper/root as if it were a raw block device. We can then create an ext4 filesystem on that encrypted device via

# mkfs.ext4 /dev/mapper/root

I repeated the same steps for the home partition:

# cat /dev/random | tee -a home.key | key_progress.py
# cryptsetup -cipher=aes-xts-plain64 -key-file=~/home.key -key-size=512 -hash=sha51 -iter-time=5000 -use-random luksFormat /dev/sda3
# cryptsetup luksOpen /dev/sda3 home
# mkfs.ext4 /dev/mapper/home

Installing Ubuntu 14.04

The installation process is another of the easy steps. Just launch "install ubuntu" from the desktop icon. When it asks where to install choose "do something else" and specify the partitions manually.

device type mount point format?
/dev/sda1 ext4 /boot No
/dev/mapper/root ext4 / No
/dev/mapper/home ext4 /home No
/dev/sda6 swap

The rest of the installation is business as usual

Post-install work

After installing, and before rebooting we need to do some work on the new system.

First, let's setup an encrypted swap. If the ubuntu live-usb system happens to be using our formatted swap space then we need to turn off the swap. Then we run the following

# swapoff -a
# cryptsetup -d /dev/random create cryptswap /dev/sda6
# mkswap -f /dev/mapper/cryptswap -v1

At this point I realized that I had no way to provide the keyfile for unlocking the root partition at boot so I added a password to encrypted root partition.

# cryptsetup -key-file=/root.key luksAddKey /dev/sda2

Now we need to setup the new systems cryptab. Start by mounting the filesystem for the new installation.

# cd /mnt
# mkdir root
# mount /dev/mapper/root root
# mount /dev/mapper/home root/home
# mount /dev/sda1 root/boot

Now we edit /etc/cryptab to setup the encrypted volumes. My crypttab looks like the following:

# root filesystem
root UUID=fdc69d8e-ede4-40f1-8a38-d69ba73c1c82 none luks

# home filesystem
home UUID=74c4c574-2396-434e-9f0f-ea9f8270c44e /root/home.key luks

# swap
cryptswap /dev/sda6 /dev/urandom swap

You can find the UUID of the relevant partitions by running blkid as root.

Note that I've placed home.key in /root so that once the root partition is decrypted the home partition can be decrypted without requiring a second input of the password.

Now that the cryptab is setup, we need to rebuild the initramfs for boot. We do this by chroot-ing into the new system and running update-initramfs -u

# mount -o bind /proc root/proc
# mount -o bind /dev root/dev
# mount -o bind /dev/pts root/dev/pts
# mount -o bind /sys root/sys
# chroot root
# update-initramfs -u

And that's it. Reboot now and ubuntu will ask you for a password to expose the root partition before mounting it. You may notice a warning about /home not being mounted and given options to wait, skip, or do something else. Just give it a few more seconds. I guess the timeout built in for mounting /home isn't long enough to also deal with decrypting it.

USB Master Key

The process of entering the password every boot is troublesome and, to be honest, my ability to recall secure passwords is not to be trusted. In order to address this shortcoming, I decided to create a USB stick whose sole purpose was to act like a password. I do not consider this insecure because, should I lose the USB key, I can revoke the encryption passphrase from the LUKS header and make it no longer usable on my system.

I considered several options for how to go about creating the USB key. I considered a separate boot initramfs with grub installed on the key to actually boot from the key. I also considered storing the keyfile on a filesystem and searching removable filesystems at boot, or trying to identify the disk by partition label or UUID. Ultimately, I decided instead to use an unformatted partition of the USB key to store the keyfile.

The following can be performed on target system, however I suggest creating a backup of the boot/initramfs-…-generic file. If you mess anything up you can drop to grub (press shift 5 times at boot) and alter the grub boot command to use the backup initramfs to get back into a useable system.

First I created the keyfile as usual:

$ cat /dev/random | tee -a keymaster_root.key | key_progress.py
$ truncate -s 512 keymaster_root.key

Then I partitioned the USB key with an unformatted partition of 1MiB and the rest as LUKS/ext4. The second partition is somewhat irrelevant for this discussion except to show that the USB key is still useful for storage.

Now copy the keyfile to the unformatted space, and add the key to an unused slot of the LUKS header for the root partition

# dd if=keymaster_root.key of=/dev/sdb1 bs=1
# cryptsetup luksAddKey /dev/sdb1 keymaster_root.key

Now we find the unique name for the USB stick, particularly it's first partition so we can identify it when present:

$ ls -l /dev/disk/by-id | grep sdb1

In my case the USB key is given the path /dev/disk/by-id/usb- ADATA_USB_Flash_Drive_1411416161600078-0:0-part1. I then edited my crypttab as follows:

# /etc/crypttab
# root
root UUID=fdc69d8e-ede4-40f1-8a38-d69ba73c1c82 /dev/disk/by-id/usb-ADATA_USB_Flash_Drive_1411416161600078-0:0-part1 luks,keyscript=/sbin/usb_keymaster

# home
home UUID=74c4c574-2396-434e-9f0f-ea9f8270c44e /root/nadie_chain_root_home.key luks

# swap
cryptswap /dev/sda6 /dev/urandom swap

The keyscript is a script which dm-crypt runs to get the passphrase. It expects the passphrase on stdout. I used the script from the stack overflow link above but took the initial parts of the script from the blog linked above (which makes the script work with plymouth).

The script attempts to read from the raw device provided in ${CRYPTTAB_KEY} (which is the third column in crypttab). If the device file does not exist after 5 seconds, it drops to asking for a password.

#!/bin/sh
#/sbin/usb_keymaster

# define counter-intuitive shell logic values (based on /bin/true & /bin/false)
# NB. use FALSE only to *set* something to false, but don't test for
# equality, because a program might return any non-zero on error
TRUE=0
FALSE=1

# set DEBUG=$TRUE to display debug messages, DEBUG=$FALSE to be quiet
DEBUG=$TRUE

# default path to key-file on the USB/MMC disk
KEYFILE=".keyfile"

# maximum time to sleep waiting for devices to become ready before
# asking for passphrase
MAX_SECONDS=2

# is plymouth available? default false
PLYMOUTH=$FALSE
if [ -x /bin/plymouth ] && plymouth -ping; then
    PLYMOUTH=$TRUE
fi

# is usplash available? default false
USPLASH=$FALSE
# test for outfifo from Ubuntu Hardy cryptroot script, the second test
# alone proves not completely reliable.
if [ -p /dev/.initramfs/usplash_outfifo -a -x /sbin/usplash_write ]; then
    # use innocuous command to determine if usplash is running
    # usplash_write will return exit-code 1 if usplash isn't running
    # need to set a flag to tell usplash_write to report no usplash
    FAIL_NO_USPLASH=1
    # enable verbose messages (required to display messages if kernel boot option "quiet" is enabled
    /sbin/usplash_write "VERBOSE on"
    if [ $? -eq $TRUE ]; then
        # usplash is running
        USPLASH=$TRUE
        /sbin/usplash_write "CLEAR"
    fi
fi

# is stty available? default false
STTY=$FALSE
STTYCMD=false
# check for stty executable
if [ -x /bin/stty ]; then
    STTY=$TRUE
    STTYCMD=/bin/stty
elif [ `(busybox stty >/dev/null 2>&1; echo $?)` -eq $TRUE ]; then
    STTY=$TRUE
    STTYCMD="busybox stty"
fi

# print message to usplash or stderr
# usage: msg  "message" [switch]
# command: TEXT | STATUS | SUCCESS | FAILURE | CLEAR (see 'man usplash_write' for all commands)
# switch : switch used for echo to stderr (ignored for usplash)
# when using usplash the command will cause "message" to be
# printed according to the usplash  definition.
# using the switch -n will allow echo to write multiple messages
# to the same line
msg ()
{
    if [ $# -gt 0 ]; then
        # handle multi-line messages
        echo $2 | while read LINE; do
            if [ $PLYMOUTH -eq $TRUE ]; then
                # use plymouth
                plymouth message -text="$LINE"
            elif [ $USPLASH -eq $TRUE ]; then
                # use usplash
                /sbin/usplash_write "$1 $LINE"
            else
                # use stderr for all messages
                echo $3 "$2″ >&2
            fi
        done
    fi
}

dbg ()
{
    if [ $DEBUG -eq $TRUE ]; then
        msg "$@"
    fi
}

# read password from console or with usplash
# usage: readpass "prompt"
readpass ()
{
    if [ $# -gt 0 ]; then
        if [ $PLYMOUTH -eq $TRUE ]; then
            PASS="$(plymouth ask-for-password -prompt "$1″)"
        elif [ $USPLASH -eq $TRUE ]; then
            usplash_write "INPUTQUIET $1
            PASS="$(cat /dev/.initramfs/usplash_outfifo)"
        else
            [ $STTY -ne $TRUE ] && msg TEXT "WARNING stty not found, password will be visible"
            /lib/cryptsetup/askpass "$1# echo -n "$1″ >&2
            # $STTYCMD -echo
            # read -r PASS /dev/null
            # [ $STTY -eq $TRUE ] && echo >&2
            # $STTYCMD echo
        fi
    fi
    echo -n "$PASS"
}

dbg STATUS "Executing usb_keymaster …"

# flag tracking key-file availability
OPENED=$FALSE

for TRY in 1 2 3 4 5
do
  if ! [ -e "${CRYPTTAB_KEY}" ]; then
    dbg TEXT "Waiting for USB stick to be recognized [${TRY}]"
    sleep 1
  fi
done # for TRY
if [ -e "${CRYPTTAB_KEY}" ]; then
  dd if="${CRYPTTAB_KEY}" bs=1 skip=0 count=512 2>/dev/nul
  OPENED=$TRUE
fi


# clear existing usplash text and status messages
[ $USPLASH -eq $TRUE ] && msg STATUS "                               " && msg CLEAR ""

if [ $OPENED -ne $TRUE ]; then
  # dbg TEXT "Failed to find suitable USB/MMC key-file …"
  readpass "$(printf "Unlocking the disk $CRYPTTAB_SOURCE ($CRYPTTAB_NAME)\nEnter passphrase: ")"
else
  # dbg TEXT "Success loading key-file from $SFS ($LABEL)"
  # msg TEXT "Unlocking the disk $CRYPTTAB_SOURCE ($CRYPTTAB_NAME)"
  msg "Unlocking ${CRYPTTAB_SOURCE} (${CRYPTTAB_NAME}) from USB key"
fi

#
[ $USPLASH -eq $TRUE ] && /sbin/usplash_write "VERBOSE default"

Be sure to chmod +x /sbin/usb_keymaster to make the script executable.

In order to get this script into the initramfs we need to add the following hook file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/sh
#/etc/initramfs-tools/hooks/usb_keymaster

PREREQ=""

prereqs() {
  echo "$PREREQ"
}

case "$1″ in
  prereqs)
    prereqs
    exit 0
  ;;
esac

. "${CONFDIR}/initramfs.conf"
. /usr/share/initramfs-tools/hook-functions

copy_exec /sbin/usb_keymaster /sbin

Lastly, we need to ensure that the USB drivers are loaded at boot time (this may not be necessary with 14.04). Edit /etc/initramfs-tools/modules.

# List of modules that you want to include in your initramfs.
# They will be loaded at boot time in the order below.
#
# Syntax:  module_name [args ...]
uhci_hcd
ehci_hcd
usb_storage

Lastly update the initramfs with

update-initramfs -u

Comments


Comments powered by Disqus