Delete signer directory

Signed-off-by: Isabel Schöps Thiel @IsabelSchoepd <155141998+IST-Github@users.noreply.github.com>
This commit is contained in:
Isabel Schöps Thiel @IsabelSchoepd 2024-01-04 04:32:05 +01:00 committed by GitHub
parent 03ee4fbcb2
commit 9d91938327
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 0 additions and 276139 deletions

View file

@ -1,670 +0,0 @@
// Copyright 2018 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/>.
package core
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"math/big"
"os"
"reflect"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/accounts/scwallet"
"github.com/ethereum/go-ethereum/accounts/usbwallet"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/internal/ethapi"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum/go-ethereum/signer/core/apitypes"
"github.com/ethereum/go-ethereum/signer/storage"
)
const (
// numberOfAccountsToDerive For hardware wallets, the number of accounts to derive
numberOfAccountsToDerive = 10
// ExternalAPIVersion -- see extapi_changelog.md
ExternalAPIVersion = "6.1.0"
// InternalAPIVersion -- see intapi_changelog.md
InternalAPIVersion = "7.0.1"
)
// ExternalAPI defines the external API through which signing requests are made.
type ExternalAPI interface {
// List available accounts
List(ctx context.Context) ([]common.Address, error)
// New request to create a new account
New(ctx context.Context) (common.Address, error)
// SignTransaction request to sign the specified transaction
SignTransaction(ctx context.Context, args apitypes.SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error)
// SignData - request to sign the given data (plus prefix)
SignData(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (hexutil.Bytes, error)
// SignTypedData - request to sign the given structured data (plus prefix)
SignTypedData(ctx context.Context, addr common.MixedcaseAddress, data apitypes.TypedData) (hexutil.Bytes, error)
// EcRecover - recover public key from given message and signature
EcRecover(ctx context.Context, data hexutil.Bytes, sig hexutil.Bytes) (common.Address, error)
// Version info about the APIs
Version(ctx context.Context) (string, error)
// SignGnosisSafeTx signs/confirms a gnosis-safe multisig transaction
SignGnosisSafeTx(ctx context.Context, signerAddress common.MixedcaseAddress, gnosisTx GnosisSafeTx, methodSelector *string) (*GnosisSafeTx, error)
}
// UIClientAPI specifies what method a UI needs to implement to be able to be used as a
// UI for the signer
type UIClientAPI interface {
// ApproveTx prompt the user for confirmation to request to sign Transaction
ApproveTx(request *SignTxRequest) (SignTxResponse, error)
// ApproveSignData prompt the user for confirmation to request to sign data
ApproveSignData(request *SignDataRequest) (SignDataResponse, error)
// ApproveListing prompt the user for confirmation to list accounts
// the list of accounts to list can be modified by the UI
ApproveListing(request *ListRequest) (ListResponse, error)
// ApproveNewAccount prompt the user for confirmation to create new Account, and reveal to caller
ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error)
// ShowError displays error message to user
ShowError(message string)
// ShowInfo displays info message to user
ShowInfo(message string)
// OnApprovedTx notifies the UI about a transaction having been successfully signed.
// This method can be used by a UI to keep track of e.g. how much has been sent to a particular recipient.
OnApprovedTx(tx ethapi.SignTransactionResult)
// OnSignerStartup is invoked when the signer boots, and tells the UI info about external API location and version
// information
OnSignerStartup(info StartupInfo)
// OnInputRequired is invoked when clef requires user input, for example master password or
// pin-code for unlocking hardware wallets
OnInputRequired(info UserInputRequest) (UserInputResponse, error)
// RegisterUIServer tells the UI to use the given UIServerAPI for ui->clef communication
RegisterUIServer(api *UIServerAPI)
}
// Validator defines the methods required to validate a transaction against some
// sanity defaults as well as any underlying 4byte method database.
//
// Use fourbyte.Database as an implementation. It is separated out of this package
// to allow pieces of the signer package to be used without having to load the
// 7MB embedded 4byte dump.
type Validator interface {
// ValidateTransaction does a number of checks on the supplied transaction, and
// returns either a list of warnings, or an error (indicating that the transaction
// should be immediately rejected).
ValidateTransaction(selector *string, tx *apitypes.SendTxArgs) (*apitypes.ValidationMessages, error)
}
// SignerAPI defines the actual implementation of ExternalAPI
type SignerAPI struct {
chainID *big.Int
am *accounts.Manager
UI UIClientAPI
validator Validator
rejectMode bool
credentials storage.Storage
}
// Metadata about a request
type Metadata struct {
Remote string `json:"remote"`
Local string `json:"local"`
Scheme string `json:"scheme"`
UserAgent string `json:"User-Agent"`
Origin string `json:"Origin"`
}
func StartClefAccountManager(ksLocation string, nousb, lightKDF bool, scpath string) *accounts.Manager {
var (
backends []accounts.Backend
n, p = keystore.StandardScryptN, keystore.StandardScryptP
)
if lightKDF {
n, p = keystore.LightScryptN, keystore.LightScryptP
}
// support password based accounts
if len(ksLocation) > 0 {
backends = append(backends, keystore.NewKeyStore(ksLocation, n, p))
}
if !nousb {
// Start a USB hub for Ledger hardware wallets
if ledgerhub, err := usbwallet.NewLedgerHub(); err != nil {
log.Warn(fmt.Sprintf("Failed to start Ledger hub, disabling: %v", err))
} else {
backends = append(backends, ledgerhub)
log.Debug("Ledger support enabled")
}
// Start a USB hub for Trezor hardware wallets (HID version)
if trezorhub, err := usbwallet.NewTrezorHubWithHID(); err != nil {
log.Warn(fmt.Sprintf("Failed to start HID Trezor hub, disabling: %v", err))
} else {
backends = append(backends, trezorhub)
log.Debug("Trezor support enabled via HID")
}
// Start a USB hub for Trezor hardware wallets (WebUSB version)
if trezorhub, err := usbwallet.NewTrezorHubWithWebUSB(); err != nil {
log.Warn(fmt.Sprintf("Failed to start WebUSB Trezor hub, disabling: %v", err))
} else {
backends = append(backends, trezorhub)
log.Debug("Trezor support enabled via WebUSB")
}
}
// Start a smart card hub
if len(scpath) > 0 {
// Sanity check that the smartcard path is valid
fi, err := os.Stat(scpath)
if err != nil {
log.Info("Smartcard socket file missing, disabling", "err", err)
} else {
if fi.Mode()&os.ModeType != os.ModeSocket {
log.Error("Invalid smartcard socket file type", "path", scpath, "type", fi.Mode().String())
} else {
if schub, err := scwallet.NewHub(scpath, scwallet.Scheme, ksLocation); err != nil {
log.Warn(fmt.Sprintf("Failed to start smart card hub, disabling: %v", err))
} else {
backends = append(backends, schub)
}
}
}
}
// Clef doesn't allow insecure http account unlock.
return accounts.NewManager(&accounts.Config{InsecureUnlockAllowed: false}, backends...)
}
// MetadataFromContext extracts Metadata from a given context.Context
func MetadataFromContext(ctx context.Context) Metadata {
info := rpc.PeerInfoFromContext(ctx)
m := Metadata{"NA", "NA", "NA", "", ""} // batman
if info.Transport != "" {
if info.Transport == "http" {
m.Scheme = info.HTTP.Version
}
m.Scheme = info.Transport
}
if info.RemoteAddr != "" {
m.Remote = info.RemoteAddr
}
if info.HTTP.Host != "" {
m.Local = info.HTTP.Host
}
m.Origin = info.HTTP.Origin
m.UserAgent = info.HTTP.UserAgent
return m
}
// String implements Stringer interface
func (m Metadata) String() string {
s, err := json.Marshal(m)
if err == nil {
return string(s)
}
return err.Error()
}
// types for the requests/response types between signer and UI
type (
// SignTxRequest contains info about a Transaction to sign
SignTxRequest struct {
Transaction apitypes.SendTxArgs `json:"transaction"`
Callinfo []apitypes.ValidationInfo `json:"call_info"`
Meta Metadata `json:"meta"`
}
// SignTxResponse result from SignTxRequest
SignTxResponse struct {
//The UI may make changes to the TX
Transaction apitypes.SendTxArgs `json:"transaction"`
Approved bool `json:"approved"`
}
SignDataRequest struct {
ContentType string `json:"content_type"`
Address common.MixedcaseAddress `json:"address"`
Rawdata []byte `json:"raw_data"`
Messages []*apitypes.NameValueType `json:"messages"`
Callinfo []apitypes.ValidationInfo `json:"call_info"`
Hash hexutil.Bytes `json:"hash"`
Meta Metadata `json:"meta"`
}
SignDataResponse struct {
Approved bool `json:"approved"`
}
NewAccountRequest struct {
Meta Metadata `json:"meta"`
}
NewAccountResponse struct {
Approved bool `json:"approved"`
}
ListRequest struct {
Accounts []accounts.Account `json:"accounts"`
Meta Metadata `json:"meta"`
}
ListResponse struct {
Accounts []accounts.Account `json:"accounts"`
}
Message struct {
Text string `json:"text"`
}
StartupInfo struct {
Info map[string]interface{} `json:"info"`
}
UserInputRequest struct {
Title string `json:"title"`
Prompt string `json:"prompt"`
IsPassword bool `json:"isPassword"`
}
UserInputResponse struct {
Text string `json:"text"`
}
)
var ErrRequestDenied = errors.New("request denied")
// NewSignerAPI creates a new API that can be used for Account management.
// ksLocation specifies the directory where to store the password protected private
// key that is generated when a new Account is created.
// noUSB disables USB support that is required to support hardware devices such as
// ledger and trezor.
func NewSignerAPI(am *accounts.Manager, chainID int64, noUSB bool, ui UIClientAPI, validator Validator, advancedMode bool, credentials storage.Storage) *SignerAPI {
if advancedMode {
log.Info("Clef is in advanced mode: will warn instead of reject")
}
signer := &SignerAPI{big.NewInt(chainID), am, ui, validator, !advancedMode, credentials}
if !noUSB {
signer.startUSBListener()
}
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() {
eventCh := make(chan accounts.WalletEvent, 16)
am := api.am
am.Subscribe(eventCh)
// 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())
}
}
}
go api.derivationLoop(eventCh)
}
// derivationLoop listens for wallet events
func (api *SignerAPI) derivationLoop(events chan accounts.WalletEvent) {
// 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)
var derive = func(limit int, next func() accounts.DerivationPath) {
// Derive first N accounts, hardcoded for now
for i := 0; i < limit; i++ {
path := next()
if acc, err := event.Wallet.Derive(path, true); err != nil {
log.Warn("Account derivation failed", "error", err)
} else {
log.Info("Derived account", "address", acc.Address, "path", path)
}
}
}
log.Info("Deriving default paths")
derive(numberOfAccountsToDerive, accounts.DefaultIterator(accounts.DefaultBaseDerivationPath))
if event.Wallet.URL().Scheme == "ledger" {
log.Info("Deriving ledger legacy paths")
derive(numberOfAccountsToDerive, accounts.DefaultIterator(accounts.LegacyLedgerBaseDerivationPath))
log.Info("Deriving ledger live paths")
// For ledger live, since it's based off the same (DefaultBaseDerivationPath)
// as one we've already used, we need to step it forward one step to avoid
// hitting the same path again
nextFn := accounts.LedgerLiveIterator(accounts.DefaultBaseDerivationPath)
nextFn()
derive(numberOfAccountsToDerive, nextFn)
}
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
// multiple accounts.
func (api *SignerAPI) List(ctx context.Context) ([]common.Address, error) {
var accs = make([]accounts.Account, 0)
// accs is initialized as empty list, not nil. We use 'nil' to signal
// rejection, as opposed to an empty list.
for _, wallet := range api.am.Wallets() {
accs = append(accs, wallet.Accounts()...)
}
result, err := api.UI.ApproveListing(&ListRequest{Accounts: accs, Meta: MetadataFromContext(ctx)})
if err != nil {
return nil, err
}
if result.Accounts == nil {
return nil, ErrRequestDenied
}
addresses := make([]common.Address, 0)
for _, acc := range result.Accounts {
addresses = append(addresses, acc.Address)
}
return addresses, nil
}
// New creates a new password protected Account. The private key is protected with
// the given password. Users are responsible to backup the private key that is stored
// in the keystore location that was specified when this API was created.
func (api *SignerAPI) New(ctx context.Context) (common.Address, error) {
if be := api.am.Backends(keystore.KeyStoreType); len(be) == 0 {
return common.Address{}, errors.New("password based accounts not supported")
}
if resp, err := api.UI.ApproveNewAccount(&NewAccountRequest{MetadataFromContext(ctx)}); err != nil {
return common.Address{}, err
} else if !resp.Approved {
return common.Address{}, ErrRequestDenied
}
return api.newAccount()
}
// newAccount is the internal method to create a new account. It should be used
// _after_ user-approval has been obtained
func (api *SignerAPI) newAccount() (common.Address, error) {
be := api.am.Backends(keystore.KeyStoreType)
if len(be) == 0 {
return common.Address{}, errors.New("password based accounts not supported")
}
// Three retries to get a valid password
for i := 0; i < 3; i++ {
resp, err := api.UI.OnInputRequired(UserInputRequest{
"New account password",
fmt.Sprintf("Please enter a password for the new account to be created (attempt %d of 3)", i),
true})
if err != nil {
log.Warn("error obtaining password", "attempt", i, "error", err)
continue
}
if pwErr := ValidatePasswordFormat(resp.Text); pwErr != nil {
api.UI.ShowError(fmt.Sprintf("Account creation attempt #%d failed due to password requirements: %v", i+1, pwErr))
} else {
// No error
acc, err := be[0].(*keystore.KeyStore).NewAccount(resp.Text)
log.Info("Your new key was generated", "address", acc.Address)
log.Warn("Please backup your key file!", "path", acc.URL.Path)
log.Warn("Please remember your password!")
return acc.Address, err
}
}
// Otherwise fail, with generic error message
return common.Address{}, errors.New("account creation failed")
}
// logDiff logs the difference between the incoming (original) transaction and the one returned from the signer.
// it also returns 'true' if the transaction was modified, to make it possible to configure the signer not to allow
// UI-modifications to requests
func logDiff(original *SignTxRequest, new *SignTxResponse) bool {
var intPtrModified = func(a, b *hexutil.Big) bool {
aBig := (*big.Int)(a)
bBig := (*big.Int)(b)
if aBig != nil && bBig != nil {
return aBig.Cmp(bBig) != 0
}
// One or both of them are nil
return a != b
}
modified := false
if f0, f1 := original.Transaction.From, new.Transaction.From; !reflect.DeepEqual(f0, f1) {
log.Info("Sender-account changed by UI", "was", f0, "is", f1)
modified = true
}
if t0, t1 := original.Transaction.To, new.Transaction.To; !reflect.DeepEqual(t0, t1) {
log.Info("Recipient-account changed by UI", "was", t0, "is", t1)
modified = true
}
if g0, g1 := original.Transaction.Gas, new.Transaction.Gas; g0 != g1 {
modified = true
log.Info("Gas changed by UI", "was", g0, "is", g1)
}
if a, b := original.Transaction.GasPrice, new.Transaction.GasPrice; intPtrModified(a, b) {
log.Info("GasPrice changed by UI", "was", a, "is", b)
modified = true
}
if a, b := original.Transaction.MaxPriorityFeePerGas, new.Transaction.MaxPriorityFeePerGas; intPtrModified(a, b) {
log.Info("maxPriorityFeePerGas changed by UI", "was", a, "is", b)
modified = true
}
if a, b := original.Transaction.MaxFeePerGas, new.Transaction.MaxFeePerGas; intPtrModified(a, b) {
log.Info("maxFeePerGas changed by UI", "was", a, "is", b)
modified = true
}
if v0, v1 := big.Int(original.Transaction.Value), big.Int(new.Transaction.Value); v0.Cmp(&v1) != 0 {
modified = true
log.Info("Value changed by UI", "was", v0, "is", v1)
}
if d0, d1 := original.Transaction.Data, new.Transaction.Data; d0 != d1 {
d0s := ""
d1s := ""
if d0 != nil {
d0s = hexutil.Encode(*d0)
}
if d1 != nil {
d1s = hexutil.Encode(*d1)
}
if d1s != d0s {
modified = true
log.Info("Data changed by UI", "was", d0s, "is", d1s)
}
}
if n0, n1 := original.Transaction.Nonce, new.Transaction.Nonce; n0 != n1 {
modified = true
log.Info("Nonce changed by UI", "was", n0, "is", n1)
}
return modified
}
func (api *SignerAPI) lookupPassword(address common.Address) (string, error) {
return api.credentials.Get(address.Hex())
}
func (api *SignerAPI) lookupOrQueryPassword(address common.Address, title, prompt string) (string, error) {
// Look up the password and return if available
if pw, err := api.lookupPassword(address); err == nil {
return pw, nil
}
// Password unavailable, request it from the user
pwResp, err := api.UI.OnInputRequired(UserInputRequest{title, prompt, true})
if err != nil {
log.Warn("error obtaining password", "error", err)
// We'll not forward the error here, in case the error contains info about the response from the UI,
// which could leak the password if it was malformed json or something
return "", errors.New("internal error")
}
return pwResp.Text, nil
}
// SignTransaction signs the given Transaction and returns it both as json and rlp-encoded form
func (api *SignerAPI) SignTransaction(ctx context.Context, args apitypes.SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error) {
var (
err error
result SignTxResponse
)
msgs, err := api.validator.ValidateTransaction(methodSelector, &args)
if err != nil {
return nil, err
}
// If we are in 'rejectMode', then reject rather than show the user warnings
if api.rejectMode {
if err := msgs.GetWarnings(); err != nil {
log.Info("Signing aborted due to warnings. In order to continue despite warnings, please use the flag '--advanced'.")
return nil, err
}
}
if args.ChainID != nil {
requestedChainId := (*big.Int)(args.ChainID)
if api.chainID.Cmp(requestedChainId) != 0 {
log.Error("Signing request with wrong chain id", "requested", requestedChainId, "configured", api.chainID)
return nil, fmt.Errorf("requested chainid %d does not match the configuration of the signer",
requestedChainId)
}
}
req := SignTxRequest{
Transaction: args,
Meta: MetadataFromContext(ctx),
Callinfo: msgs.Messages,
}
// Process approval
result, err = api.UI.ApproveTx(&req)
if err != nil {
return nil, err
}
if !result.Approved {
return nil, ErrRequestDenied
}
// Log changes made by the UI to the signing-request
logDiff(&req, &result)
var (
acc accounts.Account
wallet accounts.Wallet
)
acc = accounts.Account{Address: result.Transaction.From.Address()}
wallet, err = api.am.Find(acc)
if err != nil {
return nil, err
}
// Convert fields into a real transaction
var unsignedTx = result.Transaction.ToTransaction()
// Get the password for the transaction
pw, err := api.lookupOrQueryPassword(acc.Address, "Account password",
fmt.Sprintf("Please enter the password for account %s", acc.Address.String()))
if err != nil {
return nil, err
}
// The one to sign is the one that was returned from the UI
signedTx, err := wallet.SignTxWithPassphrase(acc, pw, unsignedTx, api.chainID)
if err != nil {
api.UI.ShowError(err.Error())
return nil, err
}
data, err := signedTx.MarshalBinary()
if err != nil {
return nil, err
}
response := ethapi.SignTransactionResult{Raw: data, Tx: signedTx}
// Finally, send the signed tx to the UI
api.UI.OnApprovedTx(response)
// ...and to the external caller
return &response, nil
}
func (api *SignerAPI) SignGnosisSafeTx(ctx context.Context, signerAddress common.MixedcaseAddress, gnosisTx GnosisSafeTx, methodSelector *string) (*GnosisSafeTx, error) {
// Do the usual validations, but on the last-stage transaction
args := gnosisTx.ArgsForValidation()
msgs, err := api.validator.ValidateTransaction(methodSelector, args)
if err != nil {
return nil, err
}
// If we are in 'rejectMode', then reject rather than show the user warnings
if api.rejectMode {
if err := msgs.GetWarnings(); err != nil {
log.Info("Signing aborted due to warnings. In order to continue despite warnings, please use the flag '--advanced'.")
return nil, err
}
}
typedData := gnosisTx.ToTypedData()
// might aswell error early.
// we are expected to sign. If our calculated hash does not match what they want,
// The gnosis safetx input contains a 'safeTxHash' which is the expected safeTxHash that
sighash, _, err := apitypes.TypedDataAndHash(typedData)
if err != nil {
return nil, err
}
if !bytes.Equal(sighash, gnosisTx.InputExpHash.Bytes()) {
// It might be the case that the json is missing chain id.
if gnosisTx.ChainId == nil {
gnosisTx.ChainId = (*math.HexOrDecimal256)(api.chainID)
typedData = gnosisTx.ToTypedData()
sighash, _, _ = apitypes.TypedDataAndHash(typedData)
if !bytes.Equal(sighash, gnosisTx.InputExpHash.Bytes()) {
return nil, fmt.Errorf("mismatched safeTxHash; have %#x want %#x", sighash, gnosisTx.InputExpHash[:])
}
}
}
signature, preimage, err := api.signTypedData(ctx, signerAddress, typedData, msgs)
if err != nil {
return nil, err
}
checkSummedSender, _ := common.NewMixedcaseAddressFromString(signerAddress.Address().Hex())
gnosisTx.Signature = signature
gnosisTx.SafeTxHash = common.BytesToHash(preimage)
gnosisTx.Sender = *checkSummedSender // Must be checksummed to be accepted by relay
return &gnosisTx, nil
}
// Returns the external api version. This method does not require user acceptance. Available methods are
// available via enumeration anyway, and this info does not contain user-specific data
func (api *SignerAPI) Version(ctx context.Context) (string, error) {
return ExternalAPIVersion, nil
}

View file

@ -1,322 +0,0 @@
// Copyright 2018 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/>.
package core_test
import (
"bytes"
"context"
"fmt"
"math/big"
"os"
"path/filepath"
"testing"
"time"
"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/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/internal/ethapi"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/signer/core"
"github.com/ethereum/go-ethereum/signer/core/apitypes"
"github.com/ethereum/go-ethereum/signer/fourbyte"
"github.com/ethereum/go-ethereum/signer/storage"
)
// Used for testing
type headlessUi struct {
approveCh chan string // to send approve/deny
inputCh chan string // to send password
}
func (ui *headlessUi) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) {
input := <-ui.inputCh
return core.UserInputResponse{Text: input}, nil
}
func (ui *headlessUi) OnSignerStartup(info core.StartupInfo) {}
func (ui *headlessUi) RegisterUIServer(api *core.UIServerAPI) {}
func (ui *headlessUi) OnApprovedTx(tx ethapi.SignTransactionResult) {}
func (ui *headlessUi) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) {
switch <-ui.approveCh {
case "Y":
return core.SignTxResponse{request.Transaction, true}, nil
case "M": // modify
// The headless UI always modifies the transaction
old := big.Int(request.Transaction.Value)
newVal := new(big.Int).Add(&old, big.NewInt(1))
request.Transaction.Value = hexutil.Big(*newVal)
return core.SignTxResponse{request.Transaction, true}, nil
default:
return core.SignTxResponse{request.Transaction, false}, nil
}
}
func (ui *headlessUi) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) {
approved := (<-ui.approveCh == "Y")
return core.SignDataResponse{approved}, nil
}
func (ui *headlessUi) ApproveListing(request *core.ListRequest) (core.ListResponse, error) {
approval := <-ui.approveCh
//fmt.Printf("approval %s\n", approval)
switch approval {
case "A":
return core.ListResponse{request.Accounts}, nil
case "1":
l := make([]accounts.Account, 1)
l[0] = request.Accounts[1]
return core.ListResponse{l}, nil
default:
return core.ListResponse{nil}, nil
}
}
func (ui *headlessUi) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) {
if <-ui.approveCh == "Y" {
return core.NewAccountResponse{true}, nil
}
return core.NewAccountResponse{false}, nil
}
func (ui *headlessUi) ShowError(message string) {
//stdout is used by communication
fmt.Fprintln(os.Stderr, message)
}
func (ui *headlessUi) ShowInfo(message string) {
//stdout is used by communication
fmt.Fprintln(os.Stderr, message)
}
func tmpDirName(t *testing.T) string {
d := t.TempDir()
d, err := filepath.EvalSymlinks(d)
if err != nil {
t.Fatal(err)
}
return d
}
func setup(t *testing.T) (*core.SignerAPI, *headlessUi) {
db, err := fourbyte.New()
if err != nil {
t.Fatal(err.Error())
}
ui := &headlessUi{make(chan string, 20), make(chan string, 20)}
am := core.StartClefAccountManager(tmpDirName(t), true, true, "")
api := core.NewSignerAPI(am, 1337, true, ui, db, true, &storage.NoStorage{})
return api, ui
}
func createAccount(ui *headlessUi, api *core.SignerAPI, t *testing.T) {
ui.approveCh <- "Y"
ui.inputCh <- "a_long_password"
_, err := api.New(context.Background())
if err != nil {
t.Fatal(err)
}
// Some time to allow changes to propagate
time.Sleep(250 * time.Millisecond)
}
func failCreateAccountWithPassword(ui *headlessUi, api *core.SignerAPI, password string, t *testing.T) {
ui.approveCh <- "Y"
// We will be asked three times to provide a suitable password
ui.inputCh <- password
ui.inputCh <- password
ui.inputCh <- password
addr, err := api.New(context.Background())
if err == nil {
t.Fatal("Should have returned an error")
}
if addr != (common.Address{}) {
t.Fatal("Empty address should be returned")
}
}
func failCreateAccount(ui *headlessUi, api *core.SignerAPI, t *testing.T) {
ui.approveCh <- "N"
addr, err := api.New(context.Background())
if err != core.ErrRequestDenied {
t.Fatal(err)
}
if addr != (common.Address{}) {
t.Fatal("Empty address should be returned")
}
}
func list(ui *headlessUi, api *core.SignerAPI, t *testing.T) ([]common.Address, error) {
ui.approveCh <- "A"
return api.List(context.Background())
}
func TestNewAcc(t *testing.T) {
t.Parallel()
api, control := setup(t)
verifyNum := func(num int) {
list, err := list(control, api, t)
if err != nil {
t.Errorf("Unexpected error %v", err)
}
if len(list) != num {
t.Errorf("Expected %d accounts, got %d", num, len(list))
}
}
// Testing create and create-deny
createAccount(control, api, t)
createAccount(control, api, t)
failCreateAccount(control, api, t)
failCreateAccount(control, api, t)
createAccount(control, api, t)
failCreateAccount(control, api, t)
createAccount(control, api, t)
failCreateAccount(control, api, t)
verifyNum(4)
// Fail to create this, due to bad password
failCreateAccountWithPassword(control, api, "short", t)
failCreateAccountWithPassword(control, api, "longerbutbad\rfoo", t)
verifyNum(4)
// Testing listing:
// Listing one Account
control.approveCh <- "1"
list, err := api.List(context.Background())
if err != nil {
t.Fatal(err)
}
if len(list) != 1 {
t.Fatalf("List should only show one Account")
}
// Listing denied
control.approveCh <- "Nope"
list, err = api.List(context.Background())
if len(list) != 0 {
t.Fatalf("List should be empty")
}
if err != core.ErrRequestDenied {
t.Fatal("Expected deny")
}
}
func mkTestTx(from common.MixedcaseAddress) apitypes.SendTxArgs {
to := common.NewMixedcaseAddress(common.HexToAddress("0x1337"))
gas := hexutil.Uint64(21000)
gasPrice := (hexutil.Big)(*big.NewInt(2000000000))
value := (hexutil.Big)(*big.NewInt(1e18))
nonce := (hexutil.Uint64)(0)
data := hexutil.Bytes(common.Hex2Bytes("01020304050607080a"))
tx := apitypes.SendTxArgs{
From: from,
To: &to,
Gas: gas,
GasPrice: &gasPrice,
Value: value,
Data: &data,
Nonce: nonce}
return tx
}
func TestSignTx(t *testing.T) {
t.Parallel()
var (
list []common.Address
res, res2 *ethapi.SignTransactionResult
err error
)
api, control := setup(t)
createAccount(control, api, t)
control.approveCh <- "A"
list, err = api.List(context.Background())
if err != nil {
t.Fatal(err)
}
if len(list) == 0 {
t.Fatal("Unexpected empty list")
}
a := common.NewMixedcaseAddress(list[0])
methodSig := "test(uint)"
tx := mkTestTx(a)
control.approveCh <- "Y"
control.inputCh <- "wrongpassword"
res, err = api.SignTransaction(context.Background(), tx, &methodSig)
if res != nil {
t.Errorf("Expected nil-response, got %v", res)
}
if err != keystore.ErrDecrypt {
t.Errorf("Expected ErrLocked! %v", err)
}
control.approveCh <- "No way"
res, err = api.SignTransaction(context.Background(), tx, &methodSig)
if res != nil {
t.Errorf("Expected nil-response, got %v", res)
}
if err != core.ErrRequestDenied {
t.Errorf("Expected ErrRequestDenied! %v", err)
}
// Sign with correct password
control.approveCh <- "Y"
control.inputCh <- "a_long_password"
res, err = api.SignTransaction(context.Background(), tx, &methodSig)
if err != nil {
t.Fatal(err)
}
parsedTx := &types.Transaction{}
rlp.DecodeBytes(res.Raw, parsedTx)
//The tx should NOT be modified by the UI
if parsedTx.Value().Cmp(tx.Value.ToInt()) != 0 {
t.Errorf("Expected value to be unchanged, expected %v got %v", tx.Value, parsedTx.Value())
}
control.approveCh <- "Y"
control.inputCh <- "a_long_password"
res2, err = api.SignTransaction(context.Background(), tx, &methodSig)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(res.Raw, res2.Raw) {
t.Error("Expected tx to be unmodified by UI")
}
//The tx is modified by the UI
control.approveCh <- "M"
control.inputCh <- "a_long_password"
res2, err = api.SignTransaction(context.Background(), tx, &methodSig)
if err != nil {
t.Fatal(err)
}
parsedTx2 := &types.Transaction{}
rlp.DecodeBytes(res.Raw, parsedTx2)
//The tx should be modified by the UI
if parsedTx2.Value().Cmp(tx.Value.ToInt()) != 0 {
t.Errorf("Expected value to be unchanged, got %v", parsedTx.Value())
}
if bytes.Equal(res.Raw, res2.Raw) {
t.Error("Expected tx to be modified by UI")
}
}

