Linux/dm-crypt

From Segfault
Jump to navigation Jump to search

The device mapper can be used with different targets, in this case dm-crypt. Apart from taking the usual precautions when dealing with cryptography, we should decide for a fast, yet secure combination of cipher, keysize, hash algorithm.[1]

Preparation

We may want to remove any pre-existing signatures from the device:

DEV=/dev/sdX                                                # Adjust as needed!

wipefs -a $DEV                                              # Wipe any pre-existing file system signatures
pv < /dev/zero > $DEV                                       # Wipe the device with zeros
shred -vn1 $DEV                                             # Wipe the device with random data

Setup

LUKS

cryptsetup is really the way to go to use dm-crypt devices and its setup is more intuitive that plain dm-crypt.

Setup the LUKS container (this has to be done only once):

cryptsetup luksFormat --type luks2 ${DEV}

Or, when using key files:[2][3][4]

head -c 256 /dev/random > keyfile
cryptsetup luksFormat --type luks2 --key-file keyfile ${DEV}

Open the newly created container:

$ cryptsetup open ${DEV} test                                                 # Add --key-file as needed.
Enter passphrase for /dev/sdx: 

$ cryptsetup status test
/dev/mapper/test is active.
  type:    LUKS2
  cipher:  aes-xts-plain64
  keysize: 512 bits
  key location: keyring
  device:  /dev/sdb1
  sector size:  512
  offset:  32768 sectors
  size:    468827312 sectors
  mode:    read/write

$ file -Ls ${DEV} /dev/mapper/test
/dev/sdx: LUKS encrypted file, ver 2, header size 16384, ID 3, algo sha256, salt 0x8f7124c9553e10fc [...]
/dev/mapper/test: data

FDE

For FDE setups, the name of the encrypted root disk can be changed[5]. In short:

Replace OLD_NAME with NEW_NAME in /etc/crypttab, then:

dmsetup rename OLD_NAME NEW_NAME
update-initramfs -c -t -k all
update-grub
reboot

For Fedora systems[6]

mkinitrd --force /boot/initramfs-`uname -r`.img `uname -r`
grub2-mkconfig -o /boot/efi/EFI/fedora/grub.cfg                       # UEFI systems
grub2-mkconfig -o /boot/grub2/grub.cfg                                # BIOS systems

Key Management

Let's use a keyfile to encrypt our volume:

openssl rand -hex 32 > key
cryptsetup luksFormat --batch-mode --verbose --key-file key /dev/vg0/lv1

Let's try to open the device:

$ cat key
eb1a3dbd7d0f3913de273ea6e999d92cdd53d29e2dd6f93f4a79495b6b92fd00

$ cryptsetup open --verbose /dev/vg0/lv1 luks-lv1
Enter passphrase for /dev/vg0/lv1: 
No key available with this passphrase.

That....didn't work. Since we encrypted the volume with a keyfile, we really need to use the --key-file option here:

$ cat key | cryptsetup open --verbose --key-file - /dev/vg0/lv1 luks-lv1
Key slot 0 unlocked.
Command successful.

We can add a second key to the same volume:

$ cryptsetup luksAddKey --key-file key /dev/vg0/lv1 
Enter new passphrase for key slot: 
Verify passphrase: 

Now we can unlock the volume with a passphrase too - or we can use the --key-file of course.

When we remove the keyfile, we can only unlock the volume with the passphrase we just supplied:

$ cryptsetup luksRemoveKey --key-file key /dev/vg0/lv1 
$ cryptsetup open --verbose /dev/vg0/lv1 luks-lv1
Enter passphrase for /dev/vg0/lv1: 
Key slot 1 unlocked.
Command successful.

Let's add the keyfile again, and remove the passphrase:

$ cryptsetup luksAddKey --new-keyfile key /dev/vg0/lv1
$ cryptsetup luksDump /dev/vg0/lv1 | grep luks2
 0: luks2                                       <== keyfile
 1: luks2                                       <== passphrase

$ cryptsetup luksKillSlot --verbose --key-file key /dev/vg0/lv1 1
Keyslot 1 is selected for deletion.
Key slot 0 unlocked.
Key slot 1 removed.
Command successful.

Token Management

We can use tokens too. For that to work, the key needs to be added to the device first, and then a token will describe how to fetch that key to unlock the device.

openssl rand -hex 32 > secret
cryptsetup luksAddKey --key-file key --new-keyfile secret /dev/vg0/lv1

Note: in this case, the secret was added to keyslot 1. Now we can use cryptsetup-ssh to specify how to get to that secret:

cryptsetup-ssh add --verbose --key-slot=1. --ssh-keypath=/home/luks/.ssh/id_ed25519 --ssh-path=secret --ssh-server=localhost --ssh-user=luks /dev/vg0/lv1

Let's have a look:

$ cryptsetup luksDump /dev/vg0/lv1 | grep -A6 Token
Tokens:
 0: ssh
       ssh_server: localhost
       ssh_user: luks
       ssh_path: secret
       ssh_key_path: /home/luks/.ssh/id_ed25519
       Keyslot:    1.

Now the volume can be unlocked w/o any interaction:

$ cryptsetup open --verbose /dev/vg0/lv1 luks-test
SSH token initiating ssh session.
Key slot 1 unlocked.
Command successful.

Note: the SSH command will use sftp to download that key. On the remote side we can restrict our user somewhat:

$ chsh -s /bin/sh luks                                   # Needed for sftp to work...?
$ head -1 ~luks/.ssh/authorized_keys 
restrict ssh-ed25519 AAAAC0d910909d2baf99d019ac096ba303d74cabfda2021f4bc6695b14f65f69 root@localhost
$ cat /etc/ssh/sshd_config
[...]
AllowUsers luks@127.0.0.1
Match User luks
       ChrootDirectory /home/luks/
       ForceCommand    internal-sftp

GPG keys

It's possible to specify the key to cryptsetup via stdin and we could use GPG encrypted key material instead of pass phrases.

FIXME: Find out about the security implications here! Also explain the relation between the GPG key size and the --key-size argument from cryptsetup!

Generate random key material and encrypt it with gpg:

openssl rand -hex 4096 | \
   gpg --armor --symmetric --cipher-algo aes256 --digest-algo sha512 > key.asc

Without openssl:

dd if=/dev/random bs=1024 count=6 2>/dev/null | base64 | \
   gpg --armor --symmetric --cipher-algo aes256 --digest-algo sha512 > key.asc

Initialize the LUKS partition (once):

DEV=/dev/sdX                                                # Adjust as needed!
gpg --decrypt key.asc | \
       cryptsetup luksFormat --cipher=aes-xts-plain64 --hash sha256 --key-size 512 --iter-time=5000 $DEV 

And open it:

gpg --decrypt key.asc | cryptsetup open $DEV test

Now the new mapping device should be active:

$ cryptsetup status test
/dev/mapper/test is active.
  type:    LUKS1
  cipher:  aes-xts-plain64
  keysize: 512 bits
  device:  /dev/sdx
  offset:  4096 sectors
  size:    20967424 sectors
  mode:    read/write

Plain

While we could setup the dm-crypt volume manually, one should really use cryptsetup instead. But just for posterity, this is how dm-crypt can be initialized:[7]

0 <sector count> crypt <sector format> <key> <IV offset> <real device> <sector offset> 
  • The sector count can be determined with blockdev
  • The sector format is the name of the (symmetric) encryption cipher and an optional IV
  • Hexadecimal representation of the encryption key; its size is bound to the selected cipher
  • The IV offset is usually just "0".

Let's do this:

DEV=/dev/sdX                                                # Adjust as needed!
KEY=`openssl rand -hex 64`
echo 0 `blockdev --getsize $DEV` crypt aes-xts-plain64 $KEY 0 $DEV 0 | dmsetup create test

And this worked:

$ dmsetup info test
Name:              test
State:             ACTIVE
Read Ahead:        256
Tables present:    LIVE
Open count:        0
Event number:      0
Major, minor:      254, 0
Number of targets: 1

Test

A quick(!) test would be to write to the encrypted volume and search for the string on the block device:

mkfs.ext4 /dev/mapper/test
mount -t ext4 /dev/mapper/test /mnt/disk
echo  "hey, it worked" > /mnt/disk/foo.txt

Let's see if we can find the string:

$ grep -Fa "hey, it worked" /dev/mapper/test | strings      # Use --devices=read if necessary
Hhey, it worked

$ grep -Fa "hey, it worked" ${DEV}
[should not return anything]

Backup

It's important to backup the LUKS header once in a while:

cryptsetup luksHeaderBackup  $DEV --header-backup-file header_sdX.backup

With that, the LUKS header can be restored should it ever get corrupted:

cryptsetup luksHeaderRestore $DEV --header-backup-file header_sdX.backup

Benchmark

Every system is different, so let's use a quick benchmark to find out which cipher/hash combination is best (fastest) for a particular machine:

$ for i in {1..10}; do echo "i: ${i}"; cryptsetup benchmark; done 2>&1 | tee c.log

$ grep -A5 Tests c.log | awk '!/^#/ {print $1}' | head -5 | while read h; do printf "hash: ${h}  "; grep ${h} c.log | awk '{sum=+$2} END {print sum}'; done | sort -nk3
hash: PBKDF2-whirlpool   10369
hash: PBKDF2-sha512      21700
hash: PBKDF2-sha256      99296
hash: PBKDF2-ripemd160  137970
hash: PBKDF2-sha1       152409                 ***

$ grep -A12 Algorithm c.log | head -13 | awk '!/^#/ {print $1, $2}' | while read c s; do 
    printf "cipher: ${c}  size: ${b}  "; grep -E "${c}.*${s}" c.log | awk '{enc+=$3; dec+=$5} END {print enc,dec}'; done  | sort -nk5,6
cipher: serpent-cbc  size: 128b  0 0
cipher: serpent-cbc  size: 256b  0 0
cipher: serpent-xts  size: 256b  0 0
cipher: serpent-xts  size: 512b  0 0
cipher: twofish-cbc  size: 128b  0 0
cipher: twofish-cbc  size: 256b  0 0
cipher: twofish-xts  size: 256b  0 0
cipher: twofish-xts  size: 512b  0 0
cipher: aes-cbc      size: 256b  216   229.2
cipher: aes-xts      size: 512b  228.7 224.1
cipher: aes-cbc      size: 128b  268.4 296.2
cipher: aes-xts      size: 256b  287.6 287     ***

As a short comparison, here's what reading from an external (USB) disk looks like:

$ pv -Ss 2G < /dev/sda > /dev/null
2GiB 0:01:00 [33.6MiB/s]

$ cryptsetup status test
/dev/mapper/test is active and is in use.
  type:    LUKS1
  cipher:  aes-cbc-essiv:sha256
  keysize: 256 bits
  device:  /dev/sda1
  offset:  2056 sectors
  size:    1953519608 sectors
  mode:    read/write

$ pv -Ss 2G < /dev/mapper/test > /dev/null
2GiB 0:01:26 [23.8MiB/s]

Links

References