One of the use cases I was hoping the Zymkey could support was the ability to securely mount an encrypted external drive automatically at boot. To my surprise, there was no tutorial/examples provided for this use case, and the forum had a single post stating that they had found a solution, but hadn’t outlined what that was.

For those of you who haven’t been able to figure it out, and have been scratching your heads like me. Here’s my solution to the problem. I’m not saying it’s perfect, or the best way of going about it, but it does work for me!

Step 1 – Get the device address

Run sudo fdisk -l after plugging in your external hard drive. Take careful note of the drive’s partition address (for example, /dev/sda1, or /dev/sdb1). You want to make absolutely sure you’re referencing the right drive and partition here! The fdisk command should provide additional information about the manufacturer and the partition size to help you determine what is what! Failure to note the correct device address can result in you destroying data on other disks, so be very careful to ensure it’s the correct address!

Step 2 – Create a backup of the external drive

We’re going to be encrypting the drive. It’s always wise to take a backup beforehand! This can either be a full sector by sector disk image backup, or a file system backup. It really depends on what you’re using the external drive for, as to which will suit your needs. If you’re just storing files on your external drive, a file system backup is probably sufficient. Note that if the drive is already encrypted, you can still continue with the process without having to decrypt the drive first (the encryption will remain as is), but I’d still take a backup first. Clearly, for a filesystem backup on an encrypted drive, the drive will need to be unlocked and mounted accordingly!

Step 3 – Create the following shell script

You can call the script what you like, but I’ve chosen zkyExtSetup.sh (it’s convention to use the .sh file extension for shell scripts if you choose a different name).

