cryptsetup: enable detached header (#3927)

* deps: update go-cryptsetup
* cryptsetup: use detached headers when opening existing crypt devices
* cryptsetup: only activate disks with detached header

---------

Signed-off-by: Daniel Weiße <dw@edgeless.systems>
This commit is contained in:
Daniel Weiße 2025-08-28 10:34:24 +02:00 committed by GitHub
parent 23fa3bb36e
commit bb8d2c8a5c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 685 additions and 103 deletions

View file

@ -14,6 +14,7 @@ go_test(
"@e2fsprogs//:bin/mkfs.ext4", "@e2fsprogs//:bin/mkfs.ext4",
"@util-linux//:bin/blkid", "@util-linux//:bin/blkid",
"@util-linux//:bin/fsck", "@util-linux//:bin/fsck",
"@util-linux//:bin/losetup",
"@util-linux//:bin/mount", "@util-linux//:bin/mount",
"@util-linux//:bin/umount", "@util-linux//:bin/umount",
], ],
@ -23,6 +24,7 @@ go_test(
"DD": "$(rlocationpath @coreutils//:bin/dd)", "DD": "$(rlocationpath @coreutils//:bin/dd)",
"FSCK": "$(rlocationpath @util-linux//:bin/fsck)", "FSCK": "$(rlocationpath @util-linux//:bin/fsck)",
"FSCK_EXT4": "$(rlocationpath @e2fsprogs//:bin/fsck.ext4)", "FSCK_EXT4": "$(rlocationpath @e2fsprogs//:bin/fsck.ext4)",
"LOSETUP": "$(rlocationpath @util-linux//:bin/losetup)",
"MKFS_EXT4": "$(rlocationpath @e2fsprogs//:bin/mkfs.ext4)", "MKFS_EXT4": "$(rlocationpath @e2fsprogs//:bin/mkfs.ext4)",
"MOUNT": "$(rlocationpath @util-linux//:bin/mount)", "MOUNT": "$(rlocationpath @util-linux//:bin/mount)",
"RM": "$(rlocationpath @coreutils//:bin/rm)", "RM": "$(rlocationpath @coreutils//:bin/rm)",

View file

@ -27,11 +27,11 @@ import (
) )
const ( const (
devicePath string = "testDevice" defaultBackingImage = "testDevice"
deviceName string = "testDeviceName" deviceName = "testDeviceName"
) )
var toolsEnvs = []string{"CP", "DD", "RM", "FSCK_EXT4", "MKFS_EXT4", "BLKID", "FSCK", "MOUNT", "UMOUNT"} var toolsEnvs = []string{"CP", "DD", "RM", "FSCK_EXT4", "MKFS_EXT4", "BLKID", "FSCK", "MOUNT", "UMOUNT", "LOSETUP"}
// addToolsToPATH is used to update the PATH to contain necessary tool binaries for // addToolsToPATH is used to update the PATH to contain necessary tool binaries for
// coreutils, util-linux and ext4. // coreutils, util-linux and ext4.
@ -57,20 +57,35 @@ func addToolsToPATH() error {
return nil return nil
} }
func setup(devicePath string) { func setup(backingDisk string) string {
if err := exec.Command("dd", "if=/dev/zero", fmt.Sprintf("of=%s", devicePath), "bs=64M", "count=1").Run(); err != nil { if err := exec.Command("dd", "if=/dev/zero", fmt.Sprintf("of=%s", backingDisk), "bs=64M", "count=1").Run(); err != nil {
panic(err)
}
out, err := exec.Command("losetup", "-f", "--show", backingDisk).CombinedOutput()
if err != nil {
panic(err)
}
return strings.TrimSpace(string(out))
}
func teardown(backingImage, devicePath string) {
if err := exec.Command("losetup", "-d", devicePath).Run(); err != nil {
panic(err)
}
if err := exec.Command("rm", "-f", backingImage).Run(); err != nil {
panic(err) panic(err)
} }
} }
func teardown(devicePath string) { func cp(source, target string) (string, error) {
if err := exec.Command("rm", "-f", devicePath).Run(); err != nil { if err := exec.Command("cp", source, target).Run(); err != nil {
panic(err) return "", err
} }
} out, err := exec.Command("losetup", "-f", "--show", target).CombinedOutput()
if err != nil {
func cp(source, target string) error { return "", err
return exec.Command("cp", source, target).Run() }
return strings.TrimSpace(string(out)), nil
} }
func resize(devicePath string) { func resize(devicePath string) {
@ -100,8 +115,8 @@ func TestMain(m *testing.M) {
func TestOpenAndClose(t *testing.T) { func TestOpenAndClose(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
require := require.New(t) require := require.New(t)
setup(devicePath) devicePath := setup(defaultBackingImage)
defer teardown(devicePath) defer teardown(defaultBackingImage, devicePath)
mapper := cryptmapper.New(&fakeKMS{}) mapper := cryptmapper.New(&fakeKMS{})
@ -124,7 +139,7 @@ func TestOpenAndClose(t *testing.T) {
assert.Equal(newPath, newPath2) assert.Equal(newPath, newPath2)
// Resize the device // Resize the device
resize(devicePath) resize(defaultBackingImage)
resizedPath, err := mapper.ResizeCryptDevice(t.Context(), deviceName) resizedPath, err := mapper.ResizeCryptDevice(t.Context(), deviceName)
require.NoError(err) require.NoError(err)
@ -145,8 +160,8 @@ func TestOpenAndClose(t *testing.T) {
func TestOpenAndCloseIntegrity(t *testing.T) { func TestOpenAndCloseIntegrity(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
require := require.New(t) require := require.New(t)
setup(devicePath) devicePath := setup(defaultBackingImage)
defer teardown(devicePath) defer teardown(defaultBackingImage, devicePath)
mapper := cryptmapper.New(&fakeKMS{}) mapper := cryptmapper.New(&fakeKMS{})
@ -167,7 +182,7 @@ func TestOpenAndCloseIntegrity(t *testing.T) {
assert.Equal(newPath, newPath2) assert.Equal(newPath, newPath2)
// integrity devices do not support resizing // integrity devices do not support resizing
resize(devicePath) resize(defaultBackingImage)
_, err = mapper.ResizeCryptDevice(t.Context(), deviceName) _, err = mapper.ResizeCryptDevice(t.Context(), deviceName)
assert.Error(err) assert.Error(err)
@ -182,25 +197,26 @@ func TestOpenAndCloseIntegrity(t *testing.T) {
// check if we can reopen the device // check if we can reopen the device
_, err = mapper.OpenCryptDevice(t.Context(), devicePath, deviceName, true) _, err = mapper.OpenCryptDevice(t.Context(), devicePath, deviceName, true)
assert.NoError(err) assert.NoError(err, "Failed to re-open crypt device")
assert.NoError(mapper.CloseCryptDevice(deviceName)) assert.NoError(mapper.CloseCryptDevice(deviceName))
} }
func TestDeviceCloning(t *testing.T) { func TestDeviceCloning(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
require := require.New(t) require := require.New(t)
setup(devicePath) devicePath := setup(defaultBackingImage)
defer teardown(devicePath) defer teardown(defaultBackingImage, devicePath)
mapper := cryptmapper.New(&dynamicKMS{}) mapper := cryptmapper.New(&dynamicKMS{})
_, err := mapper.OpenCryptDevice(t.Context(), devicePath, deviceName, false) _, err := mapper.OpenCryptDevice(t.Context(), devicePath, deviceName, false)
assert.NoError(err) assert.NoError(err)
require.NoError(cp(devicePath, devicePath+"-copy")) cpDevice, err := cp(defaultBackingImage, defaultBackingImage+"-copy")
defer teardown(devicePath + "-copy") require.NoError(err)
defer teardown(defaultBackingImage+"-copy", cpDevice)
_, err = mapper.OpenCryptDevice(t.Context(), devicePath+"-copy", deviceName+"-copy", false) _, err = mapper.OpenCryptDevice(t.Context(), cpDevice, deviceName+"-copy", false)
assert.NoError(err) assert.NoError(err)
assert.NoError(mapper.CloseCryptDevice(deviceName)) assert.NoError(mapper.CloseCryptDevice(deviceName))
@ -209,12 +225,12 @@ func TestDeviceCloning(t *testing.T) {
func TestConcurrency(t *testing.T) { func TestConcurrency(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
setup(devicePath) devicePath := setup(defaultBackingImage)
defer teardown(devicePath) defer teardown(defaultBackingImage, devicePath)
device2 := devicePath + "-2" backingImage2 := defaultBackingImage + "-2"
setup(device2) device2 := setup(backingImage2)
defer teardown(device2) defer teardown(backingImage2, device2)
mapper := cryptmapper.New(&fakeKMS{}) mapper := cryptmapper.New(&fakeKMS{})

View file

@ -9,9 +9,11 @@ go_test(
data = [ data = [
"@coreutils//:bin/dd", "@coreutils//:bin/dd",
"@coreutils//:bin/rm", "@coreutils//:bin/rm",
"@util-linux//:bin/losetup",
], ],
env = { env = {
"DD": "$(rlocationpath @coreutils//:bin/dd)", "DD": "$(rlocationpath @coreutils//:bin/dd)",
"LOSETUP": "$(rlocationpath @util-linux//:bin/losetup)",
"RM": "$(rlocationpath @coreutils//:bin/rm)", "RM": "$(rlocationpath @coreutils//:bin/rm)",
}, },
# keep # keep

View file

@ -10,6 +10,7 @@ package integration
import ( import (
"encoding/json" "encoding/json"
"errors"
"flag" "flag"
"fmt" "fmt"
"log/slog" "log/slog"
@ -31,13 +32,15 @@ import (
) )
const ( const (
devicePath = "testDevice" backingDisk = "testDevice"
mappedDevice = "mappedDevice" mappedDevice = "mappedDevice"
) )
var devicePath string
var diskPath = flag.String("disk", "", "Path to the disk to use for the benchmark") var diskPath = flag.String("disk", "", "Path to the disk to use for the benchmark")
var toolsEnvs = []string{"DD", "RM"} var toolsEnvs = []string{"DD", "RM", "LOSETUP"}
// addToolsToPATH is used to update the PATH to contain necessary tool binaries for // addToolsToPATH is used to update the PATH to contain necessary tool binaries for
// coreutils. // coreutils.
@ -64,11 +67,22 @@ func addToolsToPATH() error {
} }
func setup(sizeGB int) error { func setup(sizeGB int) error {
return exec.Command("dd", "if=/dev/random", fmt.Sprintf("of=%s", devicePath), "bs=1G", fmt.Sprintf("count=%d", sizeGB)).Run() if err := exec.Command("dd", "if=/dev/random", fmt.Sprintf("of=%s", backingDisk), "bs=1G", fmt.Sprintf("count=%d", sizeGB)).Run(); err != nil {
return err
}
cmd := exec.Command("losetup", "-f", "--show", backingDisk)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("losetup failed: %w\nOutput: %s", err, out)
}
devicePath = strings.TrimSpace(string(out))
return nil
} }
func teardown() error { func teardown() error {
return exec.Command("rm", "-f", devicePath).Run() err := exec.Command("losetup", "-d", devicePath).Run()
errors.Join(err, exec.Command("rm", "-f", backingDisk).Run())
return err
} }
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
@ -137,6 +151,27 @@ func TestMapper(t *testing.T) {
// Disk should still be marked as not initialized because token is set to false. // Disk should still be marked as not initialized because token is set to false.
assert.False(mapper.IsInitialized()) assert.False(mapper.IsInitialized())
// Set disk as initialized
assert.NoError(ccrypt.SetConstellationStateDiskToken(ccryptsetup.SetDiskInitialized))
// Set up a new client and check if the client still sees the disk as initialized
ccrypt2 := ccryptsetup.New()
freeDevice2, err := ccrypt2.Init(devicePath)
require.NoError(err, "failed to initialize crypt device")
defer freeDevice2()
require.NoError(ccrypt2.LoadLUKS2(), "failed to load LUKS2")
tokenJSON, err = ccrypt2.TokenJSONGet(ccryptsetup.ConstellationStateDiskTokenID)
require.NoError(err, "token should have been set")
var token2 struct {
Type string `json:"type"`
Keyslots []string `json:"keyslots"`
DiskIsInitialized bool `json:"diskIsInitialized"`
}
require.NoError(json.Unmarshal([]byte(tokenJSON), &token2))
assert.True(token2.DiskIsInitialized, "disk should be marked as initialized")
assert.True(ccrypt2.ConstellationStateDiskTokenIsInitialized(), "disk should be marked as initialized")
// Try to map disk with incorrect passphrase // Try to map disk with incorrect passphrase
assert.Error(mapper.MapDisk(mappedDevice, "invalid-passphrase"), "was able to map disk with incorrect passphrase") assert.Error(mapper.MapDisk(mappedDevice, "invalid-passphrase"), "was able to map disk with incorrect passphrase")

3
go.mod
View file

@ -2,8 +2,7 @@ module github.com/edgelesssys/constellation/v2
go 1.24.6 go 1.24.6
// TODO(daniel-weisse): revert after merging https://github.com/martinjungblut/go-cryptsetup/pull/16. replace github.com/martinjungblut/go-cryptsetup => github.com/edgelesssys/go-cryptsetup v0.0.0-20250822075033-840d240dddf8
replace github.com/martinjungblut/go-cryptsetup => github.com/daniel-weisse/go-cryptsetup v0.0.0-20230705150314-d8c07bd1723c
// TODO(daniel-weisse): revert after merging https://github.com/google/go-sev-guest/pull/173. // TODO(daniel-weisse): revert after merging https://github.com/google/go-sev-guest/pull/173.
replace github.com/google/go-sev-guest => github.com/daniel-weisse/go-sev-guest v0.0.0-20250728114912-0c2ba277c52b replace github.com/google/go-sev-guest => github.com/daniel-weisse/go-sev-guest v0.0.0-20250728114912-0c2ba277c52b

4
go.sum
View file

@ -237,8 +237,6 @@ github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7 h
github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/daniel-weisse/go-cryptsetup v0.0.0-20230705150314-d8c07bd1723c h1:ToajP6trZoiqlZ3Z4uoG1P02/wtqSw1AcowOXOYjATk=
github.com/daniel-weisse/go-cryptsetup v0.0.0-20230705150314-d8c07bd1723c/go.mod h1:gZoZ0+POlM1ge/VUxWpMmZVNPzzMJ7l436CgkQ5+qzU=
github.com/daniel-weisse/go-sev-guest v0.0.0-20250728114912-0c2ba277c52b h1:pElX9BS0PnYZS/tznradDYbo82kvG2yisWGvZGsDnVs= github.com/daniel-weisse/go-sev-guest v0.0.0-20250728114912-0c2ba277c52b h1:pElX9BS0PnYZS/tznradDYbo82kvG2yisWGvZGsDnVs=
github.com/daniel-weisse/go-sev-guest v0.0.0-20250728114912-0c2ba277c52b/go.mod h1:SK9vW+uyfuzYdVN0m8BShL3OQCtXZe/JPF7ZkpD3760= github.com/daniel-weisse/go-sev-guest v0.0.0-20250728114912-0c2ba277c52b/go.mod h1:SK9vW+uyfuzYdVN0m8BShL3OQCtXZe/JPF7ZkpD3760=
github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8GQs= github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8GQs=
@ -271,6 +269,8 @@ github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
github.com/edgelesssys/go-azguestattestation v0.0.0-20250408071817-8c4457b235ff h1:V6A5kD0+c1Qg4X72Lg+zxhCZk+par436sQdgLvMCBBc= github.com/edgelesssys/go-azguestattestation v0.0.0-20250408071817-8c4457b235ff h1:V6A5kD0+c1Qg4X72Lg+zxhCZk+par436sQdgLvMCBBc=
github.com/edgelesssys/go-azguestattestation v0.0.0-20250408071817-8c4457b235ff/go.mod h1:Lz4QaomI4wU2YbatD4/W7vatW2Q35tnkoJezB1clscc= github.com/edgelesssys/go-azguestattestation v0.0.0-20250408071817-8c4457b235ff/go.mod h1:Lz4QaomI4wU2YbatD4/W7vatW2Q35tnkoJezB1clscc=
github.com/edgelesssys/go-cryptsetup v0.0.0-20250822075033-840d240dddf8 h1:aZsuG/e0UNZSttE63TplTeTpYjpl8A2GXbNQYMRUktw=
github.com/edgelesssys/go-cryptsetup v0.0.0-20250822075033-840d240dddf8/go.mod h1:gZoZ0+POlM1ge/VUxWpMmZVNPzzMJ7l436CgkQ5+qzU=
github.com/edgelesssys/go-tdx-qpl v0.0.0-20250129202750-607ac61e2377 h1:5JMJiBhvOUUR7EZ0UyeSy7a1WrqB2eM+DX3odLSHAh4= github.com/edgelesssys/go-tdx-qpl v0.0.0-20250129202750-607ac61e2377 h1:5JMJiBhvOUUR7EZ0UyeSy7a1WrqB2eM+DX3odLSHAh4=
github.com/edgelesssys/go-tdx-qpl v0.0.0-20250129202750-607ac61e2377/go.mod h1:IC72qyykUIWl0ZmSk53L4xbLCFDBEGZVaujUmPQOEyw= github.com/edgelesssys/go-tdx-qpl v0.0.0-20250129202750-607ac61e2377/go.mod h1:IC72qyykUIWl0ZmSk53L4xbLCFDBEGZVaujUmPQOEyw=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=

View file

@ -16,10 +16,14 @@ go_library(
visibility = ["//:__subpackages__"], visibility = ["//:__subpackages__"],
deps = select({ deps = select({
"@io_bazel_rules_go//go/platform:android": [ "@io_bazel_rules_go//go/platform:android": [
"@com_github_google_uuid//:uuid",
"@com_github_martinjungblut_go_cryptsetup//:go-cryptsetup", "@com_github_martinjungblut_go_cryptsetup//:go-cryptsetup",
"@org_golang_x_sys//unix",
], ],
"@io_bazel_rules_go//go/platform:linux": [ "@io_bazel_rules_go//go/platform:linux": [
"@com_github_google_uuid//:uuid",
"@com_github_martinjungblut_go_cryptsetup//:go-cryptsetup", "@com_github_martinjungblut_go_cryptsetup//:go-cryptsetup",
"@org_golang_x_sys//unix",
], ],
"//conditions:default": [], "//conditions:default": [],
}), }),

View file

@ -17,6 +17,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"reflect"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -50,9 +51,18 @@ var (
// CryptSetup manages encrypted devices. // CryptSetup manages encrypted devices.
type CryptSetup struct { type CryptSetup struct {
nameInit func(name string) (cryptDevice, error) nameInit func(name string) (deviceDetachedHeader, deviceAttachedHeader cryptDevice, headerDevice string, headerFile string, err error)
pathInit func(path string) (cryptDevice, error) pathInit func(path string) (deviceDetachedHeader, deviceAttachedHeader cryptDevice, headerDevice string, headerFile string, err error)
device cryptDevice // deviceWithDetachedHeader is the cryptsetup device with detached header we are working on.
deviceWithDetachedHeader cryptDevice
// deviceWithAttachedHeader is a cryptsetup device loaded without a separate, detached header.
// If this is not a fresh disk, this device is purely used to write back changes that affect the header to the original disk.
deviceWithAttachedHeader cryptDevice
// headerDevice is the name of the loopback device containing the detached cryptsetup header.
headerDevice string
// headerFile is the path to the file containing the detached cryptsetup header.
// The file is mounted as a loopback device on "headerDevice".
headerFile string
} }
// New creates a new CryptSetup. // New creates a new CryptSetup.
@ -68,14 +78,17 @@ func New() *CryptSetup {
func (c *CryptSetup) Init(devicePath string) (free func(), err error) { func (c *CryptSetup) Init(devicePath string) (free func(), err error) {
packageLock.Lock() packageLock.Lock()
defer packageLock.Unlock() defer packageLock.Unlock()
if c.device != nil { if c.hasDetachedHeaderDevice() || c.hasAttachedHeaderDevice() {
return nil, errDeviceAlreadyOpen return nil, errDeviceAlreadyOpen
} }
device, err := c.pathInit(devicePath) deviceDetachedHeader, deviceAttachedHeader, headerDevice, headerFile, err := c.pathInit(devicePath)
if err != nil { if err != nil {
return nil, fmt.Errorf("init cryptsetup by device path %q: %w", devicePath, err) return nil, fmt.Errorf("init cryptsetup by device path %q: %w", devicePath, err)
} }
c.device = device c.deviceWithDetachedHeader = deviceDetachedHeader
c.deviceWithAttachedHeader = deviceAttachedHeader
c.headerDevice = headerDevice
c.headerFile = headerFile
return c.Free, nil return c.Free, nil
} }
@ -83,14 +96,17 @@ func (c *CryptSetup) Init(devicePath string) (free func(), err error) {
func (c *CryptSetup) InitByName(name string) (free func(), err error) { func (c *CryptSetup) InitByName(name string) (free func(), err error) {
packageLock.Lock() packageLock.Lock()
defer packageLock.Unlock() defer packageLock.Unlock()
if c.device != nil { if c.hasDetachedHeaderDevice() || c.hasAttachedHeaderDevice() {
return nil, errDeviceAlreadyOpen return nil, errDeviceAlreadyOpen
} }
device, err := c.nameInit(name) deviceDetachedHeader, deviceAttachedHeader, headerDevice, headerFile, err := c.nameInit(name)
if err != nil { if err != nil {
return nil, fmt.Errorf("init cryptsetup by name %q: %w", name, err) return nil, fmt.Errorf("init cryptsetup by name %q: %w", name, err)
} }
c.device = device c.deviceWithDetachedHeader = deviceDetachedHeader
c.deviceWithAttachedHeader = deviceAttachedHeader
c.headerDevice = headerDevice
c.headerFile = headerFile
return c.Free, nil return c.Free, nil
} }
@ -98,20 +114,19 @@ func (c *CryptSetup) InitByName(name string) (free func(), err error) {
func (c *CryptSetup) Free() { func (c *CryptSetup) Free() {
packageLock.Lock() packageLock.Lock()
defer packageLock.Unlock() defer packageLock.Unlock()
if c.device != nil { c.free()
c.device.Free()
c.device = nil
}
} }
// ActivateByPassphrase actives a crypt device using a passphrase. // ActivateByPassphrase actives a crypt device using a passphrase.
func (c *CryptSetup) ActivateByPassphrase(deviceName string, keyslot int, passphrase string, flags int) error { func (c *CryptSetup) ActivateByPassphrase(deviceName string, keyslot int, passphrase string, flags int) error {
packageLock.Lock() packageLock.Lock()
defer packageLock.Unlock() defer packageLock.Unlock()
if c.device == nil { if !c.hasDetachedHeaderDevice() {
return errDeviceNotOpen if err := c.reload(); err != nil {
return fmt.Errorf("re-loading crypt device for activation: %w", err)
}
} }
if err := c.device.ActivateByPassphrase(deviceName, keyslot, passphrase, flags); err != nil { if err := c.deviceWithDetachedHeader.ActivateByPassphrase(deviceName, keyslot, passphrase, flags); err != nil {
return fmt.Errorf("activating crypt device %q using passphrase: %w", deviceName, err) return fmt.Errorf("activating crypt device %q using passphrase: %w", deviceName, err)
} }
return nil return nil
@ -122,10 +137,12 @@ func (c *CryptSetup) ActivateByPassphrase(deviceName string, keyslot int, passph
func (c *CryptSetup) ActivateByVolumeKey(deviceName, volumeKey string, volumeKeySize, flags int) error { func (c *CryptSetup) ActivateByVolumeKey(deviceName, volumeKey string, volumeKeySize, flags int) error {
packageLock.Lock() packageLock.Lock()
defer packageLock.Unlock() defer packageLock.Unlock()
if c.device == nil { if !c.hasDetachedHeaderDevice() {
return errDeviceNotOpen if err := c.reload(); err != nil {
return fmt.Errorf("re-loading crypt device for activation: %w", err)
}
} }
if err := c.device.ActivateByVolumeKey(deviceName, volumeKey, volumeKeySize, flags); err != nil { if err := c.deviceWithDetachedHeader.ActivateByVolumeKey(deviceName, volumeKey, volumeKeySize, flags); err != nil {
return fmt.Errorf("activating crypt device %q using volume key: %w", deviceName, err) return fmt.Errorf("activating crypt device %q using volume key: %w", deviceName, err)
} }
return nil return nil
@ -135,10 +152,10 @@ func (c *CryptSetup) ActivateByVolumeKey(deviceName, volumeKey string, volumeKey
func (c *CryptSetup) Deactivate(deviceName string) error { func (c *CryptSetup) Deactivate(deviceName string) error {
packageLock.Lock() packageLock.Lock()
defer packageLock.Unlock() defer packageLock.Unlock()
if c.device == nil { if !c.hasDetachedHeaderDevice() {
return errDeviceNotOpen return errDeviceNotOpen
} }
if err := c.device.Deactivate(deviceName); err != nil { if err := c.deviceWithDetachedHeader.Deactivate(deviceName); err != nil {
return fmt.Errorf("deactivating crypt device %q: %w", deviceName, err) return fmt.Errorf("deactivating crypt device %q: %w", deviceName, err)
} }
return nil return nil
@ -149,18 +166,42 @@ func (c *CryptSetup) Deactivate(deviceName string) error {
func (c *CryptSetup) Format(integrity bool) error { func (c *CryptSetup) Format(integrity bool) error {
packageLock.Lock() packageLock.Lock()
defer packageLock.Unlock() defer packageLock.Unlock()
if c.device == nil { var device cryptDevice
// If we are re-formatting an existing device, we start from scratch without a detached header
if c.hasAttachedHeaderDevice() {
if c.deviceWithDetachedHeader != nil {
c.deviceWithDetachedHeader.Free()
c.deviceWithDetachedHeader = nil
}
if c.headerDevice != "" {
_ = detachLoopbackDevice(c.headerDevice)
c.headerDevice = ""
}
c.headerFile = ""
device = c.deviceWithAttachedHeader
} else {
return errDeviceNotOpen return errDeviceNotOpen
} }
if err := format(c.device, integrity); err != nil {
return fmt.Errorf("formatting crypt device %q: %w", c.device.GetDeviceName(), err) if err := format(device, integrity); err != nil {
return fmt.Errorf("formatting crypt device %q: %w", device.GetDeviceName(), err)
}
if err := c.createHeaderBackup(); err != nil {
return err
} }
return nil return nil
} }
// GetDeviceName gets the path to the underlying device. // GetDeviceName gets the path to the underlying device.
func (c *CryptSetup) GetDeviceName() string { func (c *CryptSetup) GetDeviceName() string {
return c.device.GetDeviceName() packageLock.Lock()
defer packageLock.Unlock()
device, err := c.getActiveDevice()
if err != nil {
return ""
}
return device.GetDeviceName()
} }
// GetUUID gets the device's LUKS2 UUID. // GetUUID gets the device's LUKS2 UUID.
@ -168,12 +209,13 @@ func (c *CryptSetup) GetDeviceName() string {
func (c *CryptSetup) GetUUID() (string, error) { func (c *CryptSetup) GetUUID() (string, error) {
packageLock.Lock() packageLock.Lock()
defer packageLock.Unlock() defer packageLock.Unlock()
if c.device == nil { device, err := c.getActiveDevice()
return "", errDeviceNotOpen if err != nil {
return "", err
} }
uuid := c.device.GetUUID() uuid := device.GetUUID()
if uuid == "" { if uuid == "" {
return "", fmt.Errorf("unable to get UUID for device %q", c.device.GetDeviceName()) return "", fmt.Errorf("unable to get UUID for device %q", device.GetDeviceName())
} }
return strings.ToLower(uuid), nil return strings.ToLower(uuid), nil
} }
@ -183,11 +225,15 @@ func (c *CryptSetup) GetUUID() (string, error) {
func (c *CryptSetup) KeyslotAddByVolumeKey(keyslot int, volumeKey string, passphrase string) error { func (c *CryptSetup) KeyslotAddByVolumeKey(keyslot int, volumeKey string, passphrase string) error {
packageLock.Lock() packageLock.Lock()
defer packageLock.Unlock() defer packageLock.Unlock()
if c.device == nil { device, err := c.getActiveDevice()
return errDeviceNotOpen if err != nil {
return err
} }
if err := c.device.KeyslotAddByVolumeKey(keyslot, volumeKey, passphrase); err != nil { if err := device.KeyslotAddByVolumeKey(keyslot, volumeKey, passphrase); err != nil {
return fmt.Errorf("adding keyslot to device %q: %w", c.device.GetDeviceName(), err) return fmt.Errorf("adding keyslot to device %q: %w", device.GetDeviceName(), err)
}
if err := c.createHeaderBackup(); err != nil {
return err
} }
return nil return nil
} }
@ -196,21 +242,30 @@ func (c *CryptSetup) KeyslotAddByVolumeKey(keyslot int, volumeKey string, passph
func (c *CryptSetup) KeyslotChangeByPassphrase(currentKeyslot, newKeyslot int, currentPassphrase, newPassphrase string) error { func (c *CryptSetup) KeyslotChangeByPassphrase(currentKeyslot, newKeyslot int, currentPassphrase, newPassphrase string) error {
packageLock.Lock() packageLock.Lock()
defer packageLock.Unlock() defer packageLock.Unlock()
if c.device == nil { device, err := c.getActiveDevice()
return errDeviceNotOpen if err != nil {
return err
} }
if err := c.device.KeyslotChangeByPassphrase(currentKeyslot, newKeyslot, currentPassphrase, newPassphrase); err != nil { if err := device.KeyslotChangeByPassphrase(currentKeyslot, newKeyslot, currentPassphrase, newPassphrase); err != nil {
return fmt.Errorf("updating passphrase for device %q: %w", c.device.GetDeviceName(), err) return fmt.Errorf("updating passphrase for device %q: %w", device.GetDeviceName(), err)
}
if err := c.createHeaderBackup(); err != nil {
return err
} }
return nil return nil
} }
// LoadLUKS2 loads the device as LUKS2 crypt device. // LoadLUKS2 loads the device as LUKS2 crypt device.
func (c *CryptSetup) LoadLUKS2() error { func (c *CryptSetup) LoadLUKS2() error {
if err := loadLUKS2(c.device); err != nil { packageLock.Lock()
return fmt.Errorf("loading LUKS2 crypt device %q: %w", c.device.GetDeviceName(), err) defer packageLock.Unlock()
if c.hasDetachedHeaderDevice() {
if err := loadLUKS2(c.deviceWithDetachedHeader); err != nil {
return fmt.Errorf("loading LUKS2 crypt device %q: %w", c.deviceWithDetachedHeader.GetDeviceName(), err)
}
return nil
} }
return nil return errors.New("cannot load LUKS2 on device with attached header")
} }
// Resize resizes a device to the given size. // Resize resizes a device to the given size.
@ -219,11 +274,15 @@ func (c *CryptSetup) LoadLUKS2() error {
func (c *CryptSetup) Resize(name string, newSize uint64) error { func (c *CryptSetup) Resize(name string, newSize uint64) error {
packageLock.Lock() packageLock.Lock()
defer packageLock.Unlock() defer packageLock.Unlock()
if c.device == nil { device, err := c.getActiveDevice()
return errDeviceNotOpen if err != nil {
return err
} }
if err := c.device.Resize(name, newSize); err != nil { if err := device.Resize(name, newSize); err != nil {
return fmt.Errorf("resizing crypt device %q: %w", c.device.GetDeviceName(), err) return fmt.Errorf("resizing crypt device %q: %w", device.GetDeviceName(), err)
}
if err := c.createHeaderBackup(); err != nil {
return err
} }
return nil return nil
} }
@ -232,10 +291,11 @@ func (c *CryptSetup) Resize(name string, newSize uint64) error {
func (c *CryptSetup) TokenJSONGet(token int) (string, error) { func (c *CryptSetup) TokenJSONGet(token int) (string, error) {
packageLock.Lock() packageLock.Lock()
defer packageLock.Unlock() defer packageLock.Unlock()
if c.device == nil { device, err := c.getActiveDevice()
return "", errDeviceNotOpen if err != nil {
return "", err
} }
json, err := c.device.TokenJSONGet(token) json, err := device.TokenJSONGet(token)
if err != nil { if err != nil {
return "", fmt.Errorf("getting JSON data for token %d: %w", token, err) return "", fmt.Errorf("getting JSON data for token %d: %w", token, err)
} }
@ -252,18 +312,25 @@ func (c *CryptSetup) TokenJSONGet(token int) (string, error) {
func (c *CryptSetup) TokenJSONSet(token int, json string) (int, error) { func (c *CryptSetup) TokenJSONSet(token int, json string) (int, error) {
packageLock.Lock() packageLock.Lock()
defer packageLock.Unlock() defer packageLock.Unlock()
if c.device == nil { device, err := c.getActiveDevice()
return -1, errDeviceNotOpen if err != nil {
return -1, err
} }
tokenID, err := c.device.TokenJSONSet(token, json)
tokenID, err := device.TokenJSONSet(token, json)
if err != nil { if err != nil {
return -1, fmt.Errorf("setting JSON data for token %d: %w", token, err) return -1, fmt.Errorf("setting JSON data for token %d: %w", token, err)
} }
if err := c.createHeaderBackup(); err != nil {
return -1, err
}
return tokenID, nil return tokenID, nil
} }
// SetConstellationStateDiskToken sets the Constellation state disk token. // SetConstellationStateDiskToken sets the Constellation state disk token.
func (c *CryptSetup) SetConstellationStateDiskToken(diskIsInitialized bool) error { func (c *CryptSetup) SetConstellationStateDiskToken(diskIsInitialized bool) error {
packageLock.Lock()
defer packageLock.Unlock()
token := constellationLUKS2Token{ token := constellationLUKS2Token{
Type: "constellation-state-disk", Type: "constellation-state-disk",
Keyslots: []string{}, Keyslots: []string{},
@ -273,15 +340,31 @@ func (c *CryptSetup) SetConstellationStateDiskToken(diskIsInitialized bool) erro
if err != nil { if err != nil {
return fmt.Errorf("marshaling token: %w", err) return fmt.Errorf("marshaling token: %w", err)
} }
if _, err := c.device.TokenJSONSet(ConstellationStateDiskTokenID, string(json)); err != nil {
device, err := c.getActiveDevice()
if err != nil {
return err
}
if _, err := device.TokenJSONSet(ConstellationStateDiskTokenID, string(json)); err != nil {
return fmt.Errorf("setting token: %w", err) return fmt.Errorf("setting token: %w", err)
} }
if err := c.createHeaderBackup(); err != nil {
return err
}
return nil return nil
} }
// ConstellationStateDiskTokenIsInitialized returns true if the Constellation state disk token is set to initialized. // ConstellationStateDiskTokenIsInitialized returns true if the Constellation state disk token is set to initialized.
func (c *CryptSetup) ConstellationStateDiskTokenIsInitialized() bool { func (c *CryptSetup) ConstellationStateDiskTokenIsInitialized() bool {
stateDiskToken, err := c.device.TokenJSONGet(ConstellationStateDiskTokenID) packageLock.Lock()
defer packageLock.Unlock()
device, err := c.getActiveDevice()
if err != nil {
return false
}
stateDiskToken, err := device.TokenJSONGet(ConstellationStateDiskTokenID)
if err != nil { if err != nil {
return false return false
} }
@ -298,17 +381,18 @@ func (c *CryptSetup) Wipe(
) (err error) { ) (err error) {
packageLock.Lock() packageLock.Lock()
defer packageLock.Unlock() defer packageLock.Unlock()
if c.device == nil { device, err := c.getActiveDevice()
return errDeviceNotOpen if err != nil {
return err
} }
// Active temporary device to perform wipe on // Active temporary device to perform wipe on
tmpDevice := tmpDevicePrefix + name tmpDevice := tmpDevicePrefix + name
if err := c.device.ActivateByVolumeKey(tmpDevice, "", 0, wipeFlags); err != nil { if err := device.ActivateByVolumeKey(tmpDevice, "", 0, wipeFlags); err != nil {
return fmt.Errorf("trying to activate temporary dm-crypt volume: %w", err) return fmt.Errorf("trying to activate temporary dm-crypt volume: %w", err)
} }
defer func() { defer func() {
if deactivateErr := c.device.Deactivate(tmpDevice); deactivateErr != nil { if deactivateErr := device.Deactivate(tmpDevice); deactivateErr != nil {
err = errors.Join(err, fmt.Errorf("deactivating temporary device %q: %w", tmpDevice, deactivateErr)) err = errors.Join(err, fmt.Errorf("deactivating temporary device %q: %w", tmpDevice, deactivateErr))
} }
}() }()
@ -330,8 +414,89 @@ func (c *CryptSetup) Wipe(
return 0 return 0
} }
if err := c.device.Wipe(mappedDevicePath+tmpDevice, wipePattern, 0, 0, blockWipeSize, flags, progressCallback); err != nil { if err := device.Wipe(mappedDevicePath+tmpDevice, wipePattern, 0, 0, blockWipeSize, flags, progressCallback); err != nil {
return fmt.Errorf("wiping disk of device %q: %w", c.device.GetDeviceName(), err) return fmt.Errorf("wiping disk of device %q: %w", device.GetDeviceName(), err)
}
if err := c.createHeaderBackup(); err != nil {
return err
}
return nil
}
func (c *CryptSetup) free() {
if c.hasDetachedHeaderDevice() {
c.deviceWithDetachedHeader.Free()
c.deviceWithDetachedHeader = nil
}
if c.hasAttachedHeaderDevice() {
c.deviceWithAttachedHeader.Free()
c.deviceWithAttachedHeader = nil
}
if c.headerDevice != "" {
_ = detachLoopbackDevice(c.headerDevice)
}
if c.headerFile != "" {
c.headerFile = ""
}
}
func (c *CryptSetup) reload() error {
if !c.hasAttachedHeaderDevice() {
return errDeviceNotOpen
}
backingDevice := c.deviceWithAttachedHeader.GetDeviceName()
c.free()
var err error
c.deviceWithDetachedHeader, c.deviceWithAttachedHeader, c.headerDevice, c.headerFile, err = c.pathInit(backingDevice)
if err != nil {
return fmt.Errorf("re-loading crypt device: %w", err)
}
if !c.hasDetachedHeaderDevice() {
return errors.New("failed to reload device without detached header")
}
if err := loadLUKS2(c.deviceWithDetachedHeader); err != nil {
return err
}
return nil
}
// getActiveDevice returns a handle to the active cryptsetup device with detached header if set,
// or one with attached header otherwise.
func (c *CryptSetup) getActiveDevice() (cryptDevice, error) {
if c.hasDetachedHeaderDevice() {
return c.deviceWithDetachedHeader, nil
}
if c.hasAttachedHeaderDevice() {
return c.deviceWithAttachedHeader, nil
}
return nil, errDeviceNotOpen
}
// hasDetachedHeaderDevice checks if the value of the [CryptSetup.deviceWithDetachedHeader] interface is not nil.
func (c *CryptSetup) hasDetachedHeaderDevice() bool {
return c.deviceWithDetachedHeader != nil && !reflect.ValueOf(c.deviceWithDetachedHeader).IsNil()
}
// hasAttachedHeaderDevice checks if the value of the [CryptSetup.deviceWithAttachedHeader] interface is not nil.
func (c *CryptSetup) hasAttachedHeaderDevice() bool {
return c.deviceWithAttachedHeader != nil && !reflect.ValueOf(c.deviceWithAttachedHeader).IsNil()
}
// createHeaderBackup creates a backup of the detached header, and saves it back to the original device.
func (c *CryptSetup) createHeaderBackup() error {
if c.hasDetachedHeaderDevice() && c.headerFile != "" {
if err := headerBackup(c.deviceWithDetachedHeader, c.headerFile); err != nil {
return fmt.Errorf("creating header backup for device %q: %w", c.deviceWithDetachedHeader.GetDeviceName(), err)
}
}
if c.hasAttachedHeaderDevice() && c.headerFile != "" {
if err := headerRestore(c.deviceWithAttachedHeader, c.headerFile); err != nil {
return fmt.Errorf("restoring header for device %q (with attached header): %w", c.deviceWithDetachedHeader.GetDeviceName(), err)
}
} }
return nil return nil
} }

View file

@ -11,9 +11,16 @@ package cryptsetup
import "C" import "C"
import ( import (
"encoding/json"
"errors" "errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/google/uuid"
"github.com/martinjungblut/go-cryptsetup" "github.com/martinjungblut/go-cryptsetup"
"golang.org/x/sys/unix"
) )
const ( const (
@ -56,12 +63,103 @@ func format(device cryptDevice, integrity bool) error {
} }
} }
func initByDevicePath(devicePath string) (cryptDevice, error) { // headerRestore restores the header of the given device from the header in the given file.
return cryptsetup.Init(devicePath) // Reloading the device is required for the changes to be reflected in the active [cryptDevice] struct.
func headerRestore(device cryptDevice, headerFile string) error {
switch d := device.(type) {
case cgoRestorer:
return d.HeaderRestore(cryptsetup.LUKS2{}, headerFile)
default:
return errInvalidType
}
} }
func initByName(name string) (cryptDevice, error) { // headerBackup creates a backup of the cryptDevice's header to the given file.
return cryptsetup.InitByName(name) func headerBackup(device cryptDevice, headerFile string) error {
switch d := device.(type) {
case cgoBackuper:
if err := os.Remove(headerFile); err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("removing existing header file %q: %w", headerFile, err)
}
if err := d.HeaderBackup(cryptsetup.LUKS2{}, headerFile); err != nil {
return fmt.Errorf("creating header backup: %w", err)
}
return nil
default:
return errInvalidType
}
}
func initByDevicePath(devicePath string) (deviceDetachedHeader, deviceAttachedHeader cryptDevice, headerDevice string, headerFile string, err error) {
tmpDevice, err := cryptsetup.Init(devicePath)
if err != nil {
return nil, nil, "", "", fmt.Errorf("init device by path %s: %w", devicePath, err)
}
// If the device is not LUKS2 formatted, this is treated as a new device,
// meaning no header exists yet
if err := tmpDevice.Load(cryptsetup.LUKS2{}); err != nil {
return nil, tmpDevice, "", "", nil
}
defer tmpDevice.Free()
deviceAttachedHeader, err = cryptsetup.Init(devicePath)
if err != nil {
return nil, nil, "", "", fmt.Errorf("init device by path %s: %w", devicePath, err)
}
defer func() {
if err != nil && deviceAttachedHeader != nil {
deviceAttachedHeader.Free()
}
}()
headerDevice, headerFile, err = detachHeader(tmpDevice)
if err != nil {
return nil, nil, "", "", err
}
defer func() {
if err != nil {
_ = detachLoopbackDevice(headerDevice)
}
}()
cryptDevice, err := cryptsetup.InitDataDevice(headerDevice, devicePath)
return cryptDevice, deviceAttachedHeader, headerDevice, headerFile, err
}
func initByName(name string) (deviceDetachedHeader, deviceAttachedHeader cryptDevice, headerDevice string, headerFile string, err error) {
tmpDevice, err := cryptsetup.InitByName(name)
if err != nil {
return nil, nil, "", "", fmt.Errorf("init device by name %s: %w", name, err)
}
// If the device is not LUKS2 formatted, this is treated as a new device,
// meaning no header exists yet
if err := tmpDevice.Load(cryptsetup.LUKS2{}); err != nil {
return nil, tmpDevice, "", "", nil
}
defer tmpDevice.Free()
deviceAttachedHeader, err = cryptsetup.InitByName(name)
if err != nil {
return nil, nil, "", "", fmt.Errorf("init device by name %s: %w", name, err)
}
defer func() {
if err != nil && deviceAttachedHeader != nil {
deviceAttachedHeader.Free()
}
}()
headerDevice, headerFile, err = detachHeader(tmpDevice)
if err != nil {
return nil, nil, "", "", err
}
defer func() {
if err != nil {
_ = detachLoopbackDevice(headerDevice)
}
}()
cryptDevice, err := cryptsetup.InitByNameAndHeader(name, headerDevice)
return cryptDevice, deviceAttachedHeader, headerDevice, headerFile, err
} }
func loadLUKS2(device cryptDevice) error { func loadLUKS2(device cryptDevice) error {
@ -73,6 +171,248 @@ func loadLUKS2(device cryptDevice) error {
} }
} }
// detachHeader loads reads the header from the given cryptsetup device and returns a loopback device with just the header.
func detachHeader(device *cryptsetup.Device) (headerDevice, headerFile string, err error) {
headerFile = filepath.Join(os.TempDir(), fmt.Sprintf("luks-header-%s", uuid.New().String()))
if err = headerBackup(device, headerFile); err != nil {
return "", "", err
}
headerDevice, err = createLoopbackDevice(headerFile)
if err != nil {
return "", "", fmt.Errorf("create loopback device: %w", err)
}
defer func() {
if err != nil {
_ = detachLoopbackDevice(headerDevice)
}
}()
headerCryptDevice, err := cryptsetup.Init(headerDevice)
if err != nil {
return "", "", fmt.Errorf("init header device: %w", err)
}
defer headerCryptDevice.Free()
if err := headerCryptDevice.Load(cryptsetup.LUKS2{}); err != nil {
return "", "", fmt.Errorf("creating header backup: %w", err)
}
metadataJSON, err := headerCryptDevice.DumpJSON()
if err != nil {
return "", "", fmt.Errorf("dumping device metadata: %w", err)
}
var metadata cryptsetupMetadata
decoder := json.NewDecoder(strings.NewReader(metadataJSON))
decoder.DisallowUnknownFields() // Ensure no unknown fields are present in the JSON data
if err := decoder.Decode(&metadata); err != nil {
return "", "", fmt.Errorf("decoding LUKS header JSON from %s: %w", headerFile, err)
}
if err := verifyLUKS2Header(metadata); err != nil {
return "", "", fmt.Errorf("verifying LUKS2 header: %w", err)
}
return headerDevice, headerFile, nil
}
// verifyLUKS2Header verifies a LUKS2 header contains the expected configuration for Constellation.
func verifyLUKS2Header(metadata cryptsetupMetadata) error {
if len(metadata.KeySlots) == 0 {
return errors.New("no key slots found in LUKS2 header")
}
for slotName, slot := range metadata.KeySlots {
if slot.Type != "luks2" {
return fmt.Errorf("unsupported key slot type %q for slot %q", slot.Type, slotName)
}
if slot.KeySize != 64 && slot.KeySize != 96 { // 64 for encryption, 96 if integrity is added
return fmt.Errorf("unsupported key size %d for slot %q", slot.KeySize, slotName)
}
if slot.AntiForensicSplitter.Type != "luks1" {
return fmt.Errorf("unsupported anti-forensic splitter type %q for slot %q", slot.AntiForensicSplitter.Type, slotName)
}
if slot.AntiForensicSplitter.Stripes != 4000 {
return fmt.Errorf("unsupported anti-forensic splitter stripes %d for slot %q", slot.AntiForensicSplitter.Stripes, slotName)
}
if slot.AntiForensicSplitter.Hash != "sha256" {
return fmt.Errorf("unsupported anti-forensic splitter hash %q for slot %q", slot.AntiForensicSplitter.Hash, slotName)
}
if slot.Area.Type != "raw" {
return fmt.Errorf("unsupported area type %q for slot %q", slot.Area.Type, slotName)
}
if slot.Area.Encryption != "aes-xts-plain64" {
return fmt.Errorf("unsupported area encryption %q for slot %q", slot.Area.Encryption, slotName)
}
if slot.Area.KeySize != 64 {
return fmt.Errorf("unsupported area key size %d for slot %q", slot.Area.KeySize, slotName)
}
if slot.KDF.Type != "argon2id" {
return fmt.Errorf("unsupported KDF type %q for slot %q", slot.KDF.Type, slotName)
}
if slot.KDF.Memory == 0 {
return fmt.Errorf("unsupported KDF memory %d for slot %q", slot.KDF.Memory, slotName)
}
if slot.KDF.Salt == "" {
return fmt.Errorf("unsupported KDF salt for slot %q", slotName)
}
}
if len(metadata.Segments) == 0 {
return errors.New("no segments found in LUKS2 header")
}
for segmentName, segment := range metadata.Segments {
if segment.Type != "crypt" {
return fmt.Errorf("unsupported segment type %q for segment %q", segment.Type, segmentName)
}
if segment.SectorSize != 4096 {
return fmt.Errorf("unsupported segment sector size %d for segment %q", segment.SectorSize, segmentName)
}
if segment.IVTweak != "0" {
return fmt.Errorf("unsupported segment IV tweak %q for segment %q", segment.IVTweak, segmentName)
}
if segment.Encryption != "aes-xts-plain64" {
return fmt.Errorf("unsupported segment encryption %q for segment %q", segment.Encryption, segmentName)
}
switch segment.Integrity.Type {
case "hmac(sha256)":
if segment.Integrity.JournalEncryption != "none" {
return fmt.Errorf("unsupported segment integrity journal encryption %q for segment %q", segment.Integrity.JournalEncryption, segmentName)
}
if segment.Integrity.JournalIntegrity != "none" {
return fmt.Errorf("unsupported segment integrity journal integrity %q for segment %q", segment.Integrity.JournalIntegrity, segmentName)
}
case "":
if segment.Integrity.JournalEncryption != "" {
return fmt.Errorf("unsupported segment integrity journal encryption %q for segment %q", segment.Integrity.JournalEncryption, segmentName)
}
if segment.Integrity.JournalIntegrity != "" {
return fmt.Errorf("unsupported segment integrity journal integrity %q for segment %q", segment.Integrity.JournalIntegrity, segmentName)
}
default:
return fmt.Errorf("unsupported segment integrity type %q for segment %q", segment.Integrity.Type, segmentName)
}
}
if len(metadata.Digests) == 0 {
return errors.New("no digests found in LUKS2 header")
}
for digestName, digest := range metadata.Digests {
if digest.Type != "pbkdf2" {
return fmt.Errorf("unsupported digest type %q for digest %q", digest.Type, digestName)
}
if digest.Hash != "sha256" {
return fmt.Errorf("unsupported digest hash %q for digest %q", digest.Hash, digestName)
}
if digest.Salt == "" {
return fmt.Errorf("unsupported digest salt for digest %q", digestName)
}
if digest.Digest == "" {
return fmt.Errorf("unsupported digest value for digest %q", digestName)
}
}
return nil
}
// createLoopbackDevice sets up a loop device for the given file and returns the loop device path (e.g., /dev/loop0).
func createLoopbackDevice(filePath string) (string, error) {
file, err := os.OpenFile(filePath, os.O_RDWR, 0)
if err != nil {
return "", fmt.Errorf("open backing file: %w", err)
}
defer file.Close()
// Get a free loop device number
ctrl, err := os.OpenFile("/dev/loop-control", os.O_RDWR, 0)
if err != nil {
return "", fmt.Errorf("open /dev/loop-control: %w", err)
}
defer ctrl.Close()
loopNum, _, errno := unix.Syscall(unix.SYS_IOCTL, ctrl.Fd(), unix.LOOP_CTL_GET_FREE, 0)
if errno != 0 {
return "", fmt.Errorf("LOOP_CTL_GET_FREE: %v", errno)
}
// Open the loop device
loopDev := fmt.Sprintf("/dev/loop%d", loopNum)
loop, err := os.OpenFile(loopDev, os.O_RDWR, 0)
if err != nil {
return "", fmt.Errorf("open loop device: %w", err)
}
defer loop.Close()
// Associate the file with the loop device
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, loop.Fd(), unix.LOOP_SET_FD, file.Fd()); errno != 0 {
return "", fmt.Errorf("LOOP_SET_FD: %v", errno)
}
return loopDev, nil
}
// detachLoopbackDevice removes the specified loopback device.
func detachLoopbackDevice(loopDev string) error {
loop, err := os.OpenFile(loopDev, os.O_RDWR, 0)
if err != nil {
return fmt.Errorf("open loop device: %w", err)
}
defer loop.Close()
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, loop.Fd(), unix.LOOP_CLR_FD, 0); errno != 0 {
return fmt.Errorf("LOOP_CLR_FD: %v", errno)
}
return nil
}
type cryptsetupMetadata struct {
KeySlots map[string]struct {
Type string `json:"type"`
KeySize int `json:"key_size"`
AntiForensicSplitter struct {
Type string `json:"type"`
Stripes int `json:"stripes"`
Hash string `json:"hash"`
} `json:"af"`
Area struct {
Type string `json:"type"`
Offset string `json:"offset"`
Size string `json:"size"`
Encryption string `json:"encryption"`
KeySize int `json:"key_size"`
} `json:"area"`
KDF struct {
Type string `json:"type"`
Time int `json:"time"`
Memory int `json:"memory"`
CPUs int `json:"cpus"`
Salt string `json:"salt"`
} `json:"kdf"`
} `json:"keyslots"`
Tokens map[string]any `json:"tokens"`
Segments map[string]struct {
Type string `json:"type"`
Offset string `json:"offset"`
Size string `json:"size"`
Flags []string `json:"flags,omitempty"`
IVTweak string `json:"iv_tweak"`
Encryption string `json:"encryption"`
SectorSize int `json:"sector_size"`
Integrity struct {
Type string `json:"type"`
JournalEncryption string `json:"journal_encryption"`
JournalIntegrity string `json:"journal_integrity"`
}
} `json:"segments"`
Digests map[string]struct {
Type string `json:"type"`
Keyslots []string `json:"keyslots"`
Segments []string `json:"segments"`
Hash string `json:"hash"`
Iterations int `json:"iterations"`
Salt string `json:"salt"`
Digest string `json:"digest"`
} `json:"digests"`
Config struct {
JSONSize string `json:"json_size"`
KeyslotsSize string `json:"keyslots_size"`
}
}
type cgoFormatter interface { type cgoFormatter interface {
Format(deviceType cryptsetup.DeviceType, genericParams cryptsetup.GenericParams) error Format(deviceType cryptsetup.DeviceType, genericParams cryptsetup.GenericParams) error
} }
@ -80,3 +420,10 @@ type cgoFormatter interface {
type cgoLoader interface { type cgoLoader interface {
Load(deviceType cryptsetup.DeviceType) error Load(deviceType cryptsetup.DeviceType) error
} }
type cgoRestorer interface {
HeaderRestore(deviceType cryptsetup.DeviceType, headerFile string) error
}
type cgoBackuper interface {
HeaderBackup(deviceType cryptsetup.DeviceType, headerFile string) error
}

View file

@ -26,14 +26,26 @@ func format(_ cryptDevice, _ bool) error {
return errCGONotSupported return errCGONotSupported
} }
func initByDevicePath(_ string) (cryptDevice, error) { func headerRestore(_ cryptDevice, _ string) error {
return nil, errCGONotSupported return errCGONotSupported
} }
func initByName(_ string) (cryptDevice, error) { func headerBackup(_ cryptDevice, _ string) error {
return nil, errCGONotSupported return errCGONotSupported
}
func initByDevicePath(_ string) (cryptDevice, cryptDevice, string, string, error) {
return nil, nil, "", "", errCGONotSupported
}
func initByName(_ string) (cryptDevice, cryptDevice, string, string, error) {
return nil, nil, "", "", errCGONotSupported
} }
func loadLUKS2(_ cryptDevice) error { func loadLUKS2(_ cryptDevice) error {
return errCGONotSupported return errCGONotSupported
} }
func detachLoopbackDevice(_ string) error {
return errCGONotSupported
}