forked from mirror/go-ethereum
clef: bidirectional communication with UI (#19018)
* clef: initial implementation of bidirectional RPC communication for the UI * signer: fix tests to pass + formatting * clef: fix unused import + formatting * signer: gosimple nitpicksChrisChinchilla-patch-3
parent
75d292bcf6
commit
b5d471a739
@ -0,0 +1,201 @@ |
||||
// Copyright 2019 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
package core |
||||
|
||||
import ( |
||||
"context" |
||||
"encoding/json" |
||||
"errors" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"math/big" |
||||
|
||||
"github.com/ethereum/go-ethereum/accounts" |
||||
"github.com/ethereum/go-ethereum/accounts/keystore" |
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/common/math" |
||||
"github.com/ethereum/go-ethereum/crypto" |
||||
) |
||||
|
||||
// SignerUIAPI implements methods Clef provides for a UI to query, in the bidirectional communication
|
||||
// channel.
|
||||
// This API is considered secure, since a request can only
|
||||
// ever arrive from the UI -- and the UI is capable of approving any action, thus we can consider these
|
||||
// requests pre-approved.
|
||||
// NB: It's very important that these methods are not ever exposed on the external service
|
||||
// registry.
|
||||
type UIServerAPI struct { |
||||
extApi *SignerAPI |
||||
am *accounts.Manager |
||||
} |
||||
|
||||
// NewUIServerAPI creates a new UIServerAPI
|
||||
func NewUIServerAPI(extapi *SignerAPI) *UIServerAPI { |
||||
return &UIServerAPI{extapi, extapi.am} |
||||
} |
||||
|
||||
// List available accounts. As opposed to the external API definition, this method delivers
|
||||
// the full Account object and not only Address.
|
||||
// Example call
|
||||
// {"jsonrpc":"2.0","method":"clef_listAccounts","params":[], "id":4}
|
||||
func (s *UIServerAPI) ListAccounts(ctx context.Context) ([]accounts.Account, error) { |
||||
var accs []accounts.Account |
||||
for _, wallet := range s.am.Wallets() { |
||||
accs = append(accs, wallet.Accounts()...) |
||||
} |
||||
return accs, nil |
||||
} |
||||
|
||||
// rawWallet is a JSON representation of an accounts.Wallet interface, with its
|
||||
// data contents extracted into plain fields.
|
||||
type rawWallet struct { |
||||
URL string `json:"url"` |
||||
Status string `json:"status"` |
||||
Failure string `json:"failure,omitempty"` |
||||
Accounts []accounts.Account `json:"accounts,omitempty"` |
||||
} |
||||
|
||||
// ListWallets will return a list of wallets that clef manages
|
||||
// Example call
|
||||
// {"jsonrpc":"2.0","method":"clef_listWallets","params":[], "id":5}
|
||||
func (s *UIServerAPI) ListWallets() []rawWallet { |
||||
wallets := make([]rawWallet, 0) // return [] instead of nil if empty
|
||||
for _, wallet := range s.am.Wallets() { |
||||
status, failure := wallet.Status() |
||||
|
||||
raw := rawWallet{ |
||||
URL: wallet.URL().String(), |
||||
Status: status, |
||||
Accounts: wallet.Accounts(), |
||||
} |
||||
if failure != nil { |
||||
raw.Failure = failure.Error() |
||||
} |
||||
wallets = append(wallets, raw) |
||||
} |
||||
return wallets |
||||
} |
||||
|
||||
// DeriveAccount requests a HD wallet to derive a new account, optionally pinning
|
||||
// it for later reuse.
|
||||
// Example call
|
||||
// {"jsonrpc":"2.0","method":"clef_deriveAccount","params":["ledger://","m/44'/60'/0'", false], "id":6}
|
||||
func (s *UIServerAPI) DeriveAccount(url string, path string, pin *bool) (accounts.Account, error) { |
||||
wallet, err := s.am.Wallet(url) |
||||
if err != nil { |
||||
return accounts.Account{}, err |
||||
} |
||||
derivPath, err := accounts.ParseDerivationPath(path) |
||||
if err != nil { |
||||
return accounts.Account{}, err |
||||
} |
||||
if pin == nil { |
||||
pin = new(bool) |
||||
} |
||||
return wallet.Derive(derivPath, *pin) |
||||
} |
||||
|
||||
// fetchKeystore retrives the encrypted keystore from the account manager.
|
||||
func fetchKeystore(am *accounts.Manager) *keystore.KeyStore { |
||||
return am.Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore) |
||||
} |
||||
|
||||
// ImportRawKey stores the given hex encoded ECDSA key into the key directory,
|
||||
// encrypting it with the passphrase.
|
||||
// Example call (should fail on password too short)
|
||||
// {"jsonrpc":"2.0","method":"clef_importRawKey","params":["1111111111111111111111111111111111111111111111111111111111111111","test"], "id":6}
|
||||
func (s *UIServerAPI) ImportRawKey(privkey string, password string) (accounts.Account, error) { |
||||
key, err := crypto.HexToECDSA(privkey) |
||||
if err != nil { |
||||
return accounts.Account{}, err |
||||
} |
||||
if err := ValidatePasswordFormat(password); err != nil { |
||||
return accounts.Account{}, fmt.Errorf("password requirements not met: %v", err) |
||||
} |
||||
// No error
|
||||
return fetchKeystore(s.am).ImportECDSA(key, password) |
||||
} |
||||
|
||||
// OpenWallet initiates a hardware wallet opening procedure, establishing a USB
|
||||
// connection and attempting to authenticate via the provided passphrase. Note,
|
||||
// the method may return an extra challenge requiring a second open (e.g. the
|
||||
// Trezor PIN matrix challenge).
|
||||
// Example
|
||||
// {"jsonrpc":"2.0","method":"clef_openWallet","params":["ledger://",""], "id":6}
|
||||
func (s *UIServerAPI) OpenWallet(url string, passphrase *string) error { |
||||
wallet, err := s.am.Wallet(url) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
pass := "" |
||||
if passphrase != nil { |
||||
pass = *passphrase |
||||
} |
||||
return wallet.Open(pass) |
||||
} |
||||
|
||||
// ChainId returns the chainid in use for Eip-155 replay protection
|
||||
// Example call
|
||||
// {"jsonrpc":"2.0","method":"clef_chainId","params":[], "id":8}
|
||||
func (s *UIServerAPI) ChainId() math.HexOrDecimal64 { |
||||
return (math.HexOrDecimal64)(s.extApi.chainID.Uint64()) |
||||
} |
||||
|
||||
// SetChainId sets the chain id to use when signing transactions.
|
||||
// Example call to set Ropsten:
|
||||
// {"jsonrpc":"2.0","method":"clef_setChainId","params":["3"], "id":8}
|
||||
func (s *UIServerAPI) SetChainId(id math.HexOrDecimal64) math.HexOrDecimal64 { |
||||
s.extApi.chainID = new(big.Int).SetUint64(uint64(id)) |
||||
return s.ChainId() |
||||
} |
||||
|
||||
// Export returns encrypted private key associated with the given address in web3 keystore format.
|
||||
// Example
|
||||
// {"jsonrpc":"2.0","method":"clef_export","params":["0x19e7e376e7c213b7e7e7e46cc70a5dd086daff2a"], "id":4}
|
||||
func (s *UIServerAPI) Export(ctx context.Context, addr common.Address) (json.RawMessage, error) { |
||||
// Look up the wallet containing the requested signer
|
||||
wallet, err := s.am.Find(accounts.Account{Address: addr}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if wallet.URL().Scheme != keystore.KeyStoreScheme { |
||||
return nil, fmt.Errorf("Account is not a keystore-account") |
||||
} |
||||
return ioutil.ReadFile(wallet.URL().Path) |
||||
} |
||||
|
||||
// Import tries to import the given keyJSON in the local keystore. The keyJSON data is expected to be
|
||||
// in web3 keystore format. It will decrypt the keyJSON with the given passphrase and on successful
|
||||
// decryption it will encrypt the key with the given newPassphrase and store it in the keystore.
|
||||
// Example (the address in question has privkey `11...11`):
|
||||
// {"jsonrpc":"2.0","method":"clef_import","params":[{"address":"19e7e376e7c213b7e7e7e46cc70a5dd086daff2a","crypto":{"cipher":"aes-128-ctr","ciphertext":"33e4cd3756091d037862bb7295e9552424a391a6e003272180a455ca2a9fb332","cipherparams":{"iv":"b54b263e8f89c42bb219b6279fba5cce"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"e4ca94644fd30569c1b1afbbc851729953c92637b7fe4bb9840bbb31ffbc64a5"},"mac":"f4092a445c2b21c0ef34f17c9cd0d873702b2869ec5df4439a0c2505823217e7"},"id":"216c7eac-e8c1-49af-a215-fa0036f29141","version":3},"test","yaddayadda"], "id":4}
|
||||
func (api *UIServerAPI) Import(ctx context.Context, keyJSON json.RawMessage, oldPassphrase, newPassphrase string) (accounts.Account, error) { |
||||
be := api.am.Backends(keystore.KeyStoreType) |
||||
|
||||
if len(be) == 0 { |
||||
return accounts.Account{}, errors.New("password based accounts not supported") |
||||
} |
||||
if err := ValidatePasswordFormat(newPassphrase); err != nil { |
||||
return accounts.Account{}, fmt.Errorf("password requirements not met: %v", err) |
||||
} |
||||
return be[0].(*keystore.KeyStore).Import(keyJSON, oldPassphrase, newPassphrase) |
||||
} |
||||
|
||||
// Other methods to be added, not yet implemented are:
|
||||
// - Ruleset interaction: add rules, attest rulefiles
|
||||
// - Store metadata about accounts, e.g. naming of accounts
|
Loading…
Reference in new issue