Clef: USB hw wallet support (#17756)
* signer: implement USB interaction with hw wallets * signer: fix failing testcases
This commit is contained in:
parent
2c110c81ee
commit
dcaabfe7f6
|
@ -1,5 +1,21 @@
|
|||
### Changelog for internal API (ui-api)
|
||||
|
||||
### 2.1.0
|
||||
|
||||
* Add `OnInputRequired(info UserInputRequest)` to internal API. This method is used when Clef needs user input, e.g. passwords.
|
||||
|
||||
The following structures are used:
|
||||
```golang
|
||||
UserInputRequest struct {
|
||||
Prompt string `json:"prompt"`
|
||||
Title string `json:"title"`
|
||||
IsPassword bool `json:"isPassword"`
|
||||
}
|
||||
UserInputResponse struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
```
|
||||
|
||||
### 2.0.0
|
||||
|
||||
* Modify how `call_info` on a transaction is conveyed. New format:
|
||||
|
|
|
@ -36,6 +36,9 @@ import (
|
|||
"github.com/ethereum/go-ethereum/rlp"
|
||||
)
|
||||
|
||||
// numberOfAccountsToDerive For hardware wallets, the number of accounts to derive
|
||||
const numberOfAccountsToDerive = 10
|
||||
|
||||
// ExternalAPI defines the external API through which signing requests are made.
|
||||
type ExternalAPI interface {
|
||||
// List available accounts
|
||||
|
@ -79,6 +82,9 @@ type SignerUI interface {
|
|||
// OnSignerStartup is invoked when the signer boots, and tells the UI info about external API location and version
|
||||
// information
|
||||
OnSignerStartup(info StartupInfo)
|
||||
// OnInputRequried is invoked when clef requires user input, for example master password or
|
||||
// pin-code for unlocking hardware wallets
|
||||
OnInputRequired(info UserInputRequest) (UserInputResponse, error)
|
||||
}
|
||||
|
||||
// SignerAPI defines the actual implementation of ExternalAPI
|
||||
|
@ -194,6 +200,14 @@ type (
|
|||
StartupInfo struct {
|
||||
Info map[string]interface{} `json:"info"`
|
||||
}
|
||||
UserInputRequest struct {
|
||||
Prompt string `json:"prompt"`
|
||||
Title string `json:"title"`
|
||||
IsPassword bool `json:"isPassword"`
|
||||
}
|
||||
UserInputResponse struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
)
|
||||
|
||||
var ErrRequestDenied = errors.New("Request denied")
|
||||
|
@ -215,6 +229,9 @@ func NewSignerAPI(chainID int64, ksLocation string, noUSB bool, ui SignerUI, abi
|
|||
if len(ksLocation) > 0 {
|
||||
backends = append(backends, keystore.NewKeyStore(ksLocation, n, p))
|
||||
}
|
||||
if advancedMode {
|
||||
log.Info("Clef is in advanced mode: will warn instead of reject")
|
||||
}
|
||||
if !noUSB {
|
||||
// Start a USB hub for Ledger hardware wallets
|
||||
if ledgerhub, err := usbwallet.NewLedgerHub(); err != nil {
|
||||
|
@ -231,10 +248,94 @@ func NewSignerAPI(chainID int64, ksLocation string, noUSB bool, ui SignerUI, abi
|
|||
log.Debug("Trezor support enabled")
|
||||
}
|
||||
}
|
||||
if advancedMode {
|
||||
log.Info("Clef is in advanced mode: will warn instead of reject")
|
||||
signer := &SignerAPI{big.NewInt(chainID), accounts.NewManager(backends...), ui, NewValidator(abidb), !advancedMode}
|
||||
if !noUSB {
|
||||
signer.startUSBListener()
|
||||
}
|
||||
return &SignerAPI{big.NewInt(chainID), accounts.NewManager(backends...), ui, NewValidator(abidb), !advancedMode}
|
||||
return signer
|
||||
}
|
||||
func (api *SignerAPI) openTrezor(url accounts.URL) {
|
||||
resp, err := api.UI.OnInputRequired(UserInputRequest{
|
||||
Prompt: "Pin required to open Trezor wallet\n" +
|
||||
"Look at the device for number positions\n\n" +
|
||||
"7 | 8 | 9\n" +
|
||||
"--+---+--\n" +
|
||||
"4 | 5 | 6\n" +
|
||||
"--+---+--\n" +
|
||||
"1 | 2 | 3\n\n",
|
||||
IsPassword: true,
|
||||
Title: "Trezor unlock",
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn("failed getting trezor pin", "err", err)
|
||||
return
|
||||
}
|
||||
// We're using the URL instead of the pointer to the
|
||||
// Wallet -- perhaps it is not actually present anymore
|
||||
w, err := api.am.Wallet(url.String())
|
||||
if err != nil {
|
||||
log.Warn("wallet unavailable", "url", url)
|
||||
return
|
||||
}
|
||||
err = w.Open(resp.Text)
|
||||
if err != nil {
|
||||
log.Warn("failed to open wallet", "wallet", url, "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// startUSBListener starts a listener for USB events, for hardware wallet interaction
|
||||
func (api *SignerAPI) startUSBListener() {
|
||||
events := make(chan accounts.WalletEvent, 16)
|
||||
am := api.am
|
||||
am.Subscribe(events)
|
||||
go func() {
|
||||
|
||||
// Open any wallets already attached
|
||||
for _, wallet := range am.Wallets() {
|
||||
if err := wallet.Open(""); err != nil {
|
||||
log.Warn("Failed to open wallet", "url", wallet.URL(), "err", err)
|
||||
if err == usbwallet.ErrTrezorPINNeeded {
|
||||
go api.openTrezor(wallet.URL())
|
||||
}
|
||||
}
|
||||
}
|
||||
// Listen for wallet event till termination
|
||||
for event := range events {
|
||||
switch event.Kind {
|
||||
case accounts.WalletArrived:
|
||||
if err := event.Wallet.Open(""); err != nil {
|
||||
log.Warn("New wallet appeared, failed to open", "url", event.Wallet.URL(), "err", err)
|
||||
if err == usbwallet.ErrTrezorPINNeeded {
|
||||
go api.openTrezor(event.Wallet.URL())
|
||||
}
|
||||
}
|
||||
case accounts.WalletOpened:
|
||||
status, _ := event.Wallet.Status()
|
||||
log.Info("New wallet appeared", "url", event.Wallet.URL(), "status", status)
|
||||
|
||||
derivationPath := accounts.DefaultBaseDerivationPath
|
||||
if event.Wallet.URL().Scheme == "ledger" {
|
||||
derivationPath = accounts.DefaultLedgerBaseDerivationPath
|
||||
}
|
||||
var nextPath = derivationPath
|
||||
// Derive first N accounts, hardcoded for now
|
||||
for i := 0; i < numberOfAccountsToDerive; i++ {
|
||||
acc, err := event.Wallet.Derive(nextPath, true)
|
||||
if err != nil {
|
||||
log.Warn("account derivation failed", "error", err)
|
||||
} else {
|
||||
log.Info("derived account", "address", acc.Address)
|
||||
}
|
||||
nextPath[len(nextPath)-1]++
|
||||
}
|
||||
case accounts.WalletDropped:
|
||||
log.Info("Old wallet dropped", "url", event.Wallet.URL())
|
||||
event.Wallet.Close()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// List returns the set of wallet this signer manages. Each wallet can contain
|
||||
|
|
|
@ -19,6 +19,7 @@ package core
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
|
@ -41,6 +42,10 @@ type HeadlessUI struct {
|
|||
controller chan string
|
||||
}
|
||||
|
||||
func (ui *HeadlessUI) OnInputRequired(info UserInputRequest) (UserInputResponse, error) {
|
||||
return UserInputResponse{}, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (ui *HeadlessUI) OnSignerStartup(info StartupInfo) {
|
||||
}
|
||||
|
||||
|
|
|
@ -83,6 +83,22 @@ func (ui *CommandlineUI) readPasswordText(inputstring string) string {
|
|||
return string(text)
|
||||
}
|
||||
|
||||
func (ui *CommandlineUI) OnInputRequired(info UserInputRequest) (UserInputResponse, error) {
|
||||
fmt.Println(info.Title)
|
||||
fmt.Println(info.Prompt)
|
||||
if info.IsPassword {
|
||||
text, err := terminal.ReadPassword(int(os.Stdin.Fd()))
|
||||
if err != nil {
|
||||
log.Error("Failed to read password", "err", err)
|
||||
}
|
||||
fmt.Println("-----------------------")
|
||||
return UserInputResponse{string(text)}, err
|
||||
}
|
||||
text := ui.readString()
|
||||
fmt.Println("-----------------------")
|
||||
return UserInputResponse{text}, nil
|
||||
}
|
||||
|
||||
// confirm returns true if user enters 'Yes', otherwise false
|
||||
func (ui *CommandlineUI) confirm() bool {
|
||||
fmt.Printf("Approve? [y/N]:\n")
|
||||
|
|
|
@ -111,3 +111,11 @@ func (ui *StdIOUI) OnSignerStartup(info StartupInfo) {
|
|||
log.Info("Error calling 'OnSignerStartup'", "exc", err.Error(), "info", info)
|
||||
}
|
||||
}
|
||||
func (ui *StdIOUI) OnInputRequired(info UserInputRequest) (UserInputResponse, error) {
|
||||
var result UserInputResponse
|
||||
err := ui.dispatch("OnInputRequired", info, &result)
|
||||
if err != nil {
|
||||
log.Info("Error calling 'OnInputRequired'", "exc", err.Error(), "info", info)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
|
|
@ -194,6 +194,11 @@ func (r *rulesetUI) ApproveImport(request *core.ImportRequest) (core.ImportRespo
|
|||
return r.next.ApproveImport(request)
|
||||
}
|
||||
|
||||
// OnInputRequired not handled by rules
|
||||
func (r *rulesetUI) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) {
|
||||
return r.next.OnInputRequired(info)
|
||||
}
|
||||
|
||||
func (r *rulesetUI) ApproveListing(request *core.ListRequest) (core.ListResponse, error) {
|
||||
jsonreq, err := json.Marshal(request)
|
||||
approved, err := r.checkApproval("ApproveListing", jsonreq, err)
|
||||
|
@ -222,6 +227,7 @@ func (r *rulesetUI) ShowInfo(message string) {
|
|||
log.Info(message)
|
||||
r.next.ShowInfo(message)
|
||||
}
|
||||
|
||||
func (r *rulesetUI) OnSignerStartup(info core.StartupInfo) {
|
||||
jsonInfo, err := json.Marshal(info)
|
||||
if err != nil {
|
||||
|
|
|
@ -74,6 +74,10 @@ func mixAddr(a string) (*common.MixedcaseAddress, error) {
|
|||
|
||||
type alwaysDenyUI struct{}
|
||||
|
||||
func (alwaysDenyUI) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) {
|
||||
return core.UserInputResponse{}, nil
|
||||
}
|
||||
|
||||
func (alwaysDenyUI) OnSignerStartup(info core.StartupInfo) {
|
||||
}
|
||||
|
||||
|
@ -200,6 +204,11 @@ type dummyUI struct {
|
|||
calls []string
|
||||
}
|
||||
|
||||
func (d *dummyUI) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) {
|
||||
d.calls = append(d.calls, "OnInputRequired")
|
||||
return core.UserInputResponse{}, nil
|
||||
}
|
||||
|
||||
func (d *dummyUI) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) {
|
||||
d.calls = append(d.calls, "ApproveTx")
|
||||
return core.SignTxResponse{}, core.ErrRequestDenied
|
||||
|
@ -509,6 +518,11 @@ type dontCallMe struct {
|
|||
t *testing.T
|
||||
}
|
||||
|
||||
func (d *dontCallMe) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) {
|
||||
d.t.Fatalf("Did not expect next-handler to be called")
|
||||
return core.UserInputResponse{}, nil
|
||||
}
|
||||
|
||||
func (d *dontCallMe) OnSignerStartup(info core.StartupInfo) {
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue