github.com/fff-chain/go-fff@v0.0.0-20220726032732-1c84420b8a99/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/ioutil" 38 "os" 39 "path/filepath" 40 "sort" 41 "sync" 42 "time" 43 44 "github.com/fff-chain/go-fff/accounts" 45 "github.com/fff-chain/go-fff/common" 46 "github.com/fff-chain/go-fff/event" 47 "github.com/fff-chain/go-fff/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 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 114 func (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 138 func (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 145 func (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. 155 func 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. 176 func (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. 193 func (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. 266 func (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. 284 func (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 }