Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions fido2.go
Original file line number Diff line number Diff line change
Expand Up @@ -1086,6 +1086,74 @@ func (d *Device) BioSetTemplateName(pin, templateId, name string) error {
return nil
}

// TouchRequest is a device touch request initiated by [Device.TouchBegin].
//
// TouchRequests are not thread-safe, much in the same way that the devices
// themselves are essentially single-threaded.
// Do not call [TouchRequest.Status] or [TouchRequest.Stop] concurrently.
type TouchRequest struct {
dev *Device
stopped bool
}

// TouchBegin initiates a device touch request.
// Verify the request status using [TouchRequest.Status] and stop the request
// using [TouchRequest.Stop].
func (d *Device) TouchBegin() (*TouchRequest, error) {
dev, err := d.getDevice()
if err != nil {
return nil, err
}

if cErr := C.fido_dev_get_touch_begin(dev); cErr != C.FIDO_OK {
return nil, fmt.Errorf("touch begin: %w", errFromCode(cErr))
}

return &TouchRequest{dev: d}, nil
}

// Status verifies the status of the touch request.
// The timeout is measured in milliseconds and enforced to not exceed a
// frequency of 5Hz or 0.2 seconds.
// See https://developers.yubico.com/libfido2/Manuals/fido_dev_get_touch_status.html#CAVEATS.
func (r *TouchRequest) Status(timeout time.Duration) (touched bool, err error) {
dev, err := r.dev.getDevice()
if err != nil {
return false, err
}

ms := C.int(timeout.Milliseconds())
const minFrequencyMs = 200 // aka 0.2 seconds
if ms < minFrequencyMs {
ms = minFrequencyMs
}

var resp C.int
if cErr := C.fido_dev_get_touch_status(dev, &resp, ms); cErr != C.FIDO_OK {
r.stopped = true // Request terminated.
Comment thread
codingllama marked this conversation as resolved.
return false, fmt.Errorf("touch status: %w", errFromCode(cErr))
}
if resp == 1 {
r.stopped = true // Request terminated.
return true, nil
}
return false, nil
}

// Stop terminates the touch request.
// Requests may be stopped on demand, or automatically stopped when
// [TouchRequest.Status] returns true or errors.
// Only the first stop is effective, therefore it's safe to defer-Stop.
// Stop is otherwise equivalent to [Device.Cancel].
func (r *TouchRequest) Stop() error {
if r.stopped {
return nil
}

r.stopped = true
Comment thread
codingllama marked this conversation as resolved.
return r.dev.Cancel()
}

func goStrings(argc C.int, argv **C.char) []string {
length := int(argc)
tmpslice := (*[1 << 30]*C.char)(unsafe.Pointer(argv))[:length:length]
Expand Down
75 changes: 75 additions & 0 deletions fido2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,78 @@ func TestDeviceAssertionCancel(t *testing.T) {
)
require.EqualError(t, errors.Cause(err), "keep alive cancel")
}

func TestDevice_TouchRequest(t *testing.T) {
locs, err := libfido2.DeviceLocations()
if err != nil {
t.Fatalf("DeviceLocations failed: %v", err)
}
if len(locs) == 0 {
t.Fatalf("No devices found")
}
loc := locs[0]

t.Logf("Using device: %+v\n", loc)
dev, err := libfido2.NewDevice(loc.Path)
if err != nil {
t.Fatalf("NewDevice failed: %v", err)
}
defer dev.Close()

t.Run("success", func(t *testing.T) {
t.Logf("Touch your %v\n", locs[0].Product)
touch, err := dev.TouchBegin()
if err != nil {
t.Fatalf("TouchBegin failed: %v", err)
}

maxWait := time.After(30 * time.Second)
for {
touched, err := touch.Status(200 * time.Second)
if err != nil {
t.Errorf("Status failed, aborting: %v", err)
break
}
if touched {
t.Log("Touch detected")
break
}

select {
case <-maxWait:
// Exit select and break from loop below.
default:
continue
}
t.Error("Timed out waiting for touch")
break
}

if err := touch.Stop(); err != nil {
t.Errorf("Stop failed: %v", err)
}
if err := touch.Stop(); err != nil {
t.Errorf("Subsequent Stops should never error: %v", err)
}
})

t.Run("cancel", func(t *testing.T) {
t.Log("Testing touch cancel")

touch, err := dev.TouchBegin()
if err != nil {
t.Fatalf("TouchBegin failed: %v", err)
}

// Give it a moment to start.
time.Sleep(2 * time.Second)

// Terminate touch request.
if err := touch.Stop(); err != nil {
t.Errorf("Stop failed: %v", err)
}
if err := touch.Stop(); err != nil {
t.Errorf("Subsequent Stops should never error: %v", err)
}
})
}