forked from TrueCloudLab/frostfs-node
[#159] Add keyer to neofs-cli
Keyer prints information about private key, public key, NEO3 Wallet, scripthash. It can generate new private key or generate multisig address. Signed-off-by: Alex Vanin <alexey@nspcc.ru>
This commit is contained in:
parent
fcb35d82cf
commit
ded45e1fbc
3 changed files with 337 additions and 0 deletions
|
@ -2,19 +2,24 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neofs-api-go/pkg"
|
"github.com/nspcc-dev/neofs-api-go/pkg"
|
||||||
"github.com/nspcc-dev/neofs-api-go/pkg/token"
|
"github.com/nspcc-dev/neofs-api-go/pkg/token"
|
||||||
v2ACL "github.com/nspcc-dev/neofs-api-go/v2/acl"
|
v2ACL "github.com/nspcc-dev/neofs-api-go/v2/acl"
|
||||||
|
"github.com/nspcc-dev/neofs-node/pkg/util/keyer"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var errKeyerSingleArgument = errors.New("pass only one argument at a time")
|
||||||
|
|
||||||
var (
|
var (
|
||||||
utilCmd = &cobra.Command{
|
utilCmd = &cobra.Command{
|
||||||
Use: "util",
|
Use: "util",
|
||||||
|
@ -42,6 +47,12 @@ var (
|
||||||
Short: "convert representation of extended ACL table",
|
Short: "convert representation of extended ACL table",
|
||||||
RunE: convertEACLTable,
|
RunE: convertEACLTable,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
keyerCmd = &cobra.Command{
|
||||||
|
Use: "keyer",
|
||||||
|
Short: "generate or print information about keys",
|
||||||
|
RunE: processKeyer,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -49,6 +60,7 @@ func init() {
|
||||||
|
|
||||||
utilCmd.AddCommand(signCmd)
|
utilCmd.AddCommand(signCmd)
|
||||||
utilCmd.AddCommand(convertCmd)
|
utilCmd.AddCommand(convertCmd)
|
||||||
|
utilCmd.AddCommand(keyerCmd)
|
||||||
|
|
||||||
signCmd.AddCommand(signBearerCmd)
|
signCmd.AddCommand(signBearerCmd)
|
||||||
signBearerCmd.Flags().String("from", "", "File with JSON or binary encoded bearer token to sign")
|
signBearerCmd.Flags().String("from", "", "File with JSON or binary encoded bearer token to sign")
|
||||||
|
@ -63,6 +75,11 @@ func init() {
|
||||||
_ = convertEACLCmd.MarkFlagRequired("from")
|
_ = convertEACLCmd.MarkFlagRequired("from")
|
||||||
convertEACLCmd.Flags().String("to", "", "File to dump extended ACL table (default: binary encoded)")
|
convertEACLCmd.Flags().String("to", "", "File to dump extended ACL table (default: binary encoded)")
|
||||||
convertEACLCmd.Flags().Bool("json", false, "Dump extended ACL table in JSON encoding")
|
convertEACLCmd.Flags().Bool("json", false, "Dump extended ACL table in JSON encoding")
|
||||||
|
|
||||||
|
keyerCmd.Flags().BoolP("generate", "g", false, "generate new private key")
|
||||||
|
keyerCmd.Flags().Bool("hex", false, "print all values in hex encoding")
|
||||||
|
keyerCmd.Flags().BoolP("uncompressed", "u", false, "use uncompressed public key format")
|
||||||
|
keyerCmd.Flags().BoolP("multisig", "m", false, "calculate multisig address from public keys")
|
||||||
}
|
}
|
||||||
|
|
||||||
func signBearerToken(cmd *cobra.Command, _ []string) error {
|
func signBearerToken(cmd *cobra.Command, _ []string) error {
|
||||||
|
@ -157,6 +174,48 @@ func convertEACLTable(cmd *cobra.Command, _ []string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func processKeyer(cmd *cobra.Command, args []string) error {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
|
||||||
|
result = new(keyer.Dashboard)
|
||||||
|
generate, _ = cmd.Flags().GetBool("generate")
|
||||||
|
useHex, _ = cmd.Flags().GetBool("hex")
|
||||||
|
uncompressed, _ = cmd.Flags().GetBool("uncompressed")
|
||||||
|
multisig, _ = cmd.Flags().GetBool("multisig")
|
||||||
|
)
|
||||||
|
|
||||||
|
if multisig {
|
||||||
|
err = result.ParseMultiSig(args)
|
||||||
|
} else {
|
||||||
|
if len(args) > 1 {
|
||||||
|
return errKeyerSingleArgument
|
||||||
|
}
|
||||||
|
|
||||||
|
var argument string
|
||||||
|
if len(args) > 0 {
|
||||||
|
argument = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case generate:
|
||||||
|
err = keyerGenerate(argument, result)
|
||||||
|
case fileExists(argument):
|
||||||
|
err = keyerParseFile(argument, result)
|
||||||
|
default:
|
||||||
|
err = result.ParseString(argument)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result.PrettyPrint(uncompressed, useHex)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func completeBearerToken(btok *token.BearerToken) error {
|
func completeBearerToken(btok *token.BearerToken) error {
|
||||||
if v2 := btok.ToV2(); v2 != nil {
|
if v2 := btok.ToV2(); v2 != nil {
|
||||||
// set eACL table version, because it usually omitted
|
// set eACL table version, because it usually omitted
|
||||||
|
@ -191,3 +250,41 @@ func prettyPrintUnixTime(s string) string {
|
||||||
|
|
||||||
return timestamp.String()
|
return timestamp.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func keyerGenerate(filename string, d *keyer.Dashboard) error {
|
||||||
|
key := make([]byte, keyer.NeoPrivateKeySize)
|
||||||
|
|
||||||
|
_, err := rand.Read(key)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't get random source: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.ParseBinary(key)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't parse key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filename != "" {
|
||||||
|
return ioutil.WriteFile(filename, key, 0600)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileExists(filename string) bool {
|
||||||
|
info, err := os.Stat(filename)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return !info.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func keyerParseFile(filename string, d *keyer.Dashboard) error {
|
||||||
|
data, err := ioutil.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't open %v file: %w", filename, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.ParseBinary(data)
|
||||||
|
}
|
||||||
|
|
122
pkg/util/keyer/dashboard.go
Normal file
122
pkg/util/keyer/dashboard.go
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
package keyer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/elliptic"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"github.com/mr-tron/base58"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Dashboard struct {
|
||||||
|
privKey *keys.PrivateKey
|
||||||
|
pubKey *keys.PublicKey
|
||||||
|
scriptHash3 util.Uint160
|
||||||
|
|
||||||
|
multisigKeys keys.PublicKeys
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d Dashboard) PrettyPrint(uncompressed, useHex bool) {
|
||||||
|
var (
|
||||||
|
data []byte
|
||||||
|
|
||||||
|
privKey, pubKey, wif, wallet3, sh3, shBE3, multiSigAddr string
|
||||||
|
)
|
||||||
|
|
||||||
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
|
||||||
|
|
||||||
|
if d.privKey != nil {
|
||||||
|
privKey = d.privKey.String()
|
||||||
|
|
||||||
|
wif = d.privKey.WIF()
|
||||||
|
if useHex {
|
||||||
|
wif = base58ToHex(wif)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.pubKey != nil {
|
||||||
|
if uncompressed {
|
||||||
|
data = elliptic.Marshal(elliptic.P256(), d.pubKey.X, d.pubKey.Y)
|
||||||
|
} else {
|
||||||
|
data = d.pubKey.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
pubKey = hex.EncodeToString(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !d.scriptHash3.Equals(util.Uint160{}) {
|
||||||
|
sh3 = d.scriptHash3.StringLE()
|
||||||
|
shBE3 = d.scriptHash3.StringBE()
|
||||||
|
wallet3 = address.Uint160ToString(d.scriptHash3)
|
||||||
|
|
||||||
|
if useHex {
|
||||||
|
wallet3 = base58ToHex(wallet3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(d.multisigKeys) != 0 {
|
||||||
|
u160, err := scriptHashFromMultikey(d.multisigKeys)
|
||||||
|
if err != nil {
|
||||||
|
panic("can't create multisig redeem script")
|
||||||
|
}
|
||||||
|
|
||||||
|
multiSigAddr = address.Uint160ToString(u160)
|
||||||
|
}
|
||||||
|
|
||||||
|
if privKey != "" {
|
||||||
|
fmt.Fprintf(w, "PrivateKey\t%s\n", privKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pubKey != "" {
|
||||||
|
fmt.Fprintf(w, "PublicKey\t%s\n", pubKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if wif != "" {
|
||||||
|
fmt.Fprintf(w, "WIF\t%s\n", wif)
|
||||||
|
}
|
||||||
|
|
||||||
|
if wallet3 != "" {
|
||||||
|
fmt.Fprintf(w, "Wallet3.0\t%s\n", wallet3)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sh3 != "" {
|
||||||
|
fmt.Fprintf(w, "ScriptHash3.0\t%s\n", sh3)
|
||||||
|
}
|
||||||
|
|
||||||
|
if shBE3 != "" {
|
||||||
|
fmt.Fprintf(w, "ScriptHash3.0BE\t%s\n", shBE3)
|
||||||
|
}
|
||||||
|
|
||||||
|
if multiSigAddr != "" {
|
||||||
|
fmt.Fprintf(w, "MultiSigAddress\t%s\n", multiSigAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func base58ToHex(data string) string {
|
||||||
|
val, err := base58.Decode(data)
|
||||||
|
if err != nil {
|
||||||
|
panic("produced incorrect base58 value")
|
||||||
|
}
|
||||||
|
|
||||||
|
return hex.EncodeToString(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scriptHashFromMultikey(k keys.PublicKeys) (util.Uint160, error) {
|
||||||
|
script, err := smartcontract.CreateDefaultMultiSigRedeemScript(k)
|
||||||
|
if err != nil {
|
||||||
|
return util.Uint160{}, fmt.Errorf("can't create multisig redeem script: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash.Hash160(script), nil
|
||||||
|
}
|
118
pkg/util/keyer/parser.go
Normal file
118
pkg/util/keyer/parser.go
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
package keyer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/elliptic"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mr-tron/base58"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
NeoPrivateKeySize = 32
|
||||||
|
|
||||||
|
scriptHashSize = 20
|
||||||
|
addressSize = 25
|
||||||
|
publicKeyCompressedSize = 33
|
||||||
|
wifSize = 38
|
||||||
|
publicKeyUncompressedSize = 65
|
||||||
|
)
|
||||||
|
|
||||||
|
var errInputType = errors.New("unknown input type")
|
||||||
|
|
||||||
|
func (d *Dashboard) ParseString(data string) error {
|
||||||
|
// just in case remove 0x prefixes if there are some
|
||||||
|
data = strings.TrimPrefix(data, "0x")
|
||||||
|
data = strings.TrimPrefix(data, "0X")
|
||||||
|
|
||||||
|
var (
|
||||||
|
rawData []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// data could be encoded in base58 or hex formats, try both
|
||||||
|
rawData, err = base58.Decode(data)
|
||||||
|
if err != nil {
|
||||||
|
rawData, err = hex.DecodeString(data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("data is not hex or base58 encoded: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.ParseBinary(rawData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dashboard) ParseBinary(data []byte) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch len(data) {
|
||||||
|
case NeoPrivateKeySize:
|
||||||
|
d.privKey, err = keys.NewPrivateKeyFromBytes(data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't parse private key: %w", err)
|
||||||
|
}
|
||||||
|
case wifSize:
|
||||||
|
d.privKey, err = keys.NewPrivateKeyFromWIF(base58.Encode(data))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't parse WIF: %w", err)
|
||||||
|
}
|
||||||
|
case publicKeyCompressedSize, publicKeyUncompressedSize:
|
||||||
|
d.pubKey, err = keys.NewPublicKeyFromBytes(data, elliptic.P256())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't parse public key: %w", err)
|
||||||
|
}
|
||||||
|
case addressSize:
|
||||||
|
d.scriptHash3, err = address.StringToUint160(base58.Encode(data))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't parse address: %w", err)
|
||||||
|
}
|
||||||
|
case scriptHashSize:
|
||||||
|
sc, err := util.Uint160DecodeBytesLE(data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't parse script hash: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.scriptHash3 = sc
|
||||||
|
default:
|
||||||
|
return errInputType
|
||||||
|
}
|
||||||
|
|
||||||
|
d.fill()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dashboard) ParseMultiSig(data []string) error {
|
||||||
|
d.multisigKeys = make(keys.PublicKeys, 0, len(data))
|
||||||
|
|
||||||
|
for i := range data {
|
||||||
|
data, err := hex.DecodeString(data[i])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pass only hex encoded public keys: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := keys.NewPublicKeyFromBytes(data, elliptic.P256())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pass only hex encoded public keys: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.multisigKeys = append(d.multisigKeys, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dashboard) fill() {
|
||||||
|
if d.privKey != nil {
|
||||||
|
d.pubKey = d.privKey.PublicKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.pubKey != nil {
|
||||||
|
d.scriptHash3 = d.pubKey.GetScriptHash()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue