[#1216] neofs-cli: Reuse key retrieving code between modules

Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
This commit is contained in:
Evgenii Stratonikov 2022-03-28 13:23:45 +03:00 committed by LeL
parent 006d6e8b48
commit 8d79168129
5 changed files with 169 additions and 127 deletions

View file

@ -0,0 +1,26 @@
package key
import (
"crypto/ecdsa"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
)
const nep2Base58Length = 58
// FromNEP2 extracts private key from NEP2-encrypted string.
func FromNEP2(encryptedWif string) (*ecdsa.PrivateKey, error) {
pass, err := getPassword()
if err != nil {
printVerbose("Can't read password: %v", err)
return nil, ErrInvalidPassword
}
k, err := keys.NEP2Decrypt(encryptedWif, pass, keys.NEP2ScryptParams())
if err != nil {
printVerbose("Invalid key or password: %v", err)
return nil, ErrInvalidPassword
}
return &k.PrivateKey, nil
}

View file

@ -0,0 +1,54 @@
package key
import (
"crypto/ecdsa"
"fmt"
"os"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/wallet"
)
// Get returns private key from the followind sources:
// 1. WIF
// 2. Raw binary key
// 3. Wallet file
// 4. NEP-2 encrypted WIF.
// Ideally we want to touch file-system on the last step.
// However, asking for NEP-2 password seems to be confusing if we provide a wallet.
func Get(keyDesc string, address string) (*ecdsa.PrivateKey, error) {
priv, err := keys.NewPrivateKeyFromWIF(keyDesc)
if err == nil {
return &priv.PrivateKey, nil
}
p, err := getKeyFromFile(keyDesc)
if err == nil {
return p, nil
}
w, err := wallet.NewWalletFromFile(keyDesc)
if err == nil {
return FromWallet(w, address)
}
if len(keyDesc) == nep2Base58Length {
return FromNEP2(keyDesc)
}
return nil, ErrInvalidKey
}
func getKeyFromFile(keyPath string) (*ecdsa.PrivateKey, error) {
data, err := os.ReadFile(keyPath)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidKey, err)
}
priv, err := keys.NewPrivateKeyFromBytes(data)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidKey, err)
}
return &priv.PrivateKey, nil
}

View file

@ -0,0 +1,74 @@
package key
import (
"crypto/ecdsa"
"errors"
"fmt"
"github.com/nspcc-dev/neo-go/cli/flags"
"github.com/nspcc-dev/neo-go/cli/input"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/spf13/viper"
)
// Key-related errors.
var (
ErrInvalidKey = errors.New("provided key is incorrect")
ErrInvalidAddress = errors.New("--address option must be specified and valid")
ErrInvalidPassword = errors.New("invalid password for the encrypted key")
)
// FromWallet returns private key of the wallet account.
func FromWallet(w *wallet.Wallet, addrStr string) (*ecdsa.PrivateKey, error) {
var (
addr util.Uint160
err error
)
if addrStr == "" {
printVerbose("Using default wallet address")
addr = w.GetChangeAddress()
} else {
addr, err = flags.ParseAddress(addrStr)
if err != nil {
printVerbose("Can't parse address: %s", addrStr)
return nil, ErrInvalidAddress
}
}
acc := w.GetAccount(addr)
if acc == nil {
printVerbose("Can't find wallet account for %s", addrStr)
return nil, ErrInvalidAddress
}
pass, err := getPassword()
if err != nil {
printVerbose("Can't read password: %v", err)
return nil, ErrInvalidPassword
}
if err := acc.Decrypt(pass, keys.NEP2ScryptParams()); err != nil {
printVerbose("Can't decrypt account: %v", err)
return nil, ErrInvalidPassword
}
return &acc.PrivateKey().PrivateKey, nil
}
func getPassword() (string, error) {
// this check allows empty passwords
if viper.IsSet("password") {
return viper.GetString("password"), nil
}
return input.ReadPassword("Enter password > ")
}
func printVerbose(format string, a ...interface{}) {
if viper.GetBool("verbose") {
fmt.Printf(format+"\n", a...)
}
}

View file

