github.com/kaleido-io/go-ethereum@v1.9.7/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/ioutil"
    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  
    99  	pairingData, err := ioutil.ReadAll(pairingFile)
   100  	if err != nil {
   101  		return err
   102  	}
   103  	var pairings []smartcardPairing
   104  	if err := json.Unmarshal(pairingData, &pairings); err != nil {
   105  		return err
   106  	}
   107  
   108  	for _, pairing := range pairings {
   109  		hub.pairings[string(pairing.PublicKey)] = pairing
   110  	}
   111  	return nil
   112  }
   113  
   114  func (hub *Hub) writePairings() error {
   115  	pairingFile, err := os.OpenFile(filepath.Join(hub.datadir, "smartcards.json"), os.O_RDWR|os.O_CREATE, 0755)
   116  	if err != nil {
   117  		return err
   118  	}
   119  	defer pairingFile.Close()
   120  
   121  	pairings := make([]smartcardPairing, 0, len(hub.pairings))
   122  	for _, pairing := range hub.pairings {
   123  		pairings = append(pairings, pairing)
   124  	}
   125  
   126  	pairingData, err := json.Marshal(pairings)
   127  	if err != nil {
   128  		return err
   129  	}
   130  
   131  	if _, err := pairingFile.Write(pairingData); err != nil {
   132  		return err
   133  	}
   134  
   135  	return nil
   136  }
   137  
   138  func (hub *Hub) pairing(wallet *Wallet) *smartcardPairing {
   139  	if pairing, ok := hub.pairings[string(wallet.PublicKey)]; ok {
   140  		return &pairing
   141  	}
   142  	return nil
   143  }
   144  
   145  func (hub *Hub) setPairing(wallet *Wallet, pairing *smartcardPairing) error {
   146  	if pairing == nil {
   147  		delete(hub.pairings, string(wallet.PublicKey))
   148  	} else {
   149  		hub.pairings[string(wallet.PublicKey)] = *pairing
   150  	}
   151  	return hub.writePairings()
   152  }
   153  
   154  // NewHub creates a new hardware wallet manager for smartcards.
   155  func NewHub(daemonPath string, scheme string, datadir string) (*Hub, error) {
   156  	context, err := pcsc.EstablishContext(daemonPath, pcsc.ScopeSystem)
   157  	if err != nil {
   158  		return nil, err
   159  	}
   160  	hub := &Hub{
   161  		scheme:  scheme,
   162  		context: context,
   163  		datadir: datadir,
   164  		wallets: make(map[string]*Wallet),
   165  		quit:    make(chan chan error),
   166  	}
   167  	if err := hub.readPairings(); err != nil {
   168  		return nil, err
   169  	}
   170  	hub.refreshWallets()
   171  	return hub, nil
   172  }
   173  
   174  // Wallets implements accounts.Backend, returning all the currently tracked smart
   175  // cards that appear to be hardware wallets.
   176  func (hub *Hub) Wallets() []accounts.Wallet {
   177  	// Make sure the list of wallets is up to date
   178  	hub.refreshWallets()
   179  
   180  	hub.stateLock.RLock()
   181  	defer hub.stateLock.RUnlock()
   182  
   183  	cpy := make([]accounts.Wallet, 0, len(hub.wallets))
   184  	for _, wallet := range hub.wallets {
   185  		cpy = append(cpy, wallet)
   186  	}
   187  	sort.Sort(accounts.WalletsByURL(cpy))
   188  	return cpy
   189  }
   190  
   191  // refreshWallets scans the devices attached to the machine and updates the
   192  // list of wallets based on the found devices.
   193  func (hub *Hub) refreshWallets() {
   194  	// Don't scan the USB like crazy it the user fetches wallets in a loop
   195  	hub.stateLock.RLock()
   196  	elapsed := time.Since(hub.refreshed)
   197  	hub.stateLock.RUnlock()
   198  
   199  	if elapsed < refreshThrottling {
   200  		return
   201  	}
   202  	// Retrieve all the smart card reader to check for cards
   203  	readers, err := hub.context.ListReaders()
   204  	if err != nil {
   205  		// This is a perverted hack, the scard library returns an error if no card
   206  		// readers are present instead of simply returning an empty list. We don't
   207  		// want to fill the user's log with errors, so filter those out.
   208  		if err.Error() != "scard: Cannot find a smart card reader." {
   209  			log.Error("Failed to enumerate smart card readers", "err", err)
   210  			return
   211  		}
   212  	}
   213  	// Transform the current list of wallets into the new one
   214  	hub.stateLock.Lock()
   215  
   216  	events := []accounts.WalletEvent{}
   217  	seen := make(map[string]struct{})
   218  
   219  	for _, reader := range readers {
   220  		// Mark the reader as present
   221  		seen[reader] = struct{}{}
   222  
   223  		// If we alreay know about this card, skip to the next reader, otherwise clean up
   224  		if wallet, ok := hub.wallets[reader]; ok {
   225  			if err := wallet.ping(); err == nil {
   226  				continue
   227  			}
   228  			wallet.Close()
   229  			events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletDropped})
   230  			delete(hub.wallets, reader)
   231  		}
   232  		// New card detected, try to connect to it
   233  		card, err := hub.context.Connect(reader, pcsc.ShareShared, pcsc.ProtocolAny)
   234  		if err != nil {
   235  			log.Debug("Failed to open smart card", "reader", reader, "err", err)
   236  			continue
   237  		}
   238  		wallet := NewWallet(hub, card)
   239  		if err = wallet.connect(); err != nil {
   240  			log.Debug("Failed to connect to smart card", "reader", reader, "err", err)
   241  			card.Disconnect(pcsc.LeaveCard)
   242  			continue
   243  		}
   244  		// Card connected, start tracking in amongs the wallets
   245  		hub.wallets[reader] = wallet
   246  		events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletArrived})
   247  	}
   248  	// Remove any wallets no longer present
   249  	for reader, wallet := range hub.wallets {
   250  		if _, ok := seen[reader]; !ok {
   251  			wallet.Close()
   252  			events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletDropped})
   253  			delete(hub.wallets, reader)
   254  		}
   255  	}
   256  	hub.refreshed = time.Now()
   257  	hub.stateLock.Unlock()
   258  
   259  	for _, event := range events {
   260  		hub.updateFeed.Send(event)
   261  	}
   262  }
   263  
   264  // Subscribe implements accounts.Backend, creating an async subscription to
   265  // receive notifications on the addition or removal of smart card wallets.
   266  func (hub *Hub) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription {
   267  	// We need the mutex to reliably start/stop the update loop
   268  	hub.stateLock.Lock()
   269  	defer hub.stateLock.Unlock()
   270  
   271  	// Subscribe the caller and track the subscriber count
   272  	sub := hub.updateScope.Track(hub.updateFeed.Subscribe(sink))
   273  
   274  	// Subscribers require an active notification loop, start it
   275  	if !hub.updating {
   276  		hub.updating = true
   277  		go hub.updater()
   278  	}
   279  	return sub
   280  }
   281  
   282  // updater is responsible for maintaining an up-to-date list of wallets managed
   283  // by the smart card hub, and for firing wallet addition/removal events.
   284  func (hub *Hub) updater() {
   285  	for {
   286  		// TODO: Wait for a USB hotplug event (not supported yet) or a refresh timeout
   287  		// <-hub.changes
   288  		time.Sleep(refreshCycle)
   289  
   290  		// Run the wallet refresher
   291  		hub.refreshWallets()
   292  
   293  		// If all our subscribers left, stop the updater
   294  		hub.stateLock.Lock()
   295  		if hub.updateScope.Count() == 0 {
   296  			hub.updating = false
   297  			hub.stateLock.Unlock()
   298  			return
   299  		}
   300  		hub.stateLock.Unlock()
   301  	}
   302  }