aboutsummaryrefslogtreecommitdiffstats
path: root/accounts/usbwallet/ledger_hub.go
blob: 2b0d56097cbae77315927274662d24dbc8487f47 (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
// Copyright 2017 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 file contains the implementation for interacting with the Ledger hardware
// wallets. The wire protocol spec can be found in the Ledger Blue GitHub repo:
// https://raw.githubusercontent.com/LedgerHQ/blue-app-eth/master/doc/ethapp.asc

package usbwallet

import (
    "errors"
    "runtime"
    "sync"
    "time"

    "github.com/ethereum/go-ethereum/accounts"
    "github.com/ethereum/go-ethereum/event"
    "github.com/ethereum/go-ethereum/log"
    "github.com/karalabe/hid"
)

// LedgerScheme is the protocol scheme prefixing account and wallet URLs.
var LedgerScheme = "ledger"

// ledgerDeviceIDs are the known device IDs that Ledger wallets use.
var ledgerDeviceIDs = []deviceID{
    {Vendor: 0x2c97, Product: 0x0000}, // Ledger Blue
    {Vendor: 0x2c97, Product: 0x0001}, // Ledger Nano S
}

// Maximum time between wallet refreshes (if USB hotplug notifications don't work).
const ledgerRefreshCycle = time.Second

// Minimum time between wallet refreshes to avoid USB trashing.
const ledgerRefreshThrottling = 500 * time.Millisecond

// LedgerHub is a accounts.Backend that can find and handle Ledger hardware wallets.
type LedgerHub struct {
    refreshed   time.Time               // Time instance when the list of wallets was last refreshed
    wallets     []accounts.Wallet       // List of Ledger devices currently tracking
    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

    // TODO(karalabe): remove if hotplug lands on Windows
    commsPend int        // Number of operations blocking enumeration
    commsLock sync.Mutex // Lock protecting the pending counter and enumeration
}

// NewLedgerHub creates a new hardware wallet manager for Ledger devices.
func NewLedgerHub() (*LedgerHub, error) {
    if !hid.Supported() {
        return nil, errors.New("unsupported platform")
    }
    hub := &LedgerHub{
        quit: make(chan chan error),
    }
    hub.refreshWallets()
    return hub, nil
}

// Wallets implements accounts.Backend, returning all the currently tracked USB
// devices that appear to be Ledger hardware wallets.
func (hub *LedgerHub) 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, len(hub.wallets))
    copy(cpy, hub.wallets)
    return cpy
}

// refreshWallets scans the USB devices attached to the machine and updates the
// list of wallets based on the found devices.
func (hub *LedgerHub) 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 < ledgerRefreshThrottling {
        return
    }
    // Retrieve the current list of Ledger devices
    var ledgers []hid.DeviceInfo

    if runtime.GOOS == "linux" {
        // hidapi on Linux opens the device during enumeration to retrieve some infos,
        // breaking the Ledger protocol if that is waiting for user confirmation. This
        // is a bug acknowledged at Ledger, but it won't be fixed on old devices so we
        // need to prevent concurrent comms ourselves. The more elegant solution would
        // be to ditch enumeration in favor of hutplug events, but that don't work yet
        // on Windows so if we need to hack it anyway, this is more elegant for now.
        hub.commsLock.Lock()
        if hub.commsPend > 0 { // A confirmation is pending, don't refresh
            hub.commsLock.Unlock()
            return
        }
    }
    for _, info := range hid.Enumerate(0, 0) { // Can't enumerate directly, one valid ID is the 0 wildcard
        for _, id := range ledgerDeviceIDs {
            if info.VendorID == id.Vendor && info.ProductID == id.Product {
                ledgers = append(ledgers, info)
                break
            }
        }
    }
    if runtime.GOOS == "linux" {
        // See rationale before the enumeration why this is needed and only on Linux.
        hub.commsLock.Unlock()
    }
    // Transform the current list of wallets into the new one
    hub.stateLock.Lock()

    wallets := make([]accounts.Wallet, 0, len(ledgers))
    events := []accounts.WalletEvent{}

    for _, ledger := range ledgers {
        url := accounts.URL{Scheme: LedgerScheme, Path: ledger.Path}

        // Drop wallets in front of the next device or those that failed for some reason
        for len(hub.wallets) > 0 && (hub.wallets[0].URL().Cmp(url) < 0 || hub.wallets[0].(*ledgerWallet).failed()) {
            events = append(events, accounts.WalletEvent{Wallet: hub.wallets[0], Arrive: false})
            hub.wallets = hub.wallets[1:]
        }
        // If there are no more wallets or the device is before the next, wrap new wallet
        if len(hub.wallets) == 0 || hub.wallets[0].URL().Cmp(url) > 0 {
            wallet := &ledgerWallet{hub: hub, url: &url, info: ledger, log: log.New("url", url)}

            events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: true})
            wallets = append(wallets, wallet)
            continue
        }
        // If the device is the same as the first wallet, keep it
        if hub.wallets[0].URL().Cmp(url) == 0 {
            wallets = append(wallets, hub.wallets[0])
            hub.wallets = hub.wallets[1:]
            continue
        }
    }
    // Drop any leftover wallets and set the new batch
    for _, wallet := range hub.wallets {
        events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: false})
    }
    hub.refreshed = time.Now()
    hub.wallets = wallets
    hub.stateLock.Unlock()

    // Fire all wallet events and return
    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 Ledger wallets.
func (hub *LedgerHub) 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 stored in
// the keystore, and for firing wallet addition/removal events. It listens for
// account change events from the underlying account cache, and also periodically
// forces a manual refresh (only triggers for systems where the filesystem notifier
// is not running).
func (hub *LedgerHub) updater() {
    for {
        // TODO: Wait for a USB hotplug event (not supported yet) or a refresh timeout
        // <-hub.changes
        time.Sleep(ledgerRefreshCycle)

        // 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()
    }
}