View file

@ -1,242 +0,0 @@
// Copyright 2019 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/>.
package apitypes
import (
"bytes"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/common/math"
)
func TestBytesPadding(t *testing.T) {
t.Parallel()
tests := []struct {
Type string
Input []byte
Output []byte // nil => error
}{
{
// Fail on wrong length
Type: "bytes20",
Input: []byte{},
Output: nil,
},
{
Type: "bytes1",
Input: []byte{1},
Output: []byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
},
{
Type: "bytes1",
Input: []byte{1, 2},
Output: nil,
},
{
Type: "bytes7",
Input: []byte{1, 2, 3, 4, 5, 6, 7},
Output: []byte{1, 2, 3, 4, 5, 6, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
},
{
Type: "bytes32",
Input: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32},
Output: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32},
},
{
Type: "bytes32",
Input: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33},
Output: nil,
},
}
d := TypedData{}
for i, test := range tests {
val, err := d.EncodePrimitiveValue(test.Type, test.Input, 1)
if test.Output == nil {
if err == nil {
t.Errorf("test %d: expected error, got no error (result %x)", i, val)
}
} else {
if err != nil {
t.Errorf("test %d: expected no error, got %v", i, err)
}
if len(val) != 32 {
t.Errorf("test %d: expected len 32, got %d", i, len(val))
}
if !bytes.Equal(val, test.Output) {
t.Errorf("test %d: expected %x, got %x", i, test.Output, val)
}
}
}
}
func TestParseAddress(t *testing.T) {
t.Parallel()
tests := []struct {
Input interface{}
Output []byte // nil => error
}{
{
Input: [20]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14},
Output: common.FromHex("0x0000000000000000000000000102030405060708090A0B0C0D0E0F1011121314"),
},
{
Input: "0x0102030405060708090A0B0C0D0E0F1011121314",
Output: common.FromHex("0x0000000000000000000000000102030405060708090A0B0C0D0E0F1011121314"),
},
{
Input: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14},
Output: common.FromHex("0x0000000000000000000000000102030405060708090A0B0C0D0E0F1011121314"),
},
// Various error-cases:
{Input: "0x000102030405060708090A0B0C0D0E0F1011121314"}, // too long string
{Input: "0x01"}, // too short string
{Input: ""},
{Input: [32]byte{}}, // too long fixed-size array
{Input: [21]byte{}}, // too long fixed-size array
{Input: make([]byte, 19)}, // too short slice
{Input: make([]byte, 21)}, // too long slice
{Input: nil},
}
d := TypedData{}
for i, test := range tests {
val, err := d.EncodePrimitiveValue("address", test.Input, 1)
if test.Output == nil {
if err == nil {
t.Errorf("test %d: expected error, got no error (result %x)", i, val)
}
continue
}
if err != nil {
t.Errorf("test %d: expected no error, got %v", i, err)
}
if have, want := len(val), 32; have != want {
t.Errorf("test %d: have len %d, want %d", i, have, want)
}
if !bytes.Equal(val, test.Output) {
t.Errorf("test %d: want %x, have %x", i, test.Output, val)
}
}
}
func TestParseBytes(t *testing.T) {
t.Parallel()
for i, tt := range []struct {
v interface{}
exp []byte
}{
{"0x", []byte{}},
{"0x1234", []byte{0x12, 0x34}},
{[]byte{12, 34}, []byte{12, 34}},
{hexutil.Bytes([]byte{12, 34}), []byte{12, 34}},
{"1234", nil}, // not a proper hex-string
{"0x01233", nil}, // nibbles should be rejected
{"not a hex string", nil},
{15, nil},
{nil, nil},
{[2]byte{12, 34}, []byte{12, 34}},
{[8]byte{12, 34, 56, 78, 90, 12, 34, 56}, []byte{12, 34, 56, 78, 90, 12, 34, 56}},
{[16]byte{12, 34, 56, 78, 90, 12, 34, 56, 12, 34, 56, 78, 90, 12, 34, 56}, []byte{12, 34, 56, 78, 90, 12, 34, 56, 12, 34, 56, 78, 90, 12, 34, 56}},
} {
out, ok := parseBytes(tt.v)
if tt.exp == nil {
if ok || out != nil {
t.Errorf("test %d: expected !ok, got ok = %v with out = %x", i, ok, out)
}
continue
}
if !ok {
t.Errorf("test %d: expected ok got !ok", i)
}
if !bytes.Equal(out, tt.exp) {
t.Errorf("test %d: expected %x got %x", i, tt.exp, out)
}
}
}
func TestParseInteger(t *testing.T) {
t.Parallel()
for i, tt := range []struct {
t string
v interface{}
exp *big.Int
}{
{"uint32", "-123", nil},
{"int32", "-123", big.NewInt(-123)},
{"int32", big.NewInt(-124), big.NewInt(-124)},
{"uint32", "0xff", big.NewInt(0xff)},
{"int8", "0xffff", nil},
} {
res, err := parseInteger(tt.t, tt.v)
if tt.exp == nil && res == nil {
continue
}
if tt.exp == nil && res != nil {
t.Errorf("test %d, got %v, expected nil", i, res)
continue
}
if tt.exp != nil && res == nil {
t.Errorf("test %d, got '%v', expected %v", i, err, tt.exp)
continue
}
if tt.exp.Cmp(res) != 0 {
t.Errorf("test %d, got %v expected %v", i, res, tt.exp)
}
}
}
func TestConvertStringDataToSlice(t *testing.T) {
t.Parallel()
slice := []string{"a", "b", "c"}
var it interface{} = slice
_, err := convertDataToSlice(it)
if err != nil {
t.Fatal(err)
}
}
func TestConvertUint256DataToSlice(t *testing.T) {
t.Parallel()
slice := []*math.HexOrDecimal256{
math.NewHexOrDecimal256(1),
math.NewHexOrDecimal256(2),
math.NewHexOrDecimal256(3),
}
var it interface{} = slice
_, err := convertDataToSlice(it)
if err != nil {
t.Fatal(err)
}
}
func TestConvertAddressDataToSlice(t *testing.T) {
t.Parallel()
slice := []common.Address{
common.HexToAddress("0x0000000000000000000000000000000000000001"),
common.HexToAddress("0x0000000000000000000000000000000000000002"),
common.HexToAddress("0x0000000000000000000000000000000000000003"),
}
var it interface{} = slice
_, err := convertDataToSlice(it)
if err != nil {
t.Fatal(err)
}
}

View file

