Initial implementation of arca, a LUKS volume manager.

Go CLI using cobra with mount, unmount, status, and init subcommands.
Unlocks via udisks2 D-Bus (passphrase/keyfile) or cryptsetup (FIDO2/TPM2)
with ordered method fallback. Includes NixOS-specific LD_LIBRARY_PATH
injection for systemd cryptsetup token plugins.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 07:42:38 -07:00
commit c835358829
538 changed files with 259597 additions and 0 deletions

177
internal/udisks/client.go Normal file
View File

@@ -0,0 +1,177 @@
package udisks
import (
"bytes"
"fmt"
"github.com/godbus/dbus/v5"
)
// Client communicates with udisks2 over D-Bus.
type Client struct {
conn *dbus.Conn
}
// NewClient connects to the system D-Bus and returns a udisks2 client.
func NewClient() (*Client, error) {
conn, err := dbus.SystemBus()
if err != nil {
return nil, fmt.Errorf("connecting to system bus: %w", err)
}
return &Client{conn: conn}, nil
}
// Close closes the D-Bus connection.
func (c *Client) Close() {
c.conn.Close()
}
// FindDevice finds a block device by UUID (if non-empty) or device path.
func (c *Client) FindDevice(uuid, pathOrName string) (*BlockDevice, error) {
devices, err := c.listBlockDevices()
if err != nil {
return nil, err
}
if uuid != "" {
for i := range devices {
if devices[i].UUID == uuid {
return &devices[i], nil
}
}
return nil, fmt.Errorf("no device with UUID %s", uuid)
}
for i := range devices {
if devices[i].DevicePath == pathOrName {
return &devices[i], nil
}
}
return nil, fmt.Errorf("device %s not found", pathOrName)
}
// ListEncryptedDevices returns all block devices with the Encrypted interface.
func (c *Client) ListEncryptedDevices() ([]BlockDevice, error) {
devices, err := c.listBlockDevices()
if err != nil {
return nil, err
}
var encrypted []BlockDevice
for _, d := range devices {
if d.HasEncrypted {
encrypted = append(encrypted, d)
}
}
return encrypted, nil
}
// CleartextDevice finds the unlocked cleartext device for a LUKS device.
func (c *Client) CleartextDevice(dev *BlockDevice) (*BlockDevice, error) {
devices, err := c.listBlockDevices()
if err != nil {
return nil, err
}
for i := range devices {
if devices[i].CryptoBackingDevice == dev.ObjectPath {
return &devices[i], nil
}
}
return nil, fmt.Errorf("no cleartext device for %s (is it unlocked?)", dev.DevicePath)
}
// MountPoint returns the current mount point of a device, or "" if not mounted.
func (c *Client) MountPoint(dev *BlockDevice) (string, error) {
obj := c.conn.Object(busName, dev.ObjectPath)
variant, err := obj.GetProperty(ifaceFilesystem + ".MountPoints")
if err != nil {
return "", err
}
mountPoints, ok := variant.Value().([][]byte)
if !ok || len(mountPoints) == 0 {
return "", nil
}
return string(bytes.TrimRight(mountPoints[0], "\x00")), nil
}
// DeviceAtPath returns the block device at the given D-Bus object path.
func (c *Client) DeviceAtPath(path dbus.ObjectPath) (*BlockDevice, error) {
devices, err := c.listBlockDevices()
if err != nil {
return nil, err
}
for i := range devices {
if devices[i].ObjectPath == path {
return &devices[i], nil
}
}
return nil, fmt.Errorf("device at %s not found", path)
}
// RootBackingDevices returns the object paths of LUKS devices that back
// the root filesystem. Used to exclude them from init config generation.
func (c *Client) RootBackingDevices() ([]dbus.ObjectPath, error) {
devices, err := c.listBlockDevices()
if err != nil {
return nil, err
}
var backing []dbus.ObjectPath
for i := range devices {
if !devices[i].HasFilesystem {
continue
}
mp, err := c.MountPoint(&devices[i])
if err != nil || mp != "/" {
continue
}
if devices[i].CryptoBackingDevice != "" && devices[i].CryptoBackingDevice != "/" {
backing = append(backing, devices[i].CryptoBackingDevice)
}
}
return backing, nil
}
func (c *Client) listBlockDevices() ([]BlockDevice, error) {
obj := c.conn.Object(busName, dbus.ObjectPath(objectPrefix))
var managed map[dbus.ObjectPath]map[string]map[string]dbus.Variant
err := obj.Call(ifaceObjectManager+".GetManagedObjects", 0).Store(&managed)
if err != nil {
return nil, fmt.Errorf("listing devices: %w", err)
}
var devices []BlockDevice
for path, ifaces := range managed {
blockProps, ok := ifaces[ifaceBlock]
if !ok {
continue
}
dev := BlockDevice{ObjectPath: path}
if v, ok := blockProps["Device"]; ok {
if bs, ok := v.Value().([]byte); ok {
dev.DevicePath = string(bytes.TrimRight(bs, "\x00"))
}
}
if v, ok := blockProps["IdUUID"]; ok {
if s, ok := v.Value().(string); ok {
dev.UUID = s
}
}
if v, ok := blockProps["CryptoBackingDevice"]; ok {
if p, ok := v.Value().(dbus.ObjectPath); ok {
dev.CryptoBackingDevice = p
}
}
_, dev.HasEncrypted = ifaces[ifaceEncrypted]
_, dev.HasFilesystem = ifaces[ifaceFilesystem]
devices = append(devices, dev)
}
return devices, nil
}

