Skip to content

Instantly share code, notes, and snippets.

@morfikov
Last active December 31, 2023 13:59
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save morfikov/0bd574817143d0239c5a0e1259613b7d to your computer and use it in GitHub Desktop.
Save morfikov/0bd574817143d0239c5a0e1259613b7d to your computer and use it in GitHub Desktop.
How to use your Android phone as a key to your encrypted linux desktop/laptop system (LUKS/LUKS2 based)

About this HowTo

Encrypted systems (desktops/laptops) have one major problem when it comes to providing security over protected files. When you sit in front of your machine, you can feel safe because no one can break into the system without your knowledge. Moreover, if they try to do some bad actions, you can detect them and avoid the imminent danger. But what if you leave your laptop alone? Even if you think that you can lock it, turn it off or hibernate it, the machine still isn't secure as you would have thought. The problem lays in the physical access that people can get when you're not around and hence set some traps for you when you're not looking. To avoid the danger that comes with leaving the laptop/desktop unattended is to not allow it to be alone, but this is almost impossible. Desktops or even laptops aren't small devices and simply can't be taken when you move around, but phones are different.

Basically all of us have some kind of a phone. Usually it's an Android based device that we use daily. When you have a smartphone that is supported by custom ROMs (the ones based on AOSP, for instance LOS), you can have a really privacy friendly and secure device, and this device can be used as a token when you want to access you encrypted desktop/laptop. All thanks to the USB MSD component that is included in the Android kernels. This makes it possible, for instance, to boot a live system image directly from your phone via some USB port (using USB Mountr). But it's not the only one thing that you can do with this feature. In this case we can make a bootable image from the linux's /boot/ partition and also move LUKS headers into the image. In this way, we can simply avoid MBR attacks, bootloader attacks and initramfs/initrd attacks making it almost impossible to access the desktop/laptop system without our phone. Moreover, when we move LUKS headers to the image, there's no encryption key left on the computer's disk, and no one can extract it even if they've managed to get the password.

We're going to create the image out of the /boot/ partition, which will include the extlinux bootloader (it can be also GRUB), extlinux configs, linux kernel, initramfs/initrd image and LUKS headers. The image will also have MBR and MS-DOS partition table that we create during the process. So let's get started.

Create the /boot/ partition image

First of all, we have to create an image that will host files from the /boot/ partition. We can do it in two ways. First one is to copy the whole /boot/ partition to a file, and the other is to create some file (via dd), file system in it (via mkfs), and copy all files from the /boot/ partition to the image. Both of the solutions have pros and cons, and in this example we will be using the first solution.

  # mkdir /media/boot/
  # cd /media/boot/
  # dd if=/dev/sda1 bs=1M | pv -s 2G > ./boot-2g.img

We have to set some loop device up for our image:

  # losetup /dev/loop4 boot-2g.img
  # losetup -a
  /dev/loop4: [65031]:6815746 (/media/boot/boot-2g.img)
  # mount -o ro /dev/loop4 /mnt
  # df -h /mnt
  Filesystem     Type  Size  Used Avail Use% Mounted on
  /dev/loop4     ext4  2.0G   51M  1.8G   3% /mnt

In this case, the /boot/ partition is 2 GiB in size, and in most cases it's a waste of space. As we can see, files in this file system use 51 MiB of space. We could shrink the image to, for instance, 256 MiB. If we want to do it, we have to shrink the file system first.

Resize the /boot/ partition image

We're going to use resize2fs in order to shrink the file system of our image. We can do it in the following way:

  # umount /mnt
  # resize2fs -p /dev/loop4 256M
  resize2fs 1.44.0 (7-Mar-2018)
  Resizing the filesystem on /dev/loop4 to 65536 (4k) blocks.
  Begin pass 2 (max = 16475)
  Relocating blocks             XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
  Begin pass 3 (max = 16)
  Scanning inode table          XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
  The filesystem on /dev/loop4 is now 65536 (4k) blocks long.

