A Practical Script for Encrypting LVM Volume Groups
Author
Warning!
If you use this script to manage your data, you should realize you are one mistake away from instantly rendering all of your precious data into incomprehensible noise. You really need to take precautions.
Here's my personal strategy for dealing with the risk. I bought two external 250GB hard drives which I use as my master backup of everything I care about (including all of my family photos and videos). I keep one attached to my main computer, and the other disconnected and in another location. Every week or so, I attach the second one to my main computer and copy everything from the main to the secondary (using rsync). As soon as that's done, I disconnect the secondary and remove it to a safe location.
I've already had one accident, despite the fact that I wrote the script myself and knew exactly what I was doing. I overwrote the key on my primary external drive, rendering the entire contents unreadable. What saved me was the secondary drive and the fact that I kept it offline, reasonably well-synched with the primary, and physically separate and safe.
I would not dream of using a whole-disk encryption script like this without having at least two hard drives that mirrored each other and neither should you.
Another important warning is that only one instance of this script should be run at a time. Trying to run two mount commands or two create commands at the same time on different drive definitions simply won't work, unless the various race conditions involved happen to work out in your favor, and that's a very big "unless".
Detailed Example
This article presents a script, usbcd, for managing filesystems embedded in encrypted LVM volume groups. The script uses the dmsetup techniques explained in the articles ../dmsetup_and_losetup and ../dmsetup_losetup_and_mount.
We have an external 40GB USB hard drive, and we want it to support two unencrypted linux filesystems plus one filesystem embedded in an encrypted LVM volume group. We want to be able to boot a computer from one of the unencrypted filesystems and from there, use the script below to mount the encrypted volume.
Partitioning
The first step is partitioning the hard drive to support all of this. Using fdisk, we create the following partition table:
Disk /dev/sdb: 40.0 GB, 40007761920 bytes 255 heads, 63 sectors/track, 4864 cylinders Units = cylinders of 16065 * 512 = 8225280 bytes Device Boot Start End Blocks Id System /dev/sdb1 1 2 16033+ 83 Linux /dev/sdb2 3 732 5863725 83 Linux /dev/sdb3 733 1462 5863725 83 Linux /dev/sdb4 1463 4864 27326565 5 Extended /dev/sdb5 1463 1949 3911796 8e Linux LVM /dev/sdb6 1950 2436 3911796 8e Linux LVM /dev/sdb7 2437 2923 3911796 8e Linux LVM /dev/sdb8 2924 3410 3911796 8e Linux LVM /dev/sdb9 3411 3897 3911796 8e Linux LVM /dev/sdb10 3898 4384 3911796 8e Linux LVM /dev/sdb11 4385 4864 3855568+ 8e Linux LVM
At the time I did this, the 40G usb drive happened to be the second external hard drive I had attached, so it shows up as "/dev/sdb".
Config File
We need to write a configuration file to let usbcd know which devices are part of the encrypted LVM volume group and which device is the key device. We may have more than one usb 40G drive, so we call this one "usb40b". Accordingly, we write the following configuration file to /etc/usbcd/usb40b:
# definitions for usb 40G drive b # list of PV partitions for VG operations vols="5 6 7 8 9 10 11" keyvol="1" keymdev=key VG=vgusb40b LV=lv_drive2 LVSIZE=31G
The variables have the following meanings:
variable |
meaning |
vols |
a list of partition numbers for the encrypted physical volumes of the volume group |
keyvol |
the partition number used to store the passphrase encrypted, random 128 bit key used to encrypt the physical volumes |
keymdev |
the name of the device-mapper device used to decrypt the key volume/device |
VG |
the name of the volume group created from the encrypted PVs |
LV |
the name of the logical volume created in the VG |
LVSIZE |
the size of the logical volume created in the VG |
The keymdev variable does not normally need to be distinct (like VG does) across drives since the keymdev device only exists while the key is being read or written.
Modifying /etc/lvm/lvm.conf
LVM won't, be default, recognize device mapper devices as possible physical volumes. It is a simple matter to tell LVM you want it to do that, however. You only need to include this line:
types = [ "device-mapper", 16 ]
in the devices section of the configuration file, /etc/lvm/lvm.conf.
Creating the Encrypted Volume Group
Now for the main event. We first tell usbcd which of the drives defined through configuration files in in /etc/usbcd we want it to work on:
usbcd sd usb40b
We have only defined one so far, or course, but usbcd is too simple of a script to figure that out for itself. The "sd" is short for "set drive".
If you want to check which drive is currently set, you can "list drive" like this:
usbcd ld
This will list all drive definition files in /etc/usbcd as well as which one is currently pointed to by the symbolic link usbcd uses to track the current drive.
This command:
usbcd create /dev/sdb
will prompt you for a passphrase, and then:
create a 128-bit random key by reading 16 bytes from /dev/random
encrypt it to the key volume using the passphrase
crypto map the physcial volumes using the key
create the volume group vgusb40b from the physical volumes
activate the VG
create a 31G logical volume inside the VG
create a resierfs filesystem on the LV.
Mounting the Encrypted Volume
The procedure for mounting a volume managed by usbcd, after connecting the drive and seeing which /dev/sdX device is assigned, is (assuming, again, that /dev/sdb was assigned):
usbcd sd usb40b usbcd /dev/sdb /mnt/hd (type passphrase)
You only have to "set drive" once before doing an entire series of usbcd commands, but when you are managing several drives simultaneously, it is wise to "set drive" before every command.
Unmounting and Unmapping
unmounting (and un-crypto-mapping) is just
usbcd sd usb40b usbcd umount
Possible Bugs
I once noticed, when experimenting with the genkey command,
usbcd genkey
that if you keep running it until you drained the entropy pool, it would pause while the system gathered entropy (which I expected) and then sometimes return with a 64 bit hexadecimal key instead of a 128 bit one. It shouldn't do that and I have yet to fix it.
I believe this had to do with the fact that I was reading from /dev/random with a block size of 16
dd if=/dev/random bs=16 count=1
instead of a block size of 1.
dd if=/dev/random bs=1 count=16
I have since changed the script to use a blocksize of 1.
I list this as a possible bug because I don't think dd should have behaved the way it did with a blocksize of 16. Since I don't really understand how it would return after a partial read from a blocked, yet still open, input file, I am still supicious about how it will behave with a block size of 1.
Script Text
I call this script "usbcd", which is short for "USB Crypto Drive", since I originally wrote it to enable me to encrypt lvm volume groups contained on external USB hard drives.
#!/bin/bash
# this script is for the setup and usage of an external usb drive
# organized as an encrypted LVM volume group (VG).
# the Physical Volumes (PVs) of the VG are created by
# crypto mapping the partitions of the hard drive with the 2.6.x device mapper.
configdir=/etc/usbcd/
if ! [ -d /etc/usbcd ] ; then
mkdir /etc/usbcd
fi
active_drive_link=${configdir}/activedrive
if [ -e "$active_drive_link" ] ; then
source "$active_drive_link"
active_drive_set="Y"
else
active_drive_set="N"
fi
function check_active_drive {
if [ $active_drive_set = "N" ] ; then
echo "no active drive set"
exit 1
fi
}
# prompt the user for a password
function get_password {
local prompt=$1
local pass
stty -F /dev/tty -echo
echo -n "$prompt" > /dev/tty
read pass < /dev/tty
stty -F /dev/tty echo
echo $pass
}
function genkey {
# I realize this is a little cryptic, no pun intended.
# /dev/random prints random bits from an entropy pool
# od (octal dump) converts the byte stream to a hex string with an address field,
# followed by space delimited blocks of 4 hex characters.
# the cut command skips past the address field
# the tr command removes spaces to give, finally, a random hex string of 32 characters
# representing 16 random bytes (or 128 random bits).
dd if=/dev/random bs=1 count=16 2>/dev/null \
| od -t x2 \
| head --lines=1 \
| cut -d" " -f2- \
| tr -d " "
}
function getkey {
local dev="$1"
local pass="$2"
if [ -z "$1" ] ; then
echo "getkey: <dev> [pass]" 1>&2
exit 1
fi
if [ "$pass" == "" ] ; then
pass=$(get_password pass)
fi
check_active_drive
cfsmakedev ${dev}${keyvol} ${keymdev} $pass
dd if=/dev/mapper/${keymdev} bs=32 count=1 2>/dev/null
# I've noticed that the remove fails sometimes if it happens too soon after
# a write to the device.
while ! dmsetup remove ${keymdev} ; do
sleep 1
done
}
function setkey {
local dev="$1"
local pass="$2"
local key="$3"
if [ -z "$1" ] ; then
echo "setkey: <dev> [pass] [key]" 1>&2
exit 1
fi
if [ "$pass" == "" ] ; then
pass=$(get_password pass)
fi
if [ "$key" == "" ] ; then
key=$(get_password key)
fi
check_active_drive
cfsmakedev ${dev}${keyvol} $keymdev $pass
echo -n "$key" | dd of=/dev/mapper/${keymdev} bs=32 count=1 2>/dev/null
dmsetup remove ${keymdev}
}
function setrkey {
local dev=$1
local pass=$2
if [ -z "$1" ] ; then
echo "setrkey: <dev> [pass]" 1>&2
exit 1
fi
if [ "$pass" == "" ] ; then
pass=$(get_password pass)
fi
check_active_drive
local key=$(genkey)
setkey $dev $pass $key
}
function setpass {
local dev=$1
local oldpass=$2
local newpass=$3
if [ -z "$1" ] ; then
echo "setpass: <dev> [oldpass] [newpass]" 1>&2
exit 1
fi
if [ "$oldpass" == "" ] ; then
oldpass=$(get_password old_pass)
fi
if [ "$newpass" == "" ] ; then
newpass=$(get_password new_pass)
fi
check_active_drive
setkey $dev $newpass $(getkey $dev $oldpass)
}
function cfsmakedev {
local dev=$1
local mdev=$2
local pass=$3
if [ -z "$2" ] ; then
echo "md <dev> <mdev> [pass]" 1>&2
exit 1
fi
if [ "$pass" == "" ] ; then
pass=$(get_password pass)
fi
local blksize=$(blockdev --getsize $dev)
local key=$(echo "$pass" | md5sum | cut -d" " -f1)
echo "0 $blksize crypt aes-plain $key 0 $dev 0" | dmsetup create ${mdev}
}
function vgmap {
local dev="$1"
local pass="$2"
if [ -z "$2" ] ; then
echo "usage: vgmap <dev> [pass]"
exit 1
fi
if [ "$pass" == "" ] ; then
pass=$(get_password pass)
fi
check_active_drive
local key=$(getkey $dev $pass)
for vol in $vols ; do
cfsmakedev ${dev}${vol} ${VG}pv${vol} $key
done
}
function vgunmap {
check_active_drive
for vol in $vols ; do
dmsetup remove /dev/mapper/${VG}pv${vol}
done
}
function vgmount {
local dev="$1"
local dir="$2"
local pass="$3"
if [ -z "$2" ] ; then
echo "vgmount <dev> <dir> [pass]"
exit 1
fi
if [ "$pass" == "" ] ; then
pass=$(get_password pass)
fi
check_active_drive
vgmap $dev $pass
vgscan
vgchange -a y $VG
mount /dev/${VG}/${LV} $dir
}
function vgumount {
check_active_drive
umount /dev/${VG}/${LV}
lvchange -a n /dev/${VG}/${LV}
vgchange -a n $VG
vgunmap
}
function create {
check_active_drive
local dev="$1"
local pass="$2"
if [ -z "$1" ] ; then
echo "usage: create <dev> [pass]"
exit 1
fi
if [ "$pass" == "" ] ; then
pass=$(get_password pass)
fi
echo "setting random disk key"
setrkey $dev $pass
echo "mapping VG's PVs"
if ! vgmap $dev $pass ; then
echo "vgmap failed"
exit 1
fi
echo "creating PV's"
for vol in $vols ; do
if ! pvcreate /dev/mapper/${VG}pv${vol} ; then
echo "coulnt create PV"
exit 1
fi
done
echo "creating VG"
local PVLIST
for vol in $vols ; do
PVLIST="${PVLIST} /dev/mapper/${VG}pv${vol}"
done
if ! vgcreate $VG $PVLIST ; then
echo "couldnt create VG"
exit 1
fi
echo "activating VG"
if ! vgchange -a y $VG ; then
echo "couldnt activate VG"
exit 1
fi
echo "creating logical volumes"
if ! lvcreate -L${LVSIZE} -n${LV} $VG ; then
echo "couldnt create logical volume"
exit 1
fi
echo "formatting with resierfs"
if ! mkreiserfs -f -f /dev/${VG}/${LV} ; then
echo "couldnt make filesystem"
exit 1
fi
vgchange -a n $VG
vgunmap
}
function list_drives {
if ! [ -e $configdir ] ; then
echo "no drives defined"
exit 1
fi
echo "drives defined in ${configdir}:"
find ${configdir} -type f -print | (
while read file ; do
echo "${file##*/}"
done
)
if [ -e "${active_drive_link}" ] ; then
activedrive=$(stat -c %N ${active_drive_link} | cut -d" " -f3 | tr -d "\`'")
echo "active drive is: ${activedrive##*/}"
else
echo "no active drive selected"
fi
}
function set_drive {
local dname="$1"
if [ -z "$1" ] ; then
echo "usage: sd <drive name>"
exit 1
fi
if [ -e /etc/usbcd/${dname} ] ; then
ln -sf ${dname} ${active_drive_link}
else
echo "$dname doesn't exist"
exit 1
fi
}
if [ -z "$1" ] ; then
echo ""
echo "usbcd (usb crypt drive)"
echo " before this command will work, you need to add this line to your /etc/lvm.conf"
echo " types = [ \"device-mapper\", 16 ]"
echo " in the \"devices\" section. Without it, lvm won't allow pvcreate to operate on /dev/mapper devices"
echo ""
echo "usage: usbcd <command> <arguments>"
echo ""
echo "commands:"
echo " ld "
echo " list available drives"
echo " sd <drive name>"
echo " set drive"
echo " mount <dev> <dir> <pass>"
echo " establish the crypto mappings and mount the logical volume"
echo " umount"
echo " undo everything the mount command did"
echo " create <dev> <pass>"
echo " create the physical volumes, logical volume, and filesystem. This will entirely destroy any data you previously stored in the logical volume so really be careful."
echo " map <dev> <pass>"
echo " (turn on crypto map for lvm vg)"
echo " umap"
echo " (turn off crypto map for lvm vg)"
echo " genkey"
echo " generate a random hex key"
echo " getkey: <dev> <pass>"
echo " get key from device"
echo " setkey: <dev> <key> <pass>"
echo " set device key"
echo " setrkey: <dev> <key> <pass>"
echo " set device key randomly"
echo " setpass: <dev> <oldpass> <newpass>"
echo " reencrypt disk key with new passphrase"
exit 1
fi
command="$1"
shift
if [ "$command" == "ld" ] ; then
list_drives "$@"
elif [ "$command" == "sd" ] ; then
set_drive "$@"
elif [ "$command" == "mount" ] ; then
vgmount "$@"
elif [ "$command" == "umount" ] ; then
vgumount "$@"
elif [ "$command" == "map" ] ; then
vgmap "$@"
elif [ "$command" == "umap" ] ; then
vgunmap "$@"
elif [ "$command" == "create" ] ; then
create "$@"
elif [ "$command" == "genkey" ] ; then
genkey "$@"
elif [ "$command" == "getkey" ] ; then
getkey "$@"
elif [ "$command" == "setkey" ] ; then
setkey "$@"
elif [ "$command" == "setrkey" ] ; then
setrkey "$@"
elif [ "$command" == "setpass" ] ; then
setpass "$@"
elif [ "$command" == "md" ] ; then
cfsmakedev "$@"
else
echo "\"$command\" is not a valid command for usbcd"
exit 1
fi
