Skip to content

Instantly share code, notes, and snippets.

Last active September 19, 2023 10:53
  • Star 15 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
Star You must be signed in to star a gist
What would you like to do?
create an encrypted dvd with squashfs and luks

1. Create compressed squashfs image

Use mksquashfs to create a compressed image from a directory. Depending on how compressible the files are you could put on much more than 4.7 GB on a single disc.

mksquashfs /path/to/my/directory image.sqfs

You can use a different compression algorithm with e.g. -comp zstd or append multiple times to one archive to fill it up to almost the allowable size.

2. Reencrypt the image to wrap it in a LUKS container

Create a little additional room for the reencryption routine at the end of the image. Only half of the additional room is used for the header, so the manpage recommends using double the recommended minimum header size – so 32 MiB for luks2. Less is okay if you don't need the metadata space or are close to the maximum disc size.

This operation can potentially eat your data, so make sure you have a backup or can regenrate the file.

truncate -s +8M image.sqfs
cryptsetup -q reencrypt --encrypt --type luks2 \
  --resilience none --disable-locks --reduce-device-size 8M image.sqfs

You can check with cryptsetup luksDump image.sqfs that the data segment is offset at only half the additional size and then trim the size accordingly.

truncate -s -4M image.sqfs

3. Burn encrypted container to a DVD

Now simply burn the image.sqfs to a disc like you would do with an *.iso. In graphical tools you probably need to select "All Files" to be able to select it.

growisofs -dvd-compat -Z /dev/sr0=image.sqfs

4. Congratulations

If your graphical desktop uses some sort of automounter you should see a password prompt pop up after the disc tray is reloaded. Otherwise handle it like you would handle a normal encrypted block device and mount the squashfs filesystem inside.

sudo cryptsetup open /dev/sr0 cryptdvd
sudo mount -t squashfs /dev/mapper/cryptdvd /mnt

Add Integrity Checking and Error Correction

With a compressed and encrypted image like the one above, everything can go to sh*t if there is a single bit flip in the encrypted container, leading to a chain of unrecoverable errors. I'm not going into any longevity comparisons between optical media and hard disks but we all know DVDs can go bad through scratches, dirt or decomposition. Ideally, you're able to correct erroneous sectors but at the absolute minimum you'll want to know when your data is garbled.

Here is a nice post on the reliability of different optical discs.

Hence, I looked into some methods to add checksumming and parity data to the image.

I) Simple and Compatible with PAR2

The simple approach would be to just generate some parity blocks with par2. This tool is widespread and will probably still be obtainable in a few decade's time. Being a Reed-Solomon erasure coding, you can specify how much parity data you would like to have, in percent. The calculations will take a lot of time on multi-Gigabyte images though.

par2 create -r10 image.sqfs

This will create a number of files next to the image, that you can then burn to the disc in a standard UDF filesystem.

growisofs -Z /dev/sr0 -udf ./

II) Integrity and Error-Correction with dm-verity

Another solution is to use another device-mapper layer of the Linux kernel. Although this is a relatively new feature, it should be widely available already – at least in a recent Ubuntu. dm-verity creates a tree-like structure of block hashes up to a root hash, that needs to be stored externally somehow. It creates a cryptographically sound method to verify the integrity of the disc and it allows adding – again Reed-Solomon coded – parity blocks to restore detected bad blocks.

Since it is a device-mapper which is supposed to work with raw disk devices, I would expect it to fare better with unresponsive or badly scratched discs, that return many bad sectors. But for lack of a reliable way to inject precise faults on optical discs I cannot test this assumption. I am not sure how this method behaves if you were to have a bad sector exactly where the verity superblock is supposed to be on the disc.

There is two methods to this. Either you create the hash-tree and parity blocks in files next to the encrypted image and then burn them in a UDF filesystem like in method I). Or you reuse the same image file and specify offsets for the different parts. The former would have the advantage that you can add README files and reuse existing recovery tools to read the files from disc and then try to restore them locally. The latter would minimize the number of layers but does require some calculation for the offsets. Either way you somehow need to store the generated root hash for this to make any sense at all! I propose writing it on the disc itself or encoding it in a QR code and printing it on the leaflet that you put in the case.

Calculating Offsets

If we want to reuse a single file with veritysetup, you need to know where to place the hash and error correction blocks.

The hash offset is relatively straightforward, since it is simply the amount of data you have, i.e. the size of the image. First of all make sure that it is a multiple of 4096 bytes, which is the default blocksize of veritysetup! mksquashfs uses a default block size of 128 KiB, so this should be given here. Therefore --hash-offsetand --data-blocks are calculated as:

stat -c%s image.sqfs |\
  awk '{ printf "--hash-offset=%d --data-blocks=%d\n", $1, $1/4096 }'

The --fec-offset is a little more tricky because you need to know how many hash blocks are going to be written, which is not completely trivial due to the tree structure. You can calculate it recursively though. The following Python snippet assumes 4k data and hash sectors and 32 bit hashes, thereby fitting 128 hashes into one hash block.

import math
# hs := hash sectors, ds := data sectors
def hs(ds, superblock=False):
  h = 1 if superblock else 0
  while ds > 1:
    ds = math.ceil(ds / 128)
    h += ds
  return h

So for a small file with 72884224 bytes or 17794 data blocks, it would result in 144 hash blocks. The --fec-offset would then be (data-blocks + hash-blocks) * 4096 – in this case 73474048. The format command for my small test file would then be:

veritysetup format --fec-roots=24 --data-blocks=17794 --hash-offset=72884224 --fec-offset=73474048 {--fec-device=,,}image.sqfs


