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:
111
internal/unlock/unlock.go
Normal file
111
internal/unlock/unlock.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package unlock
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.wntrmute.dev/kyle/arca/internal/cryptsetup"
|
||||
"git.wntrmute.dev/kyle/arca/internal/udisks"
|
||||
)
|
||||
|
||||
// Result holds the outcome of a successful unlock.
|
||||
type Result struct {
|
||||
Device *udisks.BlockDevice
|
||||
Privileged bool // true if unlock required root (cryptsetup path)
|
||||
}
|
||||
|
||||
// Options configures the unlock behavior.
|
||||
type Options struct {
|
||||
ReadPassphrase func() (string, error)
|
||||
KeyfilePath string
|
||||
}
|
||||
|
||||
// Unlocker tries unlock methods in sequence.
|
||||
type Unlocker struct {
|
||||
client *udisks.Client
|
||||
opts Options
|
||||
}
|
||||
|
||||
// New creates a new Unlocker.
|
||||
func New(client *udisks.Client, opts Options) *Unlocker {
|
||||
return &Unlocker{client: client, opts: opts}
|
||||
}
|
||||
|
||||
// Unlock tries each method in order and returns the result on first success.
|
||||
func (u *Unlocker) Unlock(dev *udisks.BlockDevice, methods []string) (*Result, error) {
|
||||
var errs []error
|
||||
for _, method := range methods {
|
||||
res, err := u.tryMethod(dev, method)
|
||||
if err == nil {
|
||||
return res, nil
|
||||
}
|
||||
errs = append(errs, fmt.Errorf("%s: %w", method, err))
|
||||
}
|
||||
return nil, fmt.Errorf("all unlock methods failed:\n%w", errors.Join(errs...))
|
||||
}
|
||||
|
||||
func (u *Unlocker) tryMethod(dev *udisks.BlockDevice, method string) (*Result, error) {
|
||||
switch method {
|
||||
case "passphrase":
|
||||
ct, err := u.unlockPassphrase(dev)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Result{Device: ct, Privileged: false}, nil
|
||||
case "keyfile":
|
||||
ct, err := u.unlockKeyfile(dev)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Result{Device: ct, Privileged: false}, nil
|
||||
case "fido2", "tpm2":
|
||||
ct, err := u.unlockCryptsetup(dev)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Result{Device: ct, Privileged: true}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown unlock method: %s", method)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *Unlocker) unlockPassphrase(dev *udisks.BlockDevice) (*udisks.BlockDevice, error) {
|
||||
if u.opts.ReadPassphrase == nil {
|
||||
return nil, fmt.Errorf("no passphrase reader configured")
|
||||
}
|
||||
pass, err := u.opts.ReadPassphrase()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return u.client.Unlock(dev, pass)
|
||||
}
|
||||
|
||||
func (u *Unlocker) unlockKeyfile(dev *udisks.BlockDevice) (*udisks.BlockDevice, error) {
|
||||
if u.opts.KeyfilePath == "" {
|
||||
return nil, fmt.Errorf("no keyfile configured")
|
||||
}
|
||||
contents, err := os.ReadFile(u.opts.KeyfilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading keyfile: %w", err)
|
||||
}
|
||||
return u.client.UnlockWithKeyfile(dev, contents)
|
||||
}
|
||||
|
||||
func (u *Unlocker) unlockCryptsetup(dev *udisks.BlockDevice) (*udisks.BlockDevice, error) {
|
||||
name := cryptsetup.MapperName(dev.DevicePath)
|
||||
if err := cryptsetup.Open(dev.DevicePath, name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Wait for udisks2 to pick up the new dm device.
|
||||
for i := 0; i < 10; i++ {
|
||||
ct, err := u.client.CleartextDevice(dev)
|
||||
if err == nil {
|
||||
return ct, nil
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
return nil, fmt.Errorf("timed out waiting for udisks2 to discover cleartext device")
|
||||
}
|
||||
Reference in New Issue
Block a user