| 
									
										
										
										
											2017-02-07 12:47:34 +02:00
										 |  |  | // Copyright 2017 The go-ethereum Authors | 
					
						
							|  |  |  | // This file is part of the go-ethereum library. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // The go-ethereum library is free software: you can redistribute it and/or modify | 
					
						
							|  |  |  | // it under the terms of the GNU Lesser General Public License as published by | 
					
						
							|  |  |  | // the Free Software Foundation, either version 3 of the License, or | 
					
						
							|  |  |  | // (at your option) any later version. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // The go-ethereum library is distributed in the hope that it will be useful, | 
					
						
							|  |  |  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | 
					
						
							|  |  |  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 
					
						
							|  |  |  | // GNU Lesser General Public License for more details. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // You should have received a copy of the GNU Lesser General Public License | 
					
						
							|  |  |  | // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // This file contains the implementation for interacting with the Ledger hardware | 
					
						
							|  |  |  | // wallets. The wire protocol spec can be found in the Ledger Blue GitHub repo: | 
					
						
							|  |  |  | // https://raw.githubusercontent.com/LedgerHQ/blue-app-eth/master/doc/ethapp.asc | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | package usbwallet | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							| 
									
										
										
										
											2017-02-16 15:36:44 +02:00
										 |  |  | 	"errors" | 
					
						
							| 
									
										
										
										
											2017-03-20 13:35:56 +02:00
										 |  |  | 	"runtime" | 
					
						
							| 
									
										
										
										
											2017-02-07 12:47:34 +02:00
										 |  |  | 	"sync" | 
					
						
							|  |  |  | 	"time" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	"github.com/ethereum/go-ethereum/accounts" | 
					
						
							|  |  |  | 	"github.com/ethereum/go-ethereum/event" | 
					
						
							| 
									
										
										
										
											2017-02-22 16:58:00 +02:00
										 |  |  | 	"github.com/ethereum/go-ethereum/log" | 
					
						
							| 
									
										
										
										
											2017-02-16 15:36:44 +02:00
										 |  |  | 	"github.com/karalabe/hid" | 
					
						
							| 
									
										
										
										
											2017-02-07 12:47:34 +02:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-02-08 15:53:02 +02:00
										 |  |  | // LedgerScheme is the protocol scheme prefixing account and wallet URLs. | 
					
						
							|  |  |  | var LedgerScheme = "ledger" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-02-07 12:47:34 +02:00
										 |  |  | // ledgerDeviceIDs are the known device IDs that Ledger wallets use. | 
					
						
							|  |  |  | var ledgerDeviceIDs = []deviceID{ | 
					
						
							|  |  |  | 	{Vendor: 0x2c97, Product: 0x0000}, // Ledger Blue | 
					
						
							|  |  |  | 	{Vendor: 0x2c97, Product: 0x0001}, // Ledger Nano S | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Maximum time between wallet refreshes (if USB hotplug notifications don't work). | 
					
						
							|  |  |  | const ledgerRefreshCycle = time.Second | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Minimum time between wallet refreshes to avoid USB trashing. | 
					
						
							|  |  |  | const ledgerRefreshThrottling = 500 * time.Millisecond | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // LedgerHub is a accounts.Backend that can find and handle Ledger hardware wallets. | 
					
						
							|  |  |  | type LedgerHub struct { | 
					
						
							|  |  |  | 	refreshed   time.Time               // Time instance when the list of wallets was last refreshed | 
					
						
							|  |  |  | 	wallets     []accounts.Wallet       // List of Ledger devices currently tracking | 
					
						
							|  |  |  | 	updateFeed  event.Feed              // Event feed to notify wallet additions/removals | 
					
						
							|  |  |  | 	updateScope event.SubscriptionScope // Subscription scope tracking current live listeners | 
					
						
							|  |  |  | 	updating    bool                    // Whether the event notification loop is running | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	quit chan chan error | 
					
						
							| 
									
										
										
										
											2017-03-20 13:35:56 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	stateLock sync.RWMutex // Protects the internals of the hub from racey access | 
					
						
							| 
									
										
										
										
											2017-03-23 17:04:39 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// TODO(karalabe): remove if hotplug lands on Windows | 
					
						
							|  |  |  | 	commsPend int        // Number of operations blocking enumeration | 
					
						
							|  |  |  | 	commsLock sync.Mutex // Lock protecting the pending counter and enumeration | 
					
						
							| 
									
										
										
										
											2017-02-07 12:47:34 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // NewLedgerHub creates a new hardware wallet manager for Ledger devices. | 
					
						
							|  |  |  | func NewLedgerHub() (*LedgerHub, error) { | 
					
						
							| 
									
										
										
										
											2017-02-16 15:36:44 +02:00
										 |  |  | 	if !hid.Supported() { | 
					
						
							|  |  |  | 		return nil, errors.New("unsupported platform") | 
					
						
							| 
									
										
										
										
											2017-02-07 12:47:34 +02:00
										 |  |  | 	} | 
					
						
							|  |  |  | 	hub := &LedgerHub{ | 
					
						
							|  |  |  | 		quit: make(chan chan error), | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	hub.refreshWallets() | 
					
						
							|  |  |  | 	return hub, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Wallets implements accounts.Backend, returning all the currently tracked USB | 
					
						
							|  |  |  | // devices that appear to be Ledger hardware wallets. | 
					
						
							|  |  |  | func (hub *LedgerHub) Wallets() []accounts.Wallet { | 
					
						
							|  |  |  | 	// Make sure the list of wallets is up to date | 
					
						
							|  |  |  | 	hub.refreshWallets() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-03-20 13:35:56 +02:00
										 |  |  | 	hub.stateLock.RLock() | 
					
						
							|  |  |  | 	defer hub.stateLock.RUnlock() | 
					
						
							| 
									
										
										
										
											2017-02-07 12:47:34 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	cpy := make([]accounts.Wallet, len(hub.wallets)) | 
					
						
							|  |  |  | 	copy(cpy, hub.wallets) | 
					
						
							|  |  |  | 	return cpy | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // refreshWallets scans the USB devices attached to the machine and updates the | 
					
						
							|  |  |  | // list of wallets based on the found devices. | 
					
						
							|  |  |  | func (hub *LedgerHub) refreshWallets() { | 
					
						
							|  |  |  | 	// Don't scan the USB like crazy it the user fetches wallets in a loop | 
					
						
							| 
									
										
										
										
											2017-03-20 13:35:56 +02:00
										 |  |  | 	hub.stateLock.RLock() | 
					
						
							| 
									
										
										
										
											2017-02-07 12:47:34 +02:00
										 |  |  | 	elapsed := time.Since(hub.refreshed) | 
					
						
							| 
									
										
										
										
											2017-03-20 13:35:56 +02:00
										 |  |  | 	hub.stateLock.RUnlock() | 
					
						
							| 
									
										
										
										
											2017-02-07 12:47:34 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	if elapsed < ledgerRefreshThrottling { | 
					
						
							|  |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	// Retrieve the current list of Ledger devices | 
					
						
							| 
									
										
										
										
											2017-02-16 15:36:44 +02:00
										 |  |  | 	var ledgers []hid.DeviceInfo | 
					
						
							| 
									
										
										
										
											2017-03-20 13:35:56 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	if runtime.GOOS == "linux" { | 
					
						
							|  |  |  | 		// hidapi on Linux opens the device during enumeration to retrieve some infos, | 
					
						
							|  |  |  | 		// breaking the Ledger protocol if that is waiting for user confirmation. This | 
					
						
							|  |  |  | 		// is a bug acknowledged at Ledger, but it won't be fixed on old devices so we | 
					
						
							|  |  |  | 		// need to prevent concurrent comms ourselves. The more elegant solution would | 
					
						
							|  |  |  | 		// be to ditch enumeration in favor of hutplug events, but that don't work yet | 
					
						
							|  |  |  | 		// on Windows so if we need to hack it anyway, this is more elegant for now. | 
					
						
							|  |  |  | 		hub.commsLock.Lock() | 
					
						
							| 
									
										
										
										
											2017-03-23 17:04:39 +02:00
										 |  |  | 		if hub.commsPend > 0 { // A confirmation is pending, don't refresh | 
					
						
							|  |  |  | 			hub.commsLock.Unlock() | 
					
						
							|  |  |  | 			return | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2017-03-20 13:35:56 +02:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2017-02-16 15:36:44 +02:00
										 |  |  | 	for _, info := range hid.Enumerate(0, 0) { // Can't enumerate directly, one valid ID is the 0 wildcard | 
					
						
							| 
									
										
										
										
											2017-02-07 12:47:34 +02:00
										 |  |  | 		for _, id := range ledgerDeviceIDs { | 
					
						
							| 
									
										
										
										
											2017-02-16 15:36:44 +02:00
										 |  |  | 			if info.VendorID == id.Vendor && info.ProductID == id.Product { | 
					
						
							|  |  |  | 				ledgers = append(ledgers, info) | 
					
						
							|  |  |  | 				break | 
					
						
							| 
									
										
										
										
											2017-02-07 12:47:34 +02:00
										 |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2017-02-16 15:36:44 +02:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2017-03-20 13:35:56 +02:00
										 |  |  | 	if runtime.GOOS == "linux" { | 
					
						
							|  |  |  | 		// See rationale before the enumeration why this is needed and only on Linux. | 
					
						
							|  |  |  | 		hub.commsLock.Unlock() | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2017-02-07 12:47:34 +02:00
										 |  |  | 	// Transform the current list of wallets into the new one | 
					
						
							| 
									
										
										
										
											2017-03-20 13:35:56 +02:00
										 |  |  | 	hub.stateLock.Lock() | 
					
						
							| 
									
										
										
										
											2017-02-07 12:47:34 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-02-16 15:36:44 +02:00
										 |  |  | 	wallets := make([]accounts.Wallet, 0, len(ledgers)) | 
					
						
							| 
									
										
										
										
											2017-02-07 12:47:34 +02:00
										 |  |  | 	events := []accounts.WalletEvent{} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-02-16 15:36:44 +02:00
										 |  |  | 	for _, ledger := range ledgers { | 
					
						
							|  |  |  | 		url := accounts.URL{Scheme: LedgerScheme, Path: ledger.Path} | 
					
						
							| 
									
										
										
										
											2017-02-07 12:47:34 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-02-09 03:28:22 +02:00
										 |  |  | 		// Drop wallets in front of the next device or those that failed for some reason | 
					
						
							|  |  |  | 		for len(hub.wallets) > 0 && (hub.wallets[0].URL().Cmp(url) < 0 || hub.wallets[0].(*ledgerWallet).failed()) { | 
					
						
							| 
									
										
										
										
											2017-02-07 12:47:34 +02:00
										 |  |  | 			events = append(events, accounts.WalletEvent{Wallet: hub.wallets[0], Arrive: false}) | 
					
						
							|  |  |  | 			hub.wallets = hub.wallets[1:] | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2017-02-09 03:28:22 +02:00
										 |  |  | 		// If there are no more wallets or the device is before the next, wrap new wallet | 
					
						
							| 
									
										
										
										
											2017-02-08 15:53:02 +02:00
										 |  |  | 		if len(hub.wallets) == 0 || hub.wallets[0].URL().Cmp(url) > 0 { | 
					
						
							| 
									
										
										
										
											2017-03-20 13:35:56 +02:00
										 |  |  | 			wallet := &ledgerWallet{hub: hub, url: &url, info: ledger, log: log.New("url", url)} | 
					
						
							| 
									
										
										
										
											2017-02-07 12:47:34 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 			events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: true}) | 
					
						
							|  |  |  | 			wallets = append(wallets, wallet) | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2017-02-09 03:28:22 +02:00
										 |  |  | 		// If the device is the same as the first wallet, keep it | 
					
						
							| 
									
										
										
										
											2017-02-08 15:53:02 +02:00
										 |  |  | 		if hub.wallets[0].URL().Cmp(url) == 0 { | 
					
						
							| 
									
										
										
										
											2017-02-07 12:47:34 +02:00
										 |  |  | 			wallets = append(wallets, hub.wallets[0]) | 
					
						
							|  |  |  | 			hub.wallets = hub.wallets[1:] | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	// Drop any leftover wallets and set the new batch | 
					
						
							|  |  |  | 	for _, wallet := range hub.wallets { | 
					
						
							|  |  |  | 		events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: false}) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	hub.refreshed = time.Now() | 
					
						
							|  |  |  | 	hub.wallets = wallets | 
					
						
							| 
									
										
										
										
											2017-03-20 13:35:56 +02:00
										 |  |  | 	hub.stateLock.Unlock() | 
					
						
							| 
									
										
										
										
											2017-02-07 12:47:34 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Fire all wallet events and return | 
					
						
							|  |  |  | 	for _, event := range events { | 
					
						
							|  |  |  | 		hub.updateFeed.Send(event) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Subscribe implements accounts.Backend, creating an async subscription to | 
					
						
							|  |  |  | // receive notifications on the addition or removal of Ledger wallets. | 
					
						
							|  |  |  | func (hub *LedgerHub) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription { | 
					
						
							|  |  |  | 	// We need the mutex to reliably start/stop the update loop | 
					
						
							| 
									
										
										
										
											2017-03-20 13:35:56 +02:00
										 |  |  | 	hub.stateLock.Lock() | 
					
						
							|  |  |  | 	defer hub.stateLock.Unlock() | 
					
						
							| 
									
										
										
										
											2017-02-07 12:47:34 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Subscribe the caller and track the subscriber count | 
					
						
							|  |  |  | 	sub := hub.updateScope.Track(hub.updateFeed.Subscribe(sink)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Subscribers require an active notification loop, start it | 
					
						
							|  |  |  | 	if !hub.updating { | 
					
						
							|  |  |  | 		hub.updating = true | 
					
						
							|  |  |  | 		go hub.updater() | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return sub | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // updater is responsible for maintaining an up-to-date list of wallets stored in | 
					
						
							|  |  |  | // the keystore, and for firing wallet addition/removal events. It listens for | 
					
						
							|  |  |  | // account change events from the underlying account cache, and also periodically | 
					
						
							|  |  |  | // forces a manual refresh (only triggers for systems where the filesystem notifier | 
					
						
							|  |  |  | // is not running). | 
					
						
							|  |  |  | func (hub *LedgerHub) updater() { | 
					
						
							|  |  |  | 	for { | 
					
						
							|  |  |  | 		// Wait for a USB hotplug event (not supported yet) or a refresh timeout | 
					
						
							|  |  |  | 		select { | 
					
						
							|  |  |  | 		//case <-hub.changes: // reenable on hutplug implementation | 
					
						
							|  |  |  | 		case <-time.After(ledgerRefreshCycle): | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		// Run the wallet refresher | 
					
						
							|  |  |  | 		hub.refreshWallets() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// If all our subscribers left, stop the updater | 
					
						
							| 
									
										
										
										
											2017-03-20 13:35:56 +02:00
										 |  |  | 		hub.stateLock.Lock() | 
					
						
							| 
									
										
										
										
											2017-02-07 12:47:34 +02:00
										 |  |  | 		if hub.updateScope.Count() == 0 { | 
					
						
							|  |  |  | 			hub.updating = false | 
					
						
							| 
									
										
										
										
											2017-03-20 13:35:56 +02:00
										 |  |  | 			hub.stateLock.Unlock() | 
					
						
							| 
									
										
										
										
											2017-02-07 12:47:34 +02:00
										 |  |  | 			return | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2017-03-20 13:35:56 +02:00
										 |  |  | 		hub.stateLock.Unlock() | 
					
						
							| 
									
										
										
										
											2017-02-07 12:47:34 +02:00
										 |  |  | 	} | 
					
						
							|  |  |  | } |