package cryptsetup import ( "fmt" "os" "os/exec" "path/filepath" "strings" "git.wntrmute.dev/kyle/arca/internal/verbose" ) // 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) verbose.Printf("exec: %s", strings.Join(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/. 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, then // removes the mount directory if it is empty. 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) } // Clean up empty mount directory. Best-effort — ignore errors // (directory may not be empty or may be a system path). rmdirArgs := withPrivilege([]string{"rmdir", mountpoint}) exec.Command(rmdirArgs[0], rmdirArgs[1:]...).Run() return nil } // MapperName returns "arca-" 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) { verbose.Printf("token plugin dir: %s", 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) { verbose.Printf("token plugin dir (from systemd-cryptenroll): %s", dir) return dir } } } verbose.Printf("no token plugin directory found") 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 }