aboutsummaryrefslogtreecommitdiff
path: root/accounts/usbwallet/trezor.go
diff options
context:
space:
mode:
authorDeterminant <[email protected]>2020-06-28 14:47:41 -0400
committerDeterminant <[email protected]>2020-06-28 14:47:41 -0400
commitd235e2c6a5788ec4a6cff15a16f56b38a3876a0d (patch)
tree5f2727f7a50ee5840f889c82776d3a30a88dd59b /accounts/usbwallet/trezor.go
parent13ebd8bd9468e9d769d598b0ca2afb72ba78cb97 (diff)
...
Diffstat (limited to 'accounts/usbwallet/trezor.go')
-rw-r--r--accounts/usbwallet/trezor.go365
1 files changed, 365 insertions, 0 deletions
diff --git a/accounts/usbwallet/trezor.go b/accounts/usbwallet/trezor.go
new file mode 100644
index 0000000..833c7a9
--- /dev/null
+++ b/accounts/usbwallet/trezor.go
@@ -0,0 +1,365 @@
+// 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/ava-labs/coreth/accounts"
+ "github.com/ava-labs/coreth/accounts/usbwallet/trezor"
+ "github.com/ava-labs/coreth/core/types"
+ "github.com/ava-labs/go-ethereum/common"
+ "github.com/ava-labs/go-ethereum/common/hexutil"
+ "github.com/ava-labs/go-ethereum/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")
+
+// ErrTrezorPassphraseNeeded is returned if opening the trezor requires a passphrase
+var ErrTrezorPassphraseNeeded = errors.New("trezor: passphrase 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
+ passphrasewait bool // Flags whether the device is waiting for passphrase 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 or three phase operation:
+// * The first phase is to initialize the connection and read the wallet's
+// features. This phase is invoked if 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).
+// * If needed the device will ask for passphrase which will require calling
+// open again with the actual passphrase (3rd phase)
+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 == "" && !w.passphrasewait {
+ // 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 and Passphrase
+ askPin := true
+ askPassphrase := true
+ res, err := w.trezorExchange(&trezor.Ping{PinProtection: &askPin, PassphraseProtection: &askPassphrase}, new(trezor.PinMatrixRequest), new(trezor.PassphraseRequest), new(trezor.Success))
+ if err != nil {
+ return err
+ }
+ // Only return the PIN request if the device wasn't unlocked until now
+ switch res {
+ case 0:
+ w.pinwait = true
+ return ErrTrezorPINNeeded
+ case 1:
+ w.pinwait = false
+ w.passphrasewait = true
+ return ErrTrezorPassphraseNeeded
+ case 2:
+ return nil // responded with trezor.Success
+ }
+ }
+ // Phase 2 requested with actual PIN entry
+ if w.pinwait {
+ w.pinwait = false
+ res, err := w.trezorExchange(&trezor.PinMatrixAck{Pin: &passphrase}, new(trezor.Success), new(trezor.PassphraseRequest))
+ if err != nil {
+ w.failure = err
+ return err
+ }
+ if res == 1 {
+ w.passphrasewait = true
+ return ErrTrezorPassphraseNeeded
+ }
+ } else if w.passphrasewait {
+ w.passphrasewait = false
+ if _, err := w.trezorExchange(&trezor.PassphraseAck{Passphrase: &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
+ }
+ if addr := address.GetAddressBin(); len(addr) > 0 { // Older firmwares use binary fomats
+ return common.BytesToAddress(addr), nil
+ }
+ if addr := address.GetAddressHex(); len(addr) > 0 { // Newer firmwares use hexadecimal fomats
+ return common.HexToAddress(addr), nil
+ }
+ return common.Address{}, errors.New("missing derived address")
+}
+
+// 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 {
+ // Non contract deploy, set recipient explicitly
+ hex := to.Hex()
+ request.ToHex = &hex // Newer firmwares (old will ignore)
+ request.ToBin = (*to)[:] // Older firmwares (new will ignore)
+ }
+ 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))
+}