1. A Practical Script for Encrypting LVM Volume Groups
    1. Author
    2. Warning!
    3. Detailed Example
      1. Partitioning
      2. Config File
      3. Modifying /etc/lvm/lvm.conf
      4. Creating the Encrypted Volume Group
      5. Mounting the Encrypted Volume
      6. Unmounting and Unmapping
    4. Possible Bugs
    5. Script Text

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:

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

hopeless_linux: encrypted filesystems/a practical script for encrypting lvm volume groups (last modified 2007-07-01 16:01:00)