The file system in the image is now 65536 blocks long, which is 65536*4096=268435456 bytes or 256 MiB. Remember that not only files reside in the image. There's also some EXT4 metadata and, of course, the EXT4 journal, which in this case is 32 MiB in size:

  # dumpe2fs /dev/loop4 | grep "Journal size"
  Journal size:             32M

So that's why we've set the new size of the image to 256 MiB (some free space for future use is also included).

Now we have to cut the rest of the image off:

  # losetup -d /dev/loop4
  # dd if=./boot-2g.img of=./boot-256m bs=4K count=65536
  65536+0 records in
  65536+0 records out
  268435456 bytes (268 MB, 256 MiB) copied, 2.25373 s, 119 MB/s

We can test whether the file system of the image is in a good shape, by mounting it:

  # losetup /dev/loop4 boot-256m
  # mount -o ro /dev/loop4 /mnt
  # df -h /mnt
  Filesystem     Type  Size  Used Avail Use% Mounted on
  /dev/loop4     ext4  188M   49M  122M  29% /mnt

If no errors, then everything is just fine, so let's unmount it.

  # umount /mnt
  # losetup -d /dev/loop4

When we check the image in parted, we can see there's no free space at the beginning of the image:

  # parted boot-256m
  GNU Parted 3.2
  Using /media/boot/boot-256m
  Welcome to GNU Parted! Type 'help' to view a list of commands.
  (parted) unit s
  (parted) print free
  Model:  (file)
  Disk /media/boot/boot-256m: 524288s
  Sector size (logical/physical): 512B/512B
  Partition Table: loop
  Disk Flags:

  Number  Start  End      Size     File system  Flags
   1      0s     524287s  524288s  ext4

So we have to add some free space at the beginning of the image.

Create MBR and partition table inside of the /boot/ partition image

The next step is to create MBR and some partition table inside of the image. In this case we will use the MS-DOS partition table. So let's create another image first:

  # dd if=/dev/zero of=./free-space.img bs=512 count=2048
  2048+0 records in
  2048+0 records out
  1048576 bytes (1.0 MB, 1.0 MiB) copied, 0.0180124 s, 58.2 MB/s

Free space of 1 MiB should be fine because all major partitioning tools use the value when they create the first partition on hard drives.

Now we have to concatenate the two images:

  # pv boot-256m >> free-space.img
   256MiB 0:00:02 [ 116MiB/s] [===========================>] 100%
  # mv free-space.img new-boot.img
  # ls -al new-boot.img
  -rw-r--r-- 1 root root 257M 2018-03-15 17:57:37 new-boot.img

And create the partition table using fdisk:

  # fdisk new-boot.img

  Welcome to fdisk (util-linux 2.31.1).
  Changes will remain in memory only, until you decide to write them.
  Be careful before using the write command.

  Device does not contain a recognized partition table.
  Created a new DOS disklabel with disk identifier 0x105e4249.

  Command (m for help): n
  Partition type
     p   primary (0 primary, 0 extended, 4 free)
     e   extended (container for logical partitions)
  Select (default p): p
  Partition number (1-4, default 1): 1
  First sector (2048-526335, default 2048): 2048
  Last sector, +sectors or +size{K,M,G,T,P} (2048-526335, default 526335):

  Created a new partition 1 of type 'Linux' and of size 256 MiB.
  Partition #1 contains a ext4 signature.

  Do you want to remove the signature? [Y]es/[N]o: n

Also set the boot flag:

  Command (m for help): a
  Selected partition 1
  The bootable flag on partition 1 is enabled now.

See if everything is fine:

  Command (m for help): p
  Disk new-boot.img: 257 MiB, 269484032 bytes, 526336 sectors
  Units: sectors of 1 * 512 = 512 bytes
  Sector size (logical/physical): 512 bytes / 512 bytes
  I/O size (minimum/optimal): 512 bytes / 512 bytes
  Disklabel type: dos
  Disk identifier: 0x105e4249

  Device        Boot Start    End Sectors  Size Id Type
  new-boot.img1 *     2048 526335  524288  256M 83 Linux

And if yes, write the partition table to the image:

  Command (m for help): w
  The partition table has been altered.
  Syncing disks.