So far I couldn't verify any actual corruption cases where I overwrote the first few blocks with dd ...

This may or may not have been due to cryptsetup/cryptsetup#554. I haven't checked again since then.

#!/usr/bin/env bash
set -eu -o pipefail
target="$1"; shift 1;
# check for arguments
if [[ -z "$target" ]] || [[ "$target" = "-h" ]] || [[ -z "${sources[*]}" ]]; then
echo "usage: $0 target.sqlk sourcedir/ [another/]" >&2
exit 1
if [[ -e "$target" ]]; then
printf "target %q already exists!\n" "$target" >&2
exit 1
# check for key-file
if ! [[ -r $keyfile ]]; then
echo "LUKS password required in \$SQUASHKEYFILE ($keyfile)" >&2
exit 1
# create squashfs archive
echo -e "\033[1m1/3: create squashfs archive ...\033[0m"
mksquashfs "${sources[@]}" "$target" -quiet -comp zstd -Xcompression-level 22
ls -lh "$target"
# luksify
echo -e "\033[1m2/3: luks-encrypt archive ...\033[0m"
truncate -s +8M "$target"
cryptsetup -q reencrypt --encrypt --type luks2 \
--resilience none --disable-locks --reduce-device-size 8M \
--key-file "$keyfile" "$target"
echo -e "\033[1m2/3: add a second password to archive ...\033[0m"
cryptsetup luksAddKey \
--key-file "$keyfile" "$target"
truncate -s -4M "$target"
# check size different discs
echo -e "\033[1m3/3: check final size ...\033[0m"
size=$(stat -c %s "$target")
for disc in "DVD-R/4707319808" "DVD+R/4700372992" "DVD-R DL/8543666176" "DVD+R DL/8547991552"; do
#capacity=$(cdrwtool -i -d /dev/sr0 | awk '$1 == "free_blocks" { print $3 * 2048 }')
awk -v disc="$(dirname "$disc")" -v cap="$(basename "$disc")" -v size="$size" </dev/null '
printf "%10s %.2fGB ", disc, cap / 1000000000;
printf "%s\n", size <= cap ? "\033[32m✔\033[0m" : "\033[31m✘\033[0m";
echo "write to disk with: $ growisofs -dvd-compat -Z /dev/sr0=$(printf "%q" "$target")"
Copy link

uriesk commented Oct 21, 2022

thanks this was very helpful

Copy link

ansemjo commented Oct 22, 2022

I'm glad it was useful! You might want to check the update I published just now: I scripted the mksquashfs and cryptsetup steps a while ago. It also tells you how large the disc needs to be to fit the created archive.

Copy link

uriesk commented Oct 24, 2022

thanks, this is the best encrypt-mksquashfs-backup method i found yet, because it reencrypts rather than creating a second encrypted image first and then moving it on it.

However, i do the mksquashfs on a drive that isn't encrypted and it would be best to not save it unencrypted at all.
What i currently ended up with:

  1. Create a LUKS image as a sparse file (so even if its 10GB, it still takes up only 1MB if only 1MB got written to it)
  2. mksquashfs directly onto it
  3. shrink size of LUKS volume and image file to the minimum

I had to do some math cause of 4k padding of squashfs and 512byte sectors of cryptsetup.
This script saves /etc into test.img.

# squashfs pads to 4096bytes (see mksquashfs manpage)
# even when the LUKS volume uses a different block size,
# the reported blocks in `cryptsetup status` and for `resize`
# seem to always be 512bytes (see manpage of cryptsetup)
set -e

echo "Creating sparse file with size ${MAX_SIZE}"
truncate -s "${MAX_SIZE}" "${IMAGE_FILE}"
echo "Set up encryption"
cryptsetup luksFormat "${IMAGE_FILE}"
echo "Open LUKS volume"
cryptsetup luksOpen "${IMAGE_FILE}" "${LUKS_NAME}"
echo "Creating squashfs"
mksquashfs "${BACKUP_FOLDER}" "/dev/mapper/${LUKS_NAME}" -progress -noappend

# Calculate used up space of squashfs
SIZE=`unsquashfs -s "/dev/mapper/${LUKS_NAME}" | grep "Filesystem size" | sed -e 's/.*size //' -e 's/ .*//'`
echo "Squashfs size: ${SIZE} bytes"
[ `expr ${SIZE} % ${BLOCK_SIZE}` != 0 ] && {
    BLOCKS=`expr ${BLOCKS} + 1`
    SIZE=`expr ${BLOCKS} \* ${BLOCK_SIZE}`
echo "Padded squashfs size on device: ${SIZE} bytes"

# Shrink LUKS volume
echo "Shrinking LUKS volume to ${LUKS_BLOCKS} blocks of ${LUKS_BLOCK_SIZE} bytes"
cryptsetup resize "${LUKS_NAME}" -b ${LUKS_BLOCKS}

# Get final size and shrink image file
LUKS_OFFSET=`cryptsetup status "${LUKS_NAME}" | grep offset | sed -e 's/ sectors$//' -e 's/.* //'`
cryptsetup luksClose "${LUKS_NAME}"
truncate -s ${FINAL_SIZE} "${IMAGE_FILE}"
echo "Final size is ${FINAL_SIZE} bytes"

I still have to do some verification to see if everything works out.

Maybe it helps you too :)

Copy link

ansemjo commented Oct 25, 2022

Huh'. That's a nice idea! I haven't thought of just using the LUKS container itself as a target for mksquashfs because I was somehow sure that you wouldn't be able to shrink it afterwards .. 🙃

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment