// 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 . // 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/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/usbwallet/internal/trezor" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/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 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). // * 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 } 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)) }