pkg/provider/googleapps/u2f.go (118 lines of code) (raw):
package googleapps
import (
"encoding/base64"
"encoding/json"
"fmt"
"time"
u2fhost "github.com/marshallbrekka/go-u2fhost"
"github.com/pkg/errors"
)
const (
maxOpenRetries = 10
retryDelay = 200 * time.Millisecond
)
var (
errNoDeviceFound = fmt.Errorf("no U2F devices found. device might not be plugged in")
)
// U2FClient represents a challenge and the device used to respond
type U2FClient struct {
ChallengeNonce string
AppID string
Facet string
Device u2fhost.Device
KeyHandle string
}
// DeviceFinder is used to mock out finding devices
type DeviceFinder interface {
findDevice() (u2fhost.Device, error)
}
// U2FDevice is used to support mocking this device with mockery https://github.com/vektra/mockery/issues/210#issuecomment-485026348
type U2FDevice interface {
u2fhost.Device
}
// NewU2FClient returns a new initialized FIDO1-based U2F client, representing a single device
func NewU2FClient(challengeNonce, appID, facet, keyHandle string, deviceFinder DeviceFinder) (*U2FClient, error) {
var device u2fhost.Device
var err error
retryCount := 0
for retryCount < maxOpenRetries {
device, err = deviceFinder.findDevice()
if err != nil {
if err == errNoDeviceFound {
return nil, err
}
retryCount++
time.Sleep(retryDelay)
continue
}
return &U2FClient{
Device: device,
ChallengeNonce: challengeNonce,
AppID: appID,
KeyHandle: keyHandle,
Facet: facet,
}, nil
}
return nil, fmt.Errorf("failed to create client: %s. exceeded max retries of %d", err, maxOpenRetries)
}
// ChallengeU2F takes a U2FClient and returns a signed assertion to send to Google
func (d *U2FClient) ChallengeU2F() (string, error) {
if d.Device == nil {
return "", errors.New("No Device Found")
}
request := &u2fhost.AuthenticateRequest{
Challenge: b64Safe(d.ChallengeNonce),
Facet: d.Facet,
AppId: d.AppID,
KeyHandle: b64Safe(d.KeyHandle),
}
// do the change
prompted := false
timeout := time.After(time.Second * 25)
interval := time.NewTicker(time.Millisecond * 250)
defer d.Device.Close()
defer interval.Stop()
for {
select {
case <-timeout:
return "", errors.New("Failed to get authentication response after 25 seconds")
case <-interval.C:
response, err := d.Device.Authenticate(request)
if err == nil {
responseJSON, err := json.Marshal(response)
if err != nil {
return "", err
}
fmt.Printf(" ==> Touch accepted. Proceeding with authentication\n")
return string(responseJSON), nil
}
switch err.(type) {
case *u2fhost.TestOfUserPresenceRequiredError:
if !prompted {
fmt.Printf("\nTouch the flashing U2F device to authenticate...\n")
prompted = true
}
default:
return "", err
}
}
}
}
// U2FDeviceFinder returns a U2F device
type U2FDeviceFinder struct{}
func (*U2FDeviceFinder) findDevice() (u2fhost.Device, error) {
var err error
allDevices := u2fhost.Devices()
if len(allDevices) == 0 {
return nil, errNoDeviceFound
}
for i, device := range allDevices {
err = device.Open()
if err != nil {
device.Close()
continue
}
return allDevices[i], nil
}
return nil, fmt.Errorf("failed to open fido U2F device: %s", err)
}
func b64Safe(data string) string {
val, err := base64.StdEncoding.DecodeString(data)
if err != nil {
panic(err)
}
return base64.RawURLEncoding.EncodeToString(val)
}