@ -1,836 +0,0 @@
// Copyright 2018 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/>.
package apitypes
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"math/big"
"reflect"
"regexp"
"sort"
"strconv"
"strings"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
)
var typedDataReferenceTypeRegexp = regexp.MustCompile(`^[A-Za-z](\w*)(\[\])?$`)
type ValidationInfo struct {
Typ string `json:"type"`
Message string `json:"message"`
}
type ValidationMessages struct {
Messages []ValidationInfo
}
const (
WARN = "WARNING"
CRIT = "CRITICAL"
INFO = "Info"
)
func (vs *ValidationMessages) Crit(msg string) {
vs.Messages = append(vs.Messages, ValidationInfo{CRIT, msg})
}
func (vs *ValidationMessages) Warn(msg string) {
vs.Messages = append(vs.Messages, ValidationInfo{WARN, msg})
}
func (vs *ValidationMessages) Info(msg string) {
vs.Messages = append(vs.Messages, ValidationInfo{INFO, msg})
}
// GetWarnings returns an error with all messages of type WARN of above, or nil if no warnings were present
func (v *ValidationMessages) GetWarnings() error {
var messages []string
for _, msg := range v.Messages {
if msg.Typ == WARN || msg.Typ == CRIT {
messages = append(messages, msg.Message)
}
}
if len(messages) > 0 {
return fmt.Errorf("validation failed: %s", strings.Join(messages, ","))
}
return nil
}
// SendTxArgs represents the arguments to submit a transaction
// This struct is identical to ethapi.TransactionArgs, except for the usage of
// common.MixedcaseAddress in From and To
type SendTxArgs struct {
From common.MixedcaseAddress `json:"from"`
To *common.MixedcaseAddress `json:"to"`
Gas hexutil.Uint64 `json:"gas"`
GasPrice *hexutil.Big `json:"gasPrice"`
MaxFeePerGas *hexutil.Big `json:"maxFeePerGas"`
MaxPriorityFeePerGas *hexutil.Big `json:"maxPriorityFeePerGas"`
Value hexutil.Big `json:"value"`
Nonce hexutil.Uint64 `json:"nonce"`
// We accept "data" and "input" for backwards-compatibility reasons.
// "input" is the newer name and should be preferred by clients.
// Issue detail: https://github.com/ethereum/go-ethereum/issues/15628
Data *hexutil.Bytes `json:"data"`
Input *hexutil.Bytes `json:"input,omitempty"`
// For non-legacy transactions
AccessList *types.AccessList `json:"accessList,omitempty"`
ChainID *hexutil.Big `json:"chainId,omitempty"`
}
func (args SendTxArgs) String() string {
s, err := json.Marshal(args)
if err == nil {
return string(s)
}
return err.Error()
}
// ToTransaction converts the arguments to a transaction.
func (args *SendTxArgs) ToTransaction() *types.Transaction {
// Add the To-field, if specified
var to *common.Address
if args.To != nil {
dstAddr := args.To.Address()
to = &dstAddr
}
var input []byte
if args.Input != nil {
input = *args.Input
} else if args.Data != nil {
input = *args.Data
}
var data types.TxData
switch {
case args.MaxFeePerGas != nil:
al := types.AccessList{}
if args.AccessList != nil {
al = *args.AccessList
}
data = &types.DynamicFeeTx{
To: to,
ChainID: (*big.Int)(args.ChainID),
Nonce: uint64(args.Nonce),
Gas: uint64(args.Gas),
GasFeeCap: (*big.Int)(args.MaxFeePerGas),
GasTipCap: (*big.Int)(args.MaxPriorityFeePerGas),
Value: (*big.Int)(&args.Value),
Data: input,
AccessList: al,
}
case args.AccessList != nil:
data = &types.AccessListTx{
To: to,
ChainID: (*big.Int)(args.ChainID),
Nonce: uint64(args.Nonce),
Gas: uint64(args.Gas),
GasPrice: (*big.Int)(args.GasPrice),
Value: (*big.Int)(&args.Value),
Data: input,
AccessList: *args.AccessList,
}
default:
data = &types.LegacyTx{
To: to,
Nonce: uint64(args.Nonce),
Gas: uint64(args.Gas),
GasPrice: (*big.Int)(args.GasPrice),
Value: (*big.Int)(&args.Value),
Data: input,
}
}
return types.NewTx(data)
}
type SigFormat struct {
Mime string
ByteVersion byte
}
var (
IntendedValidator = SigFormat{
accounts.MimetypeDataWithValidator,
0x00,
}
DataTyped = SigFormat{
accounts.MimetypeTypedData,
0x01,
}
ApplicationClique = SigFormat{
accounts.MimetypeClique,
0x02,
}
TextPlain = SigFormat{
accounts.MimetypeTextPlain,
0x45,
}
)
type ValidatorData struct {
Address common.Address
Message hexutil.Bytes
}
// TypedData is a type to encapsulate EIP-712 typed messages
type TypedData struct {
Types Types `json:"types"`
PrimaryType string `json:"primaryType"`
Domain TypedDataDomain `json:"domain"`
Message TypedDataMessage `json:"message"`
}
// Type is the inner type of an EIP-712 message
type Type struct {
Name string `json:"name"`
Type string `json:"type"`
}
func (t *Type) isArray() bool {
return strings.HasSuffix(t.Type, "[]")
}
// typeName returns the canonical name of the type. If the type is 'Person[]', then
// this method returns 'Person'
func (t *Type) typeName() string {
if strings.HasSuffix(t.Type, "[]") {
return strings.TrimSuffix(t.Type, "[]")
}
return t.Type
}
type Types map[string][]Type
type TypePriority struct {
Type string
Value uint
}
type TypedDataMessage = map[string]interface{}
// TypedDataDomain represents the domain part of an EIP-712 message.
type TypedDataDomain struct {
Name string `json:"name"`
Version string `json:"version"`
ChainId *math.HexOrDecimal256 `json:"chainId"`
VerifyingContract string `json:"verifyingContract"`
Salt string `json:"salt"`
}
// TypedDataAndHash is a helper function that calculates a hash for typed data conforming to EIP-712.
// This hash can then be safely used to calculate a signature.
//
// See https://eips.ethereum.org/EIPS/eip-712 for the full specification.
//
// This gives context to the signed typed data and prevents signing of transactions.
func TypedDataAndHash(typedData TypedData) ([]byte, string, error) {
domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map())
if err != nil {
return nil, "", err
}
typedDataHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message)
if err != nil {
return nil, "", err
}
rawData := fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(typedDataHash))
return crypto.Keccak256([]byte(rawData)), rawData, nil
}
// HashStruct generates a keccak256 hash of the encoding of the provided data
func (typedData *TypedData) HashStruct(primaryType string, data TypedDataMessage) (hexutil.Bytes, error) {
encodedData, err := typedData.EncodeData(primaryType, data, 1)
if err != nil {
return nil, err
}
return crypto.Keccak256(encodedData), nil
}
// Dependencies returns an array of custom types ordered by their hierarchical reference tree
func (typedData *TypedData) Dependencies(primaryType string, found []string) []string {
primaryType = strings.TrimSuffix(primaryType, "[]")
includes := func(arr []string, str string) bool {
for _, obj := range arr {
if obj == str {
return true
}
}
return false
}
if includes(found, primaryType) {
return found
}
if typedData.Types[primaryType] == nil {
return found
}
found = append(found, primaryType)
for _, field := range typedData.Types[primaryType] {
for _, dep := range typedData.Dependencies(field.Type, found) {
if !includes(found, dep) {
found = append(found, dep)
}
}
}
return found
}
// EncodeType generates the following encoding:
// `name ‖ "(" ‖ member₁ ‖ "," ‖ member₂ ‖ "," ‖ … ‖ memberₙ ")"`
//
// each member is written as `type ‖ " " ‖ name` encodings cascade down and are sorted by name
func (typedData *TypedData) EncodeType(primaryType string) hexutil.Bytes {
// Get dependencies primary first, then alphabetical
deps := typedData.Dependencies(primaryType, []string{})
if len(deps) > 0 {
slicedDeps := deps[1:]
sort.Strings(slicedDeps)
deps = append([]string{primaryType}, slicedDeps...)
}
// Format as a string with fields
var buffer bytes.Buffer
for _, dep := range deps {
buffer.WriteString(dep)
buffer.WriteString("(")
for _, obj := range typedData.Types[dep] {
buffer.WriteString(obj.Type)
buffer.WriteString(" ")
buffer.WriteString(obj.Name)
buffer.WriteString(",")
}
buffer.Truncate(buffer.Len() - 1)
buffer.WriteString(")")
}
return buffer.Bytes()
}
// TypeHash creates the keccak256 hash of the data
func (typedData *TypedData) TypeHash(primaryType string) hexutil.Bytes {
return crypto.Keccak256(typedData.EncodeType(primaryType))
}
// EncodeData generates the following encoding:
// `enc(value₁) ‖ enc(value₂) ‖ … ‖ enc(valueₙ)`
//
// each encoded member is 32-byte long
func (typedData *TypedData) EncodeData(primaryType string, data map[string]interface{}, depth int) (hexutil.Bytes, error) {
if err := typedData.validate(); err != nil {
return nil, err
}
buffer := bytes.Buffer{}
// Verify extra data
if exp, got := len(typedData.Types[primaryType]), len(data); exp < got {
return nil, fmt.Errorf("there is extra data provided in the message (%d < %d)", exp, got)
}
// Add typehash
buffer.Write(typedData.TypeHash(primaryType))
// Add field contents. Structs and arrays have special handlers.
for _, field := range typedData.Types[primaryType] {
encType := field.Type
encValue := data[field.Name]
if encType[len(encType)-1:] == "]" {
arrayValue, err := convertDataToSlice(encValue)
if err != nil {
return nil, dataMismatchError(encType, encValue)
}
arrayBuffer := bytes.Buffer{}
parsedType := strings.Split(encType, "[")[0]
for _, item := range arrayValue {
if typedData.Types[parsedType] != nil {
mapValue, ok := item.(map[string]interface{})
if !ok {
return nil, dataMismatchError(parsedType, item)
}
encodedData, err := typedData.EncodeData(parsedType, mapValue, depth+1)
if err != nil {
return nil, err
}
arrayBuffer.Write(crypto.Keccak256(encodedData))
} else {
bytesValue, err := typedData.EncodePrimitiveValue(parsedType, item, depth)
if err != nil {
return nil, err
}
arrayBuffer.Write(bytesValue)
}
}
buffer.Write(crypto.Keccak256(arrayBuffer.Bytes()))
} else if typedData.Types[field.Type] != nil {
mapValue, ok := encValue.(map[string]interface{})
if !ok {
return nil, dataMismatchError(encType, encValue)
}
encodedData, err := typedData.EncodeData(field.Type, mapValue, depth+1)
if err != nil {
return nil, err
}
buffer.Write(crypto.Keccak256(encodedData))
} else {
byteValue, err := typedData.EncodePrimitiveValue(encType, encValue, depth)
if err != nil {
return nil, err
}
buffer.Write(byteValue)
}
}
return buffer.Bytes(), nil
}
// Attempt to parse bytes in different formats: byte array, hex string, hexutil.Bytes.
func parseBytes(encType interface{}) ([]byte, bool) {
// Handle array types.
val := reflect.ValueOf(encType)
if val.Kind() == reflect.Array && val.Type().Elem().Kind() == reflect.Uint8 {
v := reflect.MakeSlice(reflect.TypeOf([]byte{}), val.Len(), val.Len())
reflect.Copy(v, val)
return v.Bytes(), true
}
switch v := encType.(type) {
case []byte:
return v, true
case hexutil.Bytes:
return v, true
case string:
bytes, err := hexutil.Decode(v)
if err != nil {
return nil, false
}
return bytes, true
default:
return nil, false
}
}
func parseInteger(encType string, encValue interface{}) (*big.Int, error) {
var (
length int
signed = strings.HasPrefix(encType, "int")
b *big.Int
)
if encType == "int" || encType == "uint" {
length = 256
} else {
lengthStr := ""
if strings.HasPrefix(encType, "uint") {
lengthStr = strings.TrimPrefix(encType, "uint")
} else {
lengthStr = strings.TrimPrefix(encType, "int")
}
atoiSize, err := strconv.Atoi(lengthStr)
if err != nil {
return nil, fmt.Errorf("invalid size on integer: %v", lengthStr)
}
length = atoiSize
}
switch v := encValue.(type) {
case *math.HexOrDecimal256:
b = (*big.Int)(v)
case *big.Int:
b = v
case string:
var hexIntValue math.HexOrDecimal256
if err := hexIntValue.UnmarshalText([]byte(v)); err != nil {
return nil, err
}
b = (*big.Int)(&hexIntValue)
case float64:
// JSON parses non-strings as float64. Fail if we cannot
// convert it losslessly
if float64(int64(v)) == v {
b = big.NewInt(int64(v))
} else {
return nil, fmt.Errorf("invalid float value %v for type %v", v, encType)
}
}
if b == nil {
return nil, fmt.Errorf("invalid integer value %v/%v for type %v", encValue, reflect.TypeOf(encValue), encType)
}
if b.BitLen() > length {
return nil, fmt.Errorf("integer larger than '%v'", encType)
}
if !signed && b.Sign() == -1 {
return nil, fmt.Errorf("invalid negative value for unsigned type %v", encType)
}
return b, nil
}
// EncodePrimitiveValue deals with the primitive values found
// while searching through the typed data
func (typedData *TypedData) EncodePrimitiveValue(encType string, encValue interface{}, depth int) ([]byte, error) {
switch encType {
case "address":
retval := make([]byte, 32)
switch val := encValue.(type) {
case string:
if common.IsHexAddress(val) {
copy(retval[12:], common.HexToAddress(val).Bytes())
return retval, nil
}
case []byte:
if len(val) == 20 {
copy(retval[12:], val)
return retval, nil
}
case [20]byte:
copy(retval[12:], val[:])
return retval, nil
}
return nil, dataMismatchError(encType, encValue)
case "bool":
boolValue, ok := encValue.(bool)
if !ok {
return nil, dataMismatchError(encType, encValue)
}
if boolValue {
return math.PaddedBigBytes(common.Big1, 32), nil
}
return math.PaddedBigBytes(common.Big0, 32), nil
case "string":
strVal, ok := encValue.(string)
if !ok {
return nil, dataMismatchError(encType, encValue)
}
return crypto.Keccak256([]byte(strVal)), nil
case "bytes":
bytesValue, ok := parseBytes(encValue)
if !ok {
return nil, dataMismatchError(encType, encValue)
}
return crypto.Keccak256(bytesValue), nil
}
if strings.HasPrefix(encType, "bytes") {
lengthStr := strings.TrimPrefix(encType, "bytes")
length, err := strconv.Atoi(lengthStr)
if err != nil {
return nil, fmt.Errorf("invalid size on bytes: %v", lengthStr)
}
if length < 0 || length > 32 {
return nil, fmt.Errorf("invalid size on bytes: %d", length)
}
if byteValue, ok := parseBytes(encValue); !ok || len(byteValue) != length {
return nil, dataMismatchError(encType, encValue)
} else {
// Right-pad the bits
dst := make([]byte, 32)
copy(dst, byteValue)
return dst, nil
}
}
if strings.HasPrefix(encType, "int") || strings.HasPrefix(encType, "uint") {
b, err := parseInteger(encType, encValue)
if err != nil {
return nil, err
}
return math.U256Bytes(b), nil
}
return nil, fmt.Errorf("unrecognized type '%s'", encType)
}
// dataMismatchError generates an error for a mismatch between
// the provided type and data
func dataMismatchError(encType string, encValue interface{}) error {
return fmt.Errorf("provided data '%v' doesn't match type '%s'", encValue, encType)
}
func convertDataToSlice(encValue interface{}) ([]interface{}, error) {
var outEncValue []interface{}
rv := reflect.ValueOf(encValue)
if rv.Kind() == reflect.Slice {
for i := 0; i < rv.Len(); i++ {
outEncValue = append(outEncValue, rv.Index(i).Interface())
}
} else {
return outEncValue, fmt.Errorf("provided data '%v' is not slice", encValue)
}
return outEncValue, nil
}
// validate makes sure the types are sound
func (typedData *TypedData) validate() error {
if err := typedData.Types.validate(); err != nil {
return err
}
if err := typedData.Domain.validate(); err != nil {
return err
}
return nil
}
// Map generates a map version of the typed data
func (typedData *TypedData) Map() map[string]interface{} {
dataMap := map[string]interface{}{
"types": typedData.Types,
"domain": typedData.Domain.Map(),
"primaryType": typedData.PrimaryType,
"message": typedData.Message,
}
return dataMap
}
// Format returns a representation of typedData, which can be easily displayed by a user-interface
// without in-depth knowledge about 712 rules
func (typedData *TypedData) Format() ([]*NameValueType, error) {
domain, err := typedData.formatData("EIP712Domain", typedData.Domain.Map())
if err != nil {
return nil, err
}
ptype, err := typedData.formatData(typedData.PrimaryType, typedData.Message)
if err != nil {
return nil, err
}
var nvts []*NameValueType
nvts = append(nvts, &NameValueType{
Name: "EIP712Domain",
Value: domain,
Typ: "domain",
})
nvts = append(nvts, &NameValueType{
Name: typedData.PrimaryType,
Value: ptype,
Typ: "primary type",
})
return nvts, nil
}
func (typedData *TypedData) formatData(primaryType string, data map[string]interface{}) ([]*NameValueType, error) {
var output []*NameValueType
// Add field contents. Structs and arrays have special handlers.
for _, field := range typedData.Types[primaryType] {
encName := field.Name
encValue := data[encName]
item := &NameValueType{
Name: encName,
Typ: field.Type,
}
if field.isArray() {
arrayValue, _ := convertDataToSlice(encValue)
parsedType := field.typeName()
for _, v := range arrayValue {
if typedData.Types[parsedType] != nil {
mapValue, _ := v.(map[string]interface{})
mapOutput, err := typedData.formatData(parsedType, mapValue)
if err != nil {
return nil, err
}
item.Value = mapOutput
} else {
primitiveOutput, err := formatPrimitiveValue(field.Type, encValue)
if err != nil {
return nil, err
}
item.Value = primitiveOutput
}
}
} else if typedData.Types[field.Type] != nil {
if mapValue, ok := encValue.(map[string]interface{}); ok {
mapOutput, err := typedData.formatData(field.Type, mapValue)
if err != nil {
return nil, err
}
item.Value = mapOutput
} else {
item.Value = "<nil>"
}
} else {
primitiveOutput, err := formatPrimitiveValue(field.Type, encValue)
if err != nil {
return nil, err
}
item.Value = primitiveOutput
}
output = append(output, item)
}
return output, nil
}
func formatPrimitiveValue(encType string, encValue interface{}) (string, error) {
switch encType {
case "address":
if stringValue, ok := encValue.(string); !ok {
return "", fmt.Errorf("could not format value %v as address", encValue)
} else {
return common.HexToAddress(stringValue).String(), nil
}
case "bool":
if boolValue, ok := encValue.(bool); !ok {
return "", fmt.Errorf("could not format value %v as bool", encValue)
} else {
return fmt.Sprintf("%t", boolValue), nil
}
case "bytes", "string":
return fmt.Sprintf("%s", encValue), nil
}
if strings.HasPrefix(encType, "bytes") {
return fmt.Sprintf("%s", encValue), nil
}
if strings.HasPrefix(encType, "uint") || strings.HasPrefix(encType, "int") {
if b, err := parseInteger(encType, encValue); err != nil {
return "", err
} else {
return fmt.Sprintf("%d (%#x)", b, b), nil
}
}
return "", fmt.Errorf("unhandled type %v", encType)
}
// Validate checks if the types object is conformant to the specs
func (t Types) validate() error {
for typeKey, typeArr := range t {
if len(typeKey) == 0 {
return fmt.Errorf("empty type key")
}
for i, typeObj := range typeArr {
if len(typeObj.Type) == 0 {
return fmt.Errorf("type %q:%d: empty Type", typeKey, i)
}
if len(typeObj.Name) == 0 {
return fmt.Errorf("type %q:%d: empty Name", typeKey, i)
}
if typeKey == typeObj.Type {
return fmt.Errorf("type %q cannot reference itself", typeObj.Type)
}
if isPrimitiveTypeValid(typeObj.Type) {
continue
}
// Must be reference type
if _, exist := t[typeObj.typeName()]; !exist {
return fmt.Errorf("reference type %q is undefined", typeObj.Type)
}
if !typedDataReferenceTypeRegexp.MatchString(typeObj.Type) {
return fmt.Errorf("unknown reference type %q", typeObj.Type)
}
}
}
return nil
}
// Checks if the primitive value is valid
func isPrimitiveTypeValid(primitiveType string) bool {
if primitiveType == "address" ||
primitiveType == "address[]" ||
primitiveType == "bool" ||
primitiveType == "bool[]" ||
primitiveType == "string" ||
primitiveType == "string[]" ||
primitiveType == "bytes" ||
primitiveType == "bytes[]" ||
primitiveType == "int" ||
primitiveType == "int[]" ||
primitiveType == "uint" ||
primitiveType == "uint[]" {
return true
}
// For 'bytesN', 'bytesN[]', we allow N from 1 to 32
for n := 1; n <= 32; n++ {
// e.g. 'bytes28' or 'bytes28[]'
if primitiveType == fmt.Sprintf("bytes%d", n) || primitiveType == fmt.Sprintf("bytes%d[]", n) {
return true
}
}
// For 'intN','intN[]' and 'uintN','uintN[]' we allow N in increments of 8, from 8 up to 256
for n := 8; n <= 256; n += 8 {
if primitiveType == fmt.Sprintf("int%d", n) || primitiveType == fmt.Sprintf("int%d[]", n) {
return true
}
if primitiveType == fmt.Sprintf("uint%d", n) || primitiveType == fmt.Sprintf("uint%d[]", n) {
return true
}
}
return false
}
// validate checks if the given domain is valid, i.e. contains at least
// the minimum viable keys and values
func (domain *TypedDataDomain) validate() error {
if domain.ChainId == nil && len(domain.Name) == 0 && len(domain.Version) == 0 && len(domain.VerifyingContract) == 0 && len(domain.Salt) == 0 {
return errors.New("domain is undefined")
}
return nil
}
// Map is a helper function to generate a map version of the domain
func (domain *TypedDataDomain) Map() map[string]interface{} {
dataMap := map[string]interface{}{}
if domain.ChainId != nil {
dataMap["chainId"] = domain.ChainId
}
if len(domain.Name) > 0 {
dataMap["name"] = domain.Name
}
if len(domain.Version) > 0 {
dataMap["version"] = domain.Version
}
if len(domain.VerifyingContract) > 0 {
dataMap["verifyingContract"] = domain.VerifyingContract
}
if len(domain.Salt) > 0 {
dataMap["salt"] = domain.Salt
}
return dataMap
}
// NameValueType is a very simple struct with Name, Value and Type. It's meant for simple
// json structures used to communicate signing-info about typed data with the UI
type NameValueType struct {
Name string `json:"name"`
Value interface{} `json:"value"`
Typ string `json:"type"`
}
// Pprint returns a pretty-printed version of nvt
func (nvt *NameValueType) Pprint(depth int) string {
output := bytes.Buffer{}
output.WriteString(strings.Repeat("\u00a0", depth*2))
output.WriteString(fmt.Sprintf("%s [%s]: ", nvt.Name, nvt.Typ))
if nvts, ok := nvt.Value.([]*NameValueType); ok {
output.WriteString("\n")
for _, next := range nvts {
sublevel := next.Pprint(depth + 1)
output.WriteString(sublevel)
}
} else {
if nvt.Value != nil {
output.WriteString(fmt.Sprintf("%q\n", nvt.Value))
} else {
output.WriteString("\n")
}
}
return output.String()
}

View file

@ -1,41 +0,0 @@
// Copyright 2023 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/>.
package apitypes
import "testing"
func TestIsPrimitive(t *testing.T) {
t.Parallel()
// Expected positives
for i, tc := range []string{
"int24", "int24[]", "uint88", "uint88[]", "uint", "uint[]", "int256", "int256[]",
"uint96", "uint96[]", "int96", "int96[]", "bytes17[]", "bytes17",
} {
if !isPrimitiveTypeValid(tc) {
t.Errorf("test %d: expected '%v' to be a valid primitive", i, tc)
}
}
// Expected negatives
for i, tc := range []string{
"int257", "int257[]", "uint88 ", "uint88 []", "uint257", "uint-1[]",
"uint0", "uint0[]", "int95", "int95[]", "uint1", "uint1[]", "bytes33[]", "bytess",
} {
if isPrimitiveTypeValid(tc) {
t.Errorf("test %d: expected '%v' to not be a valid primitive", i, tc)
}
}
}

