diff --git a/fido2.go b/fido2.go index 829a4d9..6828564 100644 --- a/fido2.go +++ b/fido2.go @@ -12,6 +12,7 @@ import ( "encoding/hex" "fmt" "sync" + "time" "unsafe" "github.com/pkg/errors" @@ -240,6 +241,85 @@ func NewDevice(path string) (*Device, error) { }, nil } +// SelectDevice returns the first device of the passed list that is touched by the user within the given timeout. +// It returns an error if no device was selected. +func SelectDevice(devs []*Device, timeout time.Duration) (*Device, error) { + selectedDev := &Device{} + done := make(chan int, len(devs)) + + pollDevice := func(d *Device) { + // make sure each thread signals `done` before exiting + defer func() { + done <- 0 + }() + + dev, err := d.open() + if err != nil { + logger.Errorf("%v", errors.Wrap(err, fmt.Sprintf("failed open device %s", d.path))) + return + } + + defer d.close(dev) + + if cErr := C.fido_dev_get_touch_begin(dev); cErr != C.FIDO_OK { + msg := fmt.Sprintf("failed to start selection for %s", d.path) + logger.Errorf("%v", errors.Wrap(errFromCode(cErr), msg)) + return + } + + tick := time.Tick(200 * time.Millisecond) + after := time.After(timeout) + for { + select { + case <-tick: + if selectedDev.path != "" { + logger.Debugf(fmt.Sprintf("stop polling: %s", d.path)) + C.fido_dev_cancel(dev) + return + } + + var touched C.int + if cErr := C.fido_dev_get_touch_status(dev, &touched, 50); cErr != C.FIDO_OK { + msg := fmt.Sprintf("failed to get touch status of %s", d.path) + logger.Errorf("%v", errors.Wrap(errFromCode(cErr), msg)) + C.fido_dev_cancel(dev) + return + } + + if touched == 1 { + logger.Debugf(fmt.Sprintf("device touched: %s", d.path)) + selectedDev.Lock() + if selectedDev.path == "" { + selectedDev.path = d.path + } + selectedDev.Unlock() + // call quit to stop polling for all devices + return + } + case <-after: + logger.Debugf(fmt.Sprintf("stop polling (timeout reached): %s", d.path)) + C.fido_dev_cancel(dev) + return + } + } + } + + for i := 0; i < len(devs); i++ { + go pollDevice(devs[i]) + } + + // wait for all threads to finish + for i := 0; i < len(devs); i++ { + <-done + } + + if selectedDev.path == "" { + return nil, fmt.Errorf("timeout reached before any device was touched") + } + + return selectedDev, nil +} + func (d *Device) open() (*C.fido_dev_t, error) { dev := C.fido_dev_new() if cErr := C.fido_dev_open(dev, C.CString(d.path)); cErr != C.FIDO_OK {