When we create a loop device for this new /boot/ image, we can see that something has changed:

  # losetup /dev/loop4 new-boot.img
  # losetup -a
  /dev/loop4: [65031]:6815748 (/media/boot/new-boot.img)
  # ls -al /dev/loop4*
  brw-rw---- 1 root disk 7, 128 2018-03-15 18:03:33 /dev/loop4
  brw-rw---- 1 root disk 7, 129 2018-03-15 18:03:33 /dev/loop4p1

We have additional device, and it's because the system sees the image as:

  # file new-boot.img
  new-boot.img: DOS/MBR boot sector; partition 1 :
    ID=0x83,
    start-CHS (0x0,32,33),
    end-CHS (0x20,194,34),
    startsector 2048,
    524288 sectors,
    extended partition table (last)

And that's why the /boot/ partition has now a separate loop4p1 device instead of taking the "main" loop4 device. If there were 4 partitions in the image, we would have loop4p1 , loop4p2 , loop4p3 and loop4p4 devices, each for one of the partitions in the image.

Now we have to mount the image. If everything is fine, the files should still be there:

  # mount -o ro /dev/loop4p1 /mnt
  # ls -al /mnt
  total 45M
  drwxr-xr-x  4 root root 4.0K 2018-03-14 19:15:28 ./
  drwxr-xr-x 25 root root 4.0K 2018-03-01 18:54:11 ../
  drwxr-xr-x  2 root root 4.0K 2018-02-26 18:00:35 extlinux/
  drwx------  2 root root  16K 2018-02-17 16:34:18 lost+found/
  -rw-r--r--  1 root root 3.0M 2018-02-18 09:36:49 System.map-4.15.0-1-amd64
  -rw-r--r--  1 root root 194K 2018-02-18 09:36:49 config-4.15.0-1-amd64
  -rw-r--r--  1 root root  37M 2018-03-14 19:15:26 initrd.img-4.15.0-1-amd64
  -rw-r--r--  1 root root 179K 2015-06-25 19:16:52 memtest86+.bin
  -rw-r--r--  1 root root 181K 2015-06-25 19:16:52 memtest86+_multiboot.bin
  -rw-r--r--  1 root root 4.7M 2018-02-18 09:36:49 vmlinuz-4.15.0-1-amd64

If there's no errors in the syslog, the operation was successful, but that's not the end. We have to install some bootloader, in this case it's syslinux/extlinux.

Install the bootloader in the /boot/ partition image

To install the syslinux/extlinux bootloader inside of the image, we have to do two things. First, we have to install bootloader's MBR code in the first sector of the image:

  # dd bs=440 count=1 conv=notrunc if=/usr/lib/syslinux/mbr/mbr.bin of=/dev/loop4

And second, we have to install extlinux's VBR (Volume Boot Record) in the /boot/ partition:

  # extlinux --install /mnt/extlinux
  /mnt/extlinux is device /dev/loop4p1
  Warning: unable to obtain device geometry (defaulting to 64 heads, 32 sectors)
           (on hard disks, this is usually harmless.)

The above warning can be ignored.

Test the image

The above steps should make the image bootable. You can transfer the file to the phone's SD card or its internal memory and test it using the USB Mountr Android app. In my case, the image was able to start my laptop's system.

Now we can simply delete the /boot/ partition from the disk because we have its copy on the phone. We don't even have to change the /etc/fstab file because the UUID of the partition is the same. But if we wish to keep back the old /boot/ partition, we have to make sure that we change the UUID of the file system inside of the created image:

  # umount /mnt
  # uuidgen
  6f3b0020-0491-4a12-98ca-c97a7a80f5b7
  # tune2fs /dev/loop4p1 -U 6f3b0020-0491-4a12-98ca-c97a7a80f5b7
  tune2fs 1.44.0 (7-Mar-2018)
  Setting UUID on a checksummed filesystem could take some time.
  Proceed anyway (or wait 5 seconds to proceed) ? (y,N) <proceeding>
  # lsblk /dev/loop4
  NAME       SIZE FSTYPE TYPE LABEL MOUNTPOINT UUID
  loop4      257M        loop
  └─loop4p1  256M ext4   loop boot             6f3b0020-0491-4a12-98ca-c97a7a80f5b7

