github.com/aigarnetwork/aigar@v0.0.0-20191115204914-d59a6eb70f8e/accounts/scwallet/hub.go (about) 1 // Copyright 2018 The go-ethereum Authors 2 // Copyright 2019 The go-aigar Authors 3 // This file is part of the go-aigar library. 4 // 5 // The go-aigar library is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Lesser General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // The go-aigar library is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Lesser General Public License for more details. 14 // 15 // You should have received a copy of the GNU Lesser General Public License 16 // along with the go-aigar library. If not, see <http://www.gnu.org/licenses/>. 17 18 // This package implements support for smartcard-based hardware wallets such as 19 // the one written by Status: https://github.com/status-im/hardware-wallet 20 // 21 // This implementation of smartcard wallets have a different interaction process 22 // to other types of hardware wallet. The process works like this: 23 // 24 // 1. (First use with a given client) Establish a pairing between hardware 25 // wallet and client. This requires a secret value called a 'pairing password'. 26 // You can pair with an unpaired wallet with `personal.openWallet(URI, pairing password)`. 27 // 2. (First use only) Initialize the wallet, which generates a keypair, stores 28 // it on the wallet, and returns it so the user can back it up. You can 29 // initialize a wallet with `personal.initializeWallet(URI)`. 30 // 3. Connect to the wallet using the pairing information established in step 1. 31 // You can connect to a paired wallet with `personal.openWallet(URI, PIN)`. 32 // 4. Interact with the wallet as normal. 33 34 package scwallet 35 36 import ( 37 "encoding/json" 38 "io/ioutil" 39 "os" 40 "path/filepath" 41 "sort" 42 "sync" 43 "time" 44 45 "github.com/AigarNetwork/aigar/accounts" 46 "github.com/AigarNetwork/aigar/common" 47 "github.com/AigarNetwork/aigar/event" 48 "github.com/AigarNetwork/aigar/log" 49 pcsc "github.com/gballet/go-libpcsclite" 50 ) 51 52 // Scheme is the URI prefix for smartcard wallets. 53 const Scheme = "keycard" 54 55 // refreshCycle is the maximum time between wallet refreshes (if USB hotplug 56 // notifications don't work). 57 const refreshCycle = time.Second 58 59 // refreshThrottling is the minimum time between wallet refreshes to avoid thrashing. 60 const refreshThrottling = 500 * time.Millisecond 61 62 // smartcardPairing contains information about a smart card we have paired with 63 // or might pair with the hub. 64 type smartcardPairing struct { 65 PublicKey []byte `json:"publicKey"` 66 PairingIndex uint8 `json:"pairingIndex"` 67 PairingKey []byte `json:"pairingKey"` 68 Accounts map[common.Address]accounts.DerivationPath `json:"accounts"` 69 } 70 71 // Hub is a accounts.Backend that can find and handle generic PC/SC hardware wallets. 72 type Hub struct { 73 scheme string // Protocol scheme prefixing account and wallet URLs. 74 75 context *pcsc.Client 76 datadir string 77 pairings map[string]smartcardPairing 78 79 refreshed time.Time // Time instance when the list of wallets was last refreshed 80 wallets map[string]*Wallet // Mapping from reader names to wallet instances 81 updateFeed event.Feed // Event feed to notify wallet additions/removals 82 updateScope event.SubscriptionScope // Subscription scope tracking current live listeners 83 updating bool // Whether the event notification loop is running 84 85 quit chan chan error 86 87 stateLock sync.RWMutex // Protects the internals of the hub from racey access 88 } 89 90 func (hub *Hub) readPairings() error { 91 hub.pairings = make(map[string]smartcardPairing) 92 pairingFile, err := os.Open(filepath.Join(hub.datadir, "smartcards.json")) 93 if err != nil { 94 if os.IsNotExist(err) { 95 return nil 96 } 97 return err 98 } 99 100 pairingData, err := ioutil.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 alreay 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 in amongs 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 }