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