View file

@ -1,127 +0,0 @@
// Copyright 2018 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/>.
package core
import (
"context"
"encoding/json"
"os"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/internal/ethapi"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/signer/core/apitypes"
"golang.org/x/exp/slog"
)
type AuditLogger struct {
log log.Logger
api ExternalAPI
}
func (l *AuditLogger) List(ctx context.Context) ([]common.Address, error) {
l.log.Info("List", "type", "request", "metadata", MetadataFromContext(ctx).String())
res, e := l.api.List(ctx)
l.log.Info("List", "type", "response", "data", res)
return res, e
}
func (l *AuditLogger) New(ctx context.Context) (common.Address, error) {
return l.api.New(ctx)
}
func (l *AuditLogger) SignTransaction(ctx context.Context, args apitypes.SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error) {
sel := "<nil>"
if methodSelector != nil {
sel = *methodSelector
}
l.log.Info("SignTransaction", "type", "request", "metadata", MetadataFromContext(ctx).String(),
"tx", args.String(),
"methodSelector", sel)
res, e := l.api.SignTransaction(ctx, args, methodSelector)
if res != nil {
l.log.Info("SignTransaction", "type", "response", "data", common.Bytes2Hex(res.Raw), "error", e)
} else {
l.log.Info("SignTransaction", "type", "response", "data", res, "error", e)
}
return res, e
}
func (l *AuditLogger) SignData(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (hexutil.Bytes, error) {
marshalledData, _ := json.Marshal(data) // can ignore error, marshalling what we just unmarshalled
l.log.Info("SignData", "type", "request", "metadata", MetadataFromContext(ctx).String(),
"addr", addr.String(), "data", marshalledData, "content-type", contentType)
b, e := l.api.SignData(ctx, contentType, addr, data)
l.log.Info("SignData", "type", "response", "data", common.Bytes2Hex(b), "error", e)
return b, e
}
func (l *AuditLogger) SignGnosisSafeTx(ctx context.Context, addr common.MixedcaseAddress, gnosisTx GnosisSafeTx, methodSelector *string) (*GnosisSafeTx, error) {
sel := "<nil>"
if methodSelector != nil {
sel = *methodSelector
}
data, _ := json.Marshal(gnosisTx) // can ignore error, marshalling what we just unmarshalled
l.log.Info("SignGnosisSafeTx", "type", "request", "metadata", MetadataFromContext(ctx).String(),
"addr", addr.String(), "data", string(data), "selector", sel)
res, e := l.api.SignGnosisSafeTx(ctx, addr, gnosisTx, methodSelector)
if res != nil {
data, _ := json.Marshal(res) // can ignore error, marshalling what we just unmarshalled
l.log.Info("SignGnosisSafeTx", "type", "response", "data", string(data), "error", e)
} else {
l.log.Info("SignGnosisSafeTx", "type", "response", "data", res, "error", e)
}
return res, e
}
func (l *AuditLogger) SignTypedData(ctx context.Context, addr common.MixedcaseAddress, data apitypes.TypedData) (hexutil.Bytes, error) {
l.log.Info("SignTypedData", "type", "request", "metadata", MetadataFromContext(ctx).String(),
"addr", addr.String(), "data", data)
b, e := l.api.SignTypedData(ctx, addr, data)
l.log.Info("SignTypedData", "type", "response", "data", common.Bytes2Hex(b), "error", e)
return b, e
}
func (l *AuditLogger) EcRecover(ctx context.Context, data hexutil.Bytes, sig hexutil.Bytes) (common.Address, error) {
l.log.Info("EcRecover", "type", "request", "metadata", MetadataFromContext(ctx).String(),
"data", common.Bytes2Hex(data), "sig", common.Bytes2Hex(sig))
b, e := l.api.EcRecover(ctx, data, sig)
l.log.Info("EcRecover", "type", "response", "address", b.String(), "error", e)
return b, e
}
func (l *AuditLogger) Version(ctx context.Context) (string, error) {
l.log.Info("Version", "type", "request", "metadata", MetadataFromContext(ctx).String())
data, err := l.api.Version(ctx)
l.log.Info("Version", "type", "response", "data", data, "error", err)
return data, err
}
func NewAuditLogger(path string, api ExternalAPI) (*AuditLogger, error) {
f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return nil, err
}
handler := slog.NewTextHandler(f, nil)
l := log.NewLogger(handler).With("api", "signer")
l.Info("Configured", "audit log", path)
return &AuditLogger{l, api}, nil
}

View file

@ -1,275 +0,0 @@
// Copyright 2018 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/>.
package core
import (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
"strings"
"sync"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/console/prompt"
"github.com/ethereum/go-ethereum/internal/ethapi"
"github.com/ethereum/go-ethereum/log"
)
type CommandlineUI struct {
in *bufio.Reader
mu sync.Mutex
api *UIServerAPI
}
func NewCommandlineUI() *CommandlineUI {
return &CommandlineUI{in: bufio.NewReader(os.Stdin)}
}
func (ui *CommandlineUI) RegisterUIServer(api *UIServerAPI) {
ui.api = api
}
// readString reads a single line from stdin, trimming if from spaces, enforcing
// non-emptyness.
func (ui *CommandlineUI) readString() string {
for {
fmt.Printf("> ")
text, err := ui.in.ReadString('\n')
if err != nil {
log.Crit("Failed to read user input", "err", err)
}
if text = strings.TrimSpace(text); text != "" {
return text
}
}
}
func (ui *CommandlineUI) OnInputRequired(info UserInputRequest) (UserInputResponse, error) {
fmt.Printf("## %s\n\n%s\n", info.Title, info.Prompt)
defer fmt.Println("-----------------------")
if info.IsPassword {
text, err := prompt.Stdin.PromptPassword("> ")
if err != nil {
log.Error("Failed to read password", "error", err)
return UserInputResponse{}, err
}
return UserInputResponse{text}, nil
}
text := ui.readString()
return UserInputResponse{text}, nil
}
// confirm returns true if user enters 'Yes', otherwise false
func (ui *CommandlineUI) confirm() bool {
fmt.Printf("Approve? [y/N]:\n")
if ui.readString() == "y" {
return true
}
fmt.Println("-----------------------")
return false
}
// sanitize quotes and truncates 'txt' if longer than 'limit'. If truncated,
// and ellipsis is added after the quoted string
func sanitize(txt string, limit int) string {
if len(txt) > limit {
return fmt.Sprintf("%q...", txt[:limit])
}
return fmt.Sprintf("%q", txt)
}
func showMetadata(metadata Metadata) {
fmt.Printf("Request context:\n\t%v -> %v -> %v\n", metadata.Remote, metadata.Scheme, metadata.Local)
fmt.Printf("\nAdditional HTTP header data, provided by the external caller:\n")
fmt.Printf("\tUser-Agent: %v\n\tOrigin: %v\n", sanitize(metadata.UserAgent, 200), sanitize(metadata.Origin, 100))
}
// ApproveTx prompt the user for confirmation to request to sign Transaction
func (ui *CommandlineUI) ApproveTx(request *SignTxRequest) (SignTxResponse, error) {
ui.mu.Lock()
defer ui.mu.Unlock()
weival := request.Transaction.Value.ToInt()
fmt.Printf("--------- Transaction request-------------\n")
if to := request.Transaction.To; to != nil {
fmt.Printf("to: %v\n", to.Original())
if !to.ValidChecksum() {
fmt.Printf("\nWARNING: Invalid checksum on to-address!\n\n")
}
} else {
fmt.Printf("to: <contact creation>\n")
}
fmt.Printf("from: %v\n", request.Transaction.From.String())
fmt.Printf("value: %v wei\n", weival)
fmt.Printf("gas: %v (%v)\n", request.Transaction.Gas, uint64(request.Transaction.Gas))
if request.Transaction.MaxFeePerGas != nil {
fmt.Printf("maxFeePerGas: %v wei\n", request.Transaction.MaxFeePerGas.ToInt())
fmt.Printf("maxPriorityFeePerGas: %v wei\n", request.Transaction.MaxPriorityFeePerGas.ToInt())
} else {
fmt.Printf("gasprice: %v wei\n", request.Transaction.GasPrice.ToInt())
}
fmt.Printf("nonce: %v (%v)\n", request.Transaction.Nonce, uint64(request.Transaction.Nonce))
if chainId := request.Transaction.ChainID; chainId != nil {
fmt.Printf("chainid: %v\n", chainId)
}
if list := request.Transaction.AccessList; list != nil {
fmt.Printf("Accesslist\n")
for i, el := range *list {
fmt.Printf(" %d. %v\n", i, el.Address)
for j, slot := range el.StorageKeys {
fmt.Printf(" %d. %v\n", j, slot)
}
}
}
if request.Transaction.Data != nil {
d := *request.Transaction.Data
if len(d) > 0 {
fmt.Printf("data: %v\n", hexutil.Encode(d))
}
}
if request.Callinfo != nil {
fmt.Printf("\nTransaction validation:\n")
for _, m := range request.Callinfo {
fmt.Printf(" * %s : %s\n", m.Typ, m.Message)
}
fmt.Println()
}
fmt.Printf("\n")
showMetadata(request.Meta)
fmt.Printf("-------------------------------------------\n")
if !ui.confirm() {
return SignTxResponse{request.Transaction, false}, nil
}
return SignTxResponse{request.Transaction, true}, nil
}
// ApproveSignData prompt the user for confirmation to request to sign data
func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest) (SignDataResponse, error) {
ui.mu.Lock()
defer ui.mu.Unlock()
fmt.Printf("-------- Sign data request--------------\n")
fmt.Printf("Account: %s\n", request.Address.String())
if len(request.Callinfo) != 0 {
fmt.Printf("\nValidation messages:\n")
for _, m := range request.Callinfo {
fmt.Printf(" * %s : %s\n", m.Typ, m.Message)
}
fmt.Println()
}
fmt.Printf("messages:\n")
for _, nvt := range request.Messages {
fmt.Printf("\u00a0\u00a0%v\n", strings.TrimSpace(nvt.Pprint(1)))
}
fmt.Printf("raw data: \n\t%q\n", request.Rawdata)
fmt.Printf("data hash: %v\n", request.Hash)
fmt.Printf("-------------------------------------------\n")
showMetadata(request.Meta)
if !ui.confirm() {
return SignDataResponse{false}, nil
}
return SignDataResponse{true}, nil
}
// ApproveListing prompt the user for confirmation to list accounts
// the list of accounts to list can be modified by the UI
func (ui *CommandlineUI) ApproveListing(request *ListRequest) (ListResponse, error) {
ui.mu.Lock()
defer ui.mu.Unlock()
fmt.Printf("-------- List Account request--------------\n")
fmt.Printf("A request has been made to list all accounts. \n")
fmt.Printf("You can select which accounts the caller can see\n")
for _, account := range request.Accounts {
fmt.Printf(" [x] %v\n", account.Address.Hex())
fmt.Printf(" URL: %v\n", account.URL)
}
fmt.Printf("-------------------------------------------\n")
showMetadata(request.Meta)
if !ui.confirm() {
return ListResponse{nil}, nil
}
return ListResponse{request.Accounts}, nil
}
// ApproveNewAccount prompt the user for confirmation to create new Account, and reveal to caller
func (ui *CommandlineUI) ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) {
ui.mu.Lock()
defer ui.mu.Unlock()
fmt.Printf("-------- New Account request--------------\n\n")
fmt.Printf("A request has been made to create a new account. \n")
fmt.Printf("Approving this operation means that a new account is created,\n")
fmt.Printf("and the address is returned to the external caller\n\n")
showMetadata(request.Meta)
if !ui.confirm() {
return NewAccountResponse{false}, nil
}
return NewAccountResponse{true}, nil
}
// ShowError displays error message to user
func (ui *CommandlineUI) ShowError(message string) {
fmt.Printf("## Error \n%s\n", message)
fmt.Printf("-------------------------------------------\n")
}
// ShowInfo displays info message to user
func (ui *CommandlineUI) ShowInfo(message string) {
fmt.Printf("## Info \n%s\n", message)
}
func (ui *CommandlineUI) OnApprovedTx(tx ethapi.SignTransactionResult) {
fmt.Printf("Transaction signed:\n ")
if jsn, err := json.MarshalIndent(tx.Tx, " ", " "); err != nil {
fmt.Printf("WARN: marshalling error %v\n", err)
} else {
fmt.Println(string(jsn))
}
}
func (ui *CommandlineUI) showAccounts() {
accounts, err := ui.api.ListAccounts(context.Background())
if err != nil {
log.Error("Error listing accounts", "err", err)
return
}
if len(accounts) == 0 {
fmt.Print("No accounts found\n")
return
}
var msg string
var out = new(strings.Builder)
if limit := 20; len(accounts) > limit {
msg = fmt.Sprintf("\nFirst %d accounts listed (%d more available).\n", limit, len(accounts)-limit)
accounts = accounts[:limit]
}
fmt.Fprint(out, "\n------- Available accounts -------\n")
for i, account := range accounts {
fmt.Fprintf(out, "%d. %s at %s\n", i, account.Address, account.URL)
}
fmt.Print(out.String(), msg)
}
func (ui *CommandlineUI) OnSignerStartup(info StartupInfo) {
fmt.Print("\n------- Signer info -------\n")
for k, v := range info.Info {
fmt.Printf("* %v : %v\n", k, v)
}
go ui.showAccounts()
}

View file

@ -1,117 +0,0 @@
// Copyright 2020 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/>.
package core
import (
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/signer/core/apitypes"
)
// GnosisSafeTx is a type to parse the safe-tx returned by the relayer,
// it also conforms to the API required by the Gnosis Safe tx relay service.
// See 'SafeMultisigTransaction' on https://safe-transaction.mainnet.gnosis.io/
type GnosisSafeTx struct {
// These fields are only used on output
Signature hexutil.Bytes `json:"signature"`
SafeTxHash common.Hash `json:"contractTransactionHash"`
Sender common.MixedcaseAddress `json:"sender"`
// These fields are used both on input and output
Safe common.MixedcaseAddress `json:"safe"`
To common.MixedcaseAddress `json:"to"`
Value math.Decimal256 `json:"value"`
GasPrice math.Decimal256 `json:"gasPrice"`
Data *hexutil.Bytes `json:"data"`
Operation uint8 `json:"operation"`
GasToken common.Address `json:"gasToken"`
RefundReceiver common.Address `json:"refundReceiver"`
BaseGas big.Int `json:"baseGas"`
SafeTxGas big.Int `json:"safeTxGas"`
Nonce big.Int `json:"nonce"`
InputExpHash common.Hash `json:"safeTxHash"`
ChainId *math.HexOrDecimal256 `json:"chainId,omitempty"`
}
// ToTypedData converts the tx to a EIP-712 Typed Data structure for signing
func (tx *GnosisSafeTx) ToTypedData() apitypes.TypedData {
var data hexutil.Bytes
if tx.Data != nil {
data = *tx.Data
}
var domainType = []apitypes.Type{{Name: "verifyingContract", Type: "address"}}
if tx.ChainId != nil {
domainType = append([]apitypes.Type{{Name: "chainId", Type: "uint256"}}, domainType[0])
}
gnosisTypedData := apitypes.TypedData{
Types: apitypes.Types{
"EIP712Domain": domainType,
"SafeTx": []apitypes.Type{
{Name: "to", Type: "address"},
{Name: "value", Type: "uint256"},
{Name: "data", Type: "bytes"},
{Name: "operation", Type: "uint8"},
{Name: "safeTxGas", Type: "uint256"},
{Name: "baseGas", Type: "uint256"},
{Name: "gasPrice", Type: "uint256"},
{Name: "gasToken", Type: "address"},
{Name: "refundReceiver", Type: "address"},
{Name: "nonce", Type: "uint256"},
},
},
Domain: apitypes.TypedDataDomain{
VerifyingContract: tx.Safe.Address().Hex(),
ChainId: tx.ChainId,
},
PrimaryType: "SafeTx",
Message: apitypes.TypedDataMessage{
"to": tx.To.Address().Hex(),
"value": tx.Value.String(),
"data": data,
"operation": fmt.Sprintf("%d", tx.Operation),
"safeTxGas": fmt.Sprintf("%#d", &tx.SafeTxGas),
"baseGas": fmt.Sprintf("%#d", &tx.BaseGas),
"gasPrice": tx.GasPrice.String(),
"gasToken": tx.GasToken.Hex(),
"refundReceiver": tx.RefundReceiver.Hex(),
"nonce": fmt.Sprintf("%d", tx.Nonce.Uint64()),
},
}
return gnosisTypedData
}
// ArgsForValidation returns a SendTxArgs struct, which can be used for the
// common validations, e.g. look up 4byte destinations
func (tx *GnosisSafeTx) ArgsForValidation() *apitypes.SendTxArgs {
gp := hexutil.Big(tx.GasPrice)
args := &apitypes.SendTxArgs{
From: tx.Safe,
To: &tx.To,
Gas: hexutil.Uint64(tx.SafeTxGas.Uint64()),
GasPrice: &gp,
Value: hexutil.Big(tx.Value),
Nonce: hexutil.Uint64(tx.Nonce.Uint64()),
Data: tx.Data,
Input: nil,
ChainID: (*hexutil.Big)(tx.ChainId),
}
return args
}

View file

