From eeb53bc14301a54aee7cd7e1475e296155ee986d Mon Sep 17 00:00:00 2001
From: Steven Roose <stevenroose@gmail.com>
Date: Thu, 21 Dec 2017 11:36:05 +0100
Subject: [PATCH] cmd/ethkey: new command line tool for keys (#15438)

ethkey is a new tool that serves as a command line interface to
the basic key management functionalities of geth. It currently
supports:

 - generating keyfiles
 - inspecting keyfiles (print public and private key)
 - signing messages
 - verifying signed messages
---
 cmd/ethkey/README.md   |  41 ++++++++++++
 cmd/ethkey/generate.go | 117 ++++++++++++++++++++++++++++++++
 cmd/ethkey/inspect.go  |  74 +++++++++++++++++++++
 cmd/ethkey/main.go     |  70 +++++++++++++++++++
 cmd/ethkey/message.go  | 148 +++++++++++++++++++++++++++++++++++++++++
 cmd/ethkey/utils.go    |  83 +++++++++++++++++++++++
 6 files changed, 533 insertions(+)
 create mode 100644 cmd/ethkey/README.md
 create mode 100644 cmd/ethkey/generate.go
 create mode 100644 cmd/ethkey/inspect.go
 create mode 100644 cmd/ethkey/main.go
 create mode 100644 cmd/ethkey/message.go
 create mode 100644 cmd/ethkey/utils.go

diff --git a/cmd/ethkey/README.md b/cmd/ethkey/README.md
new file mode 100644
index 000000000..cf72ba43d
--- /dev/null
+++ b/cmd/ethkey/README.md
@@ -0,0 +1,41 @@
+ethkey
+======
+
+ethkey is a simple command-line tool for working with Ethereum keyfiles.
+
+
+# Usage
+
+### `ethkey generate`
+
+Generate a new keyfile.
+If you want to use an existing private key to use in the keyfile, it can be 
+specified by setting `--privatekey` with the location of the file containing the 
+private key.
+
+
+### `ethkey inspect <keyfile>`
+
+Print various information about the keyfile.
+Private key information can be printed by using the `--private` flag;
+make sure to use this feature with great caution!
+
+
+### `ethkey sign <keyfile> <message/file>`
+
+Sign the message with a keyfile.
+It is possible to refer to a file containing the message.
+
+
+### `ethkey verify <address> <signature> <message/file>`
+
+Verify the signature of the message.
+It is possible to refer to a file containing the message.
+
+
+## Passphrases
+
+For every command that uses a keyfile, you will be prompted to provide the 
+passphrase for decrypting the keyfile.  To avoid this message, it is possible
+to pass the passphrase by using the `--passphrase` flag pointing to a file that
+contains the passphrase.
diff --git a/cmd/ethkey/generate.go b/cmd/ethkey/generate.go
new file mode 100644
index 000000000..dee0e9d70
--- /dev/null
+++ b/cmd/ethkey/generate.go
@@ -0,0 +1,117 @@
+package main
+
+import (
+	"crypto/ecdsa"
+	"crypto/rand"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+
+	"github.com/ethereum/go-ethereum/accounts/keystore"
+	"github.com/ethereum/go-ethereum/cmd/utils"
+	"github.com/ethereum/go-ethereum/crypto"
+	"github.com/pborman/uuid"
+	"gopkg.in/urfave/cli.v1"
+)
+
+type outputGenerate struct {
+	Address      string
+	AddressEIP55 string
+}
+
+var commandGenerate = cli.Command{
+	Name:      "generate",
+	Usage:     "generate new keyfile",
+	ArgsUsage: "[ <keyfile> ]",
+	Description: `
+Generate a new keyfile.
+If you want to use an existing private key to use in the keyfile, it can be 
+specified by setting --privatekey with the location of the file containing the 
+private key.`,
+	Flags: []cli.Flag{
+		passphraseFlag,
+		jsonFlag,
+		cli.StringFlag{
+			Name: "privatekey",
+			Usage: "the file from where to read the private key to " +
+				"generate a keyfile for",
+		},
+	},
+	Action: func(ctx *cli.Context) error {
+		// Check if keyfile path given and make sure it doesn't already exist.
+		keyfilepath := ctx.Args().First()
+		if keyfilepath == "" {
+			keyfilepath = defaultKeyfileName
+		}
+		if _, err := os.Stat(keyfilepath); err == nil {
+			utils.Fatalf("Keyfile already exists at %s.", keyfilepath)
+		} else if !os.IsNotExist(err) {
+			utils.Fatalf("Error checking if keyfile exists: %v", err)
+		}
+
+		var privateKey *ecdsa.PrivateKey
+
+		// First check if a private key file is provided.
+		privateKeyFile := ctx.String("privatekey")
+		if privateKeyFile != "" {
+			privateKeyBytes, err := ioutil.ReadFile(privateKeyFile)
+			if err != nil {
+				utils.Fatalf("Failed to read the private key file '%s': %v",
+					privateKeyFile, err)
+			}
+
+			pk, err := crypto.HexToECDSA(string(privateKeyBytes))
+			if err != nil {
+				utils.Fatalf(
+					"Could not construct ECDSA private key from file content: %v",
+					err)
+			}
+			privateKey = pk
+		}
+
+		// If not loaded, generate random.
+		if privateKey == nil {
+			pk, err := ecdsa.GenerateKey(crypto.S256(), rand.Reader)
+			if err != nil {
+				utils.Fatalf("Failed to generate random private key: %v", err)
+			}
+			privateKey = pk
+		}
+
+		// Create the keyfile object with a random UUID.
+		id := uuid.NewRandom()
+		key := &keystore.Key{
+			Id:         id,
+			Address:    crypto.PubkeyToAddress(privateKey.PublicKey),
+			PrivateKey: privateKey,
+		}
+
+		// Encrypt key with passphrase.
+		passphrase := getPassPhrase(ctx, true)
+		keyjson, err := keystore.EncryptKey(key, passphrase,
+			keystore.StandardScryptN, keystore.StandardScryptP)
+		if err != nil {
+			utils.Fatalf("Error encrypting key: %v", err)
+		}
+
+		// Store the file to disk.
+		if err := os.MkdirAll(filepath.Dir(keyfilepath), 0700); err != nil {
+			utils.Fatalf("Could not create directory %s", filepath.Dir(keyfilepath))
+		}
+		if err := ioutil.WriteFile(keyfilepath, keyjson, 0600); err != nil {
+			utils.Fatalf("Failed to write keyfile to %s: %v", keyfilepath, err)
+		}
+
+		// Output some information.
+		out := outputGenerate{
+			Address: key.Address.Hex(),
+		}
+		if ctx.Bool(jsonFlag.Name) {
+			mustPrintJSON(out)
+		} else {
+			fmt.Println("Address:       ", out.Address)
+		}
+		return nil
+	},
+}
diff --git a/cmd/ethkey/inspect.go b/cmd/ethkey/inspect.go
new file mode 100644
index 000000000..8a7aeef84
--- /dev/null
+++ b/cmd/ethkey/inspect.go
@@ -0,0 +1,74 @@
+package main
+
+import (
+	"encoding/hex"
+	"fmt"
+	"io/ioutil"
+
+	"github.com/ethereum/go-ethereum/accounts/keystore"
+	"github.com/ethereum/go-ethereum/cmd/utils"
+	"github.com/ethereum/go-ethereum/crypto"
+	"gopkg.in/urfave/cli.v1"
+)
+
+type outputInspect struct {
+	Address    string
+	PublicKey  string
+	PrivateKey string
+}
+
+var commandInspect = cli.Command{
+	Name:      "inspect",
+	Usage:     "inspect a keyfile",
+	ArgsUsage: "<keyfile>",
+	Description: `
+Print various information about the keyfile.
+Private key information can be printed by using the --private flag;
+make sure to use this feature with great caution!`,
+	Flags: []cli.Flag{
+		passphraseFlag,
+		jsonFlag,
+		cli.BoolFlag{
+			Name:  "private",
+			Usage: "include the private key in the output",
+		},
+	},
+	Action: func(ctx *cli.Context) error {
+		keyfilepath := ctx.Args().First()
+
+		// Read key from file.
+		keyjson, err := ioutil.ReadFile(keyfilepath)
+		if err != nil {
+			utils.Fatalf("Failed to read the keyfile at '%s': %v", keyfilepath, err)
+		}
+
+		// Decrypt key with passphrase.
+		passphrase := getPassPhrase(ctx, false)
+		key, err := keystore.DecryptKey(keyjson, passphrase)
+		if err != nil {
+			utils.Fatalf("Error decrypting key: %v", err)
+		}
+
+		// Output all relevant information we can retrieve.
+		showPrivate := ctx.Bool("private")
+		out := outputInspect{
+			Address: key.Address.Hex(),
+			PublicKey: hex.EncodeToString(
+				crypto.FromECDSAPub(&key.PrivateKey.PublicKey)),
+		}
+		if showPrivate {
+			out.PrivateKey = hex.EncodeToString(crypto.FromECDSA(key.PrivateKey))
+		}
+
+		if ctx.Bool(jsonFlag.Name) {
+			mustPrintJSON(out)
+		} else {
+			fmt.Println("Address:       ", out.Address)
+			fmt.Println("Public key:    ", out.PublicKey)
+			if showPrivate {
+				fmt.Println("Private key:   ", out.PrivateKey)
+			}
+		}
+		return nil
+	},
+}
diff --git a/cmd/ethkey/main.go b/cmd/ethkey/main.go
new file mode 100644
index 000000000..b9b7a18e0
--- /dev/null
+++ b/cmd/ethkey/main.go
@@ -0,0 +1,70 @@
+// Copyright 2017 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// go-ethereum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+
+package main
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/ethereum/go-ethereum/cmd/utils"
+	"gopkg.in/urfave/cli.v1"
+)
+
+const (
+	defaultKeyfileName = "keyfile.json"
+)
+
+var (
+	gitCommit = "" // Git SHA1 commit hash of the release (set via linker flags)
+
+	app *cli.App // the main app instance
+)
+
+var ( // Commonly used command line flags.
+	passphraseFlag = cli.StringFlag{
+		Name:  "passwordfile",
+		Usage: "the file that contains the passphrase for the keyfile",
+	}
+
+	jsonFlag = cli.BoolFlag{
+		Name:  "json",
+		Usage: "output JSON instead of human-readable format",
+	}
+
+	messageFlag = cli.StringFlag{
+		Name:  "message",
+		Usage: "the file that contains the message to sign/verify",
+	}
+)
+
+// Configure the app instance.
+func init() {
+	app = utils.NewApp(gitCommit, "an Ethereum key manager")
+	app.Commands = []cli.Command{
+		commandGenerate,
+		commandInspect,
+		commandSignMessage,
+		commandVerifyMessage,
+	}
+}
+
+func main() {
+	if err := app.Run(os.Args); err != nil {
+		fmt.Fprintln(os.Stderr, err)
+		os.Exit(1)
+	}
+}
diff --git a/cmd/ethkey/message.go b/cmd/ethkey/message.go
new file mode 100644
index 000000000..ae6b6552d
--- /dev/null
+++ b/cmd/ethkey/message.go
@@ -0,0 +1,148 @@
+package main
+
+import (
+	"encoding/hex"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"strings"
+
+	"github.com/ethereum/go-ethereum/accounts/keystore"
+	"github.com/ethereum/go-ethereum/cmd/utils"
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/crypto"
+	"gopkg.in/urfave/cli.v1"
+)
+
+type outputSign struct {
+	Signature string
+}
+
+var commandSignMessage = cli.Command{
+	Name:      "signmessage",
+	Usage:     "sign a message",
+	ArgsUsage: "<keyfile> <message/file>",
+	Description: `
+Sign the message with a keyfile.
+It is possible to refer to a file containing the message.`,
+	Flags: []cli.Flag{
+		passphraseFlag,
+		jsonFlag,
+	},
+	Action: func(ctx *cli.Context) error {
+		keyfilepath := ctx.Args().First()
+		message := []byte(ctx.Args().Get(1))
+
+		// Load the keyfile.
+		keyjson, err := ioutil.ReadFile(keyfilepath)
+		if err != nil {
+			utils.Fatalf("Failed to read the keyfile at '%s': %v",
+				keyfilepath, err)
+		}
+
+		// Decrypt key with passphrase.
+		passphrase := getPassPhrase(ctx, false)
+		key, err := keystore.DecryptKey(keyjson, passphrase)
+		if err != nil {
+			utils.Fatalf("Error decrypting key: %v", err)
+		}
+
+		if len(message) == 0 {
+			utils.Fatalf("A message must be provided")
+		}
+		// Read message if file.
+		if _, err := os.Stat(string(message)); err == nil {
+			message, err = ioutil.ReadFile(string(message))
+			if err != nil {
+				utils.Fatalf("Failed to read the message file: %v", err)
+			}
+		}
+
+		signature, err := crypto.Sign(signHash(message), key.PrivateKey)
+		if err != nil {
+			utils.Fatalf("Failed to sign message: %v", err)
+		}
+
+		out := outputSign{
+			Signature: hex.EncodeToString(signature),
+		}
+		if ctx.Bool(jsonFlag.Name) {
+			mustPrintJSON(out)
+		} else {
+			fmt.Println("Signature: ", out.Signature)
+		}
+		return nil
+	},
+}
+
+type outputVerify struct {
+	Success            bool
+	RecoveredAddress   string
+	RecoveredPublicKey string
+}
+
+var commandVerifyMessage = cli.Command{
+	Name:      "verifymessage",
+	Usage:     "verify the signature of a signed message",
+	ArgsUsage: "<address> <signature> <message/file>",
+	Description: `
+Verify the signature of the message.
+It is possible to refer to a file containing the message.`,
+	Flags: []cli.Flag{
+		jsonFlag,
+	},
+	Action: func(ctx *cli.Context) error {
+		addressStr := ctx.Args().First()
+		signatureHex := ctx.Args().Get(1)
+		message := []byte(ctx.Args().Get(2))
+
+		// Determine whether it is a keyfile, public key or address.
+		if !common.IsHexAddress(addressStr) {
+			utils.Fatalf("Invalid address: %s", addressStr)
+		}
+		address := common.HexToAddress(addressStr)
+
+		signature, err := hex.DecodeString(signatureHex)
+		if err != nil {
+			utils.Fatalf("Signature encoding is not hexadecimal: %v", err)
+		}
+
+		if len(message) == 0 {
+			utils.Fatalf("A message must be provided")
+		}
+		// Read message if file.
+		if _, err := os.Stat(string(message)); err == nil {
+			message, err = ioutil.ReadFile(string(message))
+			if err != nil {
+				utils.Fatalf("Failed to read the message file: %v", err)
+			}
+		}
+
+		recoveredPubkey, err := crypto.SigToPub(signHash(message), signature)
+		if err != nil || recoveredPubkey == nil {
+			utils.Fatalf("Signature verification failed: %v", err)
+		}
+		recoveredPubkeyBytes := crypto.FromECDSAPub(recoveredPubkey)
+		recoveredAddress := crypto.PubkeyToAddress(*recoveredPubkey)
+
+		success := address == recoveredAddress
+
+		out := outputVerify{
+			Success:            success,
+			RecoveredPublicKey: hex.EncodeToString(recoveredPubkeyBytes),
+			RecoveredAddress:   strings.ToLower(recoveredAddress.Hex()),
+		}
+		if ctx.Bool(jsonFlag.Name) {
+			mustPrintJSON(out)
+		} else {
+			if out.Success {
+				fmt.Println("Signature verification successful!")
+			} else {
+				fmt.Println("Signature verification failed!")
+			}
+			fmt.Println("Recovered public key: ", out.RecoveredPublicKey)
+			fmt.Println("Recovered address: ", out.RecoveredAddress)
+		}
+		return nil
+	},
+}
diff --git a/cmd/ethkey/utils.go b/cmd/ethkey/utils.go
new file mode 100644
index 000000000..0e563bf92
--- /dev/null
+++ b/cmd/ethkey/utils.go
@@ -0,0 +1,83 @@
+// Copyright 2017 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// go-ethereum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"strings"
+
+	"github.com/ethereum/go-ethereum/cmd/utils"
+	"github.com/ethereum/go-ethereum/console"
+	"github.com/ethereum/go-ethereum/crypto"
+	"gopkg.in/urfave/cli.v1"
+)
+
+// getPassPhrase obtains a passphrase given by the user.  It first checks the
+// --passphrase command line flag and ultimately prompts the user for a
+// passphrase.
+func getPassPhrase(ctx *cli.Context, confirmation bool) string {
+	// Look for the --passphrase flag.
+	passphraseFile := ctx.String(passphraseFlag.Name)
+	if passphraseFile != "" {
+		content, err := ioutil.ReadFile(passphraseFile)
+		if err != nil {
+			utils.Fatalf("Failed to read passphrase file '%s': %v",
+				passphraseFile, err)
+		}
+		return strings.TrimRight(string(content), "\r\n")
+	}
+
+	// Otherwise prompt the user for the passphrase.
+	passphrase, err := console.Stdin.PromptPassword("Passphrase: ")
+	if err != nil {
+		utils.Fatalf("Failed to read passphrase: %v", err)
+	}
+	if confirmation {
+		confirm, err := console.Stdin.PromptPassword("Repeat passphrase: ")
+		if err != nil {
+			utils.Fatalf("Failed to read passphrase confirmation: %v", err)
+		}
+		if passphrase != confirm {
+			utils.Fatalf("Passphrases do not match")
+		}
+	}
+	return passphrase
+}
+
+// signHash is a helper function that calculates a hash for the given message
+// that can be safely used to calculate a signature from.
+//
+// The hash is calulcated as
+//   keccak256("\x19Ethereum Signed Message:\n"${message length}${message}).
+//
+// This gives context to the signed message and prevents signing of transactions.
+func signHash(data []byte) []byte {
+	msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data)
+	return crypto.Keccak256([]byte(msg))
+}
+
+// mustPrintJSON prints the JSON encoding of the given object and
+// exits the program with an error message when the marshaling fails.
+func mustPrintJSON(jsonObject interface{}) {
+	str, err := json.MarshalIndent(jsonObject, "", "  ")
+	if err != nil {
+		utils.Fatalf("Failed to marshal JSON object: %v", err)
+	}
+	fmt.Println(string(str))
+}
-- 
GitLab