aboutsummaryrefslogtreecommitdiffstats
path: root/accounts/scwallet/hub.go
blob: d265458b94e0a5d38fe0ad59ea9ecd799cb3f2e2 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
// Copyright 2018 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

// This package implements support for smartcard-based hardware wallets such as
// the one written by Status: https://github.com/status-im/hardware-wallet
//
// This implementation of smartcard wallets have a different interaction process
// to other types of hardware wallet. The process works like this:
//
// 1. (First use with a given client) Establish a pairing between hardware
//    wallet and client. This requires a secret value called a 'PUK'. You can
//    pair with an unpaired wallet with `personal.openWallet(URI, PUK)`.
// 2. (First use only) Initialize the wallet, which generates a keypair, stores
//    it on the wallet, and returns it so the user can back it up. You can
//    initialize a wallet with `personal.initializeWallet(URI)`.
// 3. Connect to the wallet using the pairing information established in step 1.
//    You can connect to a paired wallet with `personal.openWallet(URI, PIN)`.
// 4. Interact with the wallet as normal.

package scwallet

import (
    "encoding/json"
    "io/ioutil"
    "os"
    "sort"
    "sync"
    "time"

    "github.com/ebfe/scard"
    "github.com/ethereum/go-ethereum/accounts"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/event"
    "github.com/ethereum/go-ethereum/log"
)

// Scheme is the URI prefix for smartcard wallets.
const Scheme = "pcsc"

// refreshCycle is the maximum time between wallet refreshes (if USB hotplug
// notifications don't work).
const refreshCycle = time.Second

// refreshThrottling is the minimum time between wallet refreshes to avoid thrashing.
const refreshThrottling = 500 * time.Millisecond

// smartcardPairing contains information about a smart card we have paired with
// or might pair with the hub.
type smartcardPairing struct {
    PublicKey    []byte                                     `json:"publicKey"`
    PairingIndex uint8                                      `json:"pairingIndex"`
    PairingKey   []byte                                     `json:"pairingKey"`
    Accounts     map[common.Address]accounts.DerivationPath `json:"accounts"`
}

// Hub is a accounts.Backend that can find and handle generic PC/SC hardware wallets.
type Hub struct {
    scheme string // Protocol scheme prefixing account and wallet URLs.

    context  *scard.Context
    datadir  string
    pairings map[string]smartcardPairing

    refreshed   time.Time               // Time instance when the list of wallets was last refreshed
    wallets     map[string]*Wallet      // Mapping from reader names to wallet instances
    updateFeed  event.Feed              // Event feed to notify wallet additions/removals
    updateScope event.SubscriptionScope // Subscription scope tracking current live listeners
    updating    bool                    // Whether the event notification loop is running

    quit chan chan error

    stateLock sync.RWMutex // Protects the internals of the hub from racey access
}

func (hub *Hub) readPairings() error {
    hub.pairings = make(map[string]smartcardPairing)
    pairingFile, err := os.Open(hub.datadir + "/smartcards.json")
    if err != nil {
        if os.IsNotExist(err) {
            return nil
        }
        return err
    }

    pairingData, err := ioutil.ReadAll(pairingFile)
    if err != nil {
        return err
    }
    var pairings []smartcardPairing
    if err := json.Unmarshal(pairingData, &pairings); err != nil {
        return err
    }

    for _, pairing := range pairings {
        hub.pairings[string(pairing.PublicKey)] = pairing
    }
    return nil
}

func (hub *Hub) writePairings() error {
    pairingFile, err := os.OpenFile(hub.datadir+"/smartcards.json", os.O_RDWR|os.O_CREATE, 0755)
    if err != nil {
        return err
    }

    pairings := make([]smartcardPairing, 0, len(hub.pairings))
    for _, pairing := range hub.pairings {
        pairings = append(pairings, pairing)
    }

    pairingData, err := json.Marshal(pairings)
    if err != nil {
        return err
    }

    if _, err := pairingFile.Write(pairingData); err != nil {
        return err
    }

    return pairingFile.Close()
}

func (hub *Hub) pairing(wallet *Wallet) *smartcardPairing {
    pairing, ok := hub.pairings[string(wallet.PublicKey)]
    if ok {
        return &pairing
    }
    return nil
}

func (hub *Hub) setPairing(wallet *Wallet, pairing *smartcardPairing) error {
    if pairing == nil {
        delete(hub.pairings, string(wallet.PublicKey))
    } else {
        hub.pairings[string(wallet.PublicKey)] = *pairing
    }
    return hub.writePairings()
}