@ -1,345 +0,0 @@
// Copyright 2019 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/>.
package core
import (
"context"
"encoding/json"
"errors"
"fmt"
"mime"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/consensus/clique"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/signer/core/apitypes"
)
// sign receives a request and produces a signature
//
// Note, the produced signature conforms to the secp256k1 curve R, S and V values,
// where the V value will be 27 or 28 for legacy reasons, if legacyV==true.
func (api *SignerAPI) sign(req *SignDataRequest, legacyV bool) (hexutil.Bytes, error) {
// We make the request prior to looking up if we actually have the account, to prevent
// account-enumeration via the API
res, err := api.UI.ApproveSignData(req)
if err != nil {
return nil, err
}
if !res.Approved {
return nil, ErrRequestDenied
}
// Look up the wallet containing the requested signer
account := accounts.Account{Address: req.Address.Address()}
wallet, err := api.am.Find(account)
if err != nil {
return nil, err
}
pw, err := api.lookupOrQueryPassword(account.Address,
"Password for signing",
fmt.Sprintf("Please enter password for signing data with account %s", account.Address.Hex()))
if err != nil {
return nil, err
}
// Sign the data with the wallet
signature, err := wallet.SignDataWithPassphrase(account, pw, req.ContentType, req.Rawdata)
if err != nil {
return nil, err
}
if legacyV {
signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper
}
return signature, nil
}
// SignData signs the hash of the provided data, but does so differently
// depending on the content-type specified.
//
// Different types of validation occur.
func (api *SignerAPI) SignData(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (hexutil.Bytes, error) {
var req, transformV, err = api.determineSignatureFormat(ctx, contentType, addr, data)
if err != nil {
return nil, err
}
signature, err := api.sign(req, transformV)
if err != nil {
api.UI.ShowError(err.Error())
return nil, err
}
return signature, nil
}
// determineSignatureFormat determines which signature method should be used based upon the mime type
// In the cases where it matters ensure that the charset is handled. The charset
// resides in the 'params' returned as the second returnvalue from mime.ParseMediaType
// charset, ok := params["charset"]
// As it is now, we accept any charset and just treat it as 'raw'.
// This method returns the mimetype for signing along with the request
func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (*SignDataRequest, bool, error) {
var (
req *SignDataRequest
useEthereumV = true // Default to use V = 27 or 28, the legacy Ethereum format
)
mediaType, _, err := mime.ParseMediaType(contentType)
if err != nil {
return nil, useEthereumV, err
}
switch mediaType {
case apitypes.IntendedValidator.Mime:
// Data with an intended validator
validatorData, err := UnmarshalValidatorData(data)
if err != nil {
return nil, useEthereumV, err
}
sighash, msg := SignTextValidator(validatorData)
messages := []*apitypes.NameValueType{
{
Name: "This is a request to sign data intended for a particular validator (see EIP 191 version 0)",
Typ: "description",
Value: "",
},
{
Name: "Intended validator address",
Typ: "address",
Value: validatorData.Address.String(),
},
{
Name: "Application-specific data",
Typ: "hexdata",
Value: validatorData.Message,
},
{
Name: "Full message for signing",
Typ: "hexdata",
Value: fmt.Sprintf("%#x", msg),
},
}
req = &SignDataRequest{ContentType: mediaType, Rawdata: []byte(msg), Messages: messages, Hash: sighash}
case apitypes.ApplicationClique.Mime:
// Clique is the Ethereum PoA standard
cliqueData, err := fromHex(data)
if err != nil {
return nil, useEthereumV, err
}
header := &types.Header{}
if err := rlp.DecodeBytes(cliqueData, header); err != nil {
return nil, useEthereumV, err
}
// Add space in the extradata to put the signature
newExtra := make([]byte, len(header.Extra)+65)
copy(newExtra, header.Extra)
header.Extra = newExtra
// Get back the rlp data, encoded by us
sighash, cliqueRlp, err := cliqueHeaderHashAndRlp(header)
if err != nil {
return nil, useEthereumV, err
}
messages := []*apitypes.NameValueType{
{
Name: "Clique header",
Typ: "clique",
Value: fmt.Sprintf("clique header %d [%#x]", header.Number, header.Hash()),
},
}
// Clique uses V on the form 0 or 1
useEthereumV = false
req = &SignDataRequest{ContentType: mediaType, Rawdata: cliqueRlp, Messages: messages, Hash: sighash}
case apitypes.DataTyped.Mime:
// EIP-712 conformant typed data
var err error
req, err = typedDataRequest(data)
if err != nil {
return nil, useEthereumV, err
}
default: // also case TextPlain.Mime:
// Calculates an Ethereum ECDSA signature for:
// hash = keccak256("\x19Ethereum Signed Message:\n${message length}${message}")
// We expect input to be a hex-encoded string
textData, err := fromHex(data)
if err != nil {
return nil, useEthereumV, err
}
sighash, msg := accounts.TextAndHash(textData)
messages := []*apitypes.NameValueType{
{
Name: "message",
Typ: accounts.MimetypeTextPlain,
Value: msg,
},
}
req = &SignDataRequest{ContentType: mediaType, Rawdata: []byte(msg), Messages: messages, Hash: sighash}
}
req.Address = addr
req.Meta = MetadataFromContext(ctx)
return req, useEthereumV, nil
}
// SignTextValidator signs the given message which can be further recovered
// with the given validator.
// hash = keccak256("\x19\x00"${address}${data}).
func SignTextValidator(validatorData apitypes.ValidatorData) (hexutil.Bytes, string) {
msg := fmt.Sprintf("\x19\x00%s%s", string(validatorData.Address.Bytes()), string(validatorData.Message))
return crypto.Keccak256([]byte(msg)), msg
}
// cliqueHeaderHashAndRlp returns the hash which is used as input for the proof-of-authority
// signing. It is the hash of the entire header apart from the 65 byte signature
// contained at the end of the extra data.
//
// The method requires the extra data to be at least 65 bytes -- the original implementation
// in clique.go panics if this is the case, thus it's been reimplemented here to avoid the panic
// and simply return an error instead
func cliqueHeaderHashAndRlp(header *types.Header) (hash, rlp []byte, err error) {
if len(header.Extra) < 65 {
err = fmt.Errorf("clique header extradata too short, %d < 65", len(header.Extra))
return
}
rlp = clique.CliqueRLP(header)
hash = clique.SealHash(header).Bytes()
return hash, rlp, err
}
// SignTypedData signs EIP-712 conformant typed data
// hash = keccak256("\x19${byteVersion}${domainSeparator}${hashStruct(message)}")
// It returns
// - the signature,
// - and/or any error
func (api *SignerAPI) SignTypedData(ctx context.Context, addr common.MixedcaseAddress, typedData apitypes.TypedData) (hexutil.Bytes, error) {
signature, _, err := api.signTypedData(ctx, addr, typedData, nil)
return signature, err
}
// signTypedData is identical to the capitalized version, except that it also returns the hash (preimage)
// - the signature preimage (hash)
func (api *SignerAPI) signTypedData(ctx context.Context, addr common.MixedcaseAddress,
typedData apitypes.TypedData, validationMessages *apitypes.ValidationMessages) (hexutil.Bytes, hexutil.Bytes, error) {
req, err := typedDataRequest(typedData)
if err != nil {
return nil, nil, err
}
req.Address = addr
req.Meta = MetadataFromContext(ctx)
if validationMessages != nil {
req.Callinfo = validationMessages.Messages
}
signature, err := api.sign(req, true)
if err != nil {
api.UI.ShowError(err.Error())
return nil, nil, err
}
return signature, req.Hash, nil
}
// fromHex tries to interpret the data as type string, and convert from
// hexadecimal to []byte
func fromHex(data any) ([]byte, error) {
if stringData, ok := data.(string); ok {
binary, err := hexutil.Decode(stringData)
return binary, err
}
return nil, fmt.Errorf("wrong type %T", data)
}
// typeDataRequest tries to convert the data into a SignDataRequest.
func typedDataRequest(data any) (*SignDataRequest, error) {
var typedData apitypes.TypedData
if td, ok := data.(apitypes.TypedData); ok {
typedData = td
} else { // Hex-encoded data
jsonData, err := fromHex(data)
if err != nil {
return nil, err
}
if err = json.Unmarshal(jsonData, &typedData); err != nil {
return nil, err
}
}
messages, err := typedData.Format()
if err != nil {
return nil, err
}
sighash, rawData, err := apitypes.TypedDataAndHash(typedData)
if err != nil {
return nil, err
}
return &SignDataRequest{
ContentType: apitypes.DataTyped.Mime,
Rawdata: []byte(rawData),
Messages: messages,
Hash: sighash}, nil
}
// EcRecover recovers the address associated with the given sig.
// Only compatible with `text/plain`
func (api *SignerAPI) EcRecover(ctx context.Context, data hexutil.Bytes, sig hexutil.Bytes) (common.Address, error) {
// Returns the address for the Account that was used to create the signature.
//
// Note, this function is compatible with eth_sign and personal_sign. As such it recovers
// the address of:
// hash = keccak256("\x19Ethereum Signed Message:\n${message length}${message}")
// addr = ecrecover(hash, signature)
//
// Note, the signature must conform to the secp256k1 curve R, S and V values, where
// the V value must be 27 or 28 for legacy reasons.
//
// https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_ecRecover
if len(sig) != 65 {
return common.Address{}, errors.New("signature must be 65 bytes long")
}
if sig[64] != 27 && sig[64] != 28 {
return common.Address{}, errors.New("invalid Ethereum signature (V is not 27 or 28)")
}
sig[64] -= 27 // Transform yellow paper V from 27/28 to 0/1
hash := accounts.TextHash(data)
rpk, err := crypto.SigToPub(hash, sig)
if err != nil {
return common.Address{}, err
}
return crypto.PubkeyToAddress(*rpk), nil
}
// UnmarshalValidatorData converts the bytes input to typed data
func UnmarshalValidatorData(data interface{}) (apitypes.ValidatorData, error) {
raw, ok := data.(map[string]interface{})
if !ok {
return apitypes.ValidatorData{}, errors.New("validator input is not a map[string]interface{}")
}
addrBytes, err := fromHex(raw["address"])
if err != nil {
return apitypes.ValidatorData{}, fmt.Errorf("validator address error: %w", err)
}
if len(addrBytes) == 0 {
return apitypes.ValidatorData{}, errors.New("validator address is undefined")
}
messageBytes, err := fromHex(raw["message"])
if err != nil {
return apitypes.ValidatorData{}, fmt.Errorf("message error: %w", err)
}
if len(messageBytes) == 0 {
return apitypes.ValidatorData{}, errors.New("message is undefined")
}
return apitypes.ValidatorData{
Address: common.BytesToAddress(addrBytes),
Message: messageBytes,
}, nil
}

File diff suppressed because it is too large Load diff

View file

@ -1,120 +0,0 @@
// Copyright 2018 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/>.
package core
import (
"context"
"github.com/ethereum/go-ethereum/internal/ethapi"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc"
)
type StdIOUI struct {
client *rpc.Client
}
func NewStdIOUI() *StdIOUI {
client, err := rpc.DialContext(context.Background(), "stdio://")
if err != nil {
log.Crit("Could not create stdio client", "err", err)
}
ui := &StdIOUI{client: client}
return ui
}
func (ui *StdIOUI) RegisterUIServer(api *UIServerAPI) {
ui.client.RegisterName("clef", api)
}
// dispatch sends a request over the stdio
func (ui *StdIOUI) dispatch(serviceMethod string, args interface{}, reply interface{}) error {
err := ui.client.Call(&reply, serviceMethod, args)
if err != nil {
log.Info("Error", "exc", err.Error())
}
return err
}
// notify sends a request over the stdio, and does not listen for a response
func (ui *StdIOUI) notify(serviceMethod string, args interface{}) error {
ctx := context.Background()
err := ui.client.Notify(ctx, serviceMethod, args)
if err != nil {
log.Info("Error", "exc", err.Error())
}
return err
}
func (ui *StdIOUI) ApproveTx(request *SignTxRequest) (SignTxResponse, error) {
var result SignTxResponse
err := ui.dispatch("ui_approveTx", request, &result)
return result, err
}
func (ui *StdIOUI) ApproveSignData(request *SignDataRequest) (SignDataResponse, error) {
var result SignDataResponse
err := ui.dispatch("ui_approveSignData", request, &result)
return result, err
}
func (ui *StdIOUI) ApproveListing(request *ListRequest) (ListResponse, error) {
var result ListResponse
err := ui.dispatch("ui_approveListing", request, &result)
return result, err
}
func (ui *StdIOUI) ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) {
var result NewAccountResponse
err := ui.dispatch("ui_approveNewAccount", request, &result)
return result, err
}
func (ui *StdIOUI) ShowError(message string) {
err := ui.notify("ui_showError", &Message{message})
if err != nil {
log.Info("Error calling 'ui_showError'", "exc", err.Error(), "msg", message)
}
}
func (ui *StdIOUI) ShowInfo(message string) {
err := ui.notify("ui_showInfo", Message{message})
if err != nil {
log.Info("Error calling 'ui_showInfo'", "exc", err.Error(), "msg", message)
}
}
func (ui *StdIOUI) OnApprovedTx(tx ethapi.SignTransactionResult) {
err := ui.notify("ui_onApprovedTx", tx)
if err != nil {
log.Info("Error calling 'ui_onApprovedTx'", "exc", err.Error(), "tx", tx)
}
}
func (ui *StdIOUI) OnSignerStartup(info StartupInfo) {
err := ui.notify("ui_onSignerStartup", info)
if err != nil {
log.Info("Error calling 'ui_onSignerStartup'", "exc", err.Error(), "info", info)
}
}
func (ui *StdIOUI) OnInputRequired(info UserInputRequest) (UserInputResponse, error) {
var result UserInputResponse
err := ui.dispatch("ui_onInputRequired", info, &result)
if err != nil {
log.Info("Error calling 'ui_onInputRequired'", "exc", err.Error(), "info", info)
}
return result, err
}

View file

@ -1,5 +0,0 @@
### EIP 712 tests
These tests are json files which are converted into eip-712 typed data.
All files are expected to be proper json, and tests will fail if they are not.
Files that begin with `expfail' are expected to not pass the hashstruct construction.

View file

@ -1,60 +0,0 @@
{
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "version",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"Foo": [
{
"name": "addys",
"type": "address[]"
},
{
"name": "stringies",
"type": "string[]"
},
{
"name": "inties",
"type": "uint[]"
}
]
},
"primaryType": "Foo",
"domain": {
"name": "Lorem",
"version": "1",
"chainId": "1",
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
},
"message": {
"addys": [
"0x0000000000000000000000000000000000000001",
"0x0000000000000000000000000000000000000002",
"0x0000000000000000000000000000000000000003"
],
"stringies": [
"lorem",
"ipsum",
"dolores"
],
"inties": [
"0x0000000000000000000000000000000000000001",
"3",
4.0
]
}
}

View file

@ -1,54 +0,0 @@
{
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "version",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"Person": [
{
"name": "name",
"type": "string"
}
],
"Mail": [
{
"name": "from",
"type": "Person"
},
{
"name": "to",
"type": "Person[]"
},
{
"name": "contents",
"type": "string"
}
]
},
"primaryType": "Mail",
"domain": {
"name": "Ether Mail",
"version": "1",
"chainId": "1",
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
},
"message": {
"from": { "name": "Cow"},
"to": [{ "name": "Moose"},{ "name": "Goose"}],
"contents": "Hello, Bob!"
}
}

View file

@ -1,76 +0,0 @@
{
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "version",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"Person": [
{
"name": "name",
"type": "string"
},
{
"name": "test",
"type": "uint8"
},
{
"name": "test2",
"type": "uint8"
},
{
"name": "wallet",
"type": "address"
}
],
"Mail": [
{
"name": "from",
"type": "Person"
},
{
"name": "to",
"type": "Person"
},
{
"name": "contents",
"type": "string"
}
]
},
"primaryType": "Mail",
"domain": {
"name": "Ether Mail",
"version": "1",
"chainId": "1",
"verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
},
"message": {
"from": {
"name": "Cow",
"test": "3",
"test2": 5.0,
"wallet": "0xcD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
},
"to": {
"name": "Bob",
"test": "0",
"test2": 5,
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
},
"contents": "Hello, Bob!"
}
}

View file

@ -1,67 +0,0 @@
{
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "version",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"Person": [
{
"name": "name",
"type": "string"
},
{
"name": "wallet",
"type": "address"
}
],
"Person[]": [
{
"name": "baz",
"type": "string"
}
],
"Mail": [
{
"name": "from",
"type": "Person"
},
{
"name": "to",
"type": "Person[]"
},
{
"name": "contents",
"type": "string"
}
]
},
"primaryType": "Mail",
"domain": {
"name": "Ether Mail",
"version": "1",
"chainId": "1",
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
},
"message": {
"from": {
"name": "Cow",
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
},
"to": {"baz": "foo"},
"contents": "Hello, Bob!"
}
}

View file

@ -1,64 +0,0 @@
{
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "version",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"Person": [
{
"name": "name",
"type": "string"
},
{
"name": "wallet",
"type": "address"
}
],
"Mail": [
{
"name": "from",
"type": "Person"
},
{
"name": "to",
"type": "Person"
},
{
"name": "contents",
"type": "Person"
}
]
},
"primaryType": "Mail",
"domain": {
"name": "Ether Mail",
"version": "1",
"chainId": "1",
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
},
"message": {
"from": {
"name": "Cow",
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
},
"to": {
"name": "Bob",
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
},
"contents": "Hello, Bob!"
}
}

View file

@ -1,77 +0,0 @@
{
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "version",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"Person": [
{
"name": "name",
"type": "string"
},
{
"name": "test",
"type": "uint8"
},
{
"name": "test2",
"type": "uint8"
},
{
"name": "wallet",
"type": "address"
}
],
"Mail": [
{
"name": "from",
"type": "Person"
},
{
"name": "to",
"type": "Person"
},
{
"name": "contents",
"type": "string"
}
]
},
"primaryType": "Mail",
"domain": {
"name": "Ether Mail",
"version": "1",
"chainId": "1",
"verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
},
"message": {
"blahonga": "zonk bonk",
"from": {
"name": "Cow",
"test": "3",
"test2": 5.0,
"wallet": "0xcD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
},
"to": {
"name": "Bob",
"test": "0",
"test2": 5,
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
},
"contents": "Hello, Bob!"
}
}

View file

@ -1,64 +0,0 @@
{
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "version",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"Person": [
{
"name": "name",
"type": "string"
},
{
"name": "wallet",
"type": "address"
}
],
"Mail": [
{
"name": "from",
"type": "Person"
},
{
"name": "to",
"type": "Person"
},
{
"name": "contents",
"type": "string"
}
]
},
"primaryType": "Mail",
"domain": {
"name": "Ether Mail",
"version": "1",
"chainId": "1",
"vFAILFAILerifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
},
"message": {
"from": {
"name": "Cow",
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
},
"to": {
"name": "Bob",
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
},
"contents": "Hello, Bob!"
}
}

View file

@ -1,64 +0,0 @@
{
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "version",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"Person": [
{
"name": "name",
"type": "string"
},
{
"name": "wallet",
"type": "address"
}
],
"Mail": [
{
"name": "from",
"type": "Person"
},
{
"name": "to",
"type": "Person"
},
{
"name": "contents",
"type": "Blahonga"
}
]
},
"primaryType": "Mail",
"domain": {
"name": "Ether Mail",
"version": "1",
"chainId": "1",
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
},
"message": {
"from": {
"name": "Cow",
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
},
"to": {
"name": "Bob",
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
},
"contents": "Hello, Bob!"
}
}

View file

@ -1,76 +0,0 @@
{
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "version",
"type": "string"
},
{
"name": "chainId",
"type": "uint256 ... and now for something completely different"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"Person": [
{
"name": "name",
"type": "string"
},
{
"name": "test",
"type": "uint8"
},
{
"name": "test2",
"type": "uint8"
},
{
"name": "wallet",
"type": "address"
}
],
"Mail": [
{
"name": "from",
"type": "Person"
},
{
"name": "to",
"type": "Person"
},
{
"name": "contents",
"type": "string"
}
]
},
"primaryType": "Mail",
"domain": {
"name": "Ether Mail",
"version": "1",
"chainId": "1",
"verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
},
"message": {
"from": {
"name": "Cow",
"test": "3",
"test2": 5.0,
"wallet": "0xcD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
},
"to": {
"name": "Bob",
"test": "0",
"test2": 5,
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
},
"contents": "Hello, Bob!"
}
}

View file

@ -1,38 +0,0 @@
{
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "version",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"Mail": [
{
"name": "test",
"type": "uint8"
}
]
},
"primaryType": "Mail",
"domain": {
"name": "Ether Mail",
"version": "1",
"chainId": "1",
"verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
},
"message": {
"test":"257"
}
}

View file

@ -1,38 +0,0 @@
{
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "version",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"Mail": [
{
"name": "test",
"type": "uint8"
}
]
},
"primaryType": "Mail",
"domain": {
"name": "Ether Mail",
"version": "1",
"chainId": "1",
"verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
},
"message": {
"test":257
}
}

View file

@ -1,38 +0,0 @@
{
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "version",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"Mail": [
{
"name": "test",
"type": "uint8"
}
]
},
"primaryType": "Mail",
"domain": {
"name": "Ether Mail",
"version": "1",
"chainId": "1",
"verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
},
"message": {
"test":"255.3"
}
}

View file

@ -1,38 +0,0 @@
{
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "version",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"Mail": [
{
"name": "test",
"type": "uint8"
}
]
},
"primaryType": "Mail",
"domain": {
"name": "Ether Mail",
"version": "1",
"chainId": "1",
"verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
},
"message": {
"test": 255.3
}
}

View file

@ -1,38 +0,0 @@
{
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "version",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"Mail": [
{
"name": "test",
"type": "uint8"
}
]
},
"primaryType": "Mail",
"domain": {
"name": "Ether Mail",
"version": "1",
"chainId": "1",
"verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
},
"message": {
"test":"255.3"
}
}

View file

@ -1,60 +0,0 @@
{
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "version",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"Foo": [
{
"name": "addys",
"type": "address[]"
},
{
"name": "stringies",
"type": "string[]"
},
{
"name": "inties",
"type": "uint[]"
}
]
},
"primaryType": "Foo",
"domain": {
"name": "Lorem",
"version": "1",
"chainId": "1",
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
},
"message": {
"addys": [
"0x0000000000000000000000000000000000000001",
"0x0000000000000000000000000000000000000002",
"0x0000000000000000000000000000000000000003"
],
"stringies": [
"lorem",
"ipsum",
"dolores"
],
"inties": [
"0x0000000000000000000000000000000000000001",
"3",
4.0
]
}
}

View file

@ -1,38 +0,0 @@
{
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "version",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"Mail": [
{
"name": "test",
"type": "uint8"
}
]
},
"primaryType": "Mail",
"domain": {
"name": "Ether Mail",
"version": "1",
"chainId": "1",
"verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
},
"message": {
"test":257
}
}

View file

@ -1 +0,0 @@
{"domain":{"version":"0","chainId":""}}

View file

@ -1,54 +0,0 @@
{ "types": { "":[ {
"name": "name",
"type":"string" },
{
"name":"version",
"type": "string" }, {
"name": "chaiI",
"type":"uint256 . ad nowretig omeedifere" }, {
"ae": "eifinC",
"ty":"dess"
}
],
"Person":[
{
"name":"name",
"type": "string"
}, {
"name":"tes", "type":"it8"
},
{ "name":"t", "tye":"uit8"
},
{
"a":"ale",
"type": "ress"
}
],
"Mail": [
{
"name":"from", "type":"Person" },
{
"name": "to", "type": "Person"
},
{
"name": "contents",
"type": "string"
}
]
}, "primaryType": "Mail",
"domain": {
"name":"theMail", "version": "1",
"chainId": "1",
"verifyingntract": "0xCcccCCCcCCCCCCCcCCcCCCcCcccccC"
},
"message": { "from": {
"name": "Cow",
"test": "3",
"est2":5.0,
"llt": "0xcD2a3938E13D947E0bE734DfDD86" }, "to": { "name": "Bob",
"ts":"",
"tet2": 5,
"allet": "0bBBBBbbBBbbbbBbbBbbbbBBBbB"
},
"contents": "Hello, Bob!" }
}

View file

@ -1,64 +0,0 @@
{
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "version",
"type": "string"
},
{
"name": "chainId",
"type": "int"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"Person": [
{
"name": "name",
"type": "string"
},
{
"name": "wallet",
"type": "address"
}
],
"Mail": [
{
"name": "from",
"type": "Person"
},
{
"name": "to",
"type": "Mail"
},
{
"name": "s",
"type": "Person"
}
]
},
"primaryType": "Mail",
"domain": {
"name": "l",
"version": "1",
"chainId": "",
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
},
"message": {
"from": {
"name": "",
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
},
"to": {
"name": "",
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
},
"": ""
}
}

View file

@ -1 +0,0 @@
{"types":{"0":[{}]}}

View file

@ -1,214 +0,0 @@
// Copyright 2019 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/>.
package core
import (
"context"
"encoding/json"
"errors"
"fmt"
"math/big"
"os"
"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"
)
// UIServerAPI 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 retrieves the encrypted keystore from the account manager.
func fetchKeystore(am *accounts.Manager) *keystore.KeyStore {
ks := am.Backends(keystore.KeyStoreType)
if len(ks) == 0 {
return nil
}
return ks[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, errors.New("account is not a keystore-account")
}
return os.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)
}
// New creates a new password protected Account. The private key is protected with
// the given password. Users are responsible to backup the private key that is stored
// in the keystore location that was specified when this API was created.
// This method is the same as New on the external API, the difference being that
// this implementation does not ask for confirmation, since it's initiated by
// the user
func (api *UIServerAPI) New(ctx context.Context) (common.Address, error) {
return api.extApi.newAccount()
}
// Other methods to be added, not yet implemented are:
// - Ruleset interaction: add rules, attest rulefiles
// - Store metadata about accounts, e.g. naming of accounts

View file

@ -1,36 +0,0 @@
// Copyright 2018 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/>.
package core
import (
"errors"
"regexp"
)
var printable7BitAscii = regexp.MustCompile("^[A-Za-z0-9!\"#$%&'()*+,\\-./:;<=>?@[\\]^_`{|}~ ]+$")
// ValidatePasswordFormat returns an error if the password is too short, or consists of characters
// outside the range of the printable 7bit ascii set
func ValidatePasswordFormat(password string) error {
if len(password) < 10 {
return errors.New("password too short (<10 characters)")
}
if !printable7BitAscii.MatchString(password) {
return errors.New("password contains invalid characters - only 7bit printable ascii allowed")
}
return nil
}