View File

@@ -0,0 +1,43 @@
package udisks
import (
"fmt"
"github.com/godbus/dbus/v5"
)
// Unlock unlocks a LUKS device with a passphrase and returns the cleartext device.
func (c *Client) Unlock(dev *BlockDevice, passphrase string) (*BlockDevice, error) {
obj := c.conn.Object(busName, dev.ObjectPath)
var cleartextPath dbus.ObjectPath
err := obj.Call(ifaceEncrypted+".Unlock", 0, passphrase, noOptions()).Store(&cleartextPath)
if err != nil {
return nil, fmt.Errorf("unlocking %s: %w", dev.DevicePath, err)
}
return c.DeviceAtPath(cleartextPath)
}
// UnlockWithKeyfile unlocks a LUKS device with keyfile contents.
func (c *Client) UnlockWithKeyfile(dev *BlockDevice, keyContents []byte) (*BlockDevice, error) {
obj := c.conn.Object(busName, dev.ObjectPath)
options := map[string]dbus.Variant{
"keyfile_contents": dbus.MakeVariant(keyContents),
}
var cleartextPath dbus.ObjectPath
err := obj.Call(ifaceEncrypted+".Unlock", 0, "", options).Store(&cleartextPath)
if err != nil {
return nil, fmt.Errorf("unlocking %s with keyfile: %w", dev.DevicePath, err)
}
return c.DeviceAtPath(cleartextPath)
}
// Lock locks a LUKS device.
func (c *Client) Lock(dev *BlockDevice) error {
obj := c.conn.Object(busName, dev.ObjectPath)
return obj.Call(ifaceEncrypted+".Lock", 0, noOptions()).Err
}

23
internal/udisks/mount.go Normal file
View File

@@ -0,0 +1,23 @@
package udisks
import (
"fmt"
)
// Mount mounts a filesystem and returns the mount point chosen by udisks2.
func (c *Client) Mount(dev *BlockDevice) (string, error) {
obj := c.conn.Object(busName, dev.ObjectPath)
var mountpoint string
err := obj.Call(ifaceFilesystem+".Mount", 0, noOptions()).Store(&mountpoint)
if err != nil {
return "", fmt.Errorf("mounting %s: %w", dev.DevicePath, err)
}
return mountpoint, nil
}
// Unmount unmounts a filesystem.
func (c *Client) Unmount(dev *BlockDevice) error {
obj := c.conn.Object(busName, dev.ObjectPath)
return obj.Call(ifaceFilesystem+".Unmount", 0, noOptions()).Err
}

27
internal/udisks/types.go Normal file
View File

@@ -0,0 +1,27 @@
package udisks
import "github.com/godbus/dbus/v5"
const (
busName = "org.freedesktop.UDisks2"
objectPrefix = "/org/freedesktop/UDisks2"
ifaceObjectManager = "org.freedesktop.DBus.ObjectManager"
ifaceBlock = "org.freedesktop.UDisks2.Block"
ifaceEncrypted = "org.freedesktop.UDisks2.Encrypted"
ifaceFilesystem = "org.freedesktop.UDisks2.Filesystem"
)
// BlockDevice represents a udisks2 block device.
type BlockDevice struct {
ObjectPath dbus.ObjectPath
DevicePath string // e.g., "/dev/sda1"
UUID string
HasEncrypted bool
HasFilesystem bool
CryptoBackingDevice dbus.ObjectPath
}
func noOptions() map[string]dbus.Variant {
return make(map[string]dbus.Variant)
}