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

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