View file

@ -1,45 +0,0 @@
// Copyright 2018 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/>.
package core
import "testing"
func TestPasswordValidation(t *testing.T) {
t.Parallel()
testcases := []struct {
pw string
shouldFail bool
}{
{"test", true},
{"testtest\xbd\xb2\x3d\xbc\x20\xe2\x8c\x98", true},
{"placeOfInterest⌘", true},
{"password\nwith\nlinebreak", true},
{"password\twith\vtabs", true},
// Ok passwords
{"password WhichIsOk", false},
{"passwordOk!@#$%^&*()", false},
{"12301203123012301230123012", false},
}
for _, test := range testcases {
err := ValidatePasswordFormat(test.pw)
if err == nil && test.shouldFail {
t.Errorf("password '%v' should fail validation", test.pw)
} else if err != nil && !test.shouldFail {
t.Errorf("password '%v' shound not fail validation, but did: %v", test.pw, err)
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,136 +0,0 @@
// Copyright 2019 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/>.
package fourbyte
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
)
// decodedCallData is an internal type to represent a method call parsed according
// to an ABI method signature.
type decodedCallData struct {
signature string
name string
inputs []decodedArgument
}
// decodedArgument is an internal type to represent an argument parsed according
// to an ABI method signature.
type decodedArgument struct {
soltype abi.Argument
value interface{}
}
// String implements stringer interface, tries to use the underlying value-type
func (arg decodedArgument) String() string {
var value string
switch val := arg.value.(type) {
case fmt.Stringer:
value = val.String()
default:
value = fmt.Sprintf("%v", val)
}
return fmt.Sprintf("%v: %v", arg.soltype.Type.String(), value)
}
// String implements stringer interface for decodedCallData
func (cd decodedCallData) String() string {
args := make([]string, len(cd.inputs))
for i, arg := range cd.inputs {
args[i] = arg.String()
}
return fmt.Sprintf("%s(%s)", cd.name, strings.Join(args, ","))
}
// verifySelector checks whether the ABI encoded data blob matches the requested
// function signature.
func verifySelector(selector string, calldata []byte) (*decodedCallData, error) {
// Parse the selector into an ABI JSON spec
abidata, err := parseSelector(selector)
if err != nil {
return nil, err
}
// Parse the call data according to the requested selector
return parseCallData(calldata, string(abidata))
}
// parseSelector converts a method selector into an ABI JSON spec. The returned
// data is a valid JSON string which can be consumed by the standard abi package.
func parseSelector(unescapedSelector string) ([]byte, error) {
selector, err := abi.ParseSelector(unescapedSelector)
if err != nil {
return nil, fmt.Errorf("failed to parse selector: %v", err)
}
return json.Marshal([]abi.SelectorMarshaling{selector})
}
// parseCallData matches the provided call data against the ABI definition and
// returns a struct containing the actual go-typed values.
func parseCallData(calldata []byte, unescapedAbidata string) (*decodedCallData, error) {
// Validate the call data that it has the 4byte prefix and the rest divisible by 32 bytes
if len(calldata) < 4 {
return nil, fmt.Errorf("invalid call data, incomplete method signature (%d bytes < 4)", len(calldata))
}
sigdata := calldata[:4]
argdata := calldata[4:]
if len(argdata)%32 != 0 {
return nil, fmt.Errorf("invalid call data; length should be a multiple of 32 bytes (was %d)", len(argdata))
}
// Validate the called method and upack the call data accordingly
abispec, err := abi.JSON(strings.NewReader(unescapedAbidata))
if err != nil {
return nil, fmt.Errorf("invalid method signature (%q): %v", unescapedAbidata, err)
}
method, err := abispec.MethodById(sigdata)
if err != nil {
return nil, err
}
values, err := method.Inputs.UnpackValues(argdata)
if err != nil {
return nil, fmt.Errorf("signature %q matches, but arguments mismatch: %v", method.String(), err)
}
// Everything valid, assemble the call infos for the signer
decoded := decodedCallData{signature: method.Sig, name: method.RawName}
for i := 0; i < len(method.Inputs); i++ {
decoded.inputs = append(decoded.inputs, decodedArgument{
soltype: method.Inputs[i],
value: values[i],
})
}
// We're finished decoding the data. At this point, we encode the decoded data
// to see if it matches with the original data. If we didn't do that, it would
// be possible to stuff extra data into the arguments, which is not detected
// by merely decoding the data.
encoded, err := method.Inputs.PackValues(values)
if err != nil {
return nil, err
}
if !bytes.Equal(encoded, argdata) {
was := common.Bytes2Hex(encoded)
exp := common.Bytes2Hex(argdata)
return nil, fmt.Errorf("WARNING: Supplied data is stuffed with extra data. \nWant %s\nHave %s\nfor method %v", exp, was, method.Sig)
}
return &decoded, nil
}

View file

@ -1,177 +0,0 @@
// Copyright 2019 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/>.
package fourbyte
import (
"math/big"
"reflect"
"strings"
"testing"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
)
func verify(t *testing.T, jsondata, calldata string, exp []interface{}) {
abispec, err := abi.JSON(strings.NewReader(jsondata))
if err != nil {
t.Fatal(err)
}
cd := common.Hex2Bytes(calldata)
sigdata, argdata := cd[:4], cd[4:]
method, err := abispec.MethodById(sigdata)
if err != nil {
t.Fatal(err)
}
data, err := method.Inputs.UnpackValues(argdata)
if err != nil {
t.Fatal(err)
}
if len(data) != len(exp) {
t.Fatalf("Mismatched length, expected %d, got %d", len(exp), len(data))
}
for i, elem := range data {
if !reflect.DeepEqual(elem, exp[i]) {
t.Fatalf("Unpack error, arg %d, got %v, want %v", i, elem, exp[i])
}
}
}
func TestNewUnpacker(t *testing.T) {
t.Parallel()
type unpackTest struct {
jsondata string
calldata string
exp []interface{}
}
testcases := []unpackTest{
{ // https://solidity.readthedocs.io/en/develop/abi-spec.html#use-of-dynamic-types
`[{"type":"function","name":"f", "inputs":[{"type":"uint256"},{"type":"uint32[]"},{"type":"bytes10"},{"type":"bytes"}]}]`,
// 0x123, [0x456, 0x789], "1234567890", "Hello, world!"
"8be65246" + "00000000000000000000000000000000000000000000000000000000000001230000000000000000000000000000000000000000000000000000000000000080313233343536373839300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000004560000000000000000000000000000000000000000000000000000000000000789000000000000000000000000000000000000000000000000000000000000000d48656c6c6f2c20776f726c642100000000000000000000000000000000000000",
[]interface{}{
big.NewInt(0x123),
[]uint32{0x456, 0x789},
[10]byte{49, 50, 51, 52, 53, 54, 55, 56, 57, 48},
common.Hex2Bytes("48656c6c6f2c20776f726c6421"),
},
}, { // https://docs.soliditylang.org/en/develop/abi-spec.html#examples
`[{"type":"function","name":"sam","inputs":[{"type":"bytes"},{"type":"bool"},{"type":"uint256[]"}]}]`,
// "dave", true and [1,2,3]
"a5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003",
[]interface{}{
[]byte{0x64, 0x61, 0x76, 0x65},
true,
[]*big.Int{big.NewInt(1), big.NewInt(2), big.NewInt(3)},
},
}, {
`[{"type":"function","name":"send","inputs":[{"type":"uint256"}]}]`,
"a52c101e0000000000000000000000000000000000000000000000000000000000000012",
[]interface{}{big.NewInt(0x12)},
}, {
`[{"type":"function","name":"compareAndApprove","inputs":[{"type":"address"},{"type":"uint256"},{"type":"uint256"}]}]`,
"751e107900000000000000000000000000000133700000deadbeef00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001",
[]interface{}{
common.HexToAddress("0x00000133700000deadbeef000000000000000000"),
new(big.Int).SetBytes([]byte{0x00}),
big.NewInt(0x1),
},
},
}
for _, c := range testcases {
verify(t, c.jsondata, c.calldata, c.exp)
}
}
func TestCalldataDecoding(t *testing.T) {
t.Parallel()
// send(uint256) : a52c101e
// compareAndApprove(address,uint256,uint256) : 751e1079
// issue(address[],uint256) : 42958b54
jsondata := `
[
{"type":"function","name":"send","inputs":[{"name":"a","type":"uint256"}]},
{"type":"function","name":"compareAndApprove","inputs":[{"name":"a","type":"address"},{"name":"a","type":"uint256"},{"name":"a","type":"uint256"}]},
{"type":"function","name":"issue","inputs":[{"name":"a","type":"address[]"},{"name":"a","type":"uint256"}]},
{"type":"function","name":"sam","inputs":[{"name":"a","type":"bytes"},{"name":"a","type":"bool"},{"name":"a","type":"uint256[]"}]}
]`
// Expected failures
for i, hexdata := range []string{
"a52c101e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000042",
"a52c101e000000000000000000000000000000000000000000000000000000000000001200",
"a52c101e00000000000000000000000000000000000000000000000000000000000000",
"a52c101e",
"a52c10",
"",
// Too short
"751e10790000000000000000000000000000000000000000000000000000000000000012",
"751e1079FFffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
// Not valid multiple of 32
"deadbeef00000000000000000000000000000000000000000000000000000000000000",
// Too short 'issue'
"42958b5400000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000042",
// Too short compareAndApprove
"a52c101e00ff0000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000042",
// From https://docs.soliditylang.org/en/develop/abi-spec.html
// contains a bool with illegal values
"a5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003",
} {
_, err := parseCallData(common.Hex2Bytes(hexdata), jsondata)
if err == nil {
t.Errorf("test %d: expected decoding to fail: %s", i, hexdata)
}
}
// Expected success
for i, hexdata := range []string{
// From https://docs.soliditylang.org/en/develop/abi-spec.html
"a5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003",
"a52c101e0000000000000000000000000000000000000000000000000000000000000012",
"a52c101eFFffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"751e1079000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"42958b54" +
// start of dynamic type
"0000000000000000000000000000000000000000000000000000000000000040" +
// uint256
"0000000000000000000000000000000000000000000000000000000000000001" +
// length of array
"0000000000000000000000000000000000000000000000000000000000000002" +
// array values
"000000000000000000000000000000000000000000000000000000000000dead" +
"000000000000000000000000000000000000000000000000000000000000beef",
} {
_, err := parseCallData(common.Hex2Bytes(hexdata), jsondata)
if err != nil {
t.Errorf("test %d: unexpected failure on input %s:\n %v (%d bytes) ", i, hexdata, err, len(common.Hex2Bytes(hexdata)))
}
}
}
func TestMaliciousABIStrings(t *testing.T) {
t.Parallel()
tests := []string{
"func(uint256,uint256,[]uint256)",
"func(uint256,uint256,uint256,)",
"func(,uint256,uint256,uint256)",
}
data := common.Hex2Bytes("4401a6e40000000000000000000000000000000000000000000000000000000000000012")
for i, tt := range tests {
_, err := verifySelector(tt, data)
if err == nil {
t.Errorf("test %d: expected error for selector '%v'", i, tt)
}
}
}

View file

@ -1,140 +0,0 @@
// Copyright 2019 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/>.
// Package fourbyte contains the 4byte database.
package fourbyte
import (
_ "embed"
"encoding/hex"
"encoding/json"
"fmt"
"os"
)
//go:embed 4byte.json
var embeddedJSON []byte
// Database is a 4byte database with the possibility of maintaining an immutable
// set (embedded) into the process and a mutable set (loaded and written to file).
type Database struct {
embedded map[string]string
custom map[string]string
customPath string
}
// newEmpty exists for testing purposes.
func newEmpty() *Database {
return &Database{
embedded: make(map[string]string),
custom: make(map[string]string),
}
}
// New loads the standard signature database embedded in the package.
func New() (*Database, error) {
return NewWithFile("")
}
// NewFromFile loads signature database from file, and errors if the file is not
// valid JSON. The constructor does no other validation of contents. This method
// does not load the embedded 4byte database.
//
// The provided path will be used to write new values into if they are submitted
// via the API.
func NewFromFile(path string) (*Database, error) {
raw, err := os.Open(path)
if err != nil {
return nil, err
}
defer raw.Close()
db := newEmpty()
if err := json.NewDecoder(raw).Decode(&db.embedded); err != nil {
return nil, err
}
return db, nil
}
// NewWithFile loads both the standard signature database (embedded resource
// file) as well as a custom database. The latter will be used to write new
// values into if they are submitted via the API.
func NewWithFile(path string) (*Database, error) {
db := &Database{make(map[string]string), make(map[string]string), path}
db.customPath = path
if err := json.Unmarshal(embeddedJSON, &db.embedded); err != nil {
return nil, err
}
// Custom file may not exist. Will be created during save, if needed.
if _, err := os.Stat(path); err == nil {
var blob []byte
if blob, err = os.ReadFile(path); err != nil {
return nil, err
}
if err := json.Unmarshal(blob, &db.custom); err != nil {
return nil, err
}
}
return db, nil
}
// Size returns the number of 4byte entries in the embedded and custom datasets.
func (db *Database) Size() (int, int) {
return len(db.embedded), len(db.custom)
}
// Selector checks the given 4byte ID against the known ABI methods.
//
// This method does not validate the match, it's assumed the caller will do.
func (db *Database) Selector(id []byte) (string, error) {
if len(id) < 4 {
return "", fmt.Errorf("expected 4-byte id, got %d", len(id))
}
sig := hex.EncodeToString(id[:4])
if selector, exists := db.embedded[sig]; exists {
return selector, nil
}
if selector, exists := db.custom[sig]; exists {
return selector, nil
}
return "", fmt.Errorf("signature %v not found", sig)
}
// AddSelector inserts a new 4byte entry into the database. If custom database
// saving is enabled, the new dataset is also persisted to disk.
//
// Node, this method does _not_ validate the correctness of the data. It assumes
// the caller has already done so.
func (db *Database) AddSelector(selector string, data []byte) error {
// If the selector is already known, skip duplicating it
if len(data) < 4 {
return nil
}
if _, err := db.Selector(data[:4]); err == nil {
return nil
}
// Inject the custom selector into the database and persist if needed
db.custom[hex.EncodeToString(data[:4])] = selector
if db.customPath == "" {
return nil
}
blob, err := json.Marshal(db.custom)
if err != nil {
return err
}
return os.WriteFile(db.customPath, blob, 0600)
}

View file

@ -1,89 +0,0 @@
// Copyright 2019 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/>.
package fourbyte
import (
"encoding/json"
"fmt"
"testing"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
)
// Tests that all the selectors contained in the 4byte database are valid.
func TestEmbeddedDatabase(t *testing.T) {
t.Parallel()
db, err := New()
if err != nil {
t.Fatal(err)
}
var abistruct abi.ABI
for id, selector := range db.embedded {
abistring, err := parseSelector(selector)
if err != nil {
t.Errorf("Failed to convert selector to ABI: %v", err)
continue
}
if err := json.Unmarshal(abistring, &abistruct); err != nil {
t.Errorf("Failed to parse ABI: %v", err)
continue
}
m, err := abistruct.MethodById(common.Hex2Bytes(id))
if err != nil {
t.Errorf("Failed to get method by id (%s): %v", id, err)
continue
}
if m.Sig != selector {
t.Errorf("Selector mismatch: have %v, want %v", m.Sig, selector)
}
}
}
// Tests that custom 4byte datasets can be handled too.
func TestCustomDatabase(t *testing.T) {
t.Parallel()
// Create a new custom 4byte database with no embedded component
tmpdir := t.TempDir()
filename := fmt.Sprintf("%s/4byte_custom.json", tmpdir)
db, err := NewWithFile(filename)
if err != nil {
t.Fatal(err)
}
db.embedded = make(map[string]string)
// Ensure the database is empty, insert and verify
calldata := common.Hex2Bytes("a52c101edeadbeef")
if _, err = db.Selector(calldata); err == nil {
t.Fatalf("Should not find a match on empty database")
}
if err = db.AddSelector("send(uint256)", calldata); err != nil {
t.Fatalf("Failed to save file: %v", err)
}
if _, err = db.Selector(calldata); err != nil {
t.Fatalf("Failed to find a match for abi signature: %v", err)
}
// Check that the file as persisted to disk by creating a new instance
db2, err := NewFromFile(filename)
if err != nil {
t.Fatalf("Failed to create new abidb: %v", err)
}
if _, err = db2.Selector(calldata); err != nil {
t.Fatalf("Failed to find a match for persisted abi signature: %v", err)
}
}

View file

@ -1,127 +0,0 @@
// Copyright 2019 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/>.
package fourbyte
import (
"bytes"
"errors"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/signer/core/apitypes"
)
// ValidateTransaction does a number of checks on the supplied transaction, and
// returns either a list of warnings, or an error (indicating that the transaction
// should be immediately rejected).
func (db *Database) ValidateTransaction(selector *string, tx *apitypes.SendTxArgs) (*apitypes.ValidationMessages, error) {
messages := new(apitypes.ValidationMessages)
// Prevent accidental erroneous usage of both 'input' and 'data' (show stopper)
if tx.Data != nil && tx.Input != nil && !bytes.Equal(*tx.Data, *tx.Input) {
return nil, errors.New(`ambiguous request: both "data" and "input" are set and are not identical`)
}
// Place data on 'data', and nil 'input'
var data []byte
if tx.Input != nil {
tx.Data = tx.Input
tx.Input = nil
}
if tx.Data != nil {
data = *tx.Data
}
// Contract creation doesn't validate call data, handle first
if tx.To == nil {
// Contract creation should contain sufficient data to deploy a contract. A
// typical error is omitting sender due to some quirk in the javascript call
// e.g. https://github.com/ethereum/go-ethereum/issues/16106.
if len(data) == 0 {
// Prevent sending ether into black hole (show stopper)
if tx.Value.ToInt().Cmp(big.NewInt(0)) > 0 {
return nil, errors.New("transaction will create a contract with value but empty code")
}
// No value submitted at least, critically Warn, but don't blow up
messages.Crit("Transaction will create a contract with empty code")
} else if len(data) < 40 { // arbitrary heuristic limit
messages.Warn(fmt.Sprintf("Transaction will create a contract, but the payload is suspiciously small (%d bytes)", len(data)))
}
// Method selector should be nil for contract creation
if selector != nil {
messages.Warn("Transaction will create a contract, but method selector supplied, indicating an intent to call a method")
}
return messages, nil
}
// Not a contract creation, validate as a plain transaction
if !tx.To.ValidChecksum() {
messages.Warn("Invalid checksum on recipient address")
}
if bytes.Equal(tx.To.Address().Bytes(), common.Address{}.Bytes()) {
messages.Crit("Transaction recipient is the zero address")
}
switch {
case tx.GasPrice == nil && tx.MaxFeePerGas == nil:
messages.Crit("Neither 'gasPrice' nor 'maxFeePerGas' specified.")
case tx.GasPrice == nil && tx.MaxPriorityFeePerGas == nil:
messages.Crit("Neither 'gasPrice' nor 'maxPriorityFeePerGas' specified.")
case tx.GasPrice != nil && tx.MaxFeePerGas != nil:
messages.Crit("Both 'gasPrice' and 'maxFeePerGas' specified.")
case tx.GasPrice != nil && tx.MaxPriorityFeePerGas != nil:
messages.Crit("Both 'gasPrice' and 'maxPriorityFeePerGas' specified.")
}
// Semantic fields validated, try to make heads or tails of the call data
db.ValidateCallData(selector, data, messages)
return messages, nil
}
// ValidateCallData checks if the ABI call-data + method selector (if given) can
// be parsed and seems to match.
func (db *Database) ValidateCallData(selector *string, data []byte, messages *apitypes.ValidationMessages) {
// If the data is empty, we have a plain value transfer, nothing more to do
if len(data) == 0 {
return
}
// Validate the call data that it has the 4byte prefix and the rest divisible by 32 bytes
if len(data) < 4 {
messages.Warn("Transaction data is not valid ABI (missing the 4 byte call prefix)")
return
}
if n := len(data) - 4; n%32 != 0 {
messages.Warn(fmt.Sprintf("Transaction data is not valid ABI (length should be a multiple of 32 (was %d))", n))
}
// If a custom method selector was provided, validate with that
if selector != nil {
if info, err := verifySelector(*selector, data); err != nil {
messages.Warn(fmt.Sprintf("Transaction contains data, but provided ABI signature could not be matched: %v", err))
} else {
messages.Info(fmt.Sprintf("Transaction invokes the following method: %q", info.String()))
db.AddSelector(*selector, data[:4])
}
return
}
// No method selector was provided, check the database for embedded ones
embedded, err := db.Selector(data[:4])
if err != nil {
messages.Warn(fmt.Sprintf("Transaction contains data, but the ABI signature could not be found: %v", err))
return
}
if info, err := verifySelector(embedded, data); err != nil {
messages.Warn(fmt.Sprintf("Transaction contains data, but provided ABI signature could not be verified: %v", err))
} else {
messages.Info(fmt.Sprintf("Transaction invokes the following method: %q", info.String()))
}
}

View file

@ -1,137 +0,0 @@
// Copyright 2019 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/>.
package fourbyte
import (
"math/big"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/signer/core/apitypes"
)
func mixAddr(a string) (*common.MixedcaseAddress, error) {
return common.NewMixedcaseAddressFromString(a)
}
func toHexBig(h string) hexutil.Big {
b := new(big.Int).SetBytes(common.FromHex(h))
return hexutil.Big(*b)
}
func toHexUint(h string) hexutil.Uint64 {
b := new(big.Int).SetBytes(common.FromHex(h))
return hexutil.Uint64(b.Uint64())
}
func dummyTxArgs(t txtestcase) *apitypes.SendTxArgs {
to, _ := mixAddr(t.to)
from, _ := mixAddr(t.from)
n := toHexUint(t.n)
gas := toHexUint(t.g)
gasPrice := toHexBig(t.gp)
value := toHexBig(t.value)
var (
data, input *hexutil.Bytes
)
if t.d != "" {
a := hexutil.Bytes(common.FromHex(t.d))
data = &a
}
if t.i != "" {
a := hexutil.Bytes(common.FromHex(t.i))
input = &a
}
return &apitypes.SendTxArgs{
From: *from,
To: to,
Value: value,
Nonce: n,
GasPrice: &gasPrice,
Gas: gas,
Data: data,
Input: input,
}
}
type txtestcase struct {
from, to, n, g, gp, value, d, i string
expectErr bool
numMessages int
}
func TestTransactionValidation(t *testing.T) {
t.Parallel()
var (
// use empty db, there are other tests for the abi-specific stuff
db = newEmpty()
)
testcases := []txtestcase{
// Invalid to checksum
{from: "000000000000000000000000000000000000dead", to: "000000000000000000000000000000000000dead",
n: "0x01", g: "0x20", gp: "0x40", value: "0x01", numMessages: 1},
// valid 0x000000000000000000000000000000000000dEaD
{from: "000000000000000000000000000000000000dead", to: "0x000000000000000000000000000000000000dEaD",
n: "0x01", g: "0x20", gp: "0x40", value: "0x01", numMessages: 0},
// conflicting input and data
{from: "000000000000000000000000000000000000dead", to: "0x000000000000000000000000000000000000dEaD",
n: "0x01", g: "0x20", gp: "0x40", value: "0x01", d: "0x01", i: "0x02", expectErr: true},
// Data can't be parsed
{from: "000000000000000000000000000000000000dead", to: "0x000000000000000000000000000000000000dEaD",
n: "0x01", g: "0x20", gp: "0x40", value: "0x01", d: "0x0102", numMessages: 1},
// Data (on Input) can't be parsed
{from: "000000000000000000000000000000000000dead", to: "0x000000000000000000000000000000000000dEaD",
n: "0x01", g: "0x20", gp: "0x40", value: "0x01", i: "0x0102", numMessages: 1},
// Send to 0
{from: "000000000000000000000000000000000000dead", to: "0x0000000000000000000000000000000000000000",
n: "0x01", g: "0x20", gp: "0x40", value: "0x01", numMessages: 1},
// Create empty contract (no value)
{from: "000000000000000000000000000000000000dead", to: "",
n: "0x01", g: "0x20", gp: "0x40", value: "0x00", numMessages: 1},
// Create empty contract (with value)
{from: "000000000000000000000000000000000000dead", to: "",
n: "0x01", g: "0x20", gp: "0x40", value: "0x01", expectErr: true},
// Small payload for create
{from: "000000000000000000000000000000000000dead", to: "",
n: "0x01", g: "0x20", gp: "0x40", value: "0x01", d: "0x01", numMessages: 1},
}
for i, test := range testcases {
msgs, err := db.ValidateTransaction(nil, dummyTxArgs(test))
if err == nil && test.expectErr {
t.Errorf("Test %d, expected error", i)
for _, msg := range msgs.Messages {
t.Logf("* %s: %s", msg.Typ, msg.Message)
}
}
if err != nil && !test.expectErr {
t.Errorf("Test %d, unexpected error: %v", i, err)
}
if err == nil {
got := len(msgs.Messages)
if got != test.numMessages {
for _, msg := range msgs.Messages {
t.Logf("* %s: %s", msg.Typ, msg.Message)
}
t.Errorf("Test %d, expected %d messages, got %d", i, test.numMessages, got)
} else {
//Debug printout, remove later
for _, msg := range msgs.Messages {
t.Logf("* [%d] %s: %s", i, msg.Typ, msg.Message)
}
t.Log()
}
}
}
}

View file

@ -1,240 +0,0 @@
// Copyright 2018 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/>.
package rules
import (
"encoding/json"
"errors"
"fmt"
"os"
"strings"
"github.com/dop251/goja"
"github.com/ethereum/go-ethereum/internal/ethapi"
"github.com/ethereum/go-ethereum/internal/jsre/deps"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/signer/core"
"github.com/ethereum/go-ethereum/signer/storage"
)
// consoleOutput is an override for the console.log and console.error methods to
// stream the output into the configured output stream instead of stdout.
func consoleOutput(call goja.FunctionCall) goja.Value {
output := []string{"JS:> "}
for _, argument := range call.Arguments {
output = append(output, fmt.Sprintf("%v", argument))
}
fmt.Fprintln(os.Stderr, strings.Join(output, " "))
return goja.Undefined()
}
// rulesetUI provides an implementation of UIClientAPI that evaluates a javascript
// file for each defined UI-method
type rulesetUI struct {
next core.UIClientAPI // The next handler, for manual processing
storage storage.Storage
jsRules string // The rules to use
}
func NewRuleEvaluator(next core.UIClientAPI, jsbackend storage.Storage) (*rulesetUI, error) {
c := &rulesetUI{
next: next,
storage: jsbackend,
jsRules: "",
}
return c, nil
}
func (r *rulesetUI) RegisterUIServer(api *core.UIServerAPI) {
r.next.RegisterUIServer(api)
// TODO, make it possible to query from js
}
func (r *rulesetUI) Init(javascriptRules string) error {
r.jsRules = javascriptRules
return nil
}
func (r *rulesetUI) execute(jsfunc string, jsarg interface{}) (goja.Value, error) {
// Instantiate a fresh vm engine every time
vm := goja.New()
// Set the native callbacks
consoleObj := vm.NewObject()
consoleObj.Set("log", consoleOutput)
consoleObj.Set("error", consoleOutput)
vm.Set("console", consoleObj)
storageObj := vm.NewObject()
storageObj.Set("put", func(call goja.FunctionCall) goja.Value {
key, val := call.Argument(0).String(), call.Argument(1).String()
if val == "" {
r.storage.Del(key)
} else {
r.storage.Put(key, val)
}
return goja.Null()
})
storageObj.Set("get", func(call goja.FunctionCall) goja.Value {
goval, _ := r.storage.Get(call.Argument(0).String())
jsval := vm.ToValue(goval)
return jsval
})
vm.Set("storage", storageObj)
// Load bootstrap libraries
script, err := goja.Compile("bignumber.js", deps.BigNumberJS, true)
if err != nil {
log.Warn("Failed loading libraries", "err", err)
return goja.Undefined(), err
}
vm.RunProgram(script)
// Run the actual rule implementation
_, err = vm.RunString(r.jsRules)
if err != nil {
log.Warn("Execution failed", "err", err)
return goja.Undefined(), err
}
// And the actual call
// All calls are objects with the parameters being keys in that object.
// To provide additional insulation between js and go, we serialize it into JSON on the Go-side,
// and deserialize it on the JS side.
jsonbytes, err := json.Marshal(jsarg)
if err != nil {
log.Warn("failed marshalling data", "data", jsarg)
return goja.Undefined(), err
}
// Now, we call foobar(JSON.parse(<jsondata>)).
var call string
if len(jsonbytes) > 0 {
call = fmt.Sprintf("%v(JSON.parse(%v))", jsfunc, string(jsonbytes))
} else {
call = fmt.Sprintf("%v()", jsfunc)
}
return vm.RunString(call)
}
func (r *rulesetUI) checkApproval(jsfunc string, jsarg []byte, err error) (bool, error) {
if err != nil {
return false, err
}
v, err := r.execute(jsfunc, string(jsarg))
if err != nil {
log.Info("error occurred during execution", "error", err)
return false, err
}
result := v.ToString().String()
if result == "Approve" {
log.Info("Op approved")
return true, nil
} else if result == "Reject" {
log.Info("Op rejected")
return false, nil
}
return false, errors.New("unknown response")
}
func (r *rulesetUI) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) {
jsonreq, err := json.Marshal(request)
approved, err := r.checkApproval("ApproveTx", jsonreq, err)
if err != nil {
log.Info("Rule-based approval error, going to manual", "error", err)
return r.next.ApproveTx(request)
}
if approved {
return core.SignTxResponse{
Transaction: request.Transaction,
Approved: true},
nil
}
return core.SignTxResponse{Approved: false}, err
}
func (r *rulesetUI) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) {
jsonreq, err := json.Marshal(request)
approved, err := r.checkApproval("ApproveSignData", jsonreq, err)
if err != nil {
log.Info("Rule-based approval error, going to manual", "error", err)
return r.next.ApproveSignData(request)
}
if approved {
return core.SignDataResponse{Approved: true}, nil
}
return core.SignDataResponse{Approved: false}, err
}
// 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)
if err != nil {
log.Info("Rule-based approval error, going to manual", "error", err)
return r.next.ApproveListing(request)
}
if approved {
return core.ListResponse{Accounts: request.Accounts}, nil
}
return core.ListResponse{}, err
}
func (r *rulesetUI) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) {
// This cannot be handled by rules, requires setting a password
// dispatch to next
return r.next.ApproveNewAccount(request)
}
func (r *rulesetUI) ShowError(message string) {
log.Error(message)
r.next.ShowError(message)
}
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 {
log.Warn("failed marshalling data", "data", info)
return
}
r.next.OnSignerStartup(info)
_, err = r.execute("OnSignerStartup", string(jsonInfo))
if err != nil {
log.Info("error occurred during execution", "error", err)
}
}
func (r *rulesetUI) OnApprovedTx(tx ethapi.SignTransactionResult) {
jsonTx, err := json.Marshal(tx)
if err != nil {
log.Warn("failed marshalling transaction", "tx", tx)
return
}
_, err = r.execute("OnApprovedTx", string(jsonTx))
if err != nil {
log.Info("error occurred during execution", "error", err)
}
}