For those of you who don’t know how to create a shell script, if you haven’t already, install the nano text editor (sudo apt install nano). Now type cd ~/ && sudo nano zkyExtSetup.sh You then just need to copy and paste the code below (updating <<EXT DEVICE>> with the device address from step 1, and <<MOUNT NAME>> with the folder name of your choice for the drive mount. Once you’ve done this, you can save the file by clicking ctrl-X and then Y on the keyboard.

The script itself does several things – first it generates a new encryption key for the drive using the Zymkey’s sudo random byte generator, it then encrypts/locks this key (again with the Zymkey), using the lock function of the Zymbit Python API. It then goes on to check if the external hard drive is already encrypted.

If you choose to use an already encrypted drive, it asks you for the passphrase which is used to add the Zymkey generated key to the LUKS header of the drive (you can have several keys associated with a LUKS encrypted drive). Doing things this way means you will always have the ability to unlock the drive, both with your original passphrase, and the new Zymkey key (providing some resilience in the face of Zymkey hardware failure).

If however, you choose to use an unencrypted drive, the script goes on to warn you that this will completely erase the disk! There is no turning back from this point, so make sure you don’t mind the disk being erased! The script will ask if you wish to add a separate password for unlocking the drive (in addition to the Zymkey key). This will help protect against hardware failure of the Zymkey device. However, please note, that any passphrase is likely to be weaker than an encryption key, and you should use a strong passphrase. Of course, I’d always recommend you ensure the password you’re using is unique to the device in question, and not one that you’ve widely used elsewhere. You may also wish to test that your password isn’t appearing within any public hacked password databases to give you peace of mind.

Finally the script sets up a service to automatically unlock the the drive at boot. It does this by using the Zymkey to unlock the encrypted drive key at boot. The unencrypted key is stored in the root partition temporarily and once it’s mounted the drive, it deletes the unencrypted key. I may update the script to store the key in memory rather than on the root drive, but in my case the root drive is also encrypted, so it doesn’t matter if the key is stored on the root drive (especially given that the script goes on to delete the unencrypted key once the drive has successfully mounted).

#!/bin/bash

# Ensure running as root or exit
if [ "$(id -u)" != "0" ]; then
    echo "run this as root or use sudo" 2>&1 && exit 1
fi

EXT_DRIVE="<<EXT DRIVE>>"
DISK_MOUNT_NAME="<<MOUNT NAME>>"
KEY_NAME="extHddKey"

echo "Creating required directories"
mkdir -p /media/$DISK_MOUNT_NAME
mkdir -p /etc/luks

echo #new line
echo "Creating generate key script"
cat > ~/generateExtHddKey.py <<EOF
import os,zymkey
from zymkey.exceptions import VerificationError
try:
    # create a pseudorandom 512 byte (4096 bit) key from zymkey's random byte
    # generator
    numBytes = 512
    extHddKey = zymkey.client.get_random(numBytes)
    
    # encrypt and sign new key using zymkey
    lockedExtHddKey = zymkey.client.lock(extHddKey)

    # write unlocked external hard drive key to same location as the zymbit root hdd
    # key (the key used to unlock the root partition)
    f = open("/etc/luks/$KEY_NAME.bin", "wb")
    f.write(extHddKey)
    f.close()
    os.chmod("/etc/luks/$KEY_NAME.bin", 0o600)

    # write locked (encrypted and signed) external hard drive key to same location
    # as the zymbit root hdd key (the key used to unlock the root partition)
    f = open("/etc/luks/$KEY_NAME.bin.lock", "wb")
    f.write(lockedExtHddKey)
    f.close()	
    os.chmod("/etc/luks/$KEY_NAME.bin.lock", 0o600)
except VerificationError as e: print(e)
EOF

# Create unlock script
cat > /etc/luks/unlockExtHddKey.py <<EOF
import time,base64,zymkey
# Open the generated locked external hard drive key for the zymbit to unlock
f = open("/etc/luks/$KEY_NAME.bin.lock", "rb")
# Output the decrypted key in base64 format
print(base64.b64encode(zymkey.client.unlock(f.read())).decode("ascii"))
f.close()
EOF

echo #new line
echo "Generating and locking encryption key using zymkey device"
python3 ~/generateExtHddKey.py
rm ~/generateExtHddKey.py
echo #new line
read -p "Are you wishing to add a Zymkey key to an already encrypted LUKS drive without erasing its content? (Y/N)" confirm

echo #new line
if [[ $confirm == [yY] || $confirm == [yY][eE][sS] ]]; then
    echo #new line
    read  -s -p "Please enter the passphrase for the existing encrypted LUKS drive (this is required to add the additional Zymkey key):" passphrase
    echo #new line
    if [[ $passphrase ]]; then
        echo -n $passphrase | cryptsetup -q -v luksAddKey $EXT_DRIVE /etc/luks/$KEY_NAME.bin >/dev/null
	
        echo #new line
	echo "Zymkey key added to existing drive"
    else
        echo #new line
	echo "No passphrase was provided, script terminated." 2>&1 && exit 1
    fi
else
    echo #new line
    read -p "!!!WARNING!!! This script will completely erase your chosen external hard drive and all it's contents. Are you sure you wish to continue? You will not be able undo the erasing of the drive! (Y/N)" erase
    
    echo #new line
    if [[ $erase == [yY] || $erase == [yY][eE][sS] ]]; then
        echo #new line
        echo "Creating LUKS volume on chosen external hard drive"

        echo #new line
        read -p "Would you like to add a passphrase to your drive to act as a failback in case of zymkey hardware failure? The drive will then be unlockable either with the zymkey key or the passphrase. (Y/N)" confirm2

        echo #new line
        if [[ $confirm2 == [yY] || $confirm2 == [yY][eE][sS] ]]; then
            echo #new line
            read  -s -p "Please enter the fallback passphrase you wish to use:" fallbackPass1
            
            echo #new line
            if [[ $fallbackPass1 ]]; then
                echo #new line
                read  -s -p "Please confirm the fallback passphrase you wish to use:" fallbackPass2

                echo #new line
                if [[ $fallbackPass2 ]] && [[ $fallbackPass1 == $fallbackPass2 ]]; then
                    echo -n $fallbackPass1 | cryptsetup -q -v luksFormat $EXT_DRIVE >/dev/null
                    echo #new line
                    echo "Passphrase added to drive"
                    echo -n $fallbackPass1 | cryptsetup -q -v luksAddKey $EXT_DRIVE /etc/luks/$KEY_NAME.bin >/dev/null
                    echo #new line
                    echo "Zymkey key added to drive"
                else
                    echo #new line
                    echo "Passphrases don't match, script terminated." 2>&1 && exit 1
                fi
            else
                echo #new line
                echo "No passphrase was provided, script terminated." 2>&1 && exit 1
            fi		
        else
            echo #new line
            echo "Zymkey key added to drive"
            cryptsetup -q -v luksFormat $EXT_DRIVE /etc/luks/$KEY_NAME.bin >/dev/null
        fi
		
        echo #new line
        echo "Opening LUKS volume"
        cryptsetup -d /etc/luks/$KEY_NAME.bin -q -v luksOpen $EXT_DRIVE $DISK_MOUNT_NAME >/dev/null
		
        echo #new line
        echo "Creating ext4 filesystem on encrypted LUKS volume"
        mkfs.ext4 -j /dev/mapper/$DISK_MOUNT_NAME -F >/dev/null || exit
    else
        echo #new line
        echo "Script terminated" 2>&1 && exit 1
    fi
fi

echo #new line
echo "Deleting unlocked encryption key"
rm /etc/luks/$KEY_NAME.bin
UUID_EXT_DIST=$(blkid -s UUID -o value $EXT_DRIVE)
echo #new line
echo "Creating shell script that unlocks external drive key and mounts drive at boot"

cat > /etc/luks/mountExtHdd.sh <<EOF
#!/bin/bash
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
# run python script to unlock encrypted external hard drive
python3 /etc/luks/unlockExtHddKey.py | base64 -d | cryptsetup -d - -v luksOpen /dev/disk/by-uuid/$UUID_EXT_DIST $DISK_MOUNT_NAME
mount /dev/mapper/$DISK_MOUNT_NAME /media/$DISK_MOUNT_NAME -o discard,defaults,noatime
EOF

chmod +x /etc/luks/mountExtHdd.sh

echo #new line
echo "Creating and enabling service to to mount external drive at boot"
cat > /etc/systemd/system/mount-ext-hdd.service <<EOF
[Unit]
Description=Example systemd service.
After=zkifc.service multi-user.target
[Service]
RemainAfterExit=true
Restart=always
RestartSec=10
TimeoutStartSec=600
ExecStart=/etc/luks/mountExtHdd.sh
[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable mount-ext-hdd.service
systemctl start mount-ext-hdd.service
echo #new line
echo "Script completed successfully" 2>&1 && exit 1

Step 4 – Making the script executable

To do this run sudo chmod +x ~/zkyExtSetup.sh (clearly, if you’ve chosen a different name and location for this file, use that one instead)

Step 5 – Run the script

If you’re not already in the home directory, run cd ~/ Once you’ve done that, run sudo ./zkeyExtSetup.sh (you need to run the script as the sudo/root user)

Step 6 – Ensure services requiring the external hard drive wait for the drive to mount at boot

If you have any services that depend on the external hard drive being mounted (for example, I required Docker to wait for the external hard drive, as that’s where I was storing my Docker volumes). You can add the following to the systemd service file under the [unit] heading: 

After=mount-ext-hdd.service

This tells the service to wait for the mount-ext-hdd.service to load first. You may also need to add a slight timeout within the service file (I had to do this for Docker). To do this I added the following under the [service] header of the Docker service file: 

ExecStartPre=/bin/sleep 40.

This tells the service to sleep for 40 seconds before loading, the exact number of seconds may need to be something you experiment with to get right.


0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *