diff --git a/examples_test.go b/examples_test.go index a179683..48ebd98 100644 --- a/examples_test.go +++ b/examples_test.go @@ -26,6 +26,7 @@ func ExampleDeviceLocations() { if err != nil { log.Fatal(err) } + defer device.Close() hidInfo, err := device.CTAPHIDInfo() if err != nil { @@ -65,10 +66,11 @@ func ExampleDevice_MakeCredential() { if err != nil { log.Fatal(err) } + defer device.Close() cdh := libfido2.RandBytes(32) userID := libfido2.RandBytes(32) - pin := "12345" + pin := getPIN() attest, err := device.MakeCredential( cdh, @@ -105,8 +107,7 @@ func ExampleDevice_Assertion() { return } - // Note: change as appropriate. - const pin = "12345" + pin := getPIN() locs, err := libfido2.DeviceLocations() if err != nil { @@ -123,6 +124,7 @@ func ExampleDevice_Assertion() { if err != nil { log.Fatal(err) } + defer device.Close() cdh := libfido2.RandBytes(32) userID := libfido2.RandBytes(32) @@ -206,8 +208,9 @@ func ExampleDevice_Credentials() { if err != nil { log.Fatal(err) } + defer device.Close() - pin := "12345" + pin := getPIN() info, err := device.CredentialsInfo(pin) if err != nil { @@ -237,74 +240,6 @@ func ExampleDevice_Credentials() { // } -func Dont_ExampleDevice_Reset() { - if os.Getenv("FIDO2_EXAMPLES") != "1" { - return - } - libfido2.SetLogger(libfido2.NewLogger(libfido2.DebugLevel)) - - if os.Getenv("FIDO2_EXAMPLES_RESET") != "1" { - log.Println("only runs if FIDO2_EXAMPLES_RESET=1") - return - } - - locs, err := libfido2.DeviceLocations() - if err != nil { - log.Fatal(err) - } - if len(locs) == 0 { - log.Println("No devices") - return - } - - log.Printf("Using device: %+v\n", locs[0]) - path := locs[0].Path - device, err := libfido2.NewDevice(path) - if err != nil { - log.Fatal(err) - } - - log.Printf("Resetting: %+v\n", locs[0]) - if err := device.Reset(); err != nil { - log.Fatal(err) - } - - // Output: - // - -} - -func Dont_ExampleDevice_SetPIN() { - if os.Getenv("FIDO2_EXAMPLES_SET_PIN") != "1" { - return - } - libfido2.SetLogger(libfido2.NewLogger(libfido2.DebugLevel)) - - locs, err := libfido2.DeviceLocations() - if err != nil { - log.Fatal(err) - } - if len(locs) == 0 { - log.Println("No devices") - return - } - - log.Printf("Using device: %+v\n", locs[0]) - path := locs[0].Path - device, err := libfido2.NewDevice(path) - if err != nil { - log.Fatal(err) - } - - pin := "12345" - if err := device.SetPIN(pin, ""); err != nil { - log.Fatal(err) - } - - // Output: - // -} - func ExampleDevice_MakeCredential_hmacSecret() { if os.Getenv("FIDO2_EXAMPLES") != "1" { return @@ -324,10 +259,11 @@ func ExampleDevice_MakeCredential_hmacSecret() { if err != nil { log.Fatal(err) } + defer device.Close() cdh := bytes.Repeat([]byte{0x01}, 32) rpID := "keys.pub" - pin := "12345" + pin := getPIN() attest, err := device.MakeCredential( cdh, @@ -353,6 +289,9 @@ func ExampleDevice_MakeCredential_hmacSecret() { } log.Printf("Credential ID: %s\n", hex.EncodeToString(attest.CredentialID)) + + // Output: + // } type testVector struct { @@ -378,12 +317,13 @@ func ExampleDevice_Assertion_hmacSecret() { if err != nil { log.Fatal(err) } + defer device.Close() name := locs[0].Product + "/" + locs[0].Manufacturer cdh := bytes.Repeat([]byte{0x01}, 32) rpID := "keys.pub" - pin := "12345" + pin := getPIN() testVectors := map[string]testVector{ "SoloKey 4.0/SoloKeys": testVector{ @@ -430,52 +370,6 @@ func ExampleDevice_Assertion_hmacSecret() { if testVector.Secret != hex.EncodeToString(assertion.HMACSecret) { log.Fatalf("Expected %s", testVector.Secret) } -} - -func ExampleDevice_DeleteCredential() { - if os.Getenv("FIDO2_EXAMPLES") != "1" { - return - } - locs, err := libfido2.DeviceLocations() - if err != nil { - log.Fatal(err) - } - if len(locs) == 0 { - log.Fatal("No devices") - return - } - - log.Printf("Using device: %+v\n", locs[0]) - path := locs[0].Path - device, err := libfido2.NewDevice(path) - if err != nil { - log.Fatal(err) - } - - pin := "12345" - - info, err := device.CredentialsInfo(pin) - if err != nil { - log.Fatal(err) - } - log.Printf("Info: %+v\n", info) - - rps, err := device.RelyingParties(pin) - if err != nil { - log.Fatal(err) - } - for _, rp := range rps { - creds, err := device.Credentials(rp.ID, pin) - if err != nil { - log.Fatal(err) - } - for _, cred := range creds { - log.Printf("Deleting: %s\n", hex.EncodeToString(cred.ID)) - if err := device.DeleteCredential(cred.ID, pin); err != nil { - log.Fatal(err) - } - } - } // Output: // @@ -500,8 +394,9 @@ func ExampleDevice_BioEnrollment() { if err != nil { log.Fatal(err) } + defer device.Close() - pin := "12345" + pin := getPIN() err = device.BioEnroll(pin) if err != nil { @@ -531,8 +426,9 @@ func ExampleDevice_BioList() { if err != nil { log.Fatal(err) } + defer device.Close() - pin := "12345" + pin := getPIN() templates, err := device.BioList(pin) if err != nil { @@ -544,82 +440,3 @@ func ExampleDevice_BioList() { // Output: // } - -func ExampleDevice_BioDelete() { - if os.Getenv("FIDO2_EXAMPLES") != "1" { - return - } - locs, err := libfido2.DeviceLocations() - if err != nil { - log.Fatal(err) - } - if len(locs) == 0 { - log.Fatal("No devices") - return - } - - log.Printf("Using device: %+v\n", locs[0]) - path := locs[0].Path - device, err := libfido2.NewDevice(path) - if err != nil { - log.Fatal(err) - } - - pin := "12345" - - templates, err := device.BioList(pin) - if err != nil { - log.Fatal(err) - } - - for _, template := range templates { - err := device.BioDelete(pin, template.ID) - if err != nil { - log.Fatal(err) - } - } - - // Output: - // -} - -func ExampleDevice_BioSetTemplateName() { - if os.Getenv("FIDO2_EXAMPLES") != "1" { - return - } - locs, err := libfido2.DeviceLocations() - if err != nil { - log.Fatal(err) - } - if len(locs) == 0 { - log.Fatal("No devices") - return - } - - log.Printf("Using device: %+v\n", locs[0]) - path := locs[0].Path - device, err := libfido2.NewDevice(path) - if err != nil { - log.Fatal(err) - } - - pin := "12345" - - templates, err := device.BioList(pin) - if err != nil { - log.Fatal(err) - } - - if len(templates) == 0 { - log.Fatal("no bio template") - return - } - - template := templates[0] - newName := "newName" - - device.BioSetTemplateName(pin, template.ID, newName) - - // Output: - // -} diff --git a/fido2.go b/fido2.go index c03f8a3..6722e51 100644 --- a/fido2.go +++ b/fido2.go @@ -15,6 +15,7 @@ import ( "encoding/hex" "fmt" "sync" + "time" "unsafe" "github.com/pkg/errors" @@ -30,9 +31,13 @@ func init() { type Device struct { path string - // Device instance if open. + // mu guards dev. + mu sync.Mutex + + // dev is the underlying libfido2 device. + // If nil either the Device wasn't properly initialized or was closed. + // Prefer getDevice instead of accessing this field directly. dev *C.fido_dev_t - sync.Mutex } // DeviceLocation ... @@ -234,56 +239,118 @@ func DeviceLocations() ([]*DeviceLocation, error) { } // NewDevice opens device at path. +// Opened devices must be explicitly closed. func NewDevice(path string) (*Device, error) { if path == "" { return nil, errors.Errorf("empty device path") } + + dev, err := openDevice(path) + if err != nil { + return nil, fmt.Errorf("open device: %w", err) + } + return &Device{ - path: fmt.Sprintf("%s", path), + path: path, + dev: dev, }, nil } -func (d *Device) open() (*C.fido_dev_t, error) { +func openDevice(path string) (*C.fido_dev_t, error) { + pathC := C.CString(path) + defer C.free(unsafe.Pointer(pathC)) + dev := C.fido_dev_new() - if cErr := C.fido_dev_open(dev, C.CString(d.path)); cErr != C.FIDO_OK { - return nil, errors.Wrap(errFromCode(cErr), "failed to open") + if cErr := C.fido_dev_open(dev, pathC); cErr != C.FIDO_OK { + return nil, errFromCode(cErr) } - d.dev = dev + return dev, nil } -// TODO(codingllama): Openning/closing the device for every operation seems -// wasteful. Consider changing it. -func (d *Device) close(dev *C.fido_dev_t) { - d.Lock() - d.dev = nil - d.Unlock() +func (d *Device) getDevice() (*C.fido_dev_t, error) { + if d == nil { + return nil, errors.New("device nil") + } + + d.mu.Lock() + defer d.mu.Unlock() - if cErr := C.fido_dev_close(dev); cErr != C.FIDO_OK { - logger.Errorf("%v", errors.Wrap(errFromCode(cErr), "failed to close")) + if d.dev == nil { + return nil, errors.New("device closed or not initialized") } - C.fido_dev_free(&dev) + + return d.dev, nil +} + +// Close closes and frees the underlying libfido2 device. +// All devices must be explicitly closed. Closing a device multiple times is +// harmless, as only the first Close takes effect. +func (d *Device) Close() error { + if d == nil { + return nil + } + + d.mu.Lock() + defer d.mu.Unlock() + + if d.dev == nil { + return nil + } + + cErr := C.fido_dev_close(d.dev) + C.fido_dev_free(&d.dev) + d.dev = nil // Likely already freed by fido_dev_free, nil again to be safe. + if cErr != C.FIDO_OK { + return fmt.Errorf("close device: %w", errFromCode(cErr)) + } + + return nil } // Cancel an action. func (d *Device) Cancel() error { - d.Lock() - defer d.Unlock() - if d.dev != nil { - if cErr := C.fido_dev_cancel(d.dev); cErr != C.FIDO_OK { - return errors.Wrap(errFromCode(cErr), "failed to cancel") - } + if d == nil { + return nil } + + d.mu.Lock() + defer d.mu.Unlock() + + if d.dev == nil { + return nil + } + + if cErr := C.fido_dev_cancel(d.dev); cErr != C.FIDO_OK { + return errors.Wrap(errFromCode(cErr), "failed to cancel") + } + + return nil +} + +// SetTimeout informs libfido2 to not block for more than timeout (in +// millisecond precision) when communicating with the device. +// Timed out functions fail with FIDO_ERR_RX. +func (d *Device) SetTimeout(timeout time.Duration) error { + dev, err := d.getDevice() + if err != nil { + return err + } + + ms := C.int(timeout.Milliseconds()) + if cErr := C.fido_dev_set_timeout(dev, ms); cErr != C.FIDO_OK { + return fmt.Errorf("set timeout: %w", errFromCode(cErr)) + } + return nil } // CTAPHIDInfo ... func (d *Device) CTAPHIDInfo() (*HIDInfo, error) { - dev, err := d.open() + dev, err := d.getDevice() if err != nil { return nil, err } - defer d.close(dev) protocol := C.fido_dev_protocol(dev) major := C.fido_dev_major(dev) @@ -302,11 +369,10 @@ func (d *Device) CTAPHIDInfo() (*HIDInfo, error) { // IsFIDO2 returns true if device supports FIDO2. func (d *Device) IsFIDO2() (bool, error) { - dev, err := d.open() + dev, err := d.getDevice() if err != nil { return false, err } - defer d.close(dev) isFIDO2 := bool(C.fido_dev_is_fido2(dev)) return isFIDO2, nil @@ -314,11 +380,10 @@ func (d *Device) IsFIDO2() (bool, error) { // Type returns device type. func (d *Device) Type() (DeviceType, error) { - dev, err := d.open() + dev, err := d.getDevice() if err != nil { return UnknownDevice, err } - defer d.close(dev) isFIDO2 := bool(C.fido_dev_is_fido2(dev)) if isFIDO2 { @@ -330,11 +395,10 @@ func (d *Device) Type() (DeviceType, error) { // Info represents authenticatorGetInfo (0x04). // https://fidoalliance.org/specs/fido2/fido-client-to-authenticator-protocol-v2.1-rd-20191217.html#authenticatorGetInfo func (d *Device) Info() (*DeviceInfo, error) { - dev, err := d.open() + dev, err := d.getDevice() if err != nil { return nil, err } - defer d.close(dev) isFIDO2 := bool(C.fido_dev_is_fido2(dev)) if !isFIDO2 { @@ -455,11 +519,10 @@ func (d *Device) MakeCredential( return nil, errors.Errorf("no user name specified") } - dev, err := d.open() + dev, err := d.getDevice() if err != nil { return nil, err } - defer d.close(dev) cCred := C.fido_cred_new() defer C.fido_cred_free(&cCred) @@ -507,6 +570,7 @@ func (d *Device) MakeCredential( } if cErr := C.fido_dev_make_cred(dev, cCred, cStringOrNil(pin)); cErr != C.FIDO_OK { + d.Cancel() return nil, errors.Wrap(errFromCode(cErr), "failed to make credential") } @@ -590,11 +654,10 @@ func credential(cCred *C.fido_cred_t) (*Credential, error) { // SetPIN ... func (d *Device) SetPIN(pin string, old string) error { - dev, err := d.open() + dev, err := d.getDevice() if err != nil { return err } - defer d.close(dev) if cErr := C.fido_dev_set_pin(dev, C.CString(pin), cStringOrNil(old)); cErr != C.FIDO_OK { return errors.Wrap(errFromCode(cErr), "failed to set pin") @@ -609,11 +672,10 @@ func (d *Device) SetPIN(pin string, old string) error { // seconds after power-up, and ErrActionTimeout if the user fails to confirm the reset by touching the key within 30 // seconds. func (d *Device) Reset() error { - dev, err := d.open() + dev, err := d.getDevice() if err != nil { return err } - defer d.close(dev) if cErr := C.fido_dev_reset(dev); cErr != C.FIDO_OK { return errors.Wrap(errFromCode(cErr), "failed to reset") @@ -623,11 +685,10 @@ func (d *Device) Reset() error { // RetryCount ... func (d *Device) RetryCount() (int, error) { - dev, err := d.open() + dev, err := d.getDevice() if err != nil { return 0, err } - defer d.close(dev) var retryCount C.int if cErr := C.fido_dev_get_retry_count(dev, &retryCount); cErr != C.FIDO_OK { @@ -663,11 +724,10 @@ func (d *Device) Assertion( return nil, errors.Errorf("no rpID specified") } - dev, err := d.open() + dev, err := d.getDevice() if err != nil { return nil, err } - defer d.close(dev) cAssert := C.fido_assert_new() defer C.fido_assert_free(&cAssert) @@ -710,6 +770,7 @@ func (d *Device) Assertion( // Get assertion if cErr := C.fido_dev_get_assert(dev, cAssert, cStringOrNil(pin)); cErr != C.FIDO_OK { + d.Cancel() return nil, errors.Wrapf(errFromCode(cErr), "failed to get assertion") } @@ -765,11 +826,11 @@ func (d *Device) CredentialsInfo(pin string) (*CredentialsInfo, error) { if pin == "" { return nil, errors.Errorf("pin is required") } - dev, err := d.open() + + dev, err := d.getDevice() if err != nil { return nil, err } - defer d.close(dev) cCredMeta := C.fido_credman_metadata_new() defer C.fido_credman_metadata_free(&cCredMeta) @@ -792,11 +853,11 @@ func (d *Device) Credentials(rpID string, pin string) ([]*Credential, error) { if rpID == "" { return nil, errors.Errorf("no rpID specified") } - dev, err := d.open() + + dev, err := d.getDevice() if err != nil { return nil, err } - defer d.close(dev) cRK := C.fido_credman_rk_new() defer C.fido_credman_rk_free(&cRK) @@ -820,11 +881,10 @@ func (d *Device) Credentials(rpID string, pin string) ([]*Credential, error) { // DeleteCredential deletes a resident credential (if credMgmt is supported). func (d *Device) DeleteCredential(credID []byte, pin string) error { - dev, err := d.open() + dev, err := d.getDevice() if err != nil { return err } - defer d.close(dev) if cErr := C.fido_credman_del_dev_rk(dev, cBytes(credID), cLen(credID), cStringOrNil(pin)); cErr != C.FIDO_OK { return errors.Wrap(errFromCode(cErr), "failed to delete key") @@ -834,11 +894,10 @@ func (d *Device) DeleteCredential(credID []byte, pin string) error { // RelyingParties ... func (d *Device) RelyingParties(pin string) ([]*RelyingParty, error) { - dev, err := d.open() + dev, err := d.getDevice() if err != nil { return nil, err } - defer d.close(dev) cRP := C.fido_credman_rp_new() defer C.fido_credman_rp_free(&cRP) @@ -871,11 +930,10 @@ func plural(n uint8) string { // BioEnrollment starts a bio-enabled device enrollment func (d *Device) BioEnroll(pin string) error { - dev, err := d.open() + dev, err := d.getDevice() if err != nil { return err } - defer d.close(dev) template := C.fido_bio_template_new() if template == nil { @@ -936,11 +994,10 @@ func goBioTemplate(tempalateArray *C.fido_bio_template_array_t, idx C.size_t) (* // BioList lists all bio templates. func (d *Device) BioList(pin string) ([]BioTemplate, error) { - dev, err := d.open() + dev, err := d.getDevice() if err != nil { return nil, err } - defer d.close(dev) templateArray := C.fido_bio_template_array_new() if templateArray == nil { @@ -971,11 +1028,10 @@ func (d *Device) BioList(pin string) ([]BioTemplate, error) { // BioDelete deletes a bio template. func (d *Device) BioDelete(pin, templateId string) error { - dev, err := d.open() + dev, err := d.getDevice() if err != nil { return err } - defer d.close(dev) template := C.fido_bio_template_new() if template == nil { @@ -1000,11 +1056,10 @@ func (d *Device) BioDelete(pin, templateId string) error { // BioSetTemplateName sets the name of template with templateId. func (d *Device) BioSetTemplateName(pin, templateId, name string) error { - dev, err := d.open() + dev, err := d.getDevice() if err != nil { return err } - defer d.close(dev) template := C.fido_bio_template_new() if template == nil { diff --git a/fido2_test.go b/fido2_test.go index d2ba865..120d89d 100644 --- a/fido2_test.go +++ b/fido2_test.go @@ -2,6 +2,7 @@ package libfido2_test import ( "log" + "os" "testing" "time" @@ -16,6 +17,10 @@ import ( // TODO: It's important tests are run serially (a device can't handle concurrent requests). +func getPIN() string { + return os.Getenv("FIDO2_PIN") +} + func TestDevices(t *testing.T) { locs, err := libfido2.DeviceLocations() require.NoError(t, err) @@ -24,6 +29,7 @@ func TestDevices(t *testing.T) { for _, loc := range locs { device, err := libfido2.NewDevice(loc.Path) require.NoError(t, err) + defer device.Close() isFIDO2, err := device.IsFIDO2() require.NoError(t, err) @@ -59,13 +65,14 @@ func TestDeviceAssertionCancel(t *testing.T) { if err != nil { log.Fatal(err) } + defer device.Close() cdh := libfido2.RandBytes(32) userID := libfido2.RandBytes(32) salt := libfido2.RandBytes(32) - pin := "12345" + pin := getPIN() - t.Logf("Make credential\n") + t.Log("Touch your device") attest, err := device.MakeCredential( cdh, libfido2.RelyingParty{ @@ -86,10 +93,11 @@ func TestDeviceAssertionCancel(t *testing.T) { go func() { time.Sleep(time.Second * 2) - t.Logf("Cancel") + t.Log("Cancel") device.Cancel() }() + t.Log("DON'T touch your device") _, err = device.Assertion( "keys.pub", cdh,