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
33package scwallet
34
35import (
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.
52const Scheme = "keycard"
53
54// refreshCycle is the maximum time between wallet refreshes (if USB hotplug
55// notifications don't work).
56const refreshCycle = time.Second
57
58// refreshThrottling is the minimum time between wallet refreshes to avoid thrashing.
59const refreshThrottling = 500 * time.Millisecond
60
61// smartcardPairing contains information about a smart card we have paired with
62// or might pair with the hub.
63type 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.
71type 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
89func (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
114func (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
138func (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
145func (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.
155func 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.
176func (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.
193func (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 already 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.
266func (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.
284func (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}
303