github.com/sberex/go-sberex@v1.8.2-0.20181113200658-ed96ac38f7d7/accounts/usbwallet/trezor.go (about) 1 // This file is part of the go-sberex library. The go-sberex library is 2 // free software: you can redistribute it and/or modify it under the terms 3 // of the GNU Lesser General Public License as published by the Free 4 // Software Foundation, either version 3 of the License, or (at your option) 5 // any later version. 6 // 7 // The go-sberex library is distributed in the hope that it will be useful, 8 // but WITHOUT ANY WARRANTY; without even the implied warranty of 9 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser 10 // General Public License <http://www.gnu.org/licenses/> for more details. 11 12 // This file contains the implementation for interacting with the Trezor hardware 13 // wallets. The wire protocol spec can be found on the SatoshiLabs website: 14 // https://doc.satoshilabs.com/trezor-tech/api-protobuf.html 15 16 package usbwallet 17 18 import ( 19 "encoding/binary" 20 "errors" 21 "fmt" 22 "io" 23 "math/big" 24 25 "github.com/Sberex/go-sberex/accounts" 26 "github.com/Sberex/go-sberex/accounts/usbwallet/internal/trezor" 27 "github.com/Sberex/go-sberex/common" 28 "github.com/Sberex/go-sberex/common/hexutil" 29 "github.com/Sberex/go-sberex/core/types" 30 "github.com/Sberex/go-sberex/log" 31 "github.com/golang/protobuf/proto" 32 ) 33 34 // ErrTrezorPINNeeded is returned if opening the trezor requires a PIN code. In 35 // this case, the calling application should display a pinpad and send back the 36 // encoded passphrase. 37 var ErrTrezorPINNeeded = errors.New("trezor: pin needed") 38 39 // errTrezorReplyInvalidHeader is the error message returned by a Trezor data exchange 40 // if the device replies with a mismatching header. This usually means the device 41 // is in browser mode. 42 var errTrezorReplyInvalidHeader = errors.New("trezor: invalid reply header") 43 44 // trezorDriver implements the communication with a Trezor hardware wallet. 45 type trezorDriver struct { 46 device io.ReadWriter // USB device connection to communicate through 47 version [3]uint32 // Current version of the Trezor firmware 48 label string // Current textual label of the Trezor device 49 pinwait bool // Flags whether the device is waiting for PIN entry 50 failure error // Any failure that would make the device unusable 51 log log.Logger // Contextual logger to tag the trezor with its id 52 } 53 54 // newTrezorDriver creates a new instance of a Trezor USB protocol driver. 55 func newTrezorDriver(logger log.Logger) driver { 56 return &trezorDriver{ 57 log: logger, 58 } 59 } 60 61 // Status implements accounts.Wallet, always whether the Trezor is opened, closed 62 // or whether the Sberex app was not started on it. 63 func (w *trezorDriver) Status() (string, error) { 64 if w.failure != nil { 65 return fmt.Sprintf("Failed: %v", w.failure), w.failure 66 } 67 if w.device == nil { 68 return "Closed", w.failure 69 } 70 if w.pinwait { 71 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 72 } 73 return fmt.Sprintf("Trezor v%d.%d.%d '%s' online", w.version[0], w.version[1], w.version[2], w.label), w.failure 74 } 75 76 // Open implements usbwallet.driver, attempting to initialize the connection to 77 // the Trezor hardware wallet. Initializing the Trezor is a two phase operation: 78 // * The first phase is to initialize the connection and read the wallet's 79 // features. This phase is invoked is the provided passphrase is empty. The 80 // device will display the pinpad as a result and will return an appropriate 81 // error to notify the user that a second open phase is needed. 82 // * The second phase is to unlock access to the Trezor, which is done by the 83 // user actually providing a passphrase mapping a keyboard keypad to the pin 84 // number of the user (shuffled according to the pinpad displayed). 85 func (w *trezorDriver) Open(device io.ReadWriter, passphrase string) error { 86 w.device, w.failure = device, nil 87 88 // If phase 1 is requested, init the connection and wait for user callback 89 if passphrase == "" { 90 // If we're already waiting for a PIN entry, insta-return 91 if w.pinwait { 92 return ErrTrezorPINNeeded 93 } 94 // Initialize a connection to the device 95 features := new(trezor.Features) 96 if _, err := w.trezorExchange(&trezor.Initialize{}, features); err != nil { 97 return err 98 } 99 w.version = [3]uint32{features.GetMajorVersion(), features.GetMinorVersion(), features.GetPatchVersion()} 100 w.label = features.GetLabel() 101 102 // Do a manual ping, forcing the device to ask for its PIN 103 askPin := true 104 res, err := w.trezorExchange(&trezor.Ping{PinProtection: &askPin}, new(trezor.PinMatrixRequest), new(trezor.Success)) 105 if err != nil { 106 return err 107 } 108 // Only return the PIN request if the device wasn't unlocked until now 109 if res == 1 { 110 return nil // Device responded with trezor.Success 111 } 112 w.pinwait = true 113 return ErrTrezorPINNeeded 114 } 115 // Phase 2 requested with actual PIN entry 116 w.pinwait = false 117 118 if _, err := w.trezorExchange(&trezor.PinMatrixAck{Pin: &passphrase}, new(trezor.Success)); err != nil { 119 w.failure = err 120 return err 121 } 122 return nil 123 } 124 125 // Close implements usbwallet.driver, cleaning up and metadata maintained within 126 // the Trezor driver. 127 func (w *trezorDriver) Close() error { 128 w.version, w.label, w.pinwait = [3]uint32{}, "", false 129 return nil 130 } 131 132 // Heartbeat implements usbwallet.driver, performing a sanity check against the 133 // Trezor to see if it's still online. 134 func (w *trezorDriver) Heartbeat() error { 135 if _, err := w.trezorExchange(&trezor.Ping{}, new(trezor.Success)); err != nil { 136 w.failure = err 137 return err 138 } 139 return nil 140 } 141 142 // Derive implements usbwallet.driver, sending a derivation request to the Trezor 143 // and returning the Sberex address located on that derivation path. 144 func (w *trezorDriver) Derive(path accounts.DerivationPath) (common.Address, error) { 145 return w.trezorDerive(path) 146 } 147 148 // SignTx implements usbwallet.driver, sending the transaction to the Trezor and 149 // waiting for the user to confirm or deny the transaction. 150 func (w *trezorDriver) SignTx(path accounts.DerivationPath, tx *types.Transaction, chainID *big.Int) (common.Address, *types.Transaction, error) { 151 if w.device == nil { 152 return common.Address{}, nil, accounts.ErrWalletClosed 153 } 154 return w.trezorSign(path, tx, chainID) 155 } 156 157 // trezorDerive sends a derivation request to the Trezor device and returns the 158 // Sberex address located on that path. 159 func (w *trezorDriver) trezorDerive(derivationPath []uint32) (common.Address, error) { 160 address := new(trezor.EthereumAddress) 161 if _, err := w.trezorExchange(&trezor.EthereumGetAddress{AddressN: derivationPath}, address); err != nil { 162 return common.Address{}, err 163 } 164 return common.BytesToAddress(address.GetAddress()), nil 165 } 166 167 // trezorSign sends the transaction to the Trezor wallet, and waits for the user 168 // to confirm or deny the transaction. 169 func (w *trezorDriver) trezorSign(derivationPath []uint32, tx *types.Transaction, chainID *big.Int) (common.Address, *types.Transaction, error) { 170 // Create the transaction initiation message 171 data := tx.Data() 172 length := uint32(len(data)) 173 174 request := &trezor.EthereumSignTx{ 175 AddressN: derivationPath, 176 Nonce: new(big.Int).SetUint64(tx.Nonce()).Bytes(), 177 GasPrice: tx.GasPrice().Bytes(), 178 GasLimit: new(big.Int).SetUint64(tx.Gas()).Bytes(), 179 Value: tx.Value().Bytes(), 180 DataLength: &length, 181 } 182 if to := tx.To(); to != nil { 183 request.To = (*to)[:] // Non contract deploy, set recipient explicitly 184 } 185 if length > 1024 { // Send the data chunked if that was requested 186 request.DataInitialChunk, data = data[:1024], data[1024:] 187 } else { 188 request.DataInitialChunk, data = data, nil 189 } 190 if chainID != nil { // EIP-155 transaction, set chain ID explicitly (only 32 bit is supported!?) 191 id := uint32(chainID.Int64()) 192 request.ChainId = &id 193 } 194 // Send the initiation message and stream content until a signature is returned 195 response := new(trezor.EthereumTxRequest) 196 if _, err := w.trezorExchange(request, response); err != nil { 197 return common.Address{}, nil, err 198 } 199 for response.DataLength != nil && int(*response.DataLength) <= len(data) { 200 chunk := data[:*response.DataLength] 201 data = data[*response.DataLength:] 202 203 if _, err := w.trezorExchange(&trezor.EthereumTxAck{DataChunk: chunk}, response); err != nil { 204 return common.Address{}, nil, err 205 } 206 } 207 // Extract the Sberex signature and do a sanity validation 208 if len(response.GetSignatureR()) == 0 || len(response.GetSignatureS()) == 0 || response.GetSignatureV() == 0 { 209 return common.Address{}, nil, errors.New("reply lacks signature") 210 } 211 signature := append(append(response.GetSignatureR(), response.GetSignatureS()...), byte(response.GetSignatureV())) 212 213 // Create the correct signer and signature transform based on the chain ID 214 var signer types.Signer 215 if chainID == nil { 216 signer = new(types.HomesteadSigner) 217 } else { 218 signer = types.NewEIP155Signer(chainID) 219 signature[64] = signature[64] - byte(chainID.Uint64()*2+35) 220 } 221 // Inject the final signature into the transaction and sanity check the sender 222 signed, err := tx.WithSignature(signer, signature) 223 if err != nil { 224 return common.Address{}, nil, err 225 } 226 sender, err := types.Sender(signer, signed) 227 if err != nil { 228 return common.Address{}, nil, err 229 } 230 return sender, signed, nil 231 } 232 233 // trezorExchange performs a data exchange with the Trezor wallet, sending it a 234 // message and retrieving the response. If multiple responses are possible, the 235 // method will also return the index of the destination object used. 236 func (w *trezorDriver) trezorExchange(req proto.Message, results ...proto.Message) (int, error) { 237 // Construct the original message payload to chunk up 238 data, err := proto.Marshal(req) 239 if err != nil { 240 return 0, err 241 } 242 payload := make([]byte, 8+len(data)) 243 copy(payload, []byte{0x23, 0x23}) 244 binary.BigEndian.PutUint16(payload[2:], trezor.Type(req)) 245 binary.BigEndian.PutUint32(payload[4:], uint32(len(data))) 246 copy(payload[8:], data) 247 248 // Stream all the chunks to the device 249 chunk := make([]byte, 64) 250 chunk[0] = 0x3f // Report ID magic number 251 252 for len(payload) > 0 { 253 // Construct the new message to stream, padding with zeroes if needed 254 if len(payload) > 63 { 255 copy(chunk[1:], payload[:63]) 256 payload = payload[63:] 257 } else { 258 copy(chunk[1:], payload) 259 copy(chunk[1+len(payload):], make([]byte, 63-len(payload))) 260 payload = nil 261 } 262 // Send over to the device 263 w.log.Trace("Data chunk sent to the Trezor", "chunk", hexutil.Bytes(chunk)) 264 if _, err := w.device.Write(chunk); err != nil { 265 return 0, err 266 } 267 } 268 // Stream the reply back from the wallet in 64 byte chunks 269 var ( 270 kind uint16 271 reply []byte 272 ) 273 for { 274 // Read the next chunk from the Trezor wallet 275 if _, err := io.ReadFull(w.device, chunk); err != nil { 276 return 0, err 277 } 278 w.log.Trace("Data chunk received from the Trezor", "chunk", hexutil.Bytes(chunk)) 279 280 // Make sure the transport header matches 281 if chunk[0] != 0x3f || (len(reply) == 0 && (chunk[1] != 0x23 || chunk[2] != 0x23)) { 282 return 0, errTrezorReplyInvalidHeader 283 } 284 // If it's the first chunk, retrieve the reply message type and total message length 285 var payload []byte 286 287 if len(reply) == 0 { 288 kind = binary.BigEndian.Uint16(chunk[3:5]) 289 reply = make([]byte, 0, int(binary.BigEndian.Uint32(chunk[5:9]))) 290 payload = chunk[9:] 291 } else { 292 payload = chunk[1:] 293 } 294 // Append to the reply and stop when filled up 295 if left := cap(reply) - len(reply); left > len(payload) { 296 reply = append(reply, payload...) 297 } else { 298 reply = append(reply, payload[:left]...) 299 break 300 } 301 } 302 // Try to parse the reply into the requested reply message 303 if kind == uint16(trezor.MessageType_MessageType_Failure) { 304 // Trezor returned a failure, extract and return the message 305 failure := new(trezor.Failure) 306 if err := proto.Unmarshal(reply, failure); err != nil { 307 return 0, err 308 } 309 return 0, errors.New("trezor: " + failure.GetMessage()) 310 } 311 if kind == uint16(trezor.MessageType_MessageType_ButtonRequest) { 312 // Trezor is waiting for user confirmation, ack and wait for the next message 313 return w.trezorExchange(&trezor.ButtonAck{}, results...) 314 } 315 for i, res := range results { 316 if trezor.Type(res) == kind { 317 return i, proto.Unmarshal(reply, res) 318 } 319 } 320 expected := make([]string, len(results)) 321 for i, res := range results { 322 expected[i] = trezor.Name(trezor.Type(res)) 323 } 324 return 0, fmt.Errorf("trezor: expected reply types %s, got %s", expected, trezor.Name(kind)) 325 }