View file

@ -1,626 +0,0 @@
// Copyright 2018 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/>.
package rules
import (
"fmt"
"math/big"
"strings"
"testing"
"github.com/ethereum/go-ethereum/accounts"
"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/internal/ethapi"
"github.com/ethereum/go-ethereum/signer/core"
"github.com/ethereum/go-ethereum/signer/core/apitypes"
"github.com/ethereum/go-ethereum/signer/storage"
)
const JS = `
/**
This is an example implementation of a Javascript rule file.
When the signer receives a request over the external API, the corresponding method is evaluated.
Three things can happen:
1. The method returns "Approve". This means the operation is permitted.
2. The method returns "Reject". This means the operation is rejected.
3. Anything else; other return values [*], method not implemented or exception occurred during processing. This means
that the operation will continue to manual processing, via the regular UI method chosen by the user.
[*] Note: Future version of the ruleset may use more complex json-based return values, making it possible to not
only respond Approve/Reject/Manual, but also modify responses. For example, choose to list only one, but not all
accounts in a list-request. The points above will continue to hold for non-json based responses ("Approve"/"Reject").
**/
function ApproveListing(request){
console.log("In js approve listing");
console.log(request.accounts[3].Address)
console.log(request.meta.Remote)
return "Approve"
}
function ApproveTx(request){
console.log("test");
console.log("from");
return "Reject";
}
function test(thing){
console.log(thing.String())
}
`
func mixAddr(a string) (*common.MixedcaseAddress, error) {
return common.NewMixedcaseAddressFromString(a)
}
type alwaysDenyUI struct{}
func (alwaysDenyUI) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) {
return core.UserInputResponse{}, nil
}
func (alwaysDenyUI) RegisterUIServer(api *core.UIServerAPI) {
}
func (alwaysDenyUI) OnSignerStartup(info core.StartupInfo) {
}
func (alwaysDenyUI) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) {
return core.SignTxResponse{Transaction: request.Transaction, Approved: false}, nil
}
func (alwaysDenyUI) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) {
return core.SignDataResponse{Approved: false}, nil
}
func (alwaysDenyUI) ApproveListing(request *core.ListRequest) (core.ListResponse, error) {
return core.ListResponse{Accounts: nil}, nil
}
func (alwaysDenyUI) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) {
return core.NewAccountResponse{Approved: false}, nil
}
func (alwaysDenyUI) ShowError(message string) {
panic("implement me")
}
func (alwaysDenyUI) ShowInfo(message string) {
panic("implement me")
}
func (alwaysDenyUI) OnApprovedTx(tx ethapi.SignTransactionResult) {
panic("implement me")
}
func initRuleEngine(js string) (*rulesetUI, error) {
r, err := NewRuleEvaluator(&alwaysDenyUI{}, storage.NewEphemeralStorage())
if err != nil {
return nil, fmt.Errorf("failed to create js engine: %v", err)
}
if err = r.Init(js); err != nil {
return nil, fmt.Errorf("failed to load bootstrap js: %v", err)
}
return r, nil
}
func TestListRequest(t *testing.T) {
t.Parallel()
accs := make([]accounts.Account, 5)
for i := range accs {
addr := fmt.Sprintf("000000000000000000000000000000000000000%x", i)
acc := accounts.Account{
Address: common.BytesToAddress(common.Hex2Bytes(addr)),
URL: accounts.URL{Scheme: "test", Path: fmt.Sprintf("acc-%d", i)},
}
accs[i] = acc
}
js := `function ApproveListing(){ return "Approve" }`
r, err := initRuleEngine(js)
if err != nil {
t.Errorf("Couldn't create evaluator %v", err)
return
}
resp, _ := r.ApproveListing(&core.ListRequest{
Accounts: accs,
Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
})
if len(resp.Accounts) != len(accs) {
t.Errorf("Expected check to resolve to 'Approve'")
}
}
func TestSignTxRequest(t *testing.T) {
t.Parallel()
js := `
function ApproveTx(r){
console.log("transaction.from", r.transaction.from);
console.log("transaction.to", r.transaction.to);
console.log("transaction.value", r.transaction.value);
console.log("transaction.nonce", r.transaction.nonce);
if(r.transaction.from.toLowerCase()=="0x0000000000000000000000000000000000001337"){ return "Approve"}
if(r.transaction.from.toLowerCase()=="0x000000000000000000000000000000000000dead"){ return "Reject"}
}`
r, err := initRuleEngine(js)
if err != nil {
t.Errorf("Couldn't create evaluator %v", err)
return
}
to, err := mixAddr("000000000000000000000000000000000000dead")
if err != nil {
t.Error(err)
return
}
from, err := mixAddr("0000000000000000000000000000000000001337")
if err != nil {
t.Error(err)
return
}
t.Logf("to %v", to.Address().String())
resp, err := r.ApproveTx(&core.SignTxRequest{
Transaction: apitypes.SendTxArgs{
From: *from,
To: to},
Callinfo: nil,
Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
})
if err != nil {
t.Errorf("Unexpected error %v", err)
}
if !resp.Approved {
t.Errorf("Expected check to resolve to 'Approve'")
}
}
type dummyUI struct {
calls []string
}
func (d *dummyUI) RegisterUIServer(api *core.UIServerAPI) {
panic("implement me")
}
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
}
func (d *dummyUI) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) {
d.calls = append(d.calls, "ApproveSignData")
return core.SignDataResponse{}, core.ErrRequestDenied
}
func (d *dummyUI) ApproveListing(request *core.ListRequest) (core.ListResponse, error) {
d.calls = append(d.calls, "ApproveListing")
return core.ListResponse{}, core.ErrRequestDenied
}
func (d *dummyUI) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) {
d.calls = append(d.calls, "ApproveNewAccount")
return core.NewAccountResponse{}, core.ErrRequestDenied
}
func (d *dummyUI) ShowError(message string) {
d.calls = append(d.calls, "ShowError")
}
func (d *dummyUI) ShowInfo(message string) {
d.calls = append(d.calls, "ShowInfo")
}
func (d *dummyUI) OnApprovedTx(tx ethapi.SignTransactionResult) {
d.calls = append(d.calls, "OnApprovedTx")
}
func (d *dummyUI) OnSignerStartup(info core.StartupInfo) {
}
// TestForwarding tests that the rule-engine correctly dispatches requests to the next caller
func TestForwarding(t *testing.T) {
t.Parallel()
js := ""
ui := &dummyUI{make([]string, 0)}
jsBackend := storage.NewEphemeralStorage()
r, err := NewRuleEvaluator(ui, jsBackend)
if err != nil {
t.Fatalf("Failed to create js engine: %v", err)
}
if err = r.Init(js); err != nil {
t.Fatalf("Failed to load bootstrap js: %v", err)
}
r.ApproveSignData(nil)
r.ApproveTx(nil)
r.ApproveNewAccount(nil)
r.ApproveListing(nil)
r.ShowError("test")
r.ShowInfo("test")
//This one is not forwarded
r.OnApprovedTx(ethapi.SignTransactionResult{})
expCalls := 6
if len(ui.calls) != expCalls {
t.Errorf("Expected %d forwarded calls, got %d: %s", expCalls, len(ui.calls), strings.Join(ui.calls, ","))
}
}
func TestMissingFunc(t *testing.T) {
t.Parallel()
r, err := initRuleEngine(JS)
if err != nil {
t.Errorf("Couldn't create evaluator %v", err)
return
}
_, err = r.execute("MissingMethod", "test")
if err == nil {
t.Error("Expected error")
}
approved, err := r.checkApproval("MissingMethod", nil, nil)
if err == nil {
t.Errorf("Expected missing method to yield error'")
}
if approved {
t.Errorf("Expected missing method to cause non-approval")
}
t.Logf("Err %v", err)
}
func TestStorage(t *testing.T) {
t.Parallel()
js := `
function testStorage(){
storage.put("mykey", "myvalue")
a = storage.get("mykey")
storage.put("mykey", ["a", "list"]) // Should result in "a,list"
a += storage.get("mykey")
storage.put("mykey", {"an": "object"}) // Should result in "[object Object]"
a += storage.get("mykey")
storage.put("mykey", JSON.stringify({"an": "object"})) // Should result in '{"an":"object"}'
a += storage.get("mykey")
a += storage.get("missingkey") //Missing keys should result in empty string
storage.put("","missing key==noop") // Can't store with 0-length key
a += storage.get("") // Should result in ''
var b = new BigNumber(2)
var c = new BigNumber(16)//"0xf0",16)
var d = b.plus(c)
console.log(d)
return a
}
`
r, err := initRuleEngine(js)
if err != nil {
t.Errorf("Couldn't create evaluator %v", err)
return
}
v, err := r.execute("testStorage", nil)
if err != nil {
t.Errorf("Unexpected error %v", err)
}
retval := v.ToString().String()
if err != nil {
t.Errorf("Unexpected error %v", err)
}
exp := `myvaluea,list[object Object]{"an":"object"}`
if retval != exp {
t.Errorf("Unexpected data, expected '%v', got '%v'", exp, retval)
}
t.Logf("Err %v", err)
}
const ExampleTxWindow = `
function big(str){
if(str.slice(0,2) == "0x"){ return new BigNumber(str.slice(2),16)}
return new BigNumber(str)
}
// Time window: 1 week
var window = 1000* 3600*24*7;
// Limit : 1 ether
var limit = new BigNumber("1e18");
function isLimitOk(transaction){
var value = big(transaction.value)
// Start of our window function
var windowstart = new Date().getTime() - window;
var txs = [];
var stored = storage.get('txs');
if(stored != ""){
txs = JSON.parse(stored)
}
// First, remove all that have passed out of the time-window
var newtxs = txs.filter(function(tx){return tx.tstamp > windowstart});
console.log(txs, newtxs.length);
// Secondly, aggregate the current sum
sum = new BigNumber(0)
sum = newtxs.reduce(function(agg, tx){ return big(tx.value).plus(agg)}, sum);
console.log("ApproveTx > Sum so far", sum);
console.log("ApproveTx > Requested", value.toNumber());
// Would we exceed weekly limit ?
return sum.plus(value).lt(limit)
}
function ApproveTx(r){
console.log(r)
console.log(typeof(r))
if (isLimitOk(r.transaction)){
return "Approve"
}
return "Nope"
}
/**
* OnApprovedTx(str) is called when a transaction has been approved and signed. The parameter
* 'response_str' contains the return value that will be sent to the external caller.
* The return value from this method is ignore - the reason for having this callback is to allow the
* ruleset to keep track of approved transactions.
*
* When implementing rate-limited rules, this callback should be used.
* If a rule responds with neither 'Approve' nor 'Reject' - the tx goes to manual processing. If the user
* then accepts the transaction, this method will be called.
*
* TLDR; Use this method to keep track of signed transactions, instead of using the data in ApproveTx.
*/
function OnApprovedTx(resp){
var value = big(resp.tx.value)
var txs = []
// Load stored transactions
var stored = storage.get('txs');
if(stored != ""){
txs = JSON.parse(stored)
}
// Add this to the storage
txs.push({tstamp: new Date().getTime(), value: value});
storage.put("txs", JSON.stringify(txs));
}
`
func dummyTx(value hexutil.Big) *core.SignTxRequest {
to, _ := mixAddr("000000000000000000000000000000000000dead")
from, _ := mixAddr("000000000000000000000000000000000000dead")
n := hexutil.Uint64(3)
gas := hexutil.Uint64(21000)
gasPrice := hexutil.Big(*big.NewInt(2000000))
return &core.SignTxRequest{
Transaction: apitypes.SendTxArgs{
From: *from,
To: to,
Value: value,
Nonce: n,
GasPrice: &gasPrice,
Gas: gas,
},
Callinfo: []apitypes.ValidationInfo{
{Typ: "Warning", Message: "All your base are belong to us"},
},
Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
}
}
func dummyTxWithV(value uint64) *core.SignTxRequest {
v := new(big.Int).SetUint64(value)
h := hexutil.Big(*v)
return dummyTx(h)
}
func dummySigned(value *big.Int) *types.Transaction {
to := common.HexToAddress("000000000000000000000000000000000000dead")
gas := uint64(21000)
gasPrice := big.NewInt(2000000)
data := make([]byte, 0)
return types.NewTransaction(3, to, value, gas, gasPrice, data)
}
func TestLimitWindow(t *testing.T) {
t.Parallel()
r, err := initRuleEngine(ExampleTxWindow)
if err != nil {
t.Errorf("Couldn't create evaluator %v", err)
return
}
// 0.3 ether: 429D069189E0000 wei
v := new(big.Int).SetBytes(common.Hex2Bytes("0429D069189E0000"))
h := hexutil.Big(*v)
// The first three should succeed
for i := 0; i < 3; i++ {
unsigned := dummyTx(h)
resp, err := r.ApproveTx(unsigned)
if err != nil {
t.Errorf("Unexpected error %v", err)
}
if !resp.Approved {
t.Errorf("Expected check to resolve to 'Approve'")
}
// Create a dummy signed transaction
response := ethapi.SignTransactionResult{
Tx: dummySigned(v),
Raw: common.Hex2Bytes("deadbeef"),
}
r.OnApprovedTx(response)
}
// Fourth should fail
resp, _ := r.ApproveTx(dummyTx(h))
if resp.Approved {
t.Errorf("Expected check to resolve to 'Reject'")
}
}
// dontCallMe is used as a next-handler that does not want to be called - it invokes test failure
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) RegisterUIServer(api *core.UIServerAPI) {
}
func (d *dontCallMe) OnSignerStartup(info core.StartupInfo) {
}
func (d *dontCallMe) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) {
d.t.Fatalf("Did not expect next-handler to be called")
return core.SignTxResponse{}, core.ErrRequestDenied
}
func (d *dontCallMe) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) {
d.t.Fatalf("Did not expect next-handler to be called")
return core.SignDataResponse{}, core.ErrRequestDenied
}
func (d *dontCallMe) ApproveListing(request *core.ListRequest) (core.ListResponse, error) {
d.t.Fatalf("Did not expect next-handler to be called")
return core.ListResponse{}, core.ErrRequestDenied
}
func (d *dontCallMe) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) {
d.t.Fatalf("Did not expect next-handler to be called")
return core.NewAccountResponse{}, core.ErrRequestDenied
}
func (d *dontCallMe) ShowError(message string) {
d.t.Fatalf("Did not expect next-handler to be called")
}
func (d *dontCallMe) ShowInfo(message string) {
d.t.Fatalf("Did not expect next-handler to be called")
}
func (d *dontCallMe) OnApprovedTx(tx ethapi.SignTransactionResult) {
d.t.Fatalf("Did not expect next-handler to be called")
}
// TestContextIsCleared tests that the rule-engine does not retain variables over several requests.
// if it does, that would be bad since developers may rely on that to store data,
// instead of using the disk-based data storage
func TestContextIsCleared(t *testing.T) {
t.Parallel()
js := `
function ApproveTx(){
if (typeof foobar == 'undefined') {
foobar = "Approve"
}
console.log(foobar)
if (foobar == "Approve"){
foobar = "Reject"
}else{
foobar = "Approve"
}
return foobar
}
`
ui := &dontCallMe{t}
r, err := NewRuleEvaluator(ui, storage.NewEphemeralStorage())
if err != nil {
t.Fatalf("Failed to create js engine: %v", err)
}
if err = r.Init(js); err != nil {
t.Fatalf("Failed to load bootstrap js: %v", err)
}
tx := dummyTxWithV(0)
r1, _ := r.ApproveTx(tx)
r2, _ := r.ApproveTx(tx)
if r1.Approved != r2.Approved {
t.Errorf("Expected execution context to be cleared between executions")
}
}
func TestSignData(t *testing.T) {
t.Parallel()
js := `function ApproveListing(){
return "Approve"
}
function ApproveSignData(r){
if( r.address.toLowerCase() == "0x694267f14675d7e1b9494fd8d72fefe1755710fa")
{
if(r.messages[0].value.indexOf("bazonk") >= 0){
return "Approve"
}
return "Reject"
}
// Otherwise goes to manual processing
}`
r, err := initRuleEngine(js)
if err != nil {
t.Errorf("Couldn't create evaluator %v", err)
return
}
message := "baz bazonk foo"
hash, rawdata := accounts.TextAndHash([]byte(message))
addr, _ := mixAddr("0x694267f14675d7e1b9494fd8d72fefe1755710fa")
t.Logf("address %v %v\n", addr.String(), addr.Original())
nvt := []*apitypes.NameValueType{
{
Name: "message",
Typ: "text/plain",
Value: message,
},
}
resp, err := r.ApproveSignData(&core.SignDataRequest{
Address: *addr,
Messages: nvt,
Hash: hash,
Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
Rawdata: []byte(rawdata),
})
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
if !resp.Approved {
t.Fatalf("Expected approved")
}
}

