github.com/ava-labs/subnet-evm@v0.6.4/accounts/scwallet/hub.go (about)

     1  // (c) 2019-2020, Ava Labs, Inc.
     2  //
     3  // This file is a derived work, based on the go-ethereum library whose original
     4  // notices appear below.
     5  //
     6  // It is distributed under a license compatible with the licensing terms of the
     7  // original code from which it is derived.
     8  //
     9  // Much love to the original authors for their work.
    10  // **********
    11  // Copyright 2018 The go-ethereum Authors
    12  // This file is part of the go-ethereum library.
    13  //
    14  // The go-ethereum library is free software: you can redistribute it and/or modify
    15  // it under the terms of the GNU Lesser General Public License as published by
    16  // the Free Software Foundation, either version 3 of the License, or
    17  // (at your option) any later version.
    18  //
    19  // The go-ethereum library is distributed in the hope that it will be useful,
    20  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    21  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    22  // GNU Lesser General Public License for more details.
    23  //
    24  // You should have received a copy of the GNU Lesser General Public License
    25  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    26  
    27  // This package implements support for smartcard-based hardware wallets such as
    28  // the one written by Status: https://github.com/status-im/hardware-wallet
    29  //
    30  // This implementation of smartcard wallets have a different interaction process
    31  // to other types of hardware wallet. The process works like this:
    32  //
    33  // 1. (First use with a given client) Establish a pairing between hardware
    34  //    wallet and client. This requires a secret value called a 'pairing password'.
    35  //    You can pair with an unpaired wallet with `personal.openWallet(URI, pairing password)`.
    36  // 2. (First use only) Initialize the wallet, which generates a keypair, stores
    37  //    it on the wallet, and returns it so the user can back it up. You can
    38  //    initialize a wallet with `personal.initializeWallet(URI)`.
    39  // 3. Connect to the wallet using the pairing information established in step 1.
    40  //    You can connect to a paired wallet with `personal.openWallet(URI, PIN)`.
    41  // 4. Interact with the wallet as normal.
    42  
    43  package scwallet
    44  
    45  import (
    46  	"encoding/json"
    47  	"io"
    48  	"os"
    49  	"path/filepath"
    50  	"sort"
    51  	"sync"
    52  	"time"
    53  
    54  	"github.com/ava-labs/subnet-evm/accounts"
    55  	"github.com/ethereum/go-ethereum/common"
    56  	"github.com/ethereum/go-ethereum/event"
    57  	"github.com/ethereum/go-ethereum/log"
    58  	pcsc "github.com/gballet/go-libpcsclite"
    59  )
    60  
    61  // Scheme is the URI prefix for smartcard wallets.
    62  const Scheme = "keycard"
    63  
    64  // refreshCycle is the maximum time between wallet refreshes (if USB hotplug
    65  // notifications don't work).
    66  const refreshCycle = time.Second
    67  
    68  // refreshThrottling is the minimum time between wallet refreshes to avoid thrashing.
    69  const refreshThrottling = 500 * time.Millisecond
    70  
    71  // smartcardPairing contains information about a smart card we have paired with
    72  // or might pair with the hub.
    73  type smartcardPairing struct {
    74  	PublicKey    []byte                                     `json:"publicKey"`
    75  	PairingIndex uint8                                      `json:"pairingIndex"`
    76  	PairingKey   []byte                                     `json:"pairingKey"`
    77  	Accounts     map[common.Address]accounts.DerivationPath `json:"accounts"`
    78  }
    79  
    80  // Hub is a accounts.Backend that can find and handle generic PC/SC hardware wallets.
    81  type Hub struct {
    82  	scheme string // Protocol scheme prefixing account and wallet URLs.
    83  
    84  	context  *pcsc.Client
    85  	datadir  string
    86  	pairings map[string]smartcardPairing
    87  
    88  	refreshed   time.Time               // Time instance when the list of wallets was last refreshed
    89  	wallets     map[string]*Wallet      // Mapping from reader names to wallet instances
    90  	updateFeed  event.Feed              // Event feed to notify wallet additions/removals
    91  	updateScope event.SubscriptionScope // Subscription scope tracking current live listeners
    92  	updating    bool                    // Whether the event notification loop is running
    93  
    94  	quit chan chan error
    95  
    96  	stateLock sync.RWMutex // Protects the internals of the hub from racey access
    97  }
    98  
    99  func (hub *Hub) readPairings() error {
   100  	hub.pairings = make(map[string]smartcardPairing)
   101  	pairingFile, err := os.Open(filepath.Join(hub.datadir, "smartcards.json"))
   102  	if err != nil {
   103  		if os.IsNotExist(err) {
   104  			return nil
   105  		}
   106  		return err
   107  	}
   108  
   109  	pairingData, err := io.ReadAll(pairingFile)
   110  	if err != nil {
   111  		return err
   112  	}
   113  	var pairings []smartcardPairing
   114  	if err := json.Unmarshal(pairingData, &pairings); err != nil {
   115  		return err
   116  	}
   117  
   118  	for _, pairing := range pairings {
   119  		hub.pairings[string(pairing.PublicKey)] = pairing
   120  	}
   121  	return nil
   122  }
   123  
   124  func (hub *Hub) writePairings() error {
   125  	pairingFile, err := os.OpenFile(filepath.Join(hub.datadir, "smartcards.json"), os.O_RDWR|os.O_CREATE, 0755)
   126  	if err != nil {
   127  		return err
   128  	}
   129  	defer pairingFile.Close()
   130  
   131  	pairings := make([]smartcardPairing, 0, len(hub.pairings))
   132  	for _, pairing := range hub.pairings {
   133  		pairings = append(pairings, pairing)
   134  	}
   135  
   136  	pairingData, err := json.Marshal(pairings)
   137  	if err != nil {
   138  		return err
   139  	}
   140  
   141  	if _, err := pairingFile.Write(pairingData); err != nil {
   142  		return err
   143  	}
   144  
   145  	return nil
   146  }
   147  
   148  func (hub *Hub) pairing(wallet *Wallet) *smartcardPairing {
   149  	if pairing, ok := hub.pairings[string(wallet.PublicKey)]; ok {
   150  		return &pairing
   151  	}
   152  	return nil
   153  }
   154  
   155  func (hub *Hub) setPairing(wallet *Wallet, pairing *smartcardPairing) error {
   156  	if pairing == nil {
   157  		delete(hub.pairings, string(wallet.PublicKey))
   158  	} else {
   159  		hub.pairings[string(wallet.PublicKey)] = *pairing
   160  	}
   161  	return hub.writePairings()
   162  }
   163  
   164  // NewHub creates a new hardware wallet manager for smartcards.
   165  func NewHub(daemonPath string, scheme string, datadir string) (*Hub, error) {
   166  	context, err := pcsc.EstablishContext(daemonPath, pcsc.ScopeSystem)
   167  	if err != nil {
   168  		return nil, err
   169  	}
   170  	hub := &Hub{
   171  		scheme:  scheme,
   172  		context: context,
   173  		datadir: datadir,
   174  		wallets: make(map[string]*Wallet),
   175  		quit:    make(chan chan error),
   176  	}
   177  	if err := hub.readPairings(); err != nil {
   178  		return nil, err
   179  	}
   180  	hub.refreshWallets()
   181  	return hub, nil
   182  }
   183  
   184  // Wallets implements accounts.Backend, returning all the currently tracked smart
   185  // cards that appear to be hardware wallets.
   186  func (hub *Hub) Wallets() []accounts.Wallet {
   187  	// Make sure the list of wallets is up to date
   188  	hub.refreshWallets()
   189  
   190  	hub.stateLock.RLock()
   191  	defer hub.stateLock.RUnlock()
   192  
   193  	cpy := make([]accounts.Wallet, 0, len(hub.wallets))
   194  	for _, wallet := range hub.wallets {
   195  		cpy = append(cpy, wallet)
   196  	}
   197  	sort.Sort(accounts.WalletsByURL(cpy))
   198  	return cpy
   199  }
   200  
   201  // refreshWallets scans the devices attached to the machine and updates the
   202  // list of wallets based on the found devices.
   203  func (hub *Hub) refreshWallets() {
   204  	// Don't scan the USB like crazy it the user fetches wallets in a loop
   205  	hub.stateLock.RLock()
   206  	elapsed := time.Since(hub.refreshed)
   207  	hub.stateLock.RUnlock()
   208  
   209  	if elapsed < refreshThrottling {
   210  		return
   211  	}
   212  	// Retrieve all the smart card reader to check for cards
   213  	readers, err := hub.context.ListReaders()
   214  	if err != nil {
   215  		// This is a perverted hack, the scard library returns an error if no card
   216  		// readers are present instead of simply returning an empty list. We don't
   217  		// want to fill the user's log with errors, so filter those out.
   218  		if err.Error() != "scard: Cannot find a smart card reader." {
   219  			log.Error("Failed to enumerate smart card readers", "err", err)
   220  			return
   221  		}
   222  	}
   223  	// Transform the current list of wallets into the new one
   224  	hub.stateLock.Lock()
   225  
   226  	events := []accounts.WalletEvent{}
   227  	seen := make(map[string]struct{})
   228  
   229  	for _, reader := range readers {
   230  		// Mark the reader as present
   231  		seen[reader] = struct{}{}
   232  
   233  		// If we already know about this card, skip to the next reader, otherwise clean up
   234  		if wallet, ok := hub.wallets[reader]; ok {
   235  			if err := wallet.ping(); err == nil {
   236  				continue
   237  			}
   238  			wallet.Close()
   239  			events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletDropped})
   240  			delete(hub.wallets, reader)
   241  		}
   242  		// New card detected, try to connect to it
   243  		card, err := hub.context.Connect(reader, pcsc.ShareShared, pcsc.ProtocolAny)
   244  		if err != nil {
   245  			log.Debug("Failed to open smart card", "reader", reader, "err", err)
   246  			continue
   247  		}
   248  		wallet := NewWallet(hub, card)
   249  		if err = wallet.connect(); err != nil {
   250  			log.Debug("Failed to connect to smart card", "reader", reader, "err", err)
   251  			card.Disconnect(pcsc.LeaveCard)
   252  			continue
   253  		}
   254  		// Card connected, start tracking in amongs the wallets
   255  		hub.wallets[reader] = wallet
   256  		events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletArrived})
   257  	}
   258  	// Remove any wallets no longer present
   259  	for reader, wallet := range hub.wallets {
   260  		if _, ok := seen[reader]; !ok {
   261  			wallet.Close()
   262  			events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletDropped})
   263  			delete(hub.wallets, reader)
   264  		}
   265  	}
   266  	hub.refreshed = time.Now()
   267  	hub.stateLock.Unlock()
   268  
   269  	for _, event := range events {
   270  		hub.updateFeed.Send(event)
   271  	}
   272  }
   273  
   274  // Subscribe implements accounts.Backend, creating an async subscription to
   275  // receive notifications on the addition or removal of smart card wallets.
   276  func (hub *Hub) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription {
   277  	// We need the mutex to reliably start/stop the update loop
   278  	hub.stateLock.Lock()
   279  	defer hub.stateLock.Unlock()
   280  
   281  	// Subscribe the caller and track the subscriber count
   282  	sub := hub.updateScope.Track(hub.updateFeed.Subscribe(sink))
   283  
   284  	// Subscribers require an active notification loop, start it
   285  	if !hub.updating {
   286  		hub.updating = true
   287  		go hub.updater()
   288  	}
   289  	return sub
   290  }
   291  
   292  // updater is responsible for maintaining an up-to-date list of wallets managed
   293  // by the smart card hub, and for firing wallet addition/removal events.
   294  func (hub *Hub) updater() {
   295  	for {
   296  		// TODO: Wait for a USB hotplug event (not supported yet) or a refresh timeout
   297  		// <-hub.changes
   298  		time.Sleep(refreshCycle)
   299  
   300  		// Run the wallet refresher
   301  		hub.refreshWallets()
   302  
   303  		// If all our subscribers left, stop the updater
   304  		hub.stateLock.Lock()
   305  		if hub.updateScope.Count() == 0 {
   306  			hub.updating = false
   307  			hub.stateLock.Unlock()
   308  			return
   309  		}
   310  		hub.stateLock.Unlock()
   311  	}
   312  }