github.com/aigarnetwork/aigar@v0.0.0-20191115204914-d59a6eb70f8e/accounts/scwallet/hub.go (about)

     1  //  Copyright 2018 The go-ethereum Authors
     2  //  Copyright 2019 The go-aigar Authors
     3  //  This file is part of the go-aigar library.
     4  //
     5  //  The go-aigar library is free software: you can redistribute it and/or modify
     6  //  it under the terms of the GNU Lesser General Public License as published by
     7  //  the Free Software Foundation, either version 3 of the License, or
     8  //  (at your option) any later version.
     9  //
    10  //  The go-aigar library is distributed in the hope that it will be useful,
    11  //  but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  //  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    13  //  GNU Lesser General Public License for more details.
    14  //
    15  //  You should have received a copy of the GNU Lesser General Public License
    16  //  along with the go-aigar library. If not, see <http://www.gnu.org/licenses/>.
    17  
    18  // This package implements support for smartcard-based hardware wallets such as
    19  // the one written by Status: https://github.com/status-im/hardware-wallet
    20  //
    21  // This implementation of smartcard wallets have a different interaction process
    22  // to other types of hardware wallet. The process works like this:
    23  //
    24  // 1. (First use with a given client) Establish a pairing between hardware
    25  //    wallet and client. This requires a secret value called a 'pairing password'.
    26  //    You can pair with an unpaired wallet with `personal.openWallet(URI, pairing password)`.
    27  // 2. (First use only) Initialize the wallet, which generates a keypair, stores
    28  //    it on the wallet, and returns it so the user can back it up. You can
    29  //    initialize a wallet with `personal.initializeWallet(URI)`.
    30  // 3. Connect to the wallet using the pairing information established in step 1.
    31  //    You can connect to a paired wallet with `personal.openWallet(URI, PIN)`.
    32  // 4. Interact with the wallet as normal.
    33  
    34  package scwallet
    35  
    36  import (
    37  	"encoding/json"
    38  	"io/ioutil"
    39  	"os"
    40  	"path/filepath"
    41  	"sort"
    42  	"sync"
    43  	"time"
    44  
    45  	"github.com/AigarNetwork/aigar/accounts"
    46  	"github.com/AigarNetwork/aigar/common"
    47  	"github.com/AigarNetwork/aigar/event"
    48  	"github.com/AigarNetwork/aigar/log"
    49  	pcsc "github.com/gballet/go-libpcsclite"
    50  )
    51  
    52  // Scheme is the URI prefix for smartcard wallets.
    53  const Scheme = "keycard"
    54  
    55  // refreshCycle is the maximum time between wallet refreshes (if USB hotplug
    56  // notifications don't work).
    57  const refreshCycle = time.Second
    58  
    59  // refreshThrottling is the minimum time between wallet refreshes to avoid thrashing.
    60  const refreshThrottling = 500 * time.Millisecond
    61  
    62  // smartcardPairing contains information about a smart card we have paired with
    63  // or might pair with the hub.
    64  type smartcardPairing struct {
    65  	PublicKey    []byte                                     `json:"publicKey"`
    66  	PairingIndex uint8                                      `json:"pairingIndex"`
    67  	PairingKey   []byte                                     `json:"pairingKey"`
    68  	Accounts     map[common.Address]accounts.DerivationPath `json:"accounts"`
    69  }
    70  
    71  // Hub is a accounts.Backend that can find and handle generic PC/SC hardware wallets.
    72  type Hub struct {
    73  	scheme string // Protocol scheme prefixing account and wallet URLs.
    74  
    75  	context  *pcsc.Client
    76  	datadir  string
    77  	pairings map[string]smartcardPairing
    78  
    79  	refreshed   time.Time               // Time instance when the list of wallets was last refreshed
    80  	wallets     map[string]*Wallet      // Mapping from reader names to wallet instances
    81  	updateFeed  event.Feed              // Event feed to notify wallet additions/removals
    82  	updateScope event.SubscriptionScope // Subscription scope tracking current live listeners
    83  	updating    bool                    // Whether the event notification loop is running
    84  
    85  	quit chan chan error
    86  
    87  	stateLock sync.RWMutex // Protects the internals of the hub from racey access
    88  }
    89  
    90  func (hub *Hub) readPairings() error {
    91  	hub.pairings = make(map[string]smartcardPairing)
    92  	pairingFile, err := os.Open(filepath.Join(hub.datadir, "smartcards.json"))
    93  	if err != nil {
    94  		if os.IsNotExist(err) {
    95  			return nil
    96  		}
    97  		return err
    98  	}
    99  
   100  	pairingData, err := ioutil.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 alreay 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 in amongs 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  }