And now we have to change this UUID in the /etc/fstab file:

  # <file system>					<mount point>	<type>	<options>			<dump>  <pass>
  ....
  UUID=6f3b0020-0491-4a12-98ca-c97a7a80f5b7	/boot           ext4    defaults,errors=remount-ro,noauto,nofail,commit=10 0 2

One note about the noauto and nofail mount flags. From man mount(8):

nofail -- Do not report errors for this device if it does not exist.

noauto -- Can only be mounted explicitly (i.e., the -a option will not cause the filesystem to be mounted).

The noauto option is useful because we don't really want the /boot/ partition to be mounted all the time (it could be dangerous for its file system). Basically we need the partition only when we want to upgrade our system, or when we want to change something in the initramfs/initrd image or in the bootloader's configuration. Except the aforementioned things, the /boot/ partition doesn't really have to be present in the system, which gives us the ability to simply plug/unplug our phone whenever we want to. But when we unplug the device, the system will have some problems with the event and print the following log:

  systemd[1]: dev-disk-by\x2duuid-6f3b0020\x2d0491\x2d4a12\x2d98ca\x2dc97a7a80f5b7.device: Job dev-disk-by\x2duuid-6f3b0020\x2d0491\x2d4a12\x2d98ca\x2dc97a7a80f5b7.device/start timed out.
  systemd[1]: Timed out waiting for device dev-disk-by\x2duuid-6f3b0020\x2d0491\x2d4a12\x2d98ca\x2dc97a7a80f5b7.device.
  systemd[1]: Dependency failed for /boot.
  systemd[1]: boot.mount: Job boot.mount/start failed with result 'dependency'.
  systemd[1]: Dependency failed for File System Check on /dev/disk/by-uuid/6f3b0020-0491-4a12-98ca-c97a7a80f5b7.
  systemd[1]: systemd-fsck@dev-disk-by\x2duuid-6f3b0020\x2d0491\x2d4a12\x2d98ca\x2dc97a7a80f5b7.service: Job systemd-fsck@dev-disk-by\x2duuid-6f3b0020\x2d0491\x2d4a12\x2d98ca\x2dc97a7a80f5b7.service/start failed with result 'dependency'.
  systemd[1]: dev-disk-by\x2duuid-6f3b0020\x2d0491\x2d4a12\x2d98ca\x2dc97a7a80f5b7.device: Job dev-disk-by\x2duuid-6f3b0020\x2d0491\x2d4a12\x2d98ca\x2dc97a7a80f5b7.device/start failed with result 'timeout'.

