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
}
}