Saturday, April 23, 2011

LUKS/XScreenSaver 1/2-factor authentication with a USB drive/password

Late last month (couldn't write about it then; had taxes, the physics GRE, and the spring Nasa EAP poster symposium to work on), I began a project: setting up Xubuntu Linux on an old Acer Aspire AS1691 for my dear old dad, who has always been quite open with me about his general dislike for computers, and his being a neo-luddite. But I wasn't just going to set up a user-friendly GNU/Linux environment for him on the thing: I made it my goal to make it secure and easy to use at the same time.



Part I: The plan


In essence, I wanted to configure a GNU/Linux installation with the following features:

  • Full-disk encryption

  • Screen locking (very easy)

  • Secure hibernation (encrypted swap)

  • Authentication with a device or a passphrase (hence 1/2-factor; one of two accepted)

    • Have the boot screen (Plymouth) display a success message when the device is attached, or:

    • Prompt for the passphrase if the device is unavailable



The reason for my decision to use the less-secure method where only one of two things are needed to unlock the hard drive/screen is that he doesn't like entering passwords all the time. The most obvious candidate for a key device: a removable USB storage device with a random key on it, one small enough to fit on a keychain, like so:





(it seems natural to store something you'd use for unlocking something else on your keychain, correct?) This left me with a goal: configure cryptsetup at boot and XScreenSaver to accept either insertion of the USB drive, with the key stored on it, or a password as authentication, hence 1/2-factor authentication (one of two). If he loses/forgets one, he can use the other.

Part II: Installation


Since I don't really like having Linux distro installation programs decide what's best for me, I repartitioned the main hard drive and created the necessary partitions (two total) manually, one for boot and one for the encrypted Linux installation;
root@ubuntu:~# cryptsetup -c aes-xts-plain -h sha512 -s 512 luksFormat /dev/sda2 

Next, to allow hibernation with secure memory storage (encrypted swap) as well as the root and home filesystems, and not have to create extra encrypted partitions, I simply set up a logical volume group on the cryptdevice, and created the necessary filesystems. Encrypting swap is necessary for full-disk encryption and hibernation, because the key may be stored in memory and dumped to swap when you power down, thus defeating the purpose of protecting the hard drive in the first place.

Anyhow,
root@ubuntu:~# cryptsetup luksOpen /dev/sda2 encrypted 

(then I entered the passphrase)
root@ubuntu:~# lvm pvcreate /dev/mapper/encrypted
root@ubuntu:~# lvm vgcreate encrypted /dev/mapper/encrypted
root@ubuntu:~# lvm lvcreate -n root -C -L 10G encrypted
root@ubuntu:~# lvm lvcreate -n swap -C -L 1G encrypted
root@ubuntu:~# lvm lvcreate -n home -C -l 100%FREE encrypted
root@ubuntu:~# lvm vgimport encrypted
root@ubuntu:~# lvm lvchange -a y encrypted
root@ubuntu:~# mkfs.ext4 -j /dev/encrypted/root
root@ubuntu:~# mkfs.ext4 -j /dev/encrypted/home
root@ubuntu:~# mkswap /dev/encrypted swap
root@ubuntu:~# mkfs.ext2 /dev/sda1


Next was the easy part — install using the Xubuntu 10.04 installer, specifying mount points and filesystems manually. But having done this sort of thing before, it was then on to configure it so that it would actually boot properly. While there's no one way to set up a Linux system with an unusual boot process, it's easy to find information on the process. Having experimented with and used combinations of LUKS, LVM2 and mdadm (Linux software RAID), and booting with Linux's root filesystem living on the resulting software device, it was a rather par for the course/cut-and-dried process; first, chrooting:


root@ubuntu:~# mount /dev/encrypted/root /mnt
root@ubuntu:~# cd /mnt
root@ubuntu:~# cp /etc/resolv.conf etc/resolv.conf
root@ubuntu:~# mount /dev/sda1 boot
root@ubuntu:~# mount -o bind /dev dev
root@ubuntu:~# mount -t devpts none dev/pts
root@ubuntu:~# mount -t sysfs none sys
root@ubuntu:~# mount -t proc none proc
root@ubuntu:~# chroot ./ /bin/bash

And then, so I could work more efficiently:
root@ubuntu:~# apt-get install emacs


Part III: Configuration



From experience, I knew that first and foremost I would have to do some work on udev rules, so that the hard drive and USB key drive would have reliable symbolic links pointing to them the moment that devices settle at boot. I wrote about the general procedure for doing this in a forum post I wrote several months ago (for more details), and to examine device trees by kernel name, I usually just use the following script (I haven't just made it an alias because bash's escaping and argument conventions have always confused the hell out of me):



#!/bin/bash
udevadm info -q path -n $1 | xargs udevadm info -a -p


Once I chose the attributes of the devices and their respective parent devices to use, writing the rules was fairly simple. For reasons of personal preference I wrote rules for both the hard drive and the USB key, while only the latter is really necessary; I just think it looks nicer.



SUBSYSTEM=="block",SUBSYSTEMS=="scsi",ATTRS{model}=="ST9808210A      ",SYMLINK+="HardDrive%n"
SUBSYSTEM=="block",ATTRS{vendor}=="USB 2.0 ",ATTRS{model}=="USB Flash Drive ",SYMLINK+="KeyDev%n"


With that taken care of, configuring fstab and crypttab were fairly straightforward; for /etc/crypttab:



#       
encrypted /dev/HardDrive2 none luks,keyscript=/usr/local/sbin/usb_or_pass.sh,lvm=encrypted


(I'll explain that keyscript later) and, for /etc/fstab:



proc             /proc   proc nodev,noexec,nosuid 0 0
/dev/encrypted/root / ext4 errors=remount-ro 0 1
# /boot was on /dev/sda1 during installation
UUID=20b37ec9-f117-4836-b0e8-dc5eb323d231 /boot ext2 defaults 0 2
/dev/encrypted/home /home ext4 defaults 0 2
/dev/encrypted/swap none swap sw 0 0


All done, I then ran "update-initramfs -u" and "update-grub" to finalize the installation. Then, once I was able to boot into the new installation, I faced the challenge of making the USB device work as an authentication method.



Part IV: Scripting



I had always wondered how a Plymouth theme was set up, where the messages were defined (i.e. the message "cryptsetup: cryptsetup failed, bad password or options?" when you enter an incorrect hard drive passphrase), and how "events" in Plymouth were controlled. Not being able to find the answer immediately through Google searching, the answer came when I unpacked my own initramfs into /tmp and recursively grepped for "cryptsetup failed"; the relevant file that taught me all I needed to know was a script called "cryptroot", and the important plymouth commands are in the function "message". Some more poking around revealed that this file is copied into the initramfs from /usr/share/initramfs-tools/scripts/local-top/cryptroot on Ubuntu derivatives. But I didn't intend to modify this file; wouldn't have been an elegant solution, as it's a part of the initramfs-tools package and would have just been overwritten during upgrades. I instead wanted to make a separate script to perform the unlocking, and fortunately, using the "keyscript" option in /etc/crypttab, it's a straightforward thing to do.



The role of the keyscript, as I learned from reading a useful and well-written post on setting up full-disk encryption unlocked with a USB device, is to produce output used as a key to unlock the root device. Also learned from the post on Oxygen Impaired: one can store the key in the unused 4096 bytes between the master boot record and first partition of the device, so that nothing needs to be mounted to acquire the key. Awesome.



root@ubuntu:~# udevadm trigger
root@ubuntu:~# dd if=/dev/urandom of=/dev/KeyDev bs=512 skip=4 count=8
root@ubuntu:~# dd if=/dev/KeyDev of=keyfile bs=512 skip=4 count=8


Next I wrote the keyscript, which took a few attempts to get working properly:



#!/bin/sh
# Really, really basic "use USB key or enter passphrase" script by Demitri Morgan
keydev="/dev/KeyDev"
passmsg="Could not unlock root; USB key device not found.\nEnter its passphrase instead: "

# Wait for settling (sometimes USB devices take time to respond):
sleep 5
/sbin/udevadm settle --timeout=30

if [ -h "$keydev" ]; then
# Grab the key's bits off the device:
plymouth message --text="Found USB device, trying key..."
dd if=$keydev bs=512 skip=4 count=8 2>/dev/null | cat
else
# No sign of the key. Ask for the passphrase:
passmsg=$(echo -e "$passmsg")
/bin/plymouth ask-for-password --prompt "$passmsg"
fi

exit 0


The sleep/udev command I added to remedy how the particular USB device I used took some time to respond and make itself available. What this script essentially does: each time cryptsetup attempts to unlock the drive (which can be more than one time each boot if the process fails), it will be called and the output read as the key, as mentioned. This means that for a user-friendly "retry" loop, all the essentials only have to happen once; the script will be called as many times as necessary to retrieve the key and unlock the root device. What this all looks like when the key is left out:



When the key is inserted:



Next came an equal challenge: writing a reliable script to unlock the screen when the device is inserted. To my chagrin, there are no command line xscreensaver utilities for unlocking the screen per se, but the next best thing is simply killing;



#!/bin/bash
# Really basic script for unlocking the screen with a USB key, by Demitri Morgan
user="xxxxx"
uuid="XXXX-XXXX"
keydev="/dev/KeyDev"
delay="2s"
keyfile=/home/$user/.XScreenSaver.key
tmpkey=/home/$user/.XScreenSaver.key-tmp
unlocked=0

while [ 1 ]
do
if [[ ! -z $(blkid | grep $uuid) ]] ; then
if [ $unlocked == 0 ] ; then
dd if=$keydev of=$tmpkey bs=512 count=8 skip=4 2> /dev/null
if [[ -z $(diff -q $keyfile $tmpkey 2> /dev/null) ]] ; then
shred -u $tmpkey 2> /dev/null
pkill xscreensaver 2> /dev/null
unlocked=1
fi
fi
else
[[ -z $(pgrep xscreensaver) ]] && sudo -u $user -i /usr/bin/xscreensaver -no-splash 2> /dev/null &
unlocked=0
fi
sleep $delay
done


I decided to put the essentials into an infinite loop and start the script at boot by putting a call to it in /etc/rc.local instead of crontabbing it because doing so meant the ability to use internal variables ("$unlocked" in my case), which means fewer system calls and less overhead. Mind you, all that this script does is kill xscreensaver when the USB device is in, and leaves the screen unlocked/without a screensaver while the key is inserted, then spawns a new xscreensaver when it is no longer present.


All done! It has been a refreshing exercise in Linux DIY. Thank you for reading, and I hope you found it helpful!

No comments:

Post a Comment