6f45fa66d8
When opening the wallet, ask for passphrase as well as for the PIN and return the relevant error (PIN/passphrase required). Open must then be called again with either PIN or passphrase to advance the process. This also updates the console bridge to support passphrase authentication.
357 lines
13 KiB
Go
357 lines
13 KiB
Go
// 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")
|
|
|
|
// 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))
|
|
}
|