// NewHub creates a new hardware wallet manager for smartcards.
func NewHub(scheme string, datadir string) (*Hub, error) {
    context, err := scard.EstablishContext()
    if err != nil {
        return nil, err
    }
    hub := &Hub{
        scheme:  scheme,
        context: context,
        datadir: datadir,
        wallets: make(map[string]*Wallet),
        quit:    make(chan chan error),
    }
    if err := hub.readPairings(); err != nil {
        return nil, err
    }
    hub.refreshWallets()
    return hub, nil
}

// Wallets implements accounts.Backend, returning all the currently tracked smart
// cards that appear to be hardware wallets.
func (hub *Hub) Wallets() []accounts.Wallet {
    // Make sure the list of wallets is up to date
    hub.refreshWallets()

    hub.stateLock.RLock()
    defer hub.stateLock.RUnlock()

    cpy := make([]accounts.Wallet, 0, len(hub.wallets))
    for _, wallet := range hub.wallets {
        cpy = append(cpy, wallet)
    }
    sort.Sort(accounts.WalletsByURL(cpy))
    return cpy
}

// refreshWallets scans the devices attached to the machine and updates the
// list of wallets based on the found devices.
func (hub *Hub) refreshWallets() {
    // Don't scan the USB like crazy it the user fetches wallets in a loop
    hub.stateLock.RLock()
    elapsed := time.Since(hub.refreshed)
    hub.stateLock.RUnlock()

    if elapsed < refreshThrottling {
        return
    }
    // Retrieve all the smart card reader to check for cards
    readers, err := hub.context.ListReaders()
    if err != nil {
        // This is a perverted hack, the scard library returns an error if no card
        // readers are present instead of simply returning an empty list. We don't
        // want to fill the user's log with errors, so filter those out.
        if err.Error() != "scard: Cannot find a smart card reader." {
            log.Error("Failed to enumerate smart card readers", "err", err)
        }
    }
    // Transform the current list of wallets into the new one
    hub.stateLock.Lock()

    events := []accounts.WalletEvent{}
    seen := make(map[string]struct{})

    for _, reader := range readers {
        // Mark the reader as present
        seen[reader] = struct{}{}

        // If we alreay know about this card, skip to the next reader, otherwise clean up
        if wallet, ok := hub.wallets[reader]; ok {
            if err := wallet.ping(); err == nil {
                continue
            }
            wallet.Close()
            events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletDropped})
            delete(hub.wallets, reader)
        }
        // New card detected, try to connect to it
        card, err := hub.context.Connect(reader, scard.ShareShared, scard.ProtocolAny)
        if err != nil {
            log.Debug("Failed to open smart card", "reader", reader, "err", err)
            continue
        }
        wallet := NewWallet(hub, card)
        if err = wallet.connect(); err != nil {
            log.Debug("Failed to connect to smart card", "reader", reader, "err", err)
            card.Disconnect(scard.LeaveCard)
            continue
        }
        // Card connected, start tracking in amongs the wallets
        hub.wallets[reader] = wallet
        events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletArrived})
    }
    // Remove any wallets no longer present
    for reader, wallet := range hub.wallets {
        if _, ok := seen[reader]; !ok {
            wallet.Close()
            events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletDropped})
            delete(hub.wallets, reader)
        }
    }
    hub.refreshed = time.Now()
    hub.stateLock.Unlock()

    for _, event := range events {
        hub.updateFeed.Send(event)
    }
}

// Subscribe implements accounts.Backend, creating an async subscription to
// receive notifications on the addition or removal of smart card wallets.
func (hub *Hub) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription {
    // We need the mutex to reliably start/stop the update loop
    hub.stateLock.Lock()
    defer hub.stateLock.Unlock()

    // Subscribe the caller and track the subscriber count
    sub := hub.updateScope.Track(hub.updateFeed.Subscribe(sink))

    // Subscribers require an active notification loop, start it
    if !hub.updating {
        hub.updating = true
        go hub.updater()
    }
    return sub
}

// updater is responsible for maintaining an up-to-date list of wallets managed
// by the smart card hub, and for firing wallet addition/removal events.
func (hub *Hub) updater() {
    for {
        // TODO: Wait for a USB hotplug event (not supported yet) or a refresh timeout
        // <-hub.changes
        time.Sleep(refreshCycle)

        // Run the wallet refresher
        hub.refreshWallets()

        // If all our subscribers left, stop the updater
        hub.stateLock.Lock()
        if hub.updateScope.Count() == 0 {
            hub.updating = false
            hub.stateLock.Unlock()
            return
        }
        hub.stateLock.Unlock()
    }
}