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