diff --git a/enumerator.go b/enumerator.go deleted file mode 100644 index 50b0f2c..0000000 --- a/enumerator.go +++ /dev/null @@ -1,17 +0,0 @@ -package usbwallet - -import "io" - -type enumerator interface { - // Infos returns the list of USB devices matching the vendor and product IDs. - Infos() ([]info, error) - // Close releases any resources held by the enumerator. - Close() -} - -type info interface { - // Path returns the USB device path, which can be used for identifying the connection. - Path() string - // Open opens a connection to the USB device and returns a ReadWriteCloser for communication. - Open() (io.ReadWriteCloser, error) -} diff --git a/go.mod b/go.mod index 36171c6..6a8433b 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,7 @@ toolchain go1.24.3 require ( github.com/ethereum/go-ethereum v1.16.1 - github.com/google/gousb v1.1.3 - github.com/karalabe/hid v1.0.1-0.20240306101548-573246063e52 + github.com/karalabe/usb v0.0.3-0.20231219215548-8627268f6b0a github.com/reserve-protocol/trezor v0.0.0-20190523030725-9e38328dde28 google.golang.org/protobuf v1.36.6 ) diff --git a/go.sum b/go.sum index c68d106..7d85dc2 100644 --- a/go.sum +++ b/go.sum @@ -34,12 +34,10 @@ github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXi github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gousb v1.1.3 h1:xt6M5TDsGSZ+rlomz5Si5Hmd/Fvbmo2YCJHN+yGaK4o= -github.com/google/gousb v1.1.3/go.mod h1:GGWUkK0gAXDzxhwrzetW592aOmkkqSGcj5KLEgmCVUg= github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= -github.com/karalabe/hid v1.0.1-0.20240306101548-573246063e52 h1:msKODTL1m0wigztaqILOtla9HeW1ciscYG4xjLtvk5I= -github.com/karalabe/hid v1.0.1-0.20240306101548-573246063e52/go.mod h1:qk1sX/IBgppQNcGCRoj90u6EGC056EBoIc1oEjCWla8= +github.com/karalabe/usb v0.0.3-0.20231219215548-8627268f6b0a h1:BGJeMa7efLsbPri2WJxtFOcjDPyjCdNlZWERE11GJAE= +github.com/karalabe/usb v0.0.3-0.20231219215548-8627268f6b0a/go.mod h1:Od972xHfMJowv7NGVDiWVxk2zxnWgjLlJzE+F4F7AGU= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= diff --git a/hid.go b/hid.go deleted file mode 100644 index ae11d60..0000000 --- a/hid.go +++ /dev/null @@ -1,68 +0,0 @@ -package usbwallet - -import ( - "errors" - "io" - - "github.com/karalabe/hid" -) - -type hidEnumerator struct { - vendorID uint16 // USB vendor identifier used for device discovery - productIDs []uint16 // USB product identifiers used for device discovery - usageID uint16 // USB usage page identifier used for macOS device discovery - endpointID int // USB endpoint identifier used for non-macOS device discovery -} - -// NewHidEnumerator creates a new USB device enumerator for HID devices. -func newHidEnumerator(vendorID uint16, productIDs []uint16, usageID uint16, endpointID int) enumerator { - return &hidEnumerator{ - vendorID: vendorID, - productIDs: productIDs, - usageID: usageID, - endpointID: endpointID, - } -} - -func (e *hidEnumerator) Infos() ([]info, error) { - if !hid.Supported() { - return nil, errors.New("unsupported platform") - } - - var infos []info - - i, err := hid.Enumerate(e.vendorID, 0) - if err != nil { - return nil, err - } - - for _, info := range i { - for _, id := range e.productIDs { - // We check both the raw ProductID (legacy) and just the upper byte, as Ledger - // uses `MMII`, encoding a model (MM) and an interface bitfield (II) - mmOnly := info.ProductID & 0xff00 - // Windows and Macos use UsageID matching, Linux uses Interface matching - if (info.ProductID == id || mmOnly == id) && (info.UsagePage == e.usageID || info.Interface == e.endpointID) { - infos = append(infos, &hidInfo{info}) - break - } - } - } - - return infos, nil -} - -func (e *hidEnumerator) Close() { -} - -type hidInfo struct { - hid.DeviceInfo -} - -func (o *hidInfo) Path() string { - return o.DeviceInfo.Path -} - -func (o *hidInfo) Open() (io.ReadWriteCloser, error) { - return o.DeviceInfo.Open() -} diff --git a/hub.go b/hub.go index 3432861..2e9d0ce 100644 --- a/hub.go +++ b/hub.go @@ -17,6 +17,7 @@ package usbwallet import ( + "errors" "runtime" "sync" "sync/atomic" @@ -25,6 +26,7 @@ import ( "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/log" + "github.com/karalabe/usb" ) // LedgerScheme is the protocol scheme prefixing account and wallet URLs. @@ -43,8 +45,11 @@ const refreshThrottling = 500 * time.Millisecond // Hub is a accounts.Backend that can find and handle generic USB hardware wallets. type Hub struct { - scheme string // Protocol scheme prefixing account and wallet URLs.\ - enumerator enumerator // Enumerator to use for discovering devices + scheme string // Protocol scheme prefixing account and wallet URLs. + vendorID uint16 // USB vendor identifier used for device discovery + productIDs []uint16 // USB product identifiers used for device discovery + usageID uint16 // USB usage page identifier used for macOS device discovery + endpointID int // USB endpoint identifier used for non-macOS device discovery makeDriver func(log.Logger) driver // Factory method to construct a vendor specific driver refreshed time.Time // Time instance when the list of wallets was last refreshed @@ -65,7 +70,7 @@ type Hub struct { // NewLedgerHub creates a new hardware wallet manager for Ledger devices. func NewLedgerHub() (*Hub, error) { - enumerator := newHidEnumerator(0x2c97, []uint16{ + return newHub(LedgerScheme, 0x2c97, []uint16{ // Device definitions taken from // https://github.com/LedgerHQ/ledger-live/blob/595cb73b7e6622dbbcfc11867082ddc886f1bf01/libs/ledgerjs/packages/devices/src/index.ts @@ -84,28 +89,31 @@ func NewLedgerHub() (*Hub, error) { 0x5000, /* WebUSB Ledger Nano S Plus */ 0x6000, /* WebUSB Ledger Nano FTS */ 0x7000, /* WebUSB Ledger Flex */ - }, 0xffa0, 0) - return newHub(LedgerScheme, enumerator, newLedgerDriver) + }, 0xffa0, 0, newLedgerDriver) } // NewTrezorHubWithHID creates a new hardware wallet manager for Trezor devices. func NewTrezorHubWithHID() (*Hub, error) { - enumerator := newHidEnumerator(0x534c, []uint16{0x0001 /* Trezor HID */}, 0xff00, 0) - return newHub(TrezorScheme, enumerator, newTrezorDriver) + return newHub(TrezorScheme, 0x534c, []uint16{0x0001 /* Trezor HID */}, 0xff00, 0, newTrezorDriver) } // NewTrezorHubWithWebUSB creates a new hardware wallet manager for Trezor devices with // firmware version > 1.8.0 func NewTrezorHubWithWebUSB() (*Hub, error) { - enumerator := newUsbEnumerator(0x1209, []uint16{0x53c1 /* Trezor WebUSB */}) - return newHub(TrezorScheme, enumerator, newTrezorDriver) + return newHub(TrezorScheme, 0x1209, []uint16{0x53c1 /* Trezor WebUSB */}, 0xffff /* No usage id on webusb, don't match unset (0) */, 0, newTrezorDriver) } // newHub creates a new hardware wallet manager for generic USB devices. -func newHub(scheme string, enumerator enumerator, makeDriver func(log.Logger) driver) (*Hub, error) { +func newHub(scheme string, vendorID uint16, productIDs []uint16, usageID uint16, endpointID int, makeDriver func(log.Logger) driver) (*Hub, error) { + if !usb.Supported() { + return nil, errors.New("unsupported platform") + } hub := &Hub{ scheme: scheme, - enumerator: enumerator, + vendorID: vendorID, + productIDs: productIDs, + usageID: usageID, + endpointID: endpointID, makeDriver: makeDriver, quit: make(chan chan error), } @@ -142,6 +150,8 @@ func (hub *Hub) refreshWallets() { if hub.enumFails.Load() > 2 { return } + // Retrieve the current list of USB wallet devices + var devices []usb.DeviceInfo if runtime.GOOS == "linux" { // hidapi on Linux opens the device during enumeration to retrieve some infos, @@ -156,8 +166,7 @@ func (hub *Hub) refreshWallets() { return } } - - infos, err := hub.enumerator.Infos() + infos, err := usb.Enumerate(hub.vendorID, 0) if err != nil { failcount := hub.enumFails.Add(1) if runtime.GOOS == "linux" { @@ -165,11 +174,23 @@ func (hub *Hub) refreshWallets() { hub.commsLock.Unlock() } log.Error("Failed to enumerate USB devices", "hub", hub.scheme, - "failcount", failcount, "err", err) + "vendor", hub.vendorID, "failcount", failcount, "err", err) return } hub.enumFails.Store(0) + for _, info := range infos { + for _, id := range hub.productIDs { + // We check both the raw ProductID (legacy) and just the upper byte, as Ledger + // uses `MMII`, encoding a model (MM) and an interface bitfield (II) + mmOnly := info.ProductID & 0xff00 + // Windows and Macos use UsageID matching, Linux uses Interface matching + if (info.ProductID == id || mmOnly == id) && (info.UsagePage == hub.usageID || info.Interface == hub.endpointID) { + devices = append(devices, info) + break + } + } + } if runtime.GOOS == "linux" { // See rationale before the enumeration why this is needed and only on Linux. hub.commsLock.Unlock() @@ -178,12 +199,12 @@ func (hub *Hub) refreshWallets() { hub.stateLock.Lock() var ( - wallets = make([]Wallet, 0, len(infos)) + wallets = make([]Wallet, 0, len(devices)) events []accounts.WalletEvent ) - for _, info := range infos { - url := accounts.URL{Scheme: hub.scheme, Path: info.Path()} + for _, device := range devices { + url := accounts.URL{Scheme: hub.scheme, Path: device.Path} // Drop wallets in front of the next device or those that failed for some reason for len(hub.wallets) > 0 { @@ -199,7 +220,7 @@ func (hub *Hub) refreshWallets() { // If there are no more wallets or the device is before the next, wrap new wallet if len(hub.wallets) == 0 || hub.wallets[0].URL().Cmp(url) > 0 { logger := log.New("url", url) - wallet := &wallet{hub: hub, driver: hub.makeDriver(logger), url: &url, info: info, log: logger} + wallet := &wallet{hub: hub, driver: hub.makeDriver(logger), url: &url, info: device, log: logger} events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletArrived}) wallets = append(wallets, wallet) diff --git a/usb.go b/usb.go deleted file mode 100644 index 1f89623..0000000 --- a/usb.go +++ /dev/null @@ -1,139 +0,0 @@ -package usbwallet - -import ( - "io" - "time" - - "github.com/google/gousb" -) - -type usbEnumerator struct { - ctx *gousb.Context - vendorID uint16 // USB vendor identifier used for device discovery - productIDs []uint16 // USB product identifiers used for device discovery -} - -// NewUsbEnumerator creates a new USB device enumerator for generic USB devices. -func newUsbEnumerator(vendorID uint16, productIDs []uint16) enumerator { - ctx := gousb.NewContext() - return &usbEnumerator{ - ctx: ctx, - vendorID: vendorID, - productIDs: productIDs, - } -} - -func (e *usbEnumerator) Infos() ([]info, error) { - var infos []info - - d, err := e.ctx.OpenDevices(func(desc *gousb.DeviceDesc) bool { - if uint16(desc.Vendor) == e.vendorID { - for _, id := range e.productIDs { - if uint16(desc.Product) == id { - return true - } - } - } - return false - }) - if err != nil { - return nil, err - } - - for _, device := range d { - infos = append(infos, &usbInfo{device}) - } - - return infos, nil -} - -func (e *usbEnumerator) Close() { - if e.ctx != nil { - _ = e.ctx.Close() - e.ctx = nil - } -} - -type usbInfo struct { - *gousb.Device -} - -func (o *usbInfo) Path() string { - return o.Device.String() -} - -func (o *usbInfo) Open() (io.ReadWriteCloser, error) { - rwc, err := o.open() - if err != nil { - return nil, err - } - - // hacky: flush the device input buffer to avoid reading stale responses from Trezor - c := make(chan interface{}) - go func() { - buf := make([]byte, 1024) - for { - _, err := rwc.Read(buf) - if err != nil { - return - } - c <- struct{}{} - } - }() - for { - select { - case <-c: - continue - case <-time.After(100 * time.Millisecond): - } - break - } - _ = rwc.Close() - - return o.open() -} - -func (o *usbInfo) open() (io.ReadWriteCloser, error) { - intf, done, err := o.Device.DefaultInterface() - if err != nil { - return nil, err - } - - in, err := intf.InEndpoint(0x01) - if err != nil { - return nil, err - } - - out, err := intf.OutEndpoint(0x01) - if err != nil { - return nil, err - } - - return &usbReadWriteCloser{ - done: done, - in: in, - out: out, - }, nil -} - -type usbReadWriteCloser struct { - done func() - in *gousb.InEndpoint - out *gousb.OutEndpoint -} - -func (r usbReadWriteCloser) Read(p []byte) (n int, err error) { - return r.in.Read(p) -} - -func (r usbReadWriteCloser) Write(p []byte) (n int, err error) { - return r.out.Write(p) -} - -func (r usbReadWriteCloser) Close() error { - if r.done != nil { - r.done() - r.done = nil - } - return nil -} diff --git a/wallet.go b/wallet.go index aeb787d..4c05058 100644 --- a/wallet.go +++ b/wallet.go @@ -32,6 +32,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/signer/core/apitypes" + "github.com/karalabe/usb" ) // Maximum time between wallet health checks to detect USB unplugs. @@ -96,8 +97,8 @@ type wallet struct { driver driver // Hardware implementation of the low level device operations url *accounts.URL // Textual URL uniquely identifying this wallet - info info // Known USB device infos about the wallet - device io.ReadWriteCloser // USB device advertising itself as a hardware wallet + info usb.DeviceInfo // Known USB device infos about the wallet + device usb.Device // USB device advertising itself as a hardware wallet accounts []accounts.Account // List of derive accounts pinned on the hardware wallet paths map[common.Address]accounts.DerivationPath // Known derivation paths for signing operations