View file

@ -1,178 +0,0 @@
// Copyright 2018 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/>.
package storage
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/json"
"io"
"os"
"github.com/ethereum/go-ethereum/log"
)
type storedCredential struct {
// The iv
Iv []byte `json:"iv"`
// The ciphertext
CipherText []byte `json:"c"`
}
// AESEncryptedStorage is a storage type which is backed by a json-file. The json-file contains
// key-value mappings, where the keys are _not_ encrypted, only the values are.
type AESEncryptedStorage struct {
// File to read/write credentials
filename string
// Key stored in base64
key []byte
}
// NewAESEncryptedStorage creates a new encrypted storage backed by the given file/key
func NewAESEncryptedStorage(filename string, key []byte) *AESEncryptedStorage {
return &AESEncryptedStorage{
filename: filename,
key: key,
}
}
// Put stores a value by key. 0-length keys results in noop.
func (s *AESEncryptedStorage) Put(key, value string) {
if len(key) == 0 {
return
}
data, err := s.readEncryptedStorage()
if err != nil {
log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename)
return
}
ciphertext, iv, err := encrypt(s.key, []byte(value), []byte(key))
if err != nil {
log.Warn("Failed to encrypt entry", "err", err)
return
}
encrypted := storedCredential{Iv: iv, CipherText: ciphertext}
data[key] = encrypted
if err = s.writeEncryptedStorage(data); err != nil {
log.Warn("Failed to write entry", "err", err)
}
}
// Get returns the previously stored value, or an error if it does not exist or
// key is of 0-length.
func (s *AESEncryptedStorage) Get(key string) (string, error) {
if len(key) == 0 {
return "", ErrZeroKey
}
data, err := s.readEncryptedStorage()
if err != nil {
log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename)
return "", err
}
encrypted, exist := data[key]
if !exist {
log.Warn("Key does not exist", "key", key)
return "", ErrNotFound
}
entry, err := decrypt(s.key, encrypted.Iv, encrypted.CipherText, []byte(key))
if err != nil {
log.Warn("Failed to decrypt key", "key", key)
return "", err
}
return string(entry), nil
}
// Del removes a key-value pair. If the key doesn't exist, the method is a noop.
func (s *AESEncryptedStorage) Del(key string) {
data, err := s.readEncryptedStorage()
if err != nil {
log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename)
return
}
delete(data, key)
if err = s.writeEncryptedStorage(data); err != nil {
log.Warn("Failed to write entry", "err", err)
}
}
// readEncryptedStorage reads the file with encrypted creds
func (s *AESEncryptedStorage) readEncryptedStorage() (map[string]storedCredential, error) {
creds := make(map[string]storedCredential)
raw, err := os.ReadFile(s.filename)
if err != nil {
if os.IsNotExist(err) {
// Doesn't exist yet
return creds, nil
}
log.Warn("Failed to read encrypted storage", "err", err, "file", s.filename)
}
if err = json.Unmarshal(raw, &creds); err != nil {
log.Warn("Failed to unmarshal encrypted storage", "err", err, "file", s.filename)
return nil, err
}
return creds, nil
}
// writeEncryptedStorage write the file with encrypted creds
func (s *AESEncryptedStorage) writeEncryptedStorage(creds map[string]storedCredential) error {
raw, err := json.Marshal(creds)
if err != nil {
return err
}
if err = os.WriteFile(s.filename, raw, 0600); err != nil {
return err
}
return nil
}
// encrypt encrypts plaintext with the given key, with additional data
// The 'additionalData' is used to place the (plaintext) KV-store key into the V,
// to prevent the possibility to alter a K, or swap two entries in the KV store with each other.
func encrypt(key []byte, plaintext []byte, additionalData []byte) ([]byte, []byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, nil, err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, nil, err
}
nonce := make([]byte, aesgcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, nil, err
}
ciphertext := aesgcm.Seal(nil, nonce, plaintext, additionalData)
return ciphertext, nonce, nil
}
func decrypt(key []byte, nonce []byte, ciphertext []byte, additionalData []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, additionalData)
if err != nil {
return nil, err
}
return plaintext, nil
}

View file

@ -1,159 +0,0 @@
// Copyright 2018 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/>.
package storage
import (
"bytes"
"encoding/json"
"fmt"
"os"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/mattn/go-colorable"
"golang.org/x/exp/slog"
)
func TestEncryption(t *testing.T) {
t.Parallel()
// key := []byte("AES256Key-32Characters1234567890")
// plaintext := []byte(value)
key := []byte("AES256Key-32Characters1234567890")
plaintext := []byte("exampleplaintext")
c, iv, err := encrypt(key, plaintext, nil)
if err != nil {
t.Fatal(err)
}
t.Logf("Ciphertext %x, nonce %x\n", c, iv)
p, err := decrypt(key, iv, c, nil)
if err != nil {
t.Fatal(err)
}
t.Logf("Plaintext %v\n", string(p))
if !bytes.Equal(plaintext, p) {
t.Errorf("Failed: expected plaintext recovery, got %v expected %v", string(plaintext), string(p))
}
}
func TestFileStorage(t *testing.T) {
t.Parallel()
a := map[string]storedCredential{
"secret": {
Iv: common.Hex2Bytes("cdb30036279601aeee60f16b"),
CipherText: common.Hex2Bytes("f311ac49859d7260c2c464c28ffac122daf6be801d3cfd3edcbde7e00c9ff74f"),
},
"secret2": {
Iv: common.Hex2Bytes("afb8a7579bf971db9f8ceeed"),
CipherText: common.Hex2Bytes("2df87baf86b5073ef1f03e3cc738de75b511400f5465bb0ddeacf47ae4dc267d"),
},
}
d := t.TempDir()
stored := &AESEncryptedStorage{
filename: fmt.Sprintf("%v/vault.json", d),
key: []byte("AES256Key-32Characters1234567890"),
}
stored.writeEncryptedStorage(a)
read := &AESEncryptedStorage{
filename: fmt.Sprintf("%v/vault.json", d),
key: []byte("AES256Key-32Characters1234567890"),
}
creds, err := read.readEncryptedStorage()
if err != nil {
t.Fatal(err)
}
for k, v := range a {
if v2, exist := creds[k]; !exist {
t.Errorf("Missing entry %v", k)
} else {
if !bytes.Equal(v.CipherText, v2.CipherText) {
t.Errorf("Wrong ciphertext, expected %x got %x", v.CipherText, v2.CipherText)
}
if !bytes.Equal(v.Iv, v2.Iv) {
t.Errorf("Wrong iv")
}
}
}
}
func TestEnd2End(t *testing.T) {
t.Parallel()
log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(colorable.NewColorableStderr(), slog.LevelInfo, true)))
d := t.TempDir()
s1 := &AESEncryptedStorage{
filename: fmt.Sprintf("%v/vault.json", d),
key: []byte("AES256Key-32Characters1234567890"),
}
s2 := &AESEncryptedStorage{
filename: fmt.Sprintf("%v/vault.json", d),
key: []byte("AES256Key-32Characters1234567890"),
}
s1.Put("bazonk", "foobar")
if v, err := s2.Get("bazonk"); v != "foobar" || err != nil {
t.Errorf("Expected bazonk->foobar (nil error), got '%v' (%v error)", v, err)
}
}
func TestSwappedKeys(t *testing.T) {
t.Parallel()
// It should not be possible to swap the keys/values, so that
// K1:V1, K2:V2 can be swapped into K1:V2, K2:V1
log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(colorable.NewColorableStderr(), slog.LevelInfo, true)))
d := t.TempDir()
s1 := &AESEncryptedStorage{
filename: fmt.Sprintf("%v/vault.json", d),
key: []byte("AES256Key-32Characters1234567890"),
}
s1.Put("k1", "v1")
s1.Put("k2", "v2")
// Now make a modified copy
creds := make(map[string]storedCredential)
raw, err := os.ReadFile(s1.filename)
if err != nil {
t.Fatal(err)
}
if err = json.Unmarshal(raw, &creds); err != nil {
t.Fatal(err)
}
swap := func() {
// Turn it into K1:V2, K2:V2
v1, v2 := creds["k1"], creds["k2"]
creds["k2"], creds["k1"] = v1, v2
raw, err = json.Marshal(creds)
if err != nil {
t.Fatal(err)
}
if err = os.WriteFile(s1.filename, raw, 0600); err != nil {
t.Fatal(err)
}
}
swap()
if v, _ := s1.Get("k1"); v != "" {
t.Errorf("swapped value should return empty")
}
swap()
if v, _ := s1.Get("k1"); v != "v1" {
t.Errorf("double-swapped value should work fine")
}
}

View file

@ -1,86 +0,0 @@
// Copyright 2018 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/>.
package storage
import "errors"
var (
// ErrZeroKey is returned if an attempt was made to inset a 0-length key.
ErrZeroKey = errors.New("0-length key")
// ErrNotFound is returned if an unknown key is attempted to be retrieved.
ErrNotFound = errors.New("not found")
)
type Storage interface {
// Put stores a value by key. 0-length keys results in noop.
Put(key, value string)
// Get returns the previously stored value, or an error if the key is 0-length
// or unknown.
Get(key string) (string, error)
// Del removes a key-value pair. If the key doesn't exist, the method is a noop.
Del(key string)
}
// EphemeralStorage is an in-memory storage that does
// not persist values to disk. Mainly used for testing
type EphemeralStorage struct {
data map[string]string
}
// Put stores a value by key. 0-length keys results in noop.
func (s *EphemeralStorage) Put(key, value string) {
if len(key) == 0 {
return
}
s.data[key] = value
}
// Get returns the previously stored value, or an error if the key is 0-length
// or unknown.
func (s *EphemeralStorage) Get(key string) (string, error) {
if len(key) == 0 {
return "", ErrZeroKey
}
if v, ok := s.data[key]; ok {
return v, nil
}
return "", ErrNotFound
}
// Del removes a key-value pair. If the key doesn't exist, the method is a noop.
func (s *EphemeralStorage) Del(key string) {
delete(s.data, key)
}
func NewEphemeralStorage() Storage {
s := &EphemeralStorage{
data: make(map[string]string),
}
return s
}
// NoStorage is a dummy construct which doesn't remember anything you tell it
type NoStorage struct{}
func (s *NoStorage) Put(key, value string) {}
func (s *NoStorage) Del(key string) {}
func (s *NoStorage) Get(key string) (string, error) {
return "", errors.New("missing key, I probably forgot")
}