UC20 FDE boot flow

I had to do exactly this to debug a device that failed to boot but I still had the recovery key as a result of previously running snap recovery --show-keys.

The code below will unlock and mount encrypted Ubuntu Core partitions. ParseRecoveryKeys came from here - https://github.com/snapcore/secboot/blob/master/crypt.go which is responsible for deriving the LUKS encryption key that gets used by cryptsetup to unlock partitions.

package main

import (
	"bufio"
	"bytes"
	"encoding/binary"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"strconv"

	"golang.org/x/xerrors"
)

// Parse[16]byte interprets the supplied string and returns the corresponding [16]byte. The recovery key is a
// 16-byte number, and the formatted version of this is represented as 8 5-digit zero-extended base-10 numbers (each
// with a range of 00000-65535) which may be separated by an optional '-', eg:
//
// "61665-00531-54469-09783-47273-19035-40077-28287"
//
// The formatted version of the recovery key is designed to be able to be inputted on a numeric keypad.
func ParseRecoveryKey(s string) (out [16]byte, err error) {
	for i := 0; i < 8; i++ {
		if len(s) < 5 {
			return [16]byte{}, errors.New("incorrectly formatted: insufficient characters")
		}
		x, err := strconv.ParseUint(s[0:5], 10, 16) // Base 10 16 bit int
		if err != nil {
			return [16]byte{}, xerrors.Errorf("incorrectly formatted: %w", err)
		}
		binary.LittleEndian.PutUint16(out[i*2:], uint16(x))

		// Move to the next 5 digits
		s = s[5:]
		// Permit each set of 5 digits to be separated by an optional '-', but don't allow the formatted key to end or begin with one.
		if len(s) > 1 && s[0] == '-' {
			s = s[1:]
		}
	}

	if len(s) > 0 {
		return [16]byte{}, errors.New("incorrectly formatted: too many characters")
	}

	return
}

func mount(key []byte, path string, name string) (string, error) {
	cmd := exec.Command("sudo", "cryptsetup", "luksOpen", path, name)
	cmd.Env = os.Environ()
	cmd.Env = append(cmd.Env, "SYSTEMD_LOG_TARGET=console")
	cmd.Stdin = bytes.NewReader(key)

	output, err := cmd.CombinedOutput()
	return string(output), err
}

func unmount(key []byte, path string, name string) (string, error) {
	exec.Command("sudo", "unmount", path)
	cmd := exec.Command("sudo", "cryptsetup", "luksClose", name)
	cmd.Env = os.Environ()
	cmd.Env = append(cmd.Env, "SYSTEMD_LOG_TARGET=console")
	cmd.Stdin = bytes.NewReader(key)

	output, err := cmd.CombinedOutput()
	return string(output), err
}

func main() {
	var devicePath string
	var deviceName string
	var recoveryKey string

	fmt.Print("Enter device path (i.e. /dev/sdc4): ")
	fmt.Scanln(&devicePath)

	fmt.Print("Enter device name (choose a unique name to map this volume): ")
	fmt.Scanln(&deviceName)

	fmt.Print("Enter recovery key: ")
	fmt.Scanln(&recoveryKey)

	key, err := ParseRecoveryKey(recoveryKey)
	if err != nil {
		fmt.Println(err)
		return
	}

	errorMessage, mountError := mount(key[:], devicePath, deviceName)
	if mountError != nil { // Failed to mount the volume
		fmt.Println(errorMessage)
		return
	}

	fmt.Print("Press 'Enter' to unmount the volume...")
	bufio.NewReader(os.Stdin).ReadBytes('\n')

	errorMessage, unMountError := unmount(key[:], devicePath, deviceName)
	if unMountError != nil { // Failed to unmount the volume
		fmt.Println(errorMessage)
		return
	}
}