The message above are pretty much harmless, but since some services (like mount and fsck) depend on the boot device (which won't be accessible after unplugging the phone), the error will be printed in the syslog many times. We can simply order our system to ignore presence of the boot device by adding the other boot option, which is nofail.

LUKS headers

Now it's the time to take care of the LUKS header. We're going to copy the LUKS header to the boot image and remove the header from the system container leaving only the encrypted data on the disk. In this way if some person took the disk, he wouldn't be able to decrypt the device because there's no master key there. Without our phone no one even would tell whether the disk is encrypted or not. So if we want to include the LUKS header in the /boot/ partition image, we can do it in the following way.

Make a backup of the LUKS header:

  # mount /dev/loop4p1 /mnt
  # mkdir /mnt/headers
  # cryptsetup luksHeaderBackup /dev/sda2 --header-backup-file /mnt/headers/head.img
  # ls -al /mnt/headers/head.img
  -r-------- 1 root root 4.0M 2018-03-15 18:58:39 /mnt/headers/head.img

As we can see, the size of the LUKS header is 4 MiB, so now we have to overwrite 4 MiB starting from the beginning of the encrypted partition:

  # dd if=/dev/urandom of=/dev/sda2 bs=1M count=4

The next thing is to update the /etc/crypttab file:

  #sda2_crypt	UUID=e017ac1c-c46f-4b3f-a319-e1f5ed15144a  none  luks
  sda2_crypt	UUID=e017ac1c-c46f-4b3f-a319-e1f5ed15144a  none  luks,header=/boot/headers/head.img

Regenerate the initramfs/initrd image

The last step is to regenerate the initramfs/initrd image, but in order to do it we have to umount the old /boot/ partition first and mount the new /boot/ partition in the place of the old one.

  # umount /boot
  # mount /boot
  # lsblk | grep boot
  NAME                              SIZE FSTYPE      TYPE  LABEL          MOUNTPOINT     UUID
  ├─sda1                              2G ext4        part  boot                          37a2b033-c47c-4c41-9287-b2f2a44e1bcd
  └─sdc1                            256M ext4        part  boot           /boot          6f3b0020-0491-4a12-98ca-c97a7a80f5b7

Now we have to add two scripts to the initramfs/initrd image. One of the scripts will mount the /boot/ partition in the initramfs/initrd phase, and the other is going to unmount/move the partition before the root file system is mounted.

Here's the /etc/initramfs-tools/scripts/local-top/mount-boot script:

  #!/bin/sh
  PREREQ=""
  prereqs()
  {
     echo "$PREREQ"
  }

  case $1 in
  prereqs)
     prereqs
     exit 0
     ;;
  esac

  . /scripts/functions

  export PATH=/sbin:/usr/sbin:/bin:/usr/bin

  [ -d /boot ] || mkdir -m 0755 /boot

  DEVICE=/dev/disk/by-uuid/6f3b0020-0491-4a12-98ca-c97a7a80f5b7
  if [ ! -b "$DEVICE" ]; then
      echo "Waiting for device..." >&2
      until [ -b "$DEVICE" ]; do
          sleep 1
      done
  fi

  mount -t ext4 -o ro "$DEVICE" /boot

  exit 0

And here's the /etc/initramfs-tools/scripts/local-bottom/umount-boot script:

  #!/bin/sh
  PREREQ=""
  prereqs()
  {
     echo "$PREREQ"
  }

  case $1 in
  prereqs)
     prereqs
     exit 0
     ;;
  esac

  . /scripts/functions

  export PATH=/sbin:/usr/sbin:/bin:/usr/bin

  #umount /boot
  mount -o move /boot ${rootmnt}/boot

  exit 0

The two files have to be executable:

  # chmod +x /etc/initramfs-tools/scripts/local-top/mount-boot
  # chmod +x /etc/initramfs-tools/scripts/local-bottom/umount-boot

Now we have to regenerate the initramfs/initrd image, so type the following command:

  # update-initramfs -u -k all
  update-initramfs: Generating /boot/initrd.img-4.15.0-1-amd64

And that's pretty much it. You can test whether your system boots from your phone directly -- mine does.

Also don't forget that whenever our system needs some upgrade, the phone has to be connected to a USB port and the /boot/ partition has to be mounted during the installation process.

How to upgrade our system

Usually, the /boot/ partition is mounted in the read-write mode, which permits us to make changes to the file system. But in this setup where we have "detached" /boot/ partition, we shouldn't mount it with write permissions by default (in our case, we don't mount it at all). It's simply because we can damage the underlying file system (by forgetting to unmount it before unplugging the phone), and hence we can loose the LUKS headers, which will make our system unbootable if we don't have a backup of the headers.

But what about the system upgrades? We don't really have to manually handle ro->rw->ro mounts of the /boot/ partition. In Debian we can use the /etc/apt/apt.conf file and add the following lines to it:

  DPkg
  {
    Pre-Invoke
    {
      "if grep -q boot /proc/mounts; then mount -o remount,rw /boot; else mount -o rw /boot; fi";
    };

    Post-Invoke
    {
      "test ${NO_APT_REMOUNT:-no} = yes || umount /boot || true";
    };
  };

So, each time we want to upgrade our system (install some packages), apt will mount the /boot/ partition with the write permissions, and after the upgrade is done, apt will unmount it.

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