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) Method string // which method succeeded: "fido2", "tpm2", "passphrase", "keyfile" KeySlot string // key slot used, from cryptsetup verbose output (or "") } // 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, Method: method}, nil case "keyfile": ct, err := u.unlockKeyfile(dev) if err != nil { return nil, err } return &Result{Device: ct, Privileged: false, Method: method}, nil case "fido2", "tpm2": ct, openResult, err := u.unlockCryptsetup(dev) if err != nil { return nil, err } return &Result{Device: ct, Privileged: true, Method: method, KeySlot: openResult.KeySlot}, 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, cryptsetup.OpenResult, error) { name := cryptsetup.MapperName(dev.DevicePath) openResult, err := cryptsetup.Open(dev.DevicePath, name) if err != nil { return nil, openResult, 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, openResult, nil } time.Sleep(200 * time.Millisecond) } return nil, openResult, fmt.Errorf("timed out waiting for udisks2 to discover cleartext device") }