github.1485827954.workers.dev/ethereum/go-ethereum@v1.14.3/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" 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 defer pairingFile.Close() 99 100 pairingData, err := io.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 already 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 among 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 }