kcrypt/main.go
Dimitris Karakasilis 018322ff00
Move from partition_info to config package
Now the kcrypt configuration will be just a block (`kcrypt`) in the regular
kairos configuration files.

We will use the config mechanism of looking up in multiple directories
becase the old code was only looking at `/oem` but at boot time
(initramfs) the configuration was present in `/sysroot/oem`.

The reason it "worked" was that we had a bug and a sealedvolume was
matching our request simply because we sent an empty UUID/Label/Name
and the sealedvolume had and empty UUID (empty matches empty, right?)

With that fixed on the challenger server side, it became obvious that we
never actually read the partition_info file because we were looking for
it at the wrong place.

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2023-01-18 14:55:25 +02:00

381 lines
9.2 KiB
Go

package main
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
luks "github.com/anatol/luks.go"
multierror "github.com/hashicorp/go-multierror"
"github.com/jaypipes/ghw"
"github.com/jaypipes/ghw/pkg/block"
"github.com/kairos-io/kcrypt/pkg/bus"
configpkg "github.com/kairos-io/kcrypt/pkg/config"
"github.com/mudler/go-pluggable"
cp "github.com/otiai10/copy"
"github.com/urfave/cli"
)
var Version = "v0.0.0-dev"
func waitdevice(device string, attempts int) error {
for tries := 0; tries < attempts; tries++ {
sh("udevadm settle")
_, err := os.Lstat(device)
if !os.IsNotExist(err) {
return nil
}
time.Sleep(1 * time.Second)
}
return fmt.Errorf("no device found")
}
// TODO: Ask to discovery a pass to unlock. keep waiting until we get it and a timeout is exhausted with retrials (exp backoff)
func getPassword(b *block.Partition) (password string, err error) {
bus.Reload()
bus.Manager.Response(bus.EventDiscoveryPassword, func(p *pluggable.Plugin, r *pluggable.EventResponse) {
password = r.Data
if r.Errored() {
err = fmt.Errorf("failed discovery: %s", r.Error)
}
})
bus.Manager.Publish(bus.EventDiscoveryPassword, b)
if password == "" {
return password, fmt.Errorf("received empty password")
}
return
}
func luksUnlock(device, mapper, password string) error {
dev, err := luks.Open(device)
if err != nil {
// handle error
return err
}
defer dev.Close()
err = dev.Unlock(0, []byte(password), mapper)
if err != nil {
return err
}
return nil
}
func unlockDisk(b *block.Partition) error {
pass, err := getPassword(b)
if err != nil {
return fmt.Errorf("error retreiving password remotely: %w", err)
}
return luksUnlock(fmt.Sprintf("/dev/%s", b.Name), b.Name, pass)
}
func createLuks(dev, password, version string, cryptsetupArgs ...string) error {
if version == "" {
version = "luks2"
}
args := []string{"luksFormat", "--type", version, "--iter-time", "5", "-q", dev}
args = append(args, cryptsetupArgs...)
cmd := exec.Command("cryptsetup", args...)
cmd.Stdin = strings.NewReader(password)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return err
}
return nil
}
func createDiskImage() (*os.File, error) {
disk, err := ioutil.TempFile("", "luksv2.go.disk")
if err != nil {
return nil, err
}
if err := disk.Truncate(24 * 1024 * 1024); err != nil {
return nil, err
}
return disk, err
}
// TODO: A crypt disk utility to call after install, that with discovery discoveries the password that should be used
// this function should delete COS_PERSISTENT. delete the partition and create a luks+type in place.
// Take a part label, and recreates it with LUKS. IT OVERWRITES DATA!
// On success, it returns a machine parseable string with the partition information
// (label:name:uuid) so that it can be stored by the caller for later use.
// This is because the label of the encrypted partition is not accessible unless
// the partition is decrypted first and the uuid changed after encryption so
// any stored information needs to be updated (by the caller).
func luksify(label string) (string, error) {
// blkid
persistent, b, err := findPartition(label)
if err != nil {
return "", err
}
pass, err := getPassword(b)
if err != nil {
return "", err
}
persistent = fmt.Sprintf("/dev/%s", persistent)
devMapper := fmt.Sprintf("/dev/mapper/%s", b.Name)
if err := createLuks(persistent, pass, "luks1"); err != nil {
return "", err
}
if err := luksUnlock(persistent, b.Name, pass); err != nil {
return "", err
}
if err := waitdevice(devMapper, 10); err != nil {
return "", err
}
out, err := sh(fmt.Sprintf("mkfs.ext4 -L %s %s", label, devMapper))
if err != nil {
return "", fmt.Errorf("err: %w, out: %s", err, out)
}
out2, err := sh(fmt.Sprintf("cryptsetup close %s", b.Name))
if err != nil {
return "", fmt.Errorf("err: %w, out: %s", err, out2)
}
return configpkg.PartitionToString(b), nil
}
func findPartition(label string) (string, *block.Partition, error) {
block, err := ghw.Block()
if err == nil {
for _, disk := range block.Disks {
for _, p := range disk.Partitions {
if p.Label == label {
return p.Name, p, nil
}
}
}
} else {
return "", nil, err
}
return "", nil, fmt.Errorf("not found")
}
func sh(c string) (string, error) {
o, err := exec.Command("/bin/sh", "-c", c).CombinedOutput()
return string(o), err
}
const (
GZType = "gz"
XZType = "xz"
LZMAType = "lzma"
)
// TODO: replace with golang native code
func detect(archive string) (string, error) {
out, err := sh(fmt.Sprintf("file %s", archive))
if err != nil {
return "", err
}
out = strings.ToLower(out)
if strings.Contains(out, "xz") {
return XZType, nil
} else if strings.Contains(out, "lzma") {
return LZMAType, nil
} else if strings.Contains(out, "gz") {
return GZType, nil
}
return "", fmt.Errorf("Unknown")
}
// TODO: replace with golang native code
func extractInitrd(initrd string, dst string) error {
os.MkdirAll(dst, os.ModePerm)
var out string
var err error
format, err := detect(initrd)
if err != nil {
return err
}
if format == XZType || format == LZMAType {
out, err = sh(fmt.Sprintf("cd %s && xz -dc < %s | cpio -idmv", dst, initrd))
} else if format == GZType {
out, err = sh(fmt.Sprintf("cd %s && zcat %s | cpio -idmv", dst, initrd))
}
fmt.Println(out)
return err
}
func createInitrd(initrd string, src string, format string) error {
fmt.Printf("Creating '%s' from '%s' as '%s'\n", initrd, src, format)
if _, err := os.Stat(src); err != nil {
return err
}
var err error
var out string
if format == XZType {
out, err = sh(fmt.Sprintf("cd %s && find . 2>/dev/null | cpio -H newc --quiet --null -o -R root:root | xz -0 --check=crc32 > %s", src, initrd))
} else if format == GZType {
out, err = sh(fmt.Sprintf("cd %s && find . | cpio -H newc -o -R root:root | gzip -9 > %s", src, initrd))
} else if format == LZMAType {
out, err = sh(fmt.Sprintf("cd %s && find . 2>/dev/null | cpio -H newc -o -R root:root | xz -9 --format=lzma > %s", src, initrd))
}
fmt.Println(out)
return err
}
// TODO: A inject initramfs command to add the discovery e.g. to use inside Dockerfiles
func injectInitrd(initrd string, file, dst string) error {
fmt.Printf("Injecting '%s' as '%s' into '%s'\n", file, dst, initrd)
format, err := detect(initrd)
if err != nil {
return err
}
tmp, err := ioutil.TempDir("", "kcrypt")
if err != nil {
return fmt.Errorf("cannot create tempdir, %s", err)
}
defer os.RemoveAll(tmp)
fmt.Printf("Extracting '%s' in '%s' ...\n", initrd, tmp)
if err := extractInitrd(initrd, tmp); err != nil {
return fmt.Errorf("cannot extract initrd, %s", err)
}
d := filepath.Join(tmp, dst)
fmt.Printf("Copying '%s' in '%s' ...\n", file, d)
if err := cp.Copy(file, d); err != nil {
return fmt.Errorf("cannot copy file, %s", err)
}
return createInitrd(initrd, tmp, format)
}
// TODO: a custom toolkit version, to build out initrd pre-built with this component
func unlockAll() error {
bus.Manager.Initialize()
config, err := configpkg.GetConfiguration(configpkg.ConfigScanDirs)
if err != nil {
fmt.Printf("Warning: Could not read kcrypt configuration '%s'\n", err.Error())
}
block, err := ghw.Block()
if err != nil {
fmt.Printf("Warning: Error reading partitions '%s \n", err.Error())
return nil
}
for _, disk := range block.Disks {
for _, p := range disk.Partitions {
if p.Type == "crypto_LUKS" {
p.Label = config.LookupLabelForUUID(p.UUID)
fmt.Printf("Unmounted Luks found at '%s' LABEL '%s' \n", p.Name, p.Label)
err = multierror.Append(err, unlockDisk(p))
if err != nil {
fmt.Printf("Unlocking failed: '%s'\n", err.Error())
}
time.Sleep(10 * time.Second)
}
}
}
return nil
}
func main() {
app := &cli.App{
Name: "kairos-kcrypt",
Version: Version,
Author: "Ettore Di Giacinto",
Usage: "kairos escrow key agent component",
Description: ``,
UsageText: ``,
Copyright: "Ettore Di Giacinto",
Commands: []cli.Command{
{
Name: "extract-initrd",
Action: func(c *cli.Context) error {
if c.NArg() != 2 {
return fmt.Errorf("requires 3 args. initrd,, dst")
}
return extractInitrd(c.Args()[0], c.Args()[1])
},
},
{
Name: "encrypt",
Description: "Encrypts a partition",
Action: func(c *cli.Context) error {
if c.NArg() != 1 {
return fmt.Errorf("requires 1 arg, the partition label")
}
out, err := luksify(c.Args().First())
if err != nil {
return err
}
fmt.Println(out)
return nil
},
},
{
Name: "inject-initrd",
Action: func(c *cli.Context) error {
if c.NArg() != 3 {
return fmt.Errorf("requires 3 args. initrd, srcfile, dst")
}
return injectInitrd(c.Args()[0], c.Args()[1], c.Args()[2])
},
},
{
Name: "unlock-all",
UsageText: "unlock-all",
Usage: "Try to unlock all LUKS partitions",
Description: `
Typically run during initrd to unlock all the LUKS partitions found
`,
ArgsUsage: "kcrypt unlock-all",
Flags: []cli.Flag{
&cli.StringFlag{},
},
Action: func(c *cli.Context) error {
return unlockAll()
},
},
},
}
if err := app.Run(os.Args); err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
}