123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331 |
- // 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/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")
- // 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] = 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))
- }
|