aboutsummaryrefslogtreecommitdiffstats
path: root/accounts/usbwallet/trezor.go
blob: 06e440cf9964feb130eebb377c24ae69db7db179 (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
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
// 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 Trezor hardware
// wallets. The wire protocol spec can be found on the SatoshiLabs website:
// https://doc.satoshilabs.com/trezor-tech/api-protobuf.html

package usbwallet

import (
    "encoding/binary"
    "errors"
    "fmt"
    "io"
    "math/big"

    "github.com/dexon-foundation/dexon/accounts"
    "github.com/dexon-foundation/dexon/accounts/usbwallet/internal/trezor"
    "github.com/dexon-foundation/dexon/common"
    "github.com/dexon-foundation/dexon/common/hexutil"
    "github.com/dexon-foundation/dexon/core/types"
    "github.com/dexon-foundation/dexon/log"
    "github.com/golang/protobuf/proto"
)

// ErrTrezorPINNeeded is returned if opening the trezor requires a PIN code. In
// this case, the calling application should display a pinpad and send back the
// encoded passphrase.
var ErrTrezorPINNeeded = errors.New("trezor: pin needed")

// errTrezorReplyInvalidHeader is the error message returned by a Trezor data exchange
// if the device replies with a mismatching header. This usually means the device
// is in browser mode.
var errTrezorReplyInvalidHeader = errors.New("trezor: invalid reply header")

// trezorDriver implements the communication with a Trezor hardware wallet.
type trezorDriver struct {
    device  io.ReadWriter // USB device connection to communicate through
    version [3]uint32     // Current version of the Trezor firmware
    label   string        // Current textual label of the Trezor device
    pinwait bool          // Flags whether the device is waiting for PIN entry
    failure error         // Any failure that would make the device unusable
    log     log.Logger    // Contextual logger to tag the trezor with its id
}

// newTrezorDriver creates a new instance of a Trezor USB protocol driver.
func newTrezorDriver(logger log.Logger) driver {
    return &trezorDriver{
        log: logger,
    }
}

// Status implements accounts.Wallet, always whether the Trezor is opened, closed
// or whether the Ethereum app was not started on it.
func (w *trezorDriver) Status() (string, error) {
    if w.failure != nil {
        return fmt.Sprintf("Failed: %v", w.failure), w.failure
    }
    if w.device == nil {
        return "Closed", w.failure
    }
    if w.pinwait {
        return fmt.Sprintf("Trezor v%d.%d.%d '%s' waiting for PIN", w.version[0], w.version[1], w.version[2], w.label), w.failure
    }
    return fmt.Sprintf("Trezor v%d.%d.%d '%s' online", w.version[0], w.version[1], w.version[2], w.label), w.failure
}

// Open implements usbwallet.driver, attempting to initialize the connection to
// the Trezor hardware wallet. Initializing the Trezor is a two phase operation:
//  * The first phase is to initialize the connection and read the wallet's
//    features. This phase is invoked is the provided passphrase is empty. The
//    device will display the pinpad as a result and will return an appropriate
//    error to notify the user that a second open phase is needed.
//  * The second phase is to unlock access to the Trezor, which is done by the
//    user actually providing a passphrase mapping a keyboard keypad to the pin
//    number of the user (shuffled according to the pinpad displayed).
func (w *trezorDriver) Open(device io.ReadWriter, passphrase string) error {
    w.device, w.failure = device, nil

    // If phase 1 is requested, init the connection and wait for user callback
    if passphrase == "" {
        // If we're already waiting for a PIN entry, insta-return
        if w.pinwait {
            return ErrTrezorPINNeeded
        }
        // Initialize a connection to the device
        features := new(trezor.Features)
        if _, err := w.trezorExchange(&trezor.Initialize{}, features); err != nil {
            return err
        }
        w.version = [3]uint32{features.GetMajorVersion(), features.GetMinorVersion(), features.GetPatchVersion()}
        w.label = features.GetLabel()

        // Do a manual ping, forcing the device to ask for its PIN
        askPin := true
        res, err := w.trezorExchange(&trezor.Ping{PinProtection: &askPin}, new(trezor.PinMatrixRequest), new(trezor.Success))
        if err != nil {
            return err
        }
        // Only return the PIN request if the device wasn't unlocked until now
        if res == 1 {
            return nil // Device responded with trezor.Success
        }
        w.pinwait = true
        return ErrTrezorPINNeeded
    }
    // Phase 2 requested with actual PIN entry
    w.pinwait = false

    if _, err := w.trezorExchange(&trezor.PinMatrixAck{Pin: &passphrase}, new(trezor.Success)); err != nil {
        w.failure = err
        return err
    }
    return nil
}

// Close implements usbwallet.driver, cleaning up and metadata maintained within
// the Trezor driver.
func (w *trezorDriver) Close() error {
    w.version, w.label, w.pinwait = [3]uint32{}, "", false
    return nil
}

// Heartbeat implements usbwallet.driver, performing a sanity check against the
// Trezor to see if it's still online.
func (w *trezorDriver) Heartbeat() error {
    if _, err := w.trezorExchange(&trezor.Ping{}, new(trezor.Success)); err != nil {
        w.failure = err
        return err
    }
    return nil
}

// Derive implements usbwallet.driver, sending a derivation request to the Trezor
// and returning the Ethereum address located on that derivation path.
func (w *trezorDriver) Derive(path accounts.DerivationPath) (common.Address, error) {
    return w.trezorDerive(path)
}

// SignTx implements usbwallet.driver, sending the transaction to the Trezor and
// waiting for the user to confirm or deny the transaction.
func (w *trezorDriver) SignTx(path accounts.DerivationPath, tx *types.Transaction, chainID *big.Int) (common.Address, *types.Transaction, error) {
    if w.device == nil {
        return common.Address{}, nil, accounts.ErrWalletClosed
    }
    return w.trezorSign(path, tx, chainID)
}

// trezorDerive sends a derivation request to the Trezor device and returns the
// Ethereum address located on that path.
func (w *trezorDriver) trezorDerive(derivationPath []uint32) (common.Address, error) {
    address := new(trezor.EthereumAddress)
    if _, err := w.trezorExchange(&trezor.EthereumGetAddress{AddressN: derivationPath}, address); err != nil {
        return common.Address{}, err
    }
    return common.BytesToAddress(address.GetAddress()), nil
}

// trezorSign sends the transaction to the Trezor wallet, and waits for the user
// to confirm or deny the transaction.
func (w *trezorDriver) trezorSign(derivationPath []uint32, tx *types.Transaction, chainID *big.Int) (common.Address, *types.Transaction, error) {
    // Create the transaction initiation message
    data := tx.Data()
    length := uint32(len(data))

    request := &trezor.EthereumSignTx{
        AddressN:   derivationPath,
        Nonce:      new(big.Int).SetUint64(tx.Nonce()).Bytes(),
        GasPrice:   tx.GasPrice().Bytes(),
        GasLimit:   new(big.Int).SetUint64(tx.Gas()).Bytes(),
        Value:      tx.Value().Bytes(),
        DataLength: &length,
    }
    if to := tx.To(); to != nil {
        request.To = (*to)[:] // Non contract deploy, set recipient explicitly
    }
    if length > 1024 { // Send the data chunked if that was requested
        request.DataInitialChunk, data = data[:1024], data[1024:]
    } else {
        request.DataInitialChunk, data = data, nil
    }
    if chainID != nil { // EIP-155 transaction, set chain ID explicitly (only 32 bit is supported!?)
        id := uint32(chainID.Int64())
        request.ChainId = &id
    }
    // Send the initiation message and stream content until a signature is returned
    response := new(trezor.EthereumTxRequest)
    if _, err := w.trezorExchange(request, response); err != nil {
        return common.Address{}, nil, err
    }
    for response.DataLength != nil && int(*response.DataLength) <= len(data) {
        chunk := data[:*response.DataLength]
        data = data[*response.DataLength:]

        if _, err := w.trezorExchange(&trezor.EthereumTxAck{DataChunk: chunk}, response); err != nil {
            return common.Address{}, nil, err
        }
    }
    // Extract the Ethereum signature and do a sanity validation
    if len(response.GetSignatureR()) == 0 || len(response.GetSignatureS()) == 0 || response.GetSignatureV() == 0 {
        return common.Address{}, nil, errors.New("reply lacks signature")
    }
    signature := append(append(response.GetSignatureR(), response.GetSignatureS()...), byte(response.GetSignatureV()))

    // Create the correct signer and signature transform based on the chain ID
    var signer types.Signer
    if chainID == nil {
        signer = new(types.HomesteadSigner)
    } else {
        signer = types.NewEIP155Signer(chainID)
        signature[64] -= byte(chainID.Uint64()*2 + 35)
    }
    // Inject the final signature into the transaction and sanity check the sender
    signed, err := tx.WithSignature(signer, signature)
    if err != nil {
        return common.Address{}, nil, err
    }
    sender, err := types.Sender(signer, signed)
    if err != nil {
        return common.Address{}, nil, err
    }
    return sender, signed, nil
}

// trezorExchange performs a data exchange with the Trezor wallet, sending it a
// message and retrieving the response. If multiple responses are possible, the
// method will also return the index of the destination object used.
func (w *trezorDriver) trezorExchange(req proto.Message, results ...proto.Message) (int, error) {
    // Construct the original message payload to chunk up
    data, err := proto.Marshal(req)
    if err != nil {
        return 0, err
    }
    payload := make([]byte, 8+len(data))
    copy(payload, []byte{0x23, 0x23})
    binary.BigEndian.PutUint16(payload[2:], trezor.Type(req))
    binary.BigEndian.PutUint32(payload[4:], uint32(len(data)))
    copy(payload[8:], data)

    // Stream all the chunks to the device
    chunk := make([]byte, 64)
    chunk[0] = 0x3f // Report ID magic number

    for len(payload) > 0 {
        // Construct the new message to stream, padding with zeroes if needed
        if len(payload) > 63 {
            copy(chunk[1:], payload[:63])
            payload = payload[63:]
        } else {
            copy(chunk[1:], payload)
            copy(chunk[1+len(payload):], make([]byte, 63-len(payload)))
            payload = nil
        }
        // Send over to the device
        w.log.Trace("Data chunk sent to the Trezor", "chunk", hexutil.Bytes(chunk))
        if _, err := w.device.Write(chunk); err != nil {
            return 0, err
        }
    }
    // Stream the reply back from the wallet in 64 byte chunks
    var (
        kind  uint16
        reply []byte
    )
    for {
        // Read the next chunk from the Trezor wallet
        if _, err := io.ReadFull(w.device, chunk); err != nil {
            return 0, err
        }
        w.log.Trace("Data chunk received from the Trezor", "chunk", hexutil.Bytes(chunk))

        // Make sure the transport header matches
        if chunk[0] != 0x3f || (len(reply) == 0 && (chunk[1] != 0x23 || chunk[2] != 0x23)) {
            return 0, errTrezorReplyInvalidHeader
        }
        // If it's the first chunk, retrieve the reply message type and total message length
        var payload []byte

        if len(reply) == 0 {
            kind = binary.BigEndian.Uint16(chunk[3:5])
            reply = make([]byte, 0, int(binary.BigEndian.Uint32(chunk[5:9])))
            payload = chunk[9:]
        } else {
            payload = chunk[1:]
        }
        // Append to the reply and stop when filled up
        if left := cap(reply) - len(reply); left > len(payload) {
            reply = append(reply, payload...)
        } else {
            reply = append(reply, payload[:left]...)
            break
        }
    }
    // Try to parse the reply into the requested reply message
    if kind == uint16(trezor.MessageType_MessageType_Failure) {
        // Trezor returned a failure, extract and return the message
        failure := new(trezor.Failure)
        if err := proto.Unmarshal(reply, failure); err != nil {
            return 0, err
        }
        return 0, errors.New("trezor: " + failure.GetMessage())
    }
    if kind == uint16(trezor.MessageType_MessageType_ButtonRequest) {
        // Trezor is waiting for user confirmation, ack and wait for the next message
        return w.trezorExchange(&trezor.ButtonAck{}, results...)
    }
    for i, res := range results {
        if trezor.Type(res) == kind {
            return i, proto.Unmarshal(reply, res)
        }
    }
    expected := make([]string, len(results))
    for i, res := range results {
        expected[i] = trezor.Name(trezor.Type(res))
    }
    return 0, fmt.Errorf("trezor: expected reply types %s, got %s", expected, trezor.Name(kind))
}