Files
arca/internal/cryptsetup/cryptsetup.go
Kyle Isom c835358829 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>
2026-03-24 07:42:38 -07:00

134 lines
3.8 KiB
Go

package cryptsetup
import (
"fmt"
"os"
"os/exec"
"path/filepath"
)
// Open opens a LUKS device using cryptsetup with token-based unlock.
func Open(devicePath, mapperName string) error {
args := withTokenPluginEnv([]string{"cryptsetup", "open", devicePath, mapperName, "--token-only"})
args = withPrivilege(args)
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("cryptsetup open --token-only: %w", err)
}
return nil
}
// Close closes a LUKS mapping.
func Close(mapperName string) error {
args := []string{"cryptsetup", "close", mapperName}
args = withPrivilege(args)
cmd := exec.Command(args[0], args[1:]...)
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("cryptsetup close: %w", err)
}
return nil
}
// Mount mounts a device at the given mountpoint using privileged mount.
// If mountpoint is empty, mounts under /mnt/<basename>.
func Mount(devicePath, mountpoint string) (string, error) {
if mountpoint == "" {
mountpoint = "/mnt/" + filepath.Base(devicePath)
}
mkdirArgs := withPrivilege([]string{"mkdir", "-p", mountpoint})
if err := exec.Command(mkdirArgs[0], mkdirArgs[1:]...).Run(); err != nil {
return "", fmt.Errorf("creating mountpoint: %w", err)
}
args := withPrivilege([]string{"mount", devicePath, mountpoint})
cmd := exec.Command(args[0], args[1:]...)
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("mount %s: %w", devicePath, err)
}
return mountpoint, nil
}
// Unmount unmounts the given mountpoint using privileged umount.
func Unmount(mountpoint string) error {
args := withPrivilege([]string{"umount", mountpoint})
cmd := exec.Command(args[0], args[1:]...)
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("umount %s: %w", mountpoint, err)
}
return nil
}
// MapperName returns "arca-<dev>" from a device path, e.g. "/dev/sda1" -> "arca-sda1".
func MapperName(devicePath string) string {
return "arca-" + filepath.Base(devicePath)
}
// withTokenPluginEnv wraps the command with `env LD_LIBRARY_PATH=...` if
// the systemd cryptsetup token plugins are found.
//
// On NixOS, cryptsetup uses dlopen to load token plugins, but glibc only
// searches the binary's baked-in RUNPATH — which doesn't include the
// systemd plugin directory. LD_LIBRARY_PATH is the only reliable way to
// make dlopen find them.
//
// We inject it via `env` rather than cmd.Env so it survives privilege
// escalation through doas/sudo.
func withTokenPluginEnv(args []string) []string {
pluginDir := findTokenPluginDir()
if pluginDir == "" {
return args
}
return append([]string{"env", "LD_LIBRARY_PATH=" + pluginDir}, args...)
}
// findTokenPluginDir locates the directory containing libcryptsetup-token-*.so.
// It checks the NixOS system profile first, then falls back to resolving
// from the systemd-cryptenroll binary.
func findTokenPluginDir() string {
// NixOS stable symlink — survives rebuilds.
const nixSystemPath = "/run/current-system/sw/lib/cryptsetup"
if hasTokenPlugins(nixSystemPath) {
return nixSystemPath
}
// Fallback: derive from systemd-cryptenroll location.
if bin, err := exec.LookPath("systemd-cryptenroll"); err == nil {
if resolved, err := filepath.EvalSymlinks(bin); err == nil {
dir := filepath.Join(filepath.Dir(filepath.Dir(resolved)), "lib", "cryptsetup")
if hasTokenPlugins(dir) {
return dir
}
}
}
return ""
}
func hasTokenPlugins(dir string) bool {
matches, _ := filepath.Glob(filepath.Join(dir, "libcryptsetup-token-*.so"))
return len(matches) > 0
}
func withPrivilege(args []string) []string {
if _, err := exec.LookPath("doas"); err == nil {
return append([]string{"doas"}, args...)
}
if _, err := exec.LookPath("sudo"); err == nil {
return append([]string{"sudo"}, args...)
}
return args
}