Add -v/--verbose persistent flag that prints debug info to stderr: D-Bus connection status, token plugin directory discovery, unlock method sequencing with per-method success/failure, and full cryptsetup command lines including LD_LIBRARY_PATH. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
117 lines
3.2 KiB
Go
117 lines
3.2 KiB
Go
package unlock
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"git.wntrmute.dev/kyle/arca/internal/cryptsetup"
|
|
"git.wntrmute.dev/kyle/arca/internal/udisks"
|
|
"git.wntrmute.dev/kyle/arca/internal/verbose"
|
|
)
|
|
|
|
// 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) {
|
|
verbose.Printf("unlock %s: methods %v", dev.DevicePath, methods)
|
|
var errs []error
|
|
for _, method := range methods {
|
|
verbose.Printf("trying method: %s", method)
|
|
res, err := u.tryMethod(dev, method)
|
|
if err == nil {
|
|
verbose.Printf("method %s succeeded", method)
|
|
return res, nil
|
|
}
|
|
verbose.Printf("method %s failed: %v", method, err)
|
|
errs = append(errs, fmt.Errorf("%s: %w", method, err))
|
|
}
|
|
return nil, fmt.Errorf("all unlock methods failed for %s:\n%w", dev.DevicePath, 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, fmt.Errorf("%w (is the FIDO2/TPM2 key plugged in?)", 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")
|
|
}
|