@ -11,6 +11,7 @@ import (
"github.com/nspcc-dev/neo-go/cli/input" "github.com/nspcc-dev/neo-go/cli/input"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/key"
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/term" "golang.org/x/term"
@ -55,13 +56,13 @@ func Test_getKey(t *testing.T) {
Writer: io.Discard, Writer: io.Discard,
}, "") }, "")
checkKeyError(t, filepath.Join(dir, "badfile"), errInvalidKey) checkKeyError(t, filepath.Join(dir, "badfile"), key.ErrInvalidKey)
t.Run("wallet", func(t *testing.T) { t.Run("wallet", func(t *testing.T) {
checkKeyError(t, wallPath, errInvalidPassword) checkKeyError(t, wallPath, key.ErrInvalidPassword)
in.WriteString("invalid\r") in.WriteString("invalid\r")
checkKeyError(t, wallPath, errInvalidPassword) checkKeyError(t, wallPath, key.ErrInvalidPassword)
in.WriteString("pass\r") in.WriteString("pass\r")
checkKey(t, wallPath, acc2.PrivateKey()) // default account checkKey(t, wallPath, acc2.PrivateKey()) // default account
@ -71,12 +72,12 @@ func Test_getKey(t *testing.T) {
checkKey(t, wallPath, acc1.PrivateKey()) checkKey(t, wallPath, acc1.PrivateKey())
viper.Set(address, "not an address") viper.Set(address, "not an address")
checkKeyError(t, wallPath, errInvalidAddress) checkKeyError(t, wallPath, key.ErrInvalidAddress)
acc, err := wallet.NewAccount() acc, err := wallet.NewAccount()
require.NoError(t, err) require.NoError(t, err)
viper.Set(address, acc.Address) viper.Set(address, acc.Address)
checkKeyError(t, wallPath, errInvalidAddress) checkKeyError(t, wallPath, key.ErrInvalidAddress)
}) })
t.Run("WIF", func(t *testing.T) { t.Run("WIF", func(t *testing.T) {
@ -84,20 +85,20 @@ func Test_getKey(t *testing.T) {
}) })
t.Run("NEP-2", func(t *testing.T) { t.Run("NEP-2", func(t *testing.T) {
checkKeyError(t, nep2, errInvalidPassword) checkKeyError(t, nep2, key.ErrInvalidPassword)
in.WriteString("invalid\r") in.WriteString("invalid\r")
checkKeyError(t, nep2, errInvalidPassword) checkKeyError(t, nep2, key.ErrInvalidPassword)
in.WriteString("pass\r") in.WriteString("pass\r")
checkKey(t, nep2, nep2Key) checkKey(t, nep2, nep2Key)
t.Run("password from config", func(t *testing.T) { t.Run("password from config", func(t *testing.T) {
viper.Set(password, "invalid") viper.Set("password", "invalid")
in.WriteString("pass\r") in.WriteString("pass\r")
checkKeyError(t, nep2, errInvalidPassword) checkKeyError(t, nep2, key.ErrInvalidPassword)
viper.Set(password, "pass") viper.Set("password", "pass")
in.WriteString("invalid\r") in.WriteString("invalid\r")
checkKey(t, nep2, nep2Key) checkKey(t, nep2, nep2Key)
}) })

View file

@ -9,12 +9,9 @@ import (
"strings" "strings"
"github.com/mitchellh/go-homedir" "github.com/mitchellh/go-homedir"
"github.com/nspcc-dev/neo-go/cli/flags"
"github.com/nspcc-dev/neo-go/cli/input"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/wallet"
internalclient "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/client" internalclient "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/client"
"github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/key"
"github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/acl" "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/acl"
bearerCli "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/bearer" bearerCli "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/bearer"
"github.com/nspcc-dev/neofs-node/misc" "github.com/nspcc-dev/neofs-node/misc"
@ -56,8 +53,6 @@ const (
addressDefault = "" addressDefault = ""
addressUsage = "address of wallet account" addressUsage = "address of wallet account"
password = "password"
rpc = "rpc-endpoint" rpc = "rpc-endpoint"
rpcShorthand = "r" rpcShorthand = "r"
rpcDefault = "" rpcDefault = ""
@ -94,11 +89,8 @@ and much more!`,
} }
var ( var (
errInvalidKey = errors.New("provided key is incorrect")
errInvalidEndpoint = errors.New("provided RPC endpoint is incorrect") errInvalidEndpoint = errors.New("provided RPC endpoint is incorrect")
errCantGenerateKey = errors.New("can't generate new private key") errCantGenerateKey = errors.New("can't generate new private key")
errInvalidAddress = errors.New("--address option must be specified and valid")
errInvalidPassword = errors.New("invalid password for the encrypted key")
) )
// Execute adds all child commands to the root command and sets flags appropriately. // Execute adds all child commands to the root command and sets flags appropriately.
@ -171,8 +163,6 @@ func initConfig() {
} }
} }
const nep2Base58Length = 58
// getKey returns private key that was provided in global arguments. // getKey returns private key that was provided in global arguments.
func getKey() (*ecdsa.PrivateKey, error) { func getKey() (*ecdsa.PrivateKey, error) {
if viper.GetBool(generateKey) { if viper.GetBool(generateKey) {
@ -186,110 +176,7 @@ func getKey() (*ecdsa.PrivateKey, error) {
} }
func getKeyNoGenerate() (*ecdsa.PrivateKey, error) { func getKeyNoGenerate() (*ecdsa.PrivateKey, error) {
// Ideally we want to touch file-system on the last step. return key.Get(viper.GetString(walletPath), viper.GetString(address))
// However, asking for NEP-2 password seems to be confusing if we provide a wallet.
// Thus we try keys in the following order:
// 1. WIF
// 2. Raw binary key
// 3. Wallet file
// 4. NEP-2 encrypted WIF.
keyDesc := viper.GetString(walletPath)
priv, err := keys.NewPrivateKeyFromWIF(keyDesc)
if err == nil {
return &priv.PrivateKey, nil
}
p, err := getKeyFromFile(keyDesc)
if err == nil {
return p, nil
}
w, err := wallet.NewWalletFromFile(keyDesc)
if err == nil {
return getKeyFromWallet(w, viper.GetString(address))
}
if len(keyDesc) == nep2Base58Length {
return getKeyFromNEP2(keyDesc)
}
return nil, errInvalidKey
}
func getPassword() (string, error) {
// this check allows empty passwords
if viper.IsSet(password) {
return viper.GetString(password), nil
}
return input.ReadPassword("Enter password > ")
}
func getKeyFromFile(keyPath string) (*ecdsa.PrivateKey, error) {
data, err := os.ReadFile(keyPath)
if err != nil {
return nil, fmt.Errorf("%w: %v", errInvalidKey, err)
}
priv, err := keys.NewPrivateKeyFromBytes(data)
if err != nil {
return nil, fmt.Errorf("%w: %v", errInvalidKey, err)
}
return &priv.PrivateKey, nil
}
func getKeyFromNEP2(encryptedWif string) (*ecdsa.PrivateKey, error) {
pass, err := getPassword()
if err != nil {
printVerbose("Can't read password: %v", err)
return nil, errInvalidPassword
}
k, err := keys.NEP2Decrypt(encryptedWif, pass, keys.NEP2ScryptParams())
if err != nil {
printVerbose("Invalid key or password: %v", err)
return nil, errInvalidPassword
}
return &k.PrivateKey, nil
}
func getKeyFromWallet(w *wallet.Wallet, addrStr string) (*ecdsa.PrivateKey, error) {
var (
addr util.Uint160
err error
)
if addrStr == "" {
printVerbose("Using default wallet address")
addr = w.GetChangeAddress()
} else {
addr, err = flags.ParseAddress(addrStr)
if err != nil {
printVerbose("Can't parse address: %s", addrStr)
return nil, errInvalidAddress
}
}
acc := w.GetAccount(addr)
if acc == nil {
printVerbose("Can't find wallet account for %s", addrStr)
return nil, errInvalidAddress
}
pass, err := getPassword()
if err != nil {
printVerbose("Can't read password: %v", err)
return nil, errInvalidPassword
}
if err := acc.Decrypt(pass, keys.NEP2ScryptParams()); err != nil {
printVerbose("Can't decrypt account: %v", err)
return nil, errInvalidPassword
}
return &acc.PrivateKey().PrivateKey, nil
} }
// getEndpointAddress returns network address structure that stores multiaddr // getEndpointAddress returns network address structure that stores multiaddr
@ -311,10 +198,10 @@ type clientWithKey interface {
// reads private key from command args and call prepareAPIClientWithKey with it. // reads private key from command args and call prepareAPIClientWithKey with it.
func prepareAPIClient(cmd *cobra.Command, dst ...clientWithKey) { func prepareAPIClient(cmd *cobra.Command, dst ...clientWithKey) {
key, err := getKey() p, err := getKey()
exitOnErr(cmd, errf("get private key: %w", err)) exitOnErr(cmd, errf("get private key: %w", err))
prepareAPIClientWithKey(cmd, key, dst...) prepareAPIClientWithKey(cmd, p, dst...)
} }
// creates NeoFS API client and writes it to target along with the private key. // creates NeoFS API client and writes it to target along with the private key.