diff --git a/go.mod b/go.mod index fb5bcb2bdf1bc..7f05fd56a7e5e 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,8 @@ require ( github.com/docker/docker v17.12.0-ce-rc1.0.20180721085148-1ef1cc838816+incompatible github.com/docker/spdystream v0.0.0-20170912183627-bc6354cbbc29 // indirect github.com/dustin/go-humanize v1.0.0 + github.com/flynn/hid v0.0.0-20190502022136-f1b9b6cc019a // indirect + github.com/flynn/u2f v0.0.0-20180613185708-15554eb68e5d github.com/fsouza/fake-gcs-server v1.11.6 github.com/ghodss/yaml v1.0.0 github.com/gizak/termui v0.0.0-20190224181052-63c2a0d70943 diff --git a/go.sum b/go.sum index ad20d1bb34747..c5b80eb023a4a 100644 --- a/go.sum +++ b/go.sum @@ -159,6 +159,10 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/flynn/hid v0.0.0-20190502022136-f1b9b6cc019a h1:fsyWnwbywFpHJS4T55vDW+UUeWP2WomJbB45/jf4If4= +github.com/flynn/hid v0.0.0-20190502022136-f1b9b6cc019a/go.mod h1:Osz+xPHFsGWK9kZCEVcwXazcF/CHjscCVZosNFgwUIY= +github.com/flynn/u2f v0.0.0-20180613185708-15554eb68e5d h1:2D6Rp/MRcrKnRFr7kfgBOJnJPFN0jPfc36ggct5MaK0= +github.com/flynn/u2f v0.0.0-20180613185708-15554eb68e5d/go.mod h1:shcCQPgKtaJz4obqb6Si031WgtSrW+Tj+ZLq/mRNrM8= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsouza/fake-gcs-server v1.11.6 h1:xRjuDnNG1xqiHyw+K15yRlWGmzgvCd4q3KnVh9duYEE= diff --git a/lib/auth/u2f/authenticate.go b/lib/auth/u2f/authenticate.go index 722b36f62a7a0..ccf0587e98e19 100644 --- a/lib/auth/u2f/authenticate.go +++ b/lib/auth/u2f/authenticate.go @@ -18,12 +18,14 @@ package u2f import ( "context" + "crypto/sha256" + "encoding/base64" "encoding/json" + "errors" "fmt" - "io" - "os/exec" "time" + "github.com/flynn/u2f/u2ftoken" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" "github.com/mailgun/ttlmap" @@ -148,79 +150,89 @@ func AuthenticateInit(ctx context.Context, params AuthenticateInitParams) (*Auth // // Note: the caller must prompt the user to tap the U2F token. func AuthenticateSignChallenge(ctx context.Context, facet string, challenges ...AuthenticateChallenge) (*AuthenticateChallengeResponse, error) { - // TODO(awly): mfa: u2f-host fails when running multiple processes in - // parallel. This means that with u2f-host, teleport can't authenticate - // using multiple U2F devices. Replace u2f-host with a Go library that can - // prompt multiple devices at once. - c := challenges[0] + base64 := base64.URLEncoding.WithPadding(base64.NoPadding) - // Pass the JSON-encoded data undecoded to the u2f-host binary - challengeRaw, err := json.Marshal(c) - if err != nil { - return nil, trace.Wrap(err) + // Convert from JS-centric github.com/tstranex/u2f format to a more + // wire-centric github.com/flynn/u2f format. + type authenticateRequest struct { + orig AuthenticateChallenge + clientData []byte + converted u2ftoken.AuthenticateRequest } - cmd := exec.CommandContext(ctx, "u2f-host", "-aauthenticate", "-o", facet) - stdin, err := cmd.StdinPipe() - if err != nil { - return nil, trace.Wrap(err) - } - stdout, err := cmd.StdoutPipe() - if err != nil { - return nil, trace.Wrap(err) - } - stderr, err := cmd.StderrPipe() - if err != nil { - return nil, trace.Wrap(err) + authRequests := make([]authenticateRequest, 0, len(challenges)) + for _, chal := range challenges { + kh, err := base64.DecodeString(chal.KeyHandle) + if err != nil { + return nil, trace.BadParameter("invalid KeyHandle %q in AuthenticateChallenge: %v", chal.KeyHandle, err) + } + cd := u2f.ClientData{ + Challenge: chal.Challenge, + Origin: facet, + Typ: "navigator.id.getAssertion", + } + cdRaw, err := json.Marshal(cd) + if err != nil { + return nil, trace.Wrap(err) + } + chHash := sha256.Sum256(cdRaw) + appHash := sha256.Sum256([]byte(chal.AppID)) + authRequests = append(authRequests, authenticateRequest{ + orig: chal, + clientData: cdRaw, + converted: u2ftoken.AuthenticateRequest{ + KeyHandle: kh, + Challenge: chHash[:], + Application: appHash[:], + }, + }) } - if err := cmd.Start(); err != nil { - return nil, trace.Wrap(err) - } - defer func() { - // If we returned before cmd.Wait was called, clean up the spawned - // process. ProcessState will be empty until cmd.Wait or cmd.Run - // return. - if cmd.ProcessState == nil || !cmd.ProcessState.Exited() { - cmd.Process.Kill() + var matchedAuthReq *authenticateRequest + var authResp *u2ftoken.AuthenticateResponse + if err := pollLocalDevices(ctx, func(t *u2ftoken.Token) error { + var errs []error + for _, req := range authRequests { + if err := t.CheckAuthenticate(req.converted); err != nil { + if err != u2ftoken.ErrUnknownKeyHandle { + errs = append(errs, trace.Wrap(err)) + } + continue + } + res, err := t.Authenticate(req.converted) + if err == u2ftoken.ErrPresenceRequired { + continue + } else if err != nil { + errs = append(errs, trace.Wrap(err)) + continue + } + matchedAuthReq = &req + authResp = res + return nil } - }() - _, err = stdin.Write(challengeRaw) - stdin.Close() - if err != nil { + if len(errs) > 0 { + return trace.NewAggregate(errs...) + } + return errAuthNoKeyOrUserPresence + }); err != nil { return nil, trace.Wrap(err) } - - // The origin URL is passed back base64-encoded and the keyHandle is passed back as is. - // A very long proxy hostname or keyHandle can overflow a fixed-size buffer. - signResponseLen := 500 + len(challengeRaw) + len(facet)*4/3 - signResponseBuf := make([]byte, signResponseLen) - signResponseLen, err = io.ReadFull(stdout, signResponseBuf) - // unexpected EOF means we have read the data completely. - if err == nil { - return nil, trace.LimitExceeded("u2f sign response exceeded buffer size") - } - - // Read error message (if any). 100 bytes is more than enough for any error message u2f-host outputs - errMsgBuf := make([]byte, 100) - errMsgLen, err := io.ReadFull(stderr, errMsgBuf) - if err == nil { - return nil, trace.LimitExceeded("u2f error message exceeded buffer size") + if authResp == nil || matchedAuthReq == nil { + // This shouldn't happen if the loop above works correctly, but check + // just in case. + return nil, trace.CompareFailed("failed getting an authentication response from a U2F device") } - err = cmd.Wait() - if err != nil { - return nil, trace.AccessDenied("u2f-host returned error: " + string(errMsgBuf[:errMsgLen])) - } else if signResponseLen == 0 { - return nil, trace.NotFound("u2f-host returned no error and no sign response") - } - - var resp AuthenticateChallengeResponse - if err := json.Unmarshal(signResponseBuf[:signResponseLen], &resp); err != nil { - return nil, trace.Wrap(err) - } - return &resp, nil + // Convert back from github.com/flynn/u2f to github.com/tstranex/u2f + // format. + return &AuthenticateChallengeResponse{ + KeyHandle: matchedAuthReq.orig.KeyHandle, + SignatureData: base64.EncodeToString(authResp.RawResponse), + ClientData: base64.EncodeToString(matchedAuthReq.clientData), + }, nil } +var errAuthNoKeyOrUserPresence = errors.New("no U2F keys for the challenge found or user hasn't tapped the key yet") + // AuthenticateVerifyParams are the parameters for verifying the // AuthenticationChallengeResponse. type AuthenticateVerifyParams struct { diff --git a/lib/auth/u2f/device.go b/lib/auth/u2f/device.go index 918d3fa26ef9e..018c014ec0e0f 100644 --- a/lib/auth/u2f/device.go +++ b/lib/auth/u2f/device.go @@ -2,8 +2,7 @@ Copyright 2021 Gravitational, Inc. Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -22,7 +21,10 @@ import ( "crypto/x509" "time" + "github.com/flynn/u2f/u2fhid" + "github.com/flynn/u2f/u2ftoken" "github.com/gravitational/trace" + "github.com/sirupsen/logrus" "github.com/tstranex/u2f" "github.com/gravitational/teleport/api/types" @@ -97,3 +99,70 @@ func decodeDevicePubKey(d *types.U2FDevice) (*ecdsa.PublicKey, error) { } return pubKey, nil } + +// pollLocalDevices calls fn against all local U2F devices until one succeeds +// (fn returns nil) or until the context is cancelled. +func pollLocalDevices(ctx context.Context, fn func(t *u2ftoken.Token) error) error { + tick := time.NewTicker(200 * time.Millisecond) + defer tick.Stop() + for { + err := foreachLocalDevice(fn) + if err == nil { + return nil + } + // Don't spam the logs while we're waiting for a key to be plugged + // in or tapped. + if err != errAuthNoKeyOrUserPresence { + logrus.WithError(err).Debugf("Error polling U2F devices for registration") + } + + select { + case <-ctx.Done(): + return trace.Wrap(ctx.Err()) + case <-tick.C: + } + } +} + +// foreachLocalDevice runs fn against each currently available U2F device. It +// stops when fn returns nil or when finished iterating against the available +// devices. +// +// You most likely want to call pollLocalDevices instead. +func foreachLocalDevice(fn func(t *u2ftoken.Token) error) error { + // Note: we fetch and open all devices on every polling iteration on + // purpose. This will handle the device that a user inserts after the + // polling has started. + devices, err := u2fhid.Devices() + if err != nil { + return trace.Wrap(err) + } + var errs []error + for _, d := range devices { + dev, err := u2fhid.Open(d) + if err != nil { + errs = append(errs, trace.Wrap(err)) + continue + } + // There are usually 1-2 devices plugged in, so deferring closing all + // of them until the end of function is not too wasteful. + defer dev.Close() + + t := u2ftoken.NewToken(dev) + // fn is usually t.Authenticate or t.Register. Both methods are + // non-blocking - the U2F device returns u2ftoken.ErrPresenceRequired + // immediately, unless the user has recently tapped the device. + if err := fn(t); err == u2ftoken.ErrPresenceRequired || err == errAuthNoKeyOrUserPresence { + continue + } else if err != nil { + errs = append(errs, trace.Wrap(err)) + continue + } + return nil + } + + if len(errs) > 0 { + return trace.NewAggregate(errs...) + } + return errAuthNoKeyOrUserPresence +} diff --git a/lib/auth/u2f/register.go b/lib/auth/u2f/register.go index 4f9f684a80d72..9eb790afa373c 100644 --- a/lib/auth/u2f/register.go +++ b/lib/auth/u2f/register.go @@ -18,14 +18,14 @@ package u2f import ( "context" + "crypto/sha256" + "encoding/base64" "encoding/json" - "io" - "os/exec" + "github.com/flynn/u2f/u2ftoken" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" "github.com/mailgun/ttlmap" - "github.com/sirupsen/logrus" "github.com/tstranex/u2f" "github.com/gravitational/teleport/api/types" @@ -136,73 +136,45 @@ func RegisterInit(params RegisterInitParams) (*RegisterChallenge, error) { // // Note: the caller must prompt the user to tap the U2F token. func RegisterSignChallenge(ctx context.Context, c RegisterChallenge, facet string) (*RegisterChallengeResponse, error) { - // Pass the JSON-encoded data undecoded to the u2f-host binary - challengeRaw, err := json.Marshal(c) + // Convert from JS-centric github.com/tstranex/u2f format to a more + // wire-centric github.com/flynn/u2f format. + appHash := sha256.Sum256([]byte(c.AppID)) + cd := u2f.ClientData{ + Challenge: c.Challenge, + Origin: facet, + Typ: "navigator.id.finishEnrollment", + } + cdRaw, err := json.Marshal(cd) if err != nil { return nil, trace.Wrap(err) } - cmd := exec.CommandContext(ctx, "u2f-host", "--action=register", "--origin="+facet) - stdin, err := cmd.StdinPipe() - if err != nil { - return nil, trace.Wrap(err) - } - stdout, err := cmd.StdoutPipe() - if err != nil { - return nil, trace.Wrap(err) - } - stderr, err := cmd.StderrPipe() - if err != nil { - return nil, trace.Wrap(err) + chHash := sha256.Sum256(cdRaw) + regReq := u2ftoken.RegisterRequest{ + Challenge: chHash[:], + Application: appHash[:], } - if err := cmd.Start(); err != nil { + var regRespRaw []byte + if err := pollLocalDevices(ctx, func(t *u2ftoken.Token) error { + var err error + regRespRaw, err = t.Register(regReq) + return err + }); err != nil { return nil, trace.Wrap(err) } - defer func() { - // If we returned before cmd.Wait was called, clean up the spawned - // process. ProcessState will be empty until cmd.Wait or cmd.Run - // return. - if cmd.ProcessState == nil || !cmd.ProcessState.Exited() { - if err := cmd.Process.Kill(); err != nil { - logrus.WithError(err).Warningf("Failed cleaning up the spawned u2f-host process (PID %v)", cmd.ProcessState.Pid()) - } - } - }() - _, err = stdin.Write(challengeRaw) - stdin.Close() - if err != nil { - return nil, trace.Wrap(err) + if len(regRespRaw) == 0 { + // This shouldn't happen if the loop above works correctly, but check + // just in case. + return nil, trace.CompareFailed("failed getting a registration response from a U2F device") } - // 16kB ought to be enough for anybody. - signResponseBuf := make([]byte, 16*1024) - n, err := io.ReadFull(stdout, signResponseBuf) - // unexpected EOF means we have read the data completely. - if err == nil { - return nil, trace.LimitExceeded("u2f sign response exceeded buffer size") - } - signResponse := signResponseBuf[:n] - - // Read error message (if any). 1kB is more than enough for any error message u2f-host outputs - errMsgBuf := make([]byte, 1024) - n, err = io.ReadFull(stderr, errMsgBuf) - if err == nil { - return nil, trace.LimitExceeded("u2f error message exceeded buffer size") - } - errMsg := string(errMsgBuf[:n]) - - err = cmd.Wait() - if err != nil { - return nil, trace.AccessDenied("u2f-host returned error: %s", errMsg) - } else if len(signResponse) == 0 { - return nil, trace.NotFound("u2f-host returned no error and no sign response") - } - - var resp RegisterChallengeResponse - if err := json.Unmarshal(signResponse, &resp); err != nil { - return nil, trace.Wrap(err) - } - return &resp, nil + // Convert back from github.com/flynn/u2f to github.com/tstranex/u2f + // format. + base64 := base64.URLEncoding.WithPadding(base64.NoPadding) + return &RegisterChallengeResponse{ + RegistrationData: base64.EncodeToString(regRespRaw), + ClientData: base64.EncodeToString(cdRaw), + }, nil } // RegisterInitParams are the parameters for verifying the diff --git a/tool/tsh/mfa.go b/tool/tsh/mfa.go index 04437222993dc..0d03f217d9a55 100644 --- a/tool/tsh/mfa.go +++ b/tool/tsh/mfa.go @@ -43,11 +43,9 @@ type mfaCommands struct { func newMFACommand(app *kingpin.Application) mfaCommands { mfa := app.Command("mfa", "Manage multi-factor authentication (MFA) devices.") return mfaCommands{ - ls: newMFALSCommand(mfa), - add: &mfaAddCommand{ - CmdClause: mfa.Command("add", "Add a new MFA device"), - }, - rm: newMFARemoveCommand(mfa), + ls: newMFALSCommand(mfa), + add: newMFAAddCommand(mfa), + rm: newMFARemoveCommand(mfa), } } @@ -126,34 +124,50 @@ func printMFADevices(devs []*types.MFADevice, verbose bool) { type mfaAddCommand struct { *kingpin.CmdClause + devName string + devType string +} + +func newMFAAddCommand(parent *kingpin.CmdClause) *mfaAddCommand { + c := &mfaAddCommand{ + CmdClause: parent.Command("add", "Add a new MFA device"), + } + c.Flag("name", "Name of the new MFA device").StringVar(&c.devName) + c.Flag("type", "Type of the new MFA device (TOTP or U2F)").StringVar(&c.devType) + return c } func (c *mfaAddCommand) run(cf *CLIConf) error { - devType, err := prompt.PickOne(os.Stdout, os.Stdin, "Choose device type", []string{"TOTP", "U2F"}) - if err != nil { - return trace.Wrap(err) + if c.devType == "" { + var err error + c.devType, err = prompt.PickOne(os.Stdout, os.Stdin, "Choose device type", []string{"TOTP", "U2F"}) + if err != nil { + return trace.Wrap(err) + } } var typ proto.AddMFADeviceRequestInit_DeviceType - switch devType { + switch strings.ToUpper(c.devType) { case "TOTP": typ = proto.AddMFADeviceRequestInit_TOTP case "U2F": typ = proto.AddMFADeviceRequestInit_U2F default: - // prompt.PickOne should catch this for us. - return trace.BadParameter("unknown device type %q", devType) + return trace.BadParameter("unknown device type %q, must be either TOTP or U2F", c.devType) } - devName, err := prompt.Input(os.Stdout, os.Stdin, "Enter device name") - if err != nil { - return trace.Wrap(err) + if c.devName == "" { + var err error + c.devName, err = prompt.Input(os.Stdout, os.Stdin, "Enter device name") + if err != nil { + return trace.Wrap(err) + } } - devName = strings.TrimSpace(devName) - if devName == "" { + c.devName = strings.TrimSpace(c.devName) + if c.devName == "" { return trace.BadParameter("device name can not be empty") } - dev, err := c.addDeviceRPC(cf, devName, typ) + dev, err := c.addDeviceRPC(cf, c.devName, typ) if err != nil { return trace.Wrap(err) } @@ -369,6 +383,7 @@ func promptRegisterChallenge(ctx context.Context, proxyAddr string, c *proto.MFA func promptTOTPRegisterChallenge(c *proto.TOTPRegisterChallenge) (*proto.MFARegisterResponse, error) { // TODO(awly): mfa: use OS-specific image viewer to show a QR code. + // TODO(awly): mfa: print OTP URL fmt.Println("Open your TOTP app and create a new manual entry with these fields:") fmt.Printf("Name: %s\n", c.Account) fmt.Printf("Issuer: %s\n", c.Issuer) diff --git a/vendor/github.com/flynn/hid/.gitignore b/vendor/github.com/flynn/hid/.gitignore new file mode 100644 index 0000000000000..c38fa4e005685 --- /dev/null +++ b/vendor/github.com/flynn/hid/.gitignore @@ -0,0 +1,2 @@ +.idea +*.iml diff --git a/vendor/github.com/flynn/hid/LICENSE b/vendor/github.com/flynn/hid/LICENSE new file mode 100644 index 0000000000000..862b0ddcd7fea --- /dev/null +++ b/vendor/github.com/flynn/hid/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Florian Sundermann + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/flynn/hid/README.md b/vendor/github.com/flynn/hid/README.md new file mode 100644 index 0000000000000..98c04f790133e --- /dev/null +++ b/vendor/github.com/flynn/hid/README.md @@ -0,0 +1,53 @@ +# HID + +A Go package to access Human Interface Devices. The platform specific parts of +this package are heavily based on [Signal 11's HIDAPI](https://github.com/signal11/hidapi). + +## Supported operating systems + +The following operating systems are supported targets +(as used by [*$GOOS* environment variable](https://golang.org/doc/install/source#environment)) + +* darwin (uses native IOKit framework) +* linux (uses hidraw) +* windows (uses native Windows HID library) + +## Known quirks for building on Windows 64-bit + +For building this HID package, you need to have a gcc.exe in your *%PATH%* environment variable. +There are two tested GCC toolchains: [tdm-gcc](http://tdm-gcc.tdragon.net/) +and [mingw-w64](http://mingw-w64.yaxm.org/). At the moment (March 2015), both toolchains +are missing some declarations in header files, which will result in the following error message, +when running the ```go build```: + +``` +D:\projects.go\src\github.com\boombuler\hid> go build -v -work +WORK=C:\Users\xxx\AppData\Local\Temp\go-build011586055 +github.com/boombuler/hid +# github.com/boombuler/hid +could not determine kind of name for C.HidD_FreePreparsedData +could not determine kind of name for C.HidD_GetPreparsedData +``` + +The solutions is simple: just add these four lines to your gcc toolchain header file ```hidsdi.h``` +````C +/* http://msdn.microsoft.com/en-us/library/windows/hardware/ff538893(v=vs.85).aspx */ +HIDAPI BOOLEAN NTAPI HidD_FreePreparsedData(PHIDP_PREPARSED_DATA PreparsedData); + +/* http://msdn.microsoft.com/en-us/library/windows/hardware/ff539679(v=vs.85).aspx */ +HIDAPI BOOLEAN NTAPI HidD_GetPreparsedData(HANDLE HidDeviceObject, PHIDP_PREPARSED_DATA *PreparsedData); +```` +Depending on your gcc toolchain installation folder, the files are located in + +``` C:\TDM-GCC-64\x86_64-w64-mingw32\include\hidsdi.h ``` + +or + +``` c:\mingw-w64\x86_64-4.9.2-win32-seh-rt_v3-rev1\mingw64\x86_64-w64-mingw32\include\hidsdi.h ``` + +After patching the header file, this package will compile. +Future releases of the gcc toolchains will surely fix this issue. + +## License + +[![License: MIT](https://img.shields.io/:license-MIT-blue.svg)](http://opensource.org/licenses/MIT) diff --git a/vendor/github.com/flynn/hid/hid.go b/vendor/github.com/flynn/hid/hid.go new file mode 100644 index 0000000000000..2cfb13bd281ea --- /dev/null +++ b/vendor/github.com/flynn/hid/hid.go @@ -0,0 +1,39 @@ +// Package hid provides access to Human Interface Devices. +package hid + +// DeviceInfo provides general information about a device. +type DeviceInfo struct { + // Path contains a platform-specific device path which is used to identify the device. + Path string + + VendorID uint16 + ProductID uint16 + VersionNumber uint16 + Manufacturer string + Product string + + UsagePage uint16 + Usage uint16 + + InputReportLength uint16 + OutputReportLength uint16 +} + +// A Device provides access to a HID device. +type Device interface { + // Close closes the device and associated resources. + Close() + + // Write writes an output report to device. The first byte must be the + // report number to write, zero if the device does not use numbered reports. + Write([]byte) error + + // ReadCh returns a channel that will be sent input reports from the device. + // If the device uses numbered reports, the first byte will be the report + // number. + ReadCh() <-chan []byte + + // ReadError returns the read error, if any after the channel returned from + // ReadCh has been closed. + ReadError() error +} diff --git a/vendor/github.com/flynn/hid/hid_darwin.go b/vendor/github.com/flynn/hid/hid_darwin.go new file mode 100644 index 0000000000000..af81ed18a887e --- /dev/null +++ b/vendor/github.com/flynn/hid/hid_darwin.go @@ -0,0 +1,413 @@ +package hid + +/* +#cgo LDFLAGS: -L . -L/usr/local/lib -framework CoreFoundation -framework IOKit + +#include +#include + + +static inline CFIndex cfstring_utf8_length(CFStringRef str, CFIndex *need) { + CFIndex n, usedBufLen; + CFRange rng = CFRangeMake(0, CFStringGetLength(str)); + + return CFStringGetBytes(str, rng, kCFStringEncodingUTF8, 0, 0, NULL, 0, need); +} + +void deviceUnplugged(IOHIDDeviceRef osd, IOReturn ret, void *dev); + +void reportCallback(void *context, IOReturn result, void *sender, IOHIDReportType report_type, uint32_t report_id, uint8_t *report, CFIndex report_length); + +*/ +import "C" + +import ( + "errors" + "fmt" + "reflect" + "runtime" + "sync" + "unsafe" +) + +func ioReturnToErr(ret C.IOReturn) error { + switch ret { + case C.kIOReturnSuccess: + return nil + case C.kIOReturnError: + return errors.New("hid: general error") + case C.kIOReturnNoMemory: + return errors.New("hid: can't allocate memory") + case C.kIOReturnNoResources: + return errors.New("hid: resource shortage") + case C.kIOReturnIPCError: + return errors.New("hid: error during IPC") + case C.kIOReturnNoDevice: + return errors.New("hid: no such device") + case C.kIOReturnNotPrivileged: + return errors.New("hid: privilege violation") + case C.kIOReturnBadArgument: + return errors.New("hid: invalid argument") + case C.kIOReturnLockedRead: + return errors.New("hid: device read locked") + case C.kIOReturnLockedWrite: + return errors.New("hid: device write locked") + case C.kIOReturnExclusiveAccess: + return errors.New("hid: exclusive access and device already open") + case C.kIOReturnBadMessageID: + return errors.New("hid: sent/received messages had different msg_id") + case C.kIOReturnUnsupported: + return errors.New("hid: unsupported function") + case C.kIOReturnVMError: + return errors.New("hid: misc. VM failure") + case C.kIOReturnInternalError: + return errors.New("hid: internal error") + case C.kIOReturnIOError: + return errors.New("hid: general I/O error") + case C.kIOReturnCannotLock: + return errors.New("hid: can't acquire lock") + case C.kIOReturnNotOpen: + return errors.New("hid: device not open") + case C.kIOReturnNotReadable: + return errors.New("hid: read not supported") + case C.kIOReturnNotWritable: + return errors.New("hid: write not supported") + case C.kIOReturnNotAligned: + return errors.New("hid: alignment error") + case C.kIOReturnBadMedia: + return errors.New("hid: media Error") + case C.kIOReturnStillOpen: + return errors.New("hid: device(s) still open") + case C.kIOReturnRLDError: + return errors.New("hid: rld failure") + case C.kIOReturnDMAError: + return errors.New("hid: DMA failure") + case C.kIOReturnBusy: + return errors.New("hid: device Busy") + case C.kIOReturnTimeout: + return errors.New("hid: i/o timeout") + case C.kIOReturnOffline: + return errors.New("hid: device offline") + case C.kIOReturnNotReady: + return errors.New("hid: not ready") + case C.kIOReturnNotAttached: + return errors.New("hid: device not attached") + case C.kIOReturnNoChannels: + return errors.New("hid: no DMA channels left") + case C.kIOReturnNoSpace: + return errors.New("hid: no space for data") + case C.kIOReturnPortExists: + return errors.New("hid: port already exists") + case C.kIOReturnCannotWire: + return errors.New("hid: can't wire down physical memory") + case C.kIOReturnNoInterrupt: + return errors.New("hid: no interrupt attached") + case C.kIOReturnNoFrames: + return errors.New("hid: no DMA frames enqueued") + case C.kIOReturnMessageTooLarge: + return errors.New("hid: oversized msg received on interrupt port") + case C.kIOReturnNotPermitted: + return errors.New("hid: not permitted") + case C.kIOReturnNoPower: + return errors.New("hid: no power to device") + case C.kIOReturnNoMedia: + return errors.New("hid: media not present") + case C.kIOReturnUnformattedMedia: + return errors.New("hid: media not formatted") + case C.kIOReturnUnsupportedMode: + return errors.New("hid: no such mode") + case C.kIOReturnUnderrun: + return errors.New("hid: data underrun") + case C.kIOReturnOverrun: + return errors.New("hid: data overrun") + case C.kIOReturnDeviceError: + return errors.New("hid: the device is not working properly!") + case C.kIOReturnNoCompletion: + return errors.New("hid: a completion routine is required") + case C.kIOReturnAborted: + return errors.New("hid: operation aborted") + case C.kIOReturnNoBandwidth: + return errors.New("hid: bus bandwidth would be exceeded") + case C.kIOReturnNotResponding: + return errors.New("hid: device not responding") + case C.kIOReturnIsoTooOld: + return errors.New("hid: isochronous I/O request for distant past!") + case C.kIOReturnIsoTooNew: + return errors.New("hid: isochronous I/O request for distant future") + case C.kIOReturnNotFound: + return errors.New("hid: data was not found") + default: + return errors.New("hid: unknown error") + } +} + +var deviceCtxMtx sync.Mutex +var deviceCtx = make(map[C.IOHIDDeviceRef]*osxDevice) + +type cleanupDeviceManagerFn func() +type osxDevice struct { + mtx sync.Mutex + osDevice C.IOHIDDeviceRef + disconnected bool + closeDM cleanupDeviceManagerFn + + readSetup sync.Once + readCh chan []byte + readErr error + readBuf []byte + runLoop C.CFRunLoopRef +} + +func cfstring(s string) C.CFStringRef { + n := C.CFIndex(len(s)) + return C.CFStringCreateWithBytes(C.kCFAllocatorDefault, *(**C.UInt8)(unsafe.Pointer(&s)), n, C.kCFStringEncodingUTF8, 0) +} + +func gostring(cfs C.CFStringRef) string { + if cfs == nilCfStringRef { + return "" + } + + var usedBufLen C.CFIndex + n := C.cfstring_utf8_length(cfs, &usedBufLen) + if n <= 0 { + return "" + } + rng := C.CFRange{location: C.CFIndex(0), length: n} + buf := make([]byte, int(usedBufLen)) + + bufp := unsafe.Pointer(&buf[0]) + C.CFStringGetBytes(cfs, rng, C.kCFStringEncodingUTF8, 0, 0, (*C.UInt8)(bufp), C.CFIndex(len(buf)), &usedBufLen) + + sh := &reflect.StringHeader{ + Data: uintptr(bufp), + Len: int(usedBufLen), + } + return *(*string)(unsafe.Pointer(sh)) +} + +func getIntProp(device C.IOHIDDeviceRef, key C.CFStringRef) int32 { + var value int32 + ref := C.IOHIDDeviceGetProperty(device, key) + if ref == nilCfTypeRef { + return 0 + } + if C.CFGetTypeID(ref) != C.CFNumberGetTypeID() { + return 0 + } + C.CFNumberGetValue(C.CFNumberRef(ref), C.kCFNumberSInt32Type, unsafe.Pointer(&value)) + return value +} + +func getStringProp(device C.IOHIDDeviceRef, key C.CFStringRef) string { + s := C.IOHIDDeviceGetProperty(device, key) + return gostring(C.CFStringRef(s)) +} + +func getPath(osDev C.IOHIDDeviceRef) string { + return fmt.Sprintf("%s_%04x_%04x_%08x", + getStringProp(osDev, cfstring(C.kIOHIDTransportKey)), + uint16(getIntProp(osDev, cfstring(C.kIOHIDVendorIDKey))), + uint16(getIntProp(osDev, cfstring(C.kIOHIDProductIDKey))), + uint32(getIntProp(osDev, cfstring(C.kIOHIDLocationIDKey)))) +} + +func iterateDevices(action func(device C.IOHIDDeviceRef) bool) cleanupDeviceManagerFn { + var mgr C.IOHIDManagerRef + mgr = C.IOHIDManagerCreate(C.kCFAllocatorDefault, C.kIOHIDOptionsTypeNone) + C.IOHIDManagerSetDeviceMatching(mgr, nilCfDictionaryRef) + C.IOHIDManagerOpen(mgr, C.kIOHIDOptionsTypeNone) + + var allDevicesSet C.CFSetRef + allDevicesSet = C.IOHIDManagerCopyDevices(mgr) + if allDevicesSet == nilCfSetRef { + return func() {} + } + defer C.CFRelease((C.CFTypeRef)(allDevicesSet)) + devCnt := C.CFSetGetCount(allDevicesSet) + allDevices := make([]unsafe.Pointer, uint64(devCnt)) + C.CFSetGetValues(allDevicesSet, &allDevices[0]) + + for _, pDev := range allDevices { + if !action(C.IOHIDDeviceRef(pDev)) { + break + } + } + return func() { + C.IOHIDManagerClose(mgr, C.kIOHIDOptionsTypeNone) + C.CFRelease(C.CFTypeRef(mgr)) + } +} + +func Devices() ([]*DeviceInfo, error) { + var result []*DeviceInfo + iterateDevices(func(device C.IOHIDDeviceRef) bool { + result = append(result, &DeviceInfo{ + VendorID: uint16(getIntProp(device, cfstring(C.kIOHIDVendorIDKey))), + ProductID: uint16(getIntProp(device, cfstring(C.kIOHIDProductIDKey))), + VersionNumber: uint16(getIntProp(device, cfstring(C.kIOHIDVersionNumberKey))), + Manufacturer: getStringProp(device, cfstring(C.kIOHIDManufacturerKey)), + Product: getStringProp(device, cfstring(C.kIOHIDProductKey)), + UsagePage: uint16(getIntProp(device, cfstring(C.kIOHIDPrimaryUsagePageKey))), + Usage: uint16(getIntProp(device, cfstring(C.kIOHIDPrimaryUsageKey))), + InputReportLength: uint16(getIntProp(device, cfstring(C.kIOHIDMaxInputReportSizeKey))), + OutputReportLength: uint16(getIntProp(device, cfstring(C.kIOHIDMaxOutputReportSizeKey))), + Path: getPath(device), + }) + return true + })() + return result, nil +} + +func ByPath(path string) (*DeviceInfo, error) { + devices, err := Devices() + if err != nil { + return nil, err + } + for _, d := range devices { + if d.Path == path { + return d, nil + } + } + return nil, errors.New("hid: device not found") +} + +func (di *DeviceInfo) Open() (Device, error) { + err := errors.New("hid: device not found") + var dev *osxDevice + closeDM := iterateDevices(func(device C.IOHIDDeviceRef) bool { + if getPath(device) == di.Path { + res := C.IOHIDDeviceOpen(device, C.kIOHIDOptionsTypeSeizeDevice) + if res == C.kIOReturnSuccess { + C.CFRetain(C.CFTypeRef(device)) + dev = &osxDevice{osDevice: device} + err = nil + deviceCtxMtx.Lock() + deviceCtx[device] = dev + deviceCtxMtx.Unlock() + C.IOHIDDeviceRegisterRemovalCallback(device, (C.IOHIDCallback)(unsafe.Pointer(C.deviceUnplugged)), unsafe.Pointer(device)) + } else { + err = ioReturnToErr(res) + } + return false + } + return true + }) + if dev != nil { + dev.closeDM = closeDM + dev.readBuf = make([]byte, int(di.InputReportLength)) + } + + return dev, err +} + +//export deviceUnplugged +func deviceUnplugged(osdev C.IOHIDDeviceRef, result C.IOReturn, dev unsafe.Pointer) { + deviceCtxMtx.Lock() + od := deviceCtx[C.IOHIDDeviceRef(dev)] + deviceCtxMtx.Unlock() + od.readErr = errors.New("hid: device unplugged") + od.close(true) +} + +func (dev *osxDevice) Close() { + dev.readErr = errors.New("hid: device closed") + dev.close(false) +} + +func (dev *osxDevice) close(disconnected bool) { + dev.mtx.Lock() + defer dev.mtx.Unlock() + + if dev.disconnected { + return + } + + if dev.readCh != nil { + if !disconnected { + C.IOHIDDeviceRegisterInputReportCallback(dev.osDevice, (*C.uint8_t)(&dev.readBuf[0]), C.CFIndex(len(dev.readBuf)), nil, unsafe.Pointer(dev.osDevice)) + C.IOHIDDeviceUnscheduleFromRunLoop(dev.osDevice, dev.runLoop, C.kCFRunLoopDefaultMode) + } + C.CFRunLoopStop(dev.runLoop) + } + if !disconnected { + C.IOHIDDeviceRegisterRemovalCallback(dev.osDevice, nil, nil) + C.IOHIDDeviceClose(dev.osDevice, C.kIOHIDOptionsTypeSeizeDevice) + } + + deviceCtxMtx.Lock() + delete(deviceCtx, dev.osDevice) + deviceCtxMtx.Unlock() + C.CFRelease(C.CFTypeRef(dev.osDevice)) + dev.osDevice = nilIOHIDDeviceRef + dev.closeDM() + dev.disconnected = true +} + +func (dev *osxDevice) setReport(typ C.IOHIDReportType, data []byte) error { + dev.mtx.Lock() + defer dev.mtx.Unlock() + + if dev.disconnected { + return errors.New("hid: device disconnected") + } + + reportNo := int32(data[0]) + if reportNo == 0 { + data = data[1:] + } + + res := C.IOHIDDeviceSetReport(dev.osDevice, typ, C.CFIndex(reportNo), (*C.uint8_t)(&data[0]), C.CFIndex(len(data))) + if res != C.kIOReturnSuccess { + return ioReturnToErr(res) + } + return nil +} + +func (dev *osxDevice) Write(data []byte) error { + return dev.setReport(C.kIOHIDReportTypeOutput, data) +} + +func (dev *osxDevice) ReadCh() <-chan []byte { + dev.readSetup.Do(dev.startReadThread) + return dev.readCh +} + +func (dev *osxDevice) startReadThread() { + dev.mtx.Lock() + dev.readCh = make(chan []byte, 30) + dev.mtx.Unlock() + + go func() { + runtime.LockOSThread() + dev.mtx.Lock() + dev.runLoop = C.CFRunLoopGetCurrent() + C.IOHIDDeviceScheduleWithRunLoop(dev.osDevice, dev.runLoop, C.kCFRunLoopDefaultMode) + C.IOHIDDeviceRegisterInputReportCallback(dev.osDevice, (*C.uint8_t)(&dev.readBuf[0]), C.CFIndex(len(dev.readBuf)), (C.IOHIDReportCallback)(unsafe.Pointer(C.reportCallback)), unsafe.Pointer(dev.osDevice)) + dev.mtx.Unlock() + C.CFRunLoopRun() + close(dev.readCh) + }() +} + +func (dev *osxDevice) ReadError() error { + return dev.readErr +} + +//export reportCallback +func reportCallback(context unsafe.Pointer, result C.IOReturn, sender unsafe.Pointer, reportType C.IOHIDReportType, reportID uint32, report *C.uint8_t, reportLength C.CFIndex) { + deviceCtxMtx.Lock() + dev, ok := deviceCtx[(C.IOHIDDeviceRef)(context)] + deviceCtxMtx.Unlock() + if !ok { + return + } + data := C.GoBytes(unsafe.Pointer(report), C.int(reportLength)) + + // readCh is buffered, drop the data if we can't send to avoid blocking the + // run loop + select { + case dev.readCh <- data: + default: + } +} diff --git a/vendor/github.com/flynn/hid/hid_darwin_go110.go b/vendor/github.com/flynn/hid/hid_darwin_go110.go new file mode 100644 index 0000000000000..8a0cda279a09d --- /dev/null +++ b/vendor/github.com/flynn/hid/hid_darwin_go110.go @@ -0,0 +1,15 @@ +// +build go1.10,darwin + +package hid + +/* +#include +#include +*/ +import "C" + +var nilCfStringRef C.CFStringRef= 0 +var nilCfTypeRef C.CFTypeRef = 0 +var nilCfSetRef C.CFSetRef = 0 +var nilIOHIDDeviceRef C.IOHIDDeviceRef = 0 +var nilCfDictionaryRef C.CFDictionaryRef = 0 diff --git a/vendor/github.com/flynn/hid/hid_darwin_go19.go b/vendor/github.com/flynn/hid/hid_darwin_go19.go new file mode 100644 index 0000000000000..7d6c4bac1d754 --- /dev/null +++ b/vendor/github.com/flynn/hid/hid_darwin_go19.go @@ -0,0 +1,15 @@ +// +build go1.9,!go1.10,darwin + +package hid + +/* +#include +#include + */ +import "C" + +var nilCfStringRef C.CFStringRef= nil +var nilCfTypeRef C.CFTypeRef = nil +var nilCfSetRef C.CFSetRef = nil +var nilIOHIDDeviceRef C.IOHIDDeviceRef = nil +var nilCfDictionaryRef C.CFDictionaryRef = nil diff --git a/vendor/github.com/flynn/hid/hid_linux.go b/vendor/github.com/flynn/hid/hid_linux.go new file mode 100644 index 0000000000000..4b1437deaccd4 --- /dev/null +++ b/vendor/github.com/flynn/hid/hid_linux.go @@ -0,0 +1,214 @@ +package hid + +// #include +import "C" + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "sync" + "unsafe" +) + +var ( + ioctlHIDIOCGRDESCSIZE = ioR('H', 0x01, C.sizeof_int) + ioctlHIDIOCGRDESC = ioR('H', 0x02, C.sizeof_struct_hidraw_report_descriptor) + ioctlHIDIOCGRAWINFO = ioR('H', 0x03, C.sizeof_struct_hidraw_devinfo) +) + +func ioctlHIDIOCGRAWNAME(size int) uintptr { + return ioR('H', 0x04, uintptr(size)) +} + +func ioctlHIDIOCGRAWPHYS(size int) uintptr { + return ioR('H', 0x05, uintptr(size)) +} + +func ioctlHIDIOCSFEATURE(size int) uintptr { + return ioRW('H', 0x06, uintptr(size)) +} + +func ioctlHIDIOCGFEATURE(size int) uintptr { + return ioRW('H', 0x07, uintptr(size)) +} + +type linuxDevice struct { + f *os.File + info *DeviceInfo + + readSetup sync.Once + readErr error + readCh chan []byte +} + +func Devices() ([]*DeviceInfo, error) { + sys, err := os.Open("/sys/class/hidraw") + if err != nil { + return nil, err + } + names, err := sys.Readdirnames(0) + sys.Close() + if err != nil { + return nil, err + } + + var res []*DeviceInfo + for _, dir := range names { + path := filepath.Join("/dev", filepath.Base(dir)) + info, err := getDeviceInfo(path) + if os.IsPermission(err) { + continue + } else if err != nil { + return nil, err + } + res = append(res, info) + } + + return res, nil +} + +func getDeviceInfo(path string) (*DeviceInfo, error) { + d := &DeviceInfo{ + Path: path, + } + + dev, err := os.OpenFile(path, os.O_RDWR, 0) + if err != nil { + return nil, err + } + defer dev.Close() + fd := uintptr(dev.Fd()) + + var descSize C.int + if err := ioctl(fd, ioctlHIDIOCGRDESCSIZE, uintptr(unsafe.Pointer(&descSize))); err != nil { + return nil, err + } + + rawDescriptor := C.struct_hidraw_report_descriptor{ + size: C.__u32(descSize), + } + if err := ioctl(fd, ioctlHIDIOCGRDESC, uintptr(unsafe.Pointer(&rawDescriptor))); err != nil { + return nil, err + } + d.parseReport(C.GoBytes(unsafe.Pointer(&rawDescriptor.value), descSize)) + + var rawInfo C.struct_hidraw_devinfo + if err := ioctl(fd, ioctlHIDIOCGRAWINFO, uintptr(unsafe.Pointer(&rawInfo))); err != nil { + return nil, err + } + d.VendorID = uint16(rawInfo.vendor) + d.ProductID = uint16(rawInfo.product) + + rawName := make([]byte, 256) + if err := ioctl(fd, ioctlHIDIOCGRAWNAME(len(rawName)), uintptr(unsafe.Pointer(&rawName[0]))); err != nil { + return nil, err + } + d.Product = string(rawName[:bytes.IndexByte(rawName, 0)]) + + if p, err := filepath.EvalSymlinks(filepath.Join("/sys/class/hidraw", filepath.Base(path), "device")); err == nil { + if rawManufacturer, err := ioutil.ReadFile(filepath.Join(p, "/../../manufacturer")); err == nil { + d.Manufacturer = string(bytes.TrimRight(rawManufacturer, "\n")) + } + } + + return d, nil +} + +// very basic report parser that will pull out the usage page, usage, and the +// sizes of the first input and output reports +func (d *DeviceInfo) parseReport(b []byte) { + var reportSize uint16 + + for len(b) > 0 { + // read item size, type, and tag + size := int(b[0] & 0x03) + if size == 3 { + size = 4 + } + typ := (b[0] >> 2) & 0x03 + tag := (b[0] >> 4) & 0x0f + b = b[1:] + + if len(b) < size { + return + } + + // read item value + var v uint64 + for i := 0; i < size; i++ { + v += uint64(b[i]) << (8 * uint(i)) + } + b = b[size:] + + switch { + case typ == 0 && tag == 8 && d.InputReportLength == 0 && reportSize > 0: // input report type + d.InputReportLength = reportSize + reportSize = 0 + case typ == 0 && tag == 9 && d.OutputReportLength == 0 && reportSize > 0: // output report type + d.OutputReportLength = reportSize + reportSize = 0 + case typ == 1 && tag == 0: // usage page + d.UsagePage = uint16(v) + case typ == 1 && tag == 9: // report count + reportSize = uint16(v) + case typ == 2 && tag == 0 && d.Usage == 0: // usage + d.Usage = uint16(v) + } + + if d.UsagePage > 0 && d.Usage > 0 && d.InputReportLength > 0 && d.OutputReportLength > 0 { + return + } + } +} + +func ByPath(path string) (*DeviceInfo, error) { + return getDeviceInfo(path) +} + +func (d *DeviceInfo) Open() (Device, error) { + f, err := os.OpenFile(d.Path, os.O_RDWR, 0) + if err != nil { + return nil, err + } + + return &linuxDevice{f: f, info: d}, nil +} + +func (d *linuxDevice) Close() { + d.f.Close() +} + +func (d *linuxDevice) Write(data []byte) error { + _, err := d.f.Write(data) + return err +} + +func (d *linuxDevice) ReadCh() <-chan []byte { + d.readSetup.Do(func() { + d.readCh = make(chan []byte, 30) + go d.readThread() + }) + return d.readCh +} + +func (d *linuxDevice) ReadError() error { + return d.readErr +} + +func (d *linuxDevice) readThread() { + defer close(d.readCh) + for { + buf := make([]byte, d.info.InputReportLength) + n, err := d.f.Read(buf) + if err != nil { + d.readErr = err + return + } + select { + case d.readCh <- buf[:n]: + default: + } + } +} diff --git a/vendor/github.com/flynn/hid/hid_windows.go b/vendor/github.com/flynn/hid/hid_windows.go new file mode 100644 index 0000000000000..591bf0a374d43 --- /dev/null +++ b/vendor/github.com/flynn/hid/hid_windows.go @@ -0,0 +1,332 @@ +package hid + +/* +#cgo LDFLAGS: -lsetupapi -lhid + +#ifdef __MINGW32__ +#include +#endif + +#include +#include +#include +*/ +import "C" + +import ( + "errors" + "fmt" + "sync" + "syscall" + "unsafe" +) + +type winDevice struct { + handle syscall.Handle + info *DeviceInfo + + readSetup sync.Once + readCh chan []byte + readErr error + readOl *syscall.Overlapped +} + +// returns the casted handle of the device +func (d *winDevice) h() C.HANDLE { + return (C.HANDLE)((unsafe.Pointer)(d.handle)) +} + +// checks if the handle of the device is valid +func (d *winDevice) isValid() bool { + return d.handle != syscall.InvalidHandle +} + +func (d *winDevice) Close() { + // cancel any pending reads and unblock read loop + d.readErr = errors.New("hid: device closed") + C.CancelIo(d.h()) + C.SetEvent(C.HANDLE(unsafe.Pointer(d.readOl.HEvent))) + syscall.CloseHandle(d.readOl.HEvent) + + syscall.CloseHandle(d.handle) + d.handle = syscall.InvalidHandle +} + +func (d *winDevice) Write(data []byte) error { + // first make sure we send the correct amount of data to the device + outSize := int(d.info.OutputReportLength + 1) + if len(data) != outSize { + buf := make([]byte, outSize) + copy(buf, data) + data = buf + } + + ol := new(syscall.Overlapped) + if err := syscall.WriteFile(d.handle, data, nil, ol); err != nil { + // IO Pending is ok we simply wait for it to finish a few lines below + // all other errors should be reported. + if err != syscall.ERROR_IO_PENDING { + return err + } + } + + // now wait for the overlapped device access to finish. + var written C.DWORD + if C.GetOverlappedResult(d.h(), (*C.OVERLAPPED)((unsafe.Pointer)(ol)), &written, C.TRUE) == 0 { + return syscall.GetLastError() + } + + if int(written) != outSize { + return errors.New("written bytes missmatch!") + } + return nil +} + +type callCFn func(buf unsafe.Pointer, bufSize *C.DWORD) unsafe.Pointer + +// simple helper function for this windows +// "call a function twice to get the amount of space that needs to be allocated" stuff +func getCString(fnCall callCFn) string { + var requiredSize C.DWORD + fnCall(nil, &requiredSize) + if requiredSize <= 0 { + return "" + } + + buffer := C.malloc((C.size_t)(requiredSize)) + defer C.free(buffer) + + strPt := fnCall(buffer, &requiredSize) + + return C.GoString((*C.char)(strPt)) +} + +func openDevice(info *DeviceInfo, enumerate bool) (*winDevice, error) { + access := uint32(syscall.GENERIC_WRITE | syscall.GENERIC_READ) + shareMode := uint32(syscall.FILE_SHARE_READ) + if enumerate { + // if we just need a handle to get the device properties + // we should not claim exclusive access on the device + access = 0 + shareMode = uint32(syscall.FILE_SHARE_READ | syscall.FILE_SHARE_WRITE) + } + pPtr, err := syscall.UTF16PtrFromString(info.Path) + if err != nil { + return nil, err + } + + hFile, err := syscall.CreateFile(pPtr, access, shareMode, nil, syscall.OPEN_EXISTING, syscall.FILE_FLAG_OVERLAPPED, 0) + if err != nil { + return nil, err + } else { + return &winDevice{ + handle: hFile, + info: info, + readOl: &syscall.Overlapped{ + HEvent: syscall.Handle(C.CreateEvent(nil, C.FALSE, C.FALSE, nil)), + }, + }, nil + } +} + +func getDeviceDetails(deviceInfoSet C.HDEVINFO, deviceInterfaceData *C.SP_DEVICE_INTERFACE_DATA) *DeviceInfo { + devicePath := getCString(func(buffer unsafe.Pointer, size *C.DWORD) unsafe.Pointer { + interfaceDetailData := (*C.SP_DEVICE_INTERFACE_DETAIL_DATA_A)(buffer) + if interfaceDetailData != nil { + interfaceDetailData.cbSize = C.DWORD(unsafe.Sizeof(interfaceDetailData)) + } + C.SetupDiGetDeviceInterfaceDetailA(deviceInfoSet, deviceInterfaceData, interfaceDetailData, *size, size, nil) + if interfaceDetailData != nil { + return (unsafe.Pointer)(&interfaceDetailData.DevicePath[0]) + } else { + return nil + } + }) + if devicePath == "" { + return nil + } + + // Make sure this device is of Setup Class "HIDClass" and has a driver bound to it. + var i C.DWORD + var devinfoData C.SP_DEVINFO_DATA + devinfoData.cbSize = C.DWORD(unsafe.Sizeof(devinfoData)) + isHID := false + for i = 0; ; i++ { + if res := C.SetupDiEnumDeviceInfo(deviceInfoSet, i, &devinfoData); res == 0 { + break + } + + classStr := getCString(func(buffer unsafe.Pointer, size *C.DWORD) unsafe.Pointer { + C.SetupDiGetDeviceRegistryPropertyA(deviceInfoSet, &devinfoData, C.SPDRP_CLASS, nil, (*C.BYTE)(buffer), *size, size) + return buffer + }) + + if classStr == "HIDClass" { + driverName := getCString(func(buffer unsafe.Pointer, size *C.DWORD) unsafe.Pointer { + C.SetupDiGetDeviceRegistryPropertyA(deviceInfoSet, &devinfoData, C.SPDRP_DRIVER, nil, (*C.BYTE)(buffer), *size, size) + return buffer + }) + isHID = driverName != "" + break + } + } + + if !isHID { + return nil + } + d, _ := ByPath(devicePath) + return d +} + +// ByPath gets the device which is bound to the given path. +func ByPath(devicePath string) (*DeviceInfo, error) { + devInfo := &DeviceInfo{Path: devicePath} + dev, err := openDevice(devInfo, true) + if err != nil { + return nil, err + } + defer dev.Close() + if !dev.isValid() { + return nil, errors.New("Failed to open device") + } + + var attrs C.HIDD_ATTRIBUTES + attrs.Size = C.DWORD(unsafe.Sizeof(attrs)) + C.HidD_GetAttributes(dev.h(), &attrs) + + devInfo.VendorID = uint16(attrs.VendorID) + devInfo.ProductID = uint16(attrs.ProductID) + devInfo.VersionNumber = uint16(attrs.VersionNumber) + + const bufLen = 256 + buff := make([]uint16, bufLen) + + C.HidD_GetManufacturerString(dev.h(), (C.PVOID)(&buff[0]), bufLen) + devInfo.Manufacturer = syscall.UTF16ToString(buff) + + C.HidD_GetProductString(dev.h(), (C.PVOID)(&buff[0]), bufLen) + devInfo.Product = syscall.UTF16ToString(buff) + + var preparsedData C.PHIDP_PREPARSED_DATA + if C.HidD_GetPreparsedData(dev.h(), &preparsedData) != 0 { + var caps C.HIDP_CAPS + + if C.HidP_GetCaps(preparsedData, &caps) == C.HIDP_STATUS_SUCCESS { + devInfo.UsagePage = uint16(caps.UsagePage) + devInfo.Usage = uint16(caps.Usage) + devInfo.InputReportLength = uint16(caps.InputReportByteLength - 1) + devInfo.OutputReportLength = uint16(caps.OutputReportByteLength - 1) + } + + C.HidD_FreePreparsedData(preparsedData) + } + + return devInfo, nil +} + +// Devices returns all HID devices which are connected to the system. +func Devices() ([]*DeviceInfo, error) { + var result []*DeviceInfo + var InterfaceClassGuid C.GUID + C.HidD_GetHidGuid(&InterfaceClassGuid) + deviceInfoSet := C.SetupDiGetClassDevsA(&InterfaceClassGuid, nil, nil, C.DIGCF_PRESENT|C.DIGCF_DEVICEINTERFACE) + defer C.SetupDiDestroyDeviceInfoList(deviceInfoSet) + + var deviceIdx C.DWORD = 0 + var deviceInterfaceData C.SP_DEVICE_INTERFACE_DATA + deviceInterfaceData.cbSize = C.DWORD(unsafe.Sizeof(deviceInterfaceData)) + + for ; ; deviceIdx++ { + res := C.SetupDiEnumDeviceInterfaces(deviceInfoSet, nil, &InterfaceClassGuid, deviceIdx, &deviceInterfaceData) + if res == 0 { + break + } + di := getDeviceDetails(deviceInfoSet, &deviceInterfaceData) + if di != nil { + result = append(result, di) + } + } + return result, nil +} + +// Open openes the device for read / write access. +func (di *DeviceInfo) Open() (Device, error) { + d, err := openDevice(di, false) + if err != nil { + return nil, err + } + if d.isValid() { + return d, nil + } else { + d.Close() + err := syscall.GetLastError() + if err == nil { + err = errors.New("Unable to open device!") + } + return nil, err + } +} + +func (d *winDevice) ReadCh() <-chan []byte { + d.readSetup.Do(func() { + d.readCh = make(chan []byte, 30) + go d.readThread() + }) + return d.readCh +} + +func (d *winDevice) ReadError() error { + return d.readErr +} + +func (d *winDevice) readThread() { + defer close(d.readCh) + + for { + buf := make([]byte, d.info.InputReportLength+1) + C.ResetEvent(C.HANDLE(unsafe.Pointer(d.readOl.HEvent))) + + if err := syscall.ReadFile(d.handle, buf, nil, d.readOl); err != nil { + if err != syscall.ERROR_IO_PENDING { + if d.readErr == nil { + d.readErr = err + } + return + } + } + + // Wait for the read to finish + res := C.WaitForSingleObject(C.HANDLE(unsafe.Pointer(d.readOl.HEvent)), C.INFINITE) + if res != C.WAIT_OBJECT_0 { + if d.readErr == nil { + d.readErr = fmt.Errorf("hid: unexpected read wait state %d", res) + } + return + } + + var n C.DWORD + if r := C.GetOverlappedResult(d.h(), (*C.OVERLAPPED)((unsafe.Pointer)(d.readOl)), &n, C.TRUE); r == 0 { + if d.readErr == nil { + d.readErr = fmt.Errorf("hid: unexpected read result state %d", r) + } + return + } + if n == 0 { + if d.readErr == nil { + d.readErr = errors.New("hid: zero byte read") + } + return + } + + if buf[0] == 0 { + // Report numbers are not being used, so remove zero to match other platforms + buf = buf[1:] + } + + select { + case d.readCh <- buf[:int(n-1)]: + default: + } + } + +} diff --git a/vendor/github.com/flynn/hid/ioctl_linux.go b/vendor/github.com/flynn/hid/ioctl_linux.go new file mode 100644 index 0000000000000..8763ce557e6c3 --- /dev/null +++ b/vendor/github.com/flynn/hid/ioctl_linux.go @@ -0,0 +1,80 @@ +package hid + +import "syscall" + +// This file is https://github.com/wolfeidau/gioctl/blob/0a268ca608219d1d45cfcc50ca4dbfe232baaf0d/ioctl.go +// +// Copyright (c) 2014 Mark Wolfe and licenced under the MIT licence. All rights +// not explicitly granted in the MIT license are reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +const ( + typeBits = 8 + numberBits = 8 + sizeBits = 14 + directionBits = 2 + + typeMask = (1 << typeBits) - 1 + numberMask = (1 << numberBits) - 1 + sizeMask = (1 << sizeBits) - 1 + directionMask = (1 << directionBits) - 1 + + directionNone = 0 + directionWrite = 1 + directionRead = 2 + + numberShift = 0 + typeShift = numberShift + numberBits + sizeShift = typeShift + typeBits + directionShift = sizeShift + sizeBits +) + +func ioc(dir, t, nr, size uintptr) uintptr { + return (dir << directionShift) | (t << typeShift) | (nr << numberShift) | (size << sizeShift) +} + +// io used for a simple ioctl that sends nothing but the type and number, and receives back nothing but an (integer) retval. +func io(t, nr uintptr) uintptr { + return ioc(directionNone, t, nr, 0) +} + +// ioR used for an ioctl that reads data from the device driver. The driver will be allowed to return sizeof(data_type) bytes to the user. +func ioR(t, nr, size uintptr) uintptr { + return ioc(directionRead, t, nr, size) +} + +// ioW used for an ioctl that writes data to the device driver. +func ioW(t, nr, size uintptr) uintptr { + return ioc(directionWrite, t, nr, size) +} + +// ioRW a combination of IoR and IoW. That is, data is both written to the driver and then read back from the driver by the client. +func ioRW(t, nr, size uintptr) uintptr { + return ioc(directionRead|directionWrite, t, nr, size) +} + +// ioctl simplified ioct call +func ioctl(fd, op, arg uintptr) error { + _, _, ep := syscall.Syscall(syscall.SYS_IOCTL, fd, op, arg) + if ep != 0 { + return syscall.Errno(ep) + } + return nil +} diff --git a/vendor/github.com/flynn/u2f/LICENSE b/vendor/github.com/flynn/u2f/LICENSE new file mode 100644 index 0000000000000..8bb161c07e168 --- /dev/null +++ b/vendor/github.com/flynn/u2f/LICENSE @@ -0,0 +1,29 @@ +Flynn is a trademark of Prime Directive, Inc. + +Copyright (c) 2016 Prime Directive, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Prime Directive, Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/flynn/u2f/u2fhid/hid.go b/vendor/github.com/flynn/u2f/u2fhid/hid.go new file mode 100644 index 0000000000000..d5faddef2972f --- /dev/null +++ b/vendor/github.com/flynn/u2f/u2fhid/hid.go @@ -0,0 +1,281 @@ +// Package u2fhid implements the low-level FIDO U2F HID protocol. +package u2fhid + +import ( + "bytes" + "crypto/rand" + "encoding/binary" + "fmt" + "io" + "sync" + "time" + + "github.com/flynn/hid" +) + +const ( + cmdPing = 0x80 | 0x01 + cmdMsg = 0x80 | 0x03 + cmdLock = 0x80 | 0x04 + cmdInit = 0x80 | 0x06 + cmdWink = 0x80 | 0x08 + cmdSync = 0x80 | 0x3c + cmdError = 0x80 | 0x3f + + broadcastChannel = 0xffffffff + + capabilityWink = 1 + + minMessageLen = 7 + maxMessageLen = 7609 + minInitResponseLen = 17 + + responseTimeout = 3 * time.Second + + fidoUsagePage = 0xF1D0 + u2fUsage = 1 +) + +var errorCodes = map[uint8]string{ + 1: "invalid command", + 2: "invalid parameter", + 3: "invalid message length", + 4: "invalid message sequencing", + 5: "message timed out", + 6: "channel busy", + 7: "command requires channel lock", + 8: "sync command failed", +} + +// Devices lists available HID devices that advertise the U2F HID protocol. +func Devices() ([]*hid.DeviceInfo, error) { + devices, err := hid.Devices() + if err != nil { + return nil, err + } + + res := make([]*hid.DeviceInfo, 0, len(devices)) + for _, d := range devices { + if d.UsagePage == fidoUsagePage && d.Usage == u2fUsage { + res = append(res, d) + } + } + + return res, nil +} + +// Open initializes a communication channel with a U2F HID device. +func Open(info *hid.DeviceInfo) (*Device, error) { + hidDev, err := info.Open() + if err != nil { + return nil, err + } + + d := &Device{ + info: info, + device: hidDev, + readCh: hidDev.ReadCh(), + } + + if err := d.init(); err != nil { + return nil, err + } + + return d, nil +} + +// A Device is used to communicate with a U2F HID device. +type Device struct { + ProtocolVersion uint8 + MajorDeviceVersion uint8 + MinorDeviceVersion uint8 + BuildDeviceVersion uint8 + + // RawCapabilities is the raw capabilities byte provided by the device + // during initialization. + RawCapabilities uint8 + + // CapabilityWink is true if the device advertised support for the wink + // command during initilization. Even if this flag is true, the device may + // not actually do anything if the command is called. + CapabilityWink bool + + info *hid.DeviceInfo + device hid.Device + channel uint32 + + mtx sync.Mutex + readCh <-chan []byte + buf []byte +} + +func (d *Device) sendCommand(channel uint32, cmd byte, data []byte) error { + if len(data) > maxMessageLen { + return fmt.Errorf("u2fhid: message is too long") + } + + // zero buffer + for i := range d.buf { + d.buf[i] = 0 + } + + binary.BigEndian.PutUint32(d.buf[1:], channel) + d.buf[5] = cmd + binary.BigEndian.PutUint16(d.buf[6:], uint16(len(data))) + + n := copy(d.buf[8:], data) + data = data[n:] + + if err := d.device.Write(d.buf); err != nil { + return err + } + + var seq uint8 + for len(data) > 0 { + // zero buffer + for i := range d.buf { + d.buf[i] = 0 + } + + binary.BigEndian.PutUint32(d.buf[1:], channel) + d.buf[5] = seq + seq++ + n := copy(d.buf[6:], data) + data = data[n:] + if err := d.device.Write(d.buf); err != nil { + return err + } + } + return nil +} + +func (d *Device) readResponse(channel uint32, cmd byte) ([]byte, error) { + timeout := time.After(responseTimeout) + + haveFirst := false + var buf []byte + var expected int + + for { + select { + case msg, ok := <-d.readCh: + if len(msg) < minMessageLen { + return nil, fmt.Errorf("u2fhid: message is too short, only received %d bytes", len(msg)) + } + if !ok { + return nil, fmt.Errorf("u2fhid: error reading response, device closed") + } + if channel != binary.BigEndian.Uint32(msg) { + continue + } + + if msg[4] == cmdError { + errMsg, ok := errorCodes[msg[7]] + if !ok { + return nil, fmt.Errorf("u2fhid: received unknown error response %d", msg[7]) + } + return nil, fmt.Errorf("u2fhid: received error from device: %s", errMsg) + } + + if !haveFirst { + if msg[4] != cmd { + return nil, fmt.Errorf("u2fhid: error reading response, unexpected command %d, wanted %d", msg[4], cmd) + } + haveFirst = true + expected = int(binary.BigEndian.Uint16(msg[5:])) + buf = make([]byte, 0, expected) + msg = msg[7:] + if len(msg) > expected { + msg = msg[:expected] + } + buf = append(buf, msg...) + } else { + if msg[4]&0x80 != 0 { + return nil, fmt.Errorf("u2fhid: error reading response, unexpected command %d, wanted continuation", msg[4]) + } + msg = msg[5:] + if len(msg) > expected-len(buf) { + msg = msg[:expected-len(buf)] + } + buf = append(buf, msg...) + } + if len(buf) >= expected { + return buf, nil + } + case <-timeout: + return nil, fmt.Errorf("u2fhid: error reading response, read timed out") + } + } +} + +func (d *Device) init() error { + d.buf = make([]byte, d.info.OutputReportLength+1) + + nonce := make([]byte, 8) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return err + } + + if err := d.sendCommand(broadcastChannel, cmdInit, nonce); err != nil { + return err + } + + for { + res, err := d.readResponse(broadcastChannel, cmdInit) + if err != nil { + return err + } + if len(res) < minInitResponseLen { + return fmt.Errorf("u2fhid: init response is short, wanted %d, got %d bytes", minInitResponseLen, len(res)) + } + if !bytes.Equal(nonce, res[:8]) { + // nonce doesn't match, this init reply isn't for us + continue + } + d.channel = binary.BigEndian.Uint32(res[8:]) + + d.ProtocolVersion = res[12] + d.MajorDeviceVersion = res[13] + d.MinorDeviceVersion = res[14] + d.BuildDeviceVersion = res[15] + d.RawCapabilities = res[16] + d.CapabilityWink = d.RawCapabilities&capabilityWink != 0 + break + } + + return nil +} + +// Command sends a command and associated data to the device and returns the +// response. +func (d *Device) Command(cmd byte, data []byte) ([]byte, error) { + d.mtx.Lock() + defer d.mtx.Unlock() + if err := d.sendCommand(d.channel, cmd, data); err != nil { + return nil, err + } + return d.readResponse(d.channel, cmd) +} + +// Ping sends data to the device that should be echoed back verbatim. +func (d *Device) Ping(data []byte) ([]byte, error) { + return d.Command(cmdPing, data) +} + +// Wink performs a vendor-defined action to identify the device, like blinking +// an LED. It is not implemented correctly or at all on all devices. +func (d *Device) Wink() error { + _, err := d.Command(cmdWink, nil) + return err +} + +// Message sends an encapsulated U2F protocol message to the device and returns +// the response. +func (d *Device) Message(data []byte) ([]byte, error) { + return d.Command(cmdMsg, data) +} + +// Close closes the device and frees associated resources. +func (d *Device) Close() { + d.device.Close() +} diff --git a/vendor/github.com/flynn/u2f/u2ftoken/token.go b/vendor/github.com/flynn/u2f/u2ftoken/token.go new file mode 100644 index 0000000000000..05f8a475c55eb --- /dev/null +++ b/vendor/github.com/flynn/u2f/u2ftoken/token.go @@ -0,0 +1,266 @@ +// Package u2ftoken implements the FIDO U2F raw message protocol used to +// communicate with U2F tokens. +package u2ftoken + +import ( + "encoding/binary" + "errors" + "fmt" +) + +const ( + cmdRegister = 1 + cmdAuthenticate = 2 + cmdVersion = 3 + + tupRequired = 1 // Test of User Presence required + tupConsume = 2 // Consume a Test of User Presence + tupTestOnly = 4 // Check valid key handle only, no test of user presence required + + authEnforce = tupRequired | tupConsume + // This makes zero sense, but the check command is all three flags, not just tupTestOnly + authCheckOnly = tupRequired | tupConsume | tupTestOnly + + statusNoError = 0x9000 + statusWrongLength = 0x6700 + statusInvalidData = 0x6984 + statusConditionsNotSatisfied = 0x6985 + statusWrongData = 0x6a80 + statusInsNotSupported = 0x6d00 +) + +// ErrPresenceRequired is returned by Register and Authenticate if proof of user +// presence must be provide before the operation can be retried successfully. +var ErrPresenceRequired = errors.New("u2ftoken: user presence required") + +// ErrUnknownKeyHandle is returned by Authenticate and CheckAuthenticate if the +// key handle is unknown to the token. +var ErrUnknownKeyHandle = errors.New("u2ftoken: unknown key handle") + +// Device implements a message transport to a concrete U2F device. It is +// implemented in package u2fhid. +type Device interface { + // Message sends a message to the device and returns the response. + Message(data []byte) ([]byte, error) +} + +// NewToken returns a token that will use Device to communicate with the device. +func NewToken(d Device) *Token { + return &Token{d: d} +} + +// A Token implements the FIDO U2F hardware token messages as defined in the Raw +// Message Formats specification. +type Token struct { + d Device +} + +// A RegisterRequest is a message used for token registration. +type RegisterRequest struct { + // Challenge is the 32-byte SHA-256 hash of the Client Data JSON prepared by + // the client. + Challenge []byte + + // Application is the 32-byte SHA-256 hash of the application identity of + // the relying party requesting registration. + Application []byte +} + +// Register registers an application with the token and returns the raw +// registration response message to be passed to the relying party. It returns +// ErrPresenceRequired if the call should be retried after proof of user +// presence is provided to the token. +func (t *Token) Register(req RegisterRequest) ([]byte, error) { + if len(req.Challenge) != 32 { + return nil, fmt.Errorf("u2ftoken: Challenge must be exactly 32 bytes") + } + if len(req.Application) != 32 { + return nil, fmt.Errorf("u2ftoken: Application must be exactly 32 bytes") + } + + res, err := t.Message(Request{ + Param1: authEnforce, + Command: cmdRegister, + Data: append(req.Challenge, req.Application...), + }) + if err != nil { + return nil, err + } + + if res.Status != statusNoError { + switch res.Status { + case statusConditionsNotSatisfied: + return nil, ErrPresenceRequired + default: + return nil, fmt.Errorf("u2ftoken: unexpected error %d during registration", res.Status) + } + } + + return res.Data, nil +} + +// An AuthenticateRequires is a message used for authenticating to a relying party +type AuthenticateRequest struct { + // Challenge is the 32-byte SHA-256 hash of the Client Data JSON prepared by + // the client. + Challenge []byte + + // Application is the 32-byte SHA-256 hash of the application identity of + // the relying party requesting authentication. + Application []byte + + // KeyHandle is the opaque key handle that was provided to the relying party + // during registration. + KeyHandle []byte +} + +// An AuthenticateResponse is a message returned in response to a successful +// authentication request. +type AuthenticateResponse struct { + // Counter is the value of the counter that is incremented by the token + // every time it performs an authentication operation. + Counter uint32 + + // Signature is the P-256 ECDSA signature over the authentication data. + Signature []byte + + // RawResponse is the raw response bytes from the U2F token. + RawResponse []byte +} + +func encodeAuthenticateRequest(req AuthenticateRequest) ([]byte, error) { + if len(req.Challenge) != 32 { + return nil, fmt.Errorf("u2ftoken: Challenge must be exactly 32 bytes") + } + if len(req.Application) != 32 { + return nil, fmt.Errorf("u2ftoken: Application must be exactly 32 bytes") + } + if len(req.KeyHandle) > 256 { + return nil, fmt.Errorf("u2ftoken: KeyHandle is too long") + } + + buf := make([]byte, 0, len(req.Challenge)+len(req.Application)+1+len(req.KeyHandle)) + buf = append(buf, req.Challenge...) + buf = append(buf, req.Application...) + buf = append(buf, byte(len(req.KeyHandle))) + buf = append(buf, req.KeyHandle...) + + return buf, nil +} + +// Authenticate peforms an authentication operation and returns the response to +// provide to the relying party. It returns ErrPresenceRequired if the call +// should be retried after proof of user presence is provided to the token and +// ErrUnknownKeyHandle if the key handle is unknown to the token. +func (t *Token) Authenticate(req AuthenticateRequest) (*AuthenticateResponse, error) { + buf, err := encodeAuthenticateRequest(req) + if err != nil { + return nil, err + } + + res, err := t.Message(Request{ + Command: cmdAuthenticate, + Param1: authEnforce, + Data: buf, + }) + if err != nil { + return nil, err + } + + if res.Status != statusNoError { + if res.Status == statusConditionsNotSatisfied { + return nil, ErrPresenceRequired + } + return nil, fmt.Errorf("u2ftoken: unexpected error %d during authentication", res.Status) + } + + if len(res.Data) < 6 { + return nil, fmt.Errorf("u2ftoken: authenticate response is too short, got %d bytes", len(res.Data)) + } + + return &AuthenticateResponse{ + Counter: binary.BigEndian.Uint32(res.Data[1:]), + Signature: res.Data[5:], + RawResponse: res.Data, + }, nil +} + +// CheckAuthenticate checks if a key handle is known to the token without +// requiring a test for user presence. It returns ErrUnknownKeyHandle if the key +// handle is unknown to the token. +func (t *Token) CheckAuthenticate(req AuthenticateRequest) error { + buf, err := encodeAuthenticateRequest(req) + if err != nil { + return err + } + + res, err := t.Message(Request{ + Command: cmdAuthenticate, + Param1: authCheckOnly, + Data: buf, + }) + if err != nil { + return err + } + + if res.Status != statusConditionsNotSatisfied { + if res.Status == statusWrongData { + return ErrUnknownKeyHandle + } + return fmt.Errorf("u2ftoken: unexpected error %d during auth check", res.Status) + } + + return nil +} + +// Version returns the U2F protocol version implemented by the token. +func (t *Token) Version() (string, error) { + res, err := t.Message(Request{Command: cmdVersion}) + if err != nil { + return "", err + } + + if res.Status != statusNoError { + return "", fmt.Errorf("u2ftoken: unexpected error %d during version request", res.Status) + } + + return string(res.Data), nil +} + +// A Request is a low-level request to the token. +type Request struct { + Command uint8 + Param1 uint8 + Param2 uint8 + Data []byte +} + +// A Response is a low-level response from the token. +type Response struct { + Data []byte + Status uint16 +} + +// Message sends a low-level request to the token and returns the response. +func (t *Token) Message(req Request) (*Response, error) { + buf := make([]byte, 7, 7+len(req.Data)) + buf[1] = req.Command + buf[2] = req.Param1 + buf[3] = req.Param2 + buf[4] = uint8(len(req.Data) >> 16) + buf[5] = uint8(len(req.Data) >> 8) + buf[6] = uint8(len(req.Data)) + buf = append(buf, req.Data...) + + data, err := t.d.Message(buf) + if err != nil { + return nil, err + } + if len(data) < 2 { + return nil, fmt.Errorf("u2ftoken: response is too short, got %d bytes", len(data)) + } + return &Response{ + Data: data[:len(data)-2], + Status: binary.BigEndian.Uint16(data[len(data)-2:]), + }, nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 85bd2812b5b44..cde69a9c3669a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -157,6 +157,13 @@ github.com/docker/spdystream/spdy # github.com/dustin/go-humanize v1.0.0 ## explicit github.com/dustin/go-humanize +# github.com/flynn/hid v0.0.0-20190502022136-f1b9b6cc019a +## explicit +github.com/flynn/hid +# github.com/flynn/u2f v0.0.0-20180613185708-15554eb68e5d +## explicit +github.com/flynn/u2f/u2fhid +github.com/flynn/u2f/u2ftoken # github.com/fsouza/fake-gcs-server v1.11.6 ## explicit github.com/fsouza/fake-gcs-server/fakestorage