github.com/ethereum/go-ethereum@v1.14.3/accounts/usbwallet/hub.go (about)

     1  // Copyright 2017 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-ethereum library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package usbwallet
    18  
    19  import (
    20  	"errors"
    21  	"runtime"
    22  	"sync"
    23  	"sync/atomic"
    24  	"time"
    25  
    26  	"github.com/ethereum/go-ethereum/accounts"
    27  	"github.com/ethereum/go-ethereum/event"
    28  	"github.com/ethereum/go-ethereum/log"
    29  	"github.com/karalabe/hid"
    30  )
    31  
    32  // LedgerScheme is the protocol scheme prefixing account and wallet URLs.
    33  const LedgerScheme = "ledger"
    34  
    35  // TrezorScheme is the protocol scheme prefixing account and wallet URLs.
    36  const TrezorScheme = "trezor"
    37  
    38  // refreshCycle is the maximum time between wallet refreshes (if USB hotplug
    39  // notifications don't work).
    40  const refreshCycle = time.Second
    41  
    42  // refreshThrottling is the minimum time between wallet refreshes to avoid USB
    43  // trashing.
    44  const refreshThrottling = 500 * time.Millisecond
    45  
    46  // Hub is a accounts.Backend that can find and handle generic USB hardware wallets.
    47  type Hub struct {
    48  	scheme     string                  // Protocol scheme prefixing account and wallet URLs.
    49  	vendorID   uint16                  // USB vendor identifier used for device discovery
    50  	productIDs []uint16                // USB product identifiers used for device discovery
    51  	usageID    uint16                  // USB usage page identifier used for macOS device discovery
    52  	endpointID int                     // USB endpoint identifier used for non-macOS device discovery
    53  	makeDriver func(log.Logger) driver // Factory method to construct a vendor specific driver
    54  
    55  	refreshed   time.Time               // Time instance when the list of wallets was last refreshed
    56  	wallets     []accounts.Wallet       // List of USB wallet devices currently tracking
    57  	updateFeed  event.Feed              // Event feed to notify wallet additions/removals
    58  	updateScope event.SubscriptionScope // Subscription scope tracking current live listeners
    59  	updating    bool                    // Whether the event notification loop is running
    60  
    61  	quit chan chan error
    62  
    63  	stateLock sync.RWMutex // Protects the internals of the hub from racey access
    64  
    65  	// TODO(karalabe): remove if hotplug lands on Windows
    66  	commsPend int           // Number of operations blocking enumeration
    67  	commsLock sync.Mutex    // Lock protecting the pending counter and enumeration
    68  	enumFails atomic.Uint32 // Number of times enumeration has failed
    69  }
    70  
    71  // NewLedgerHub creates a new hardware wallet manager for Ledger devices.
    72  func NewLedgerHub() (*Hub, error) {
    73  	return newHub(LedgerScheme, 0x2c97, []uint16{
    74  
    75  		// Device definitions taken from
    76  		// https://github.com/LedgerHQ/ledger-live/blob/38012bc8899e0f07149ea9cfe7e64b2c146bc92b/libs/ledgerjs/packages/devices/src/index.ts
    77  
    78  		// Original product IDs
    79  		0x0000, /* Ledger Blue */
    80  		0x0001, /* Ledger Nano S */
    81  		0x0004, /* Ledger Nano X */
    82  		0x0005, /* Ledger Nano S Plus */
    83  		0x0006, /* Ledger Nano FTS */
    84  
    85  		0x0015, /* HID + U2F + WebUSB Ledger Blue */
    86  		0x1015, /* HID + U2F + WebUSB Ledger Nano S */
    87  		0x4015, /* HID + U2F + WebUSB Ledger Nano X */
    88  		0x5015, /* HID + U2F + WebUSB Ledger Nano S Plus */
    89  		0x6015, /* HID + U2F + WebUSB Ledger Nano FTS */
    90  
    91  		0x0011, /* HID + WebUSB Ledger Blue */
    92  		0x1011, /* HID + WebUSB Ledger Nano S */
    93  		0x4011, /* HID + WebUSB Ledger Nano X */
    94  		0x5011, /* HID + WebUSB Ledger Nano S Plus */
    95  		0x6011, /* HID + WebUSB Ledger Nano FTS */
    96  	}, 0xffa0, 0, newLedgerDriver)
    97  }
    98  
    99  // NewTrezorHubWithHID creates a new hardware wallet manager for Trezor devices.
   100  func NewTrezorHubWithHID() (*Hub, error) {
   101  	return newHub(TrezorScheme, 0x534c, []uint16{0x0001 /* Trezor HID */}, 0xff00, 0, newTrezorDriver)
   102  }
   103  
   104  // NewTrezorHubWithWebUSB creates a new hardware wallet manager for Trezor devices with
   105  // firmware version > 1.8.0
   106  func NewTrezorHubWithWebUSB() (*Hub, error) {
   107  	return newHub(TrezorScheme, 0x1209, []uint16{0x53c1 /* Trezor WebUSB */}, 0xffff /* No usage id on webusb, don't match unset (0) */, 0, newTrezorDriver)
   108  }
   109  
   110  // newHub creates a new hardware wallet manager for generic USB devices.
   111  func newHub(scheme string, vendorID uint16, productIDs []uint16, usageID uint16, endpointID int, makeDriver func(log.Logger) driver) (*Hub, error) {
   112  	if !hid.Supported() {
   113  		return nil, errors.New("unsupported platform")
   114  	}
   115  	hub := &Hub{
   116  		scheme:     scheme,
   117  		vendorID:   vendorID,
   118  		productIDs: productIDs,
   119  		usageID:    usageID,
   120  		endpointID: endpointID,
   121  		makeDriver: makeDriver,
   122  		quit:       make(chan chan error),
   123  	}
   124  	hub.refreshWallets()
   125  	return hub, nil
   126  }
   127  
   128  // Wallets implements accounts.Backend, returning all the currently tracked USB
   129  // devices that appear to be hardware wallets.
   130  func (hub *Hub) Wallets() []accounts.Wallet {
   131  	// Make sure the list of wallets is up to date
   132  	hub.refreshWallets()
   133  
   134  	hub.stateLock.RLock()
   135  	defer hub.stateLock.RUnlock()
   136  
   137  	cpy := make([]accounts.Wallet, len(hub.wallets))
   138  	copy(cpy, hub.wallets)
   139  	return cpy
   140  }
   141  
   142  // refreshWallets scans the USB devices attached to the machine and updates the
   143  // list of wallets based on the found devices.
   144  func (hub *Hub) refreshWallets() {
   145  	// Don't scan the USB like crazy it the user fetches wallets in a loop
   146  	hub.stateLock.RLock()
   147  	elapsed := time.Since(hub.refreshed)
   148  	hub.stateLock.RUnlock()
   149  
   150  	if elapsed < refreshThrottling {
   151  		return
   152  	}
   153  	// If USB enumeration is continually failing, don't keep trying indefinitely
   154  	if hub.enumFails.Load() > 2 {
   155  		return
   156  	}
   157  	// Retrieve the current list of USB wallet devices
   158  	var devices []hid.DeviceInfo
   159  
   160  	if runtime.GOOS == "linux" {
   161  		// hidapi on Linux opens the device during enumeration to retrieve some infos,
   162  		// breaking the Ledger protocol if that is waiting for user confirmation. This
   163  		// is a bug acknowledged at Ledger, but it won't be fixed on old devices so we
   164  		// need to prevent concurrent comms ourselves. The more elegant solution would
   165  		// be to ditch enumeration in favor of hotplug events, but that don't work yet
   166  		// on Windows so if we need to hack it anyway, this is more elegant for now.
   167  		hub.commsLock.Lock()
   168  		if hub.commsPend > 0 { // A confirmation is pending, don't refresh
   169  			hub.commsLock.Unlock()
   170  			return
   171  		}
   172  	}
   173  	infos, err := hid.Enumerate(hub.vendorID, 0)
   174  	if err != nil {
   175  		failcount := hub.enumFails.Add(1)
   176  		if runtime.GOOS == "linux" {
   177  			// See rationale before the enumeration why this is needed and only on Linux.
   178  			hub.commsLock.Unlock()
   179  		}
   180  		log.Error("Failed to enumerate USB devices", "hub", hub.scheme,
   181  			"vendor", hub.vendorID, "failcount", failcount, "err", err)
   182  		return
   183  	}
   184  	hub.enumFails.Store(0)
   185  
   186  	for _, info := range infos {
   187  		for _, id := range hub.productIDs {
   188  			// Windows and Macos use UsageID matching, Linux uses Interface matching
   189  			if info.ProductID == id && (info.UsagePage == hub.usageID || info.Interface == hub.endpointID) {
   190  				devices = append(devices, info)
   191  				break
   192  			}
   193  		}
   194  	}
   195  	if runtime.GOOS == "linux" {
   196  		// See rationale before the enumeration why this is needed and only on Linux.
   197  		hub.commsLock.Unlock()
   198  	}
   199  	// Transform the current list of wallets into the new one
   200  	hub.stateLock.Lock()
   201  
   202  	var (
   203  		wallets = make([]accounts.Wallet, 0, len(devices))
   204  		events  []accounts.WalletEvent
   205  	)
   206  
   207  	for _, device := range devices {
   208  		url := accounts.URL{Scheme: hub.scheme, Path: device.Path}
   209  
   210  		// Drop wallets in front of the next device or those that failed for some reason
   211  		for len(hub.wallets) > 0 {
   212  			// Abort if we're past the current device and found an operational one
   213  			_, failure := hub.wallets[0].Status()
   214  			if hub.wallets[0].URL().Cmp(url) >= 0 || failure == nil {
   215  				break
   216  			}
   217  			// Drop the stale and failed devices
   218  			events = append(events, accounts.WalletEvent{Wallet: hub.wallets[0], Kind: accounts.WalletDropped})
   219  			hub.wallets = hub.wallets[1:]
   220  		}
   221  		// If there are no more wallets or the device is before the next, wrap new wallet
   222  		if len(hub.wallets) == 0 || hub.wallets[0].URL().Cmp(url) > 0 {
   223  			logger := log.New("url", url)
   224  			wallet := &wallet{hub: hub, driver: hub.makeDriver(logger), url: &url, info: device, log: logger}
   225  
   226  			events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletArrived})
   227  			wallets = append(wallets, wallet)
   228  			continue
   229  		}
   230  		// If the device is the same as the first wallet, keep it
   231  		if hub.wallets[0].URL().Cmp(url) == 0 {
   232  			wallets = append(wallets, hub.wallets[0])
   233  			hub.wallets = hub.wallets[1:]
   234  			continue
   235  		}
   236  	}
   237  	// Drop any leftover wallets and set the new batch
   238  	for _, wallet := range hub.wallets {
   239  		events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletDropped})
   240  	}
   241  	hub.refreshed = time.Now()
   242  	hub.wallets = wallets
   243  	hub.stateLock.Unlock()
   244  
   245  	// Fire all wallet events and return
   246  	for _, event := range events {
   247  		hub.updateFeed.Send(event)
   248  	}
   249  }
   250  
   251  // Subscribe implements accounts.Backend, creating an async subscription to
   252  // receive notifications on the addition or removal of USB wallets.
   253  func (hub *Hub) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription {
   254  	// We need the mutex to reliably start/stop the update loop
   255  	hub.stateLock.Lock()
   256  	defer hub.stateLock.Unlock()
   257  
   258  	// Subscribe the caller and track the subscriber count
   259  	sub := hub.updateScope.Track(hub.updateFeed.Subscribe(sink))
   260  
   261  	// Subscribers require an active notification loop, start it
   262  	if !hub.updating {
   263  		hub.updating = true
   264  		go hub.updater()
   265  	}
   266  	return sub
   267  }
   268  
   269  // updater is responsible for maintaining an up-to-date list of wallets managed
   270  // by the USB hub, and for firing wallet addition/removal events.
   271  func (hub *Hub) updater() {
   272  	for {
   273  		// TODO: Wait for a USB hotplug event (not supported yet) or a refresh timeout
   274  		// <-hub.changes
   275  		time.Sleep(refreshCycle)
   276  
   277  		// Run the wallet refresher
   278  		hub.refreshWallets()
   279  
   280  		// If all our subscribers left, stop the updater
   281  		hub.stateLock.Lock()
   282  		if hub.updateScope.Count() == 0 {
   283  			hub.updating = false
   284  			hub.stateLock.Unlock()
   285  			return
   286  		}
   287  		hub.stateLock.Unlock()
   288  	}
   289  }