From ded45e1fbce76f0b15b61991d35ab8733216369e Mon Sep 17 00:00:00 2001 From: Alex Vanin Date: Fri, 6 Nov 2020 15:58:39 +0300 Subject: [PATCH] [#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 --- cmd/neofs-cli/modules/util.go | 97 +++++++++++++++++++++++++++ pkg/util/keyer/dashboard.go | 122 ++++++++++++++++++++++++++++++++++ pkg/util/keyer/parser.go | 118 ++++++++++++++++++++++++++++++++ 3 files changed, 337 insertions(+) create mode 100644 pkg/util/keyer/dashboard.go create mode 100644 pkg/util/keyer/parser.go diff --git a/cmd/neofs-cli/modules/util.go b/cmd/neofs-cli/modules/util.go index e2576adec..f3ec57b75 100644 --- a/cmd/neofs-cli/modules/util.go +++ b/cmd/neofs-cli/modules/util.go @@ -2,19 +2,24 @@ package cmd import ( "bytes" + "crypto/rand" "encoding/json" "errors" "fmt" "io/ioutil" + "os" "strconv" "time" "github.com/nspcc-dev/neofs-api-go/pkg" "github.com/nspcc-dev/neofs-api-go/pkg/token" v2ACL "github.com/nspcc-dev/neofs-api-go/v2/acl" + "github.com/nspcc-dev/neofs-node/pkg/util/keyer" "github.com/spf13/cobra" ) +var errKeyerSingleArgument = errors.New("pass only one argument at a time") + var ( utilCmd = &cobra.Command{ Use: "util", @@ -42,6 +47,12 @@ var ( Short: "convert representation of extended ACL table", RunE: convertEACLTable, } + + keyerCmd = &cobra.Command{ + Use: "keyer", + Short: "generate or print information about keys", + RunE: processKeyer, + } ) func init() { @@ -49,6 +60,7 @@ func init() { utilCmd.AddCommand(signCmd) utilCmd.AddCommand(convertCmd) + utilCmd.AddCommand(keyerCmd) signCmd.AddCommand(signBearerCmd) signBearerCmd.Flags().String("from", "", "File with JSON or binary encoded bearer token to sign") @@ -63,6 +75,11 @@ func init() { _ = convertEACLCmd.MarkFlagRequired("from") 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") + + 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 { @@ -157,6 +174,48 @@ func convertEACLTable(cmd *cobra.Command, _ []string) error { 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 { if v2 := btok.ToV2(); v2 != nil { // set eACL table version, because it usually omitted @@ -191,3 +250,41 @@ func prettyPrintUnixTime(s string) 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) +} diff --git a/pkg/util/keyer/dashboard.go b/pkg/util/keyer/dashboard.go new file mode 100644 index 000000000..bc5f2e76e --- /dev/null +++ b/pkg/util/keyer/dashboard.go @@ -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 +} diff --git a/pkg/util/keyer/parser.go b/pkg/util/keyer/parser.go new file mode 100644 index 000000000..45ed3ebf1 --- /dev/null +++ b/pkg/util/keyer/parser.go @@ -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() + } +}