Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: store private key in keyring or keyfile #64

Merged
merged 15 commits into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions cmd/devtools/clean.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ var cleanCmd = &cobra.Command{
Run: runClean,
}

func runClean(cmd *cobra.Command, args []string) {
func runClean(c *cobra.Command, _ []string) {
// Get the instance name from the -i flag or use the default
instance := cmd.Flag("instance").Value.String()
instance := c.Flag("instance").Value.String()
IsInstanceNameValidOrPanic(instance)

homePath, err := os.UserHomeDir()
Expand All @@ -41,7 +41,7 @@ func runClean(cmd *cobra.Command, args []string) {
}

log.Infof("Recreating data dir for instance '%s'", instance)
CreateDirOrPanic(dataDir)
cmd.CreateDirOrPanic(dataDir)
}

var allCmd = &cobra.Command{
Expand All @@ -52,7 +52,7 @@ var allCmd = &cobra.Command{
Run: runCleanAll,
}

func runCleanAll(cmd *cobra.Command, args []string) {
func runCleanAll(_ *cobra.Command, _ []string) {
homePath, err := os.UserHomeDir()
if err != nil {
log.WithError(err).Error("Error getting home dir")
Expand Down
10 changes: 0 additions & 10 deletions cmd/devtools/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,6 @@ and may contain dashes. It can't begin or end with a dash. No repeating dashes.
}
}

// CreateDirOrPanic creates a directory with the given name with 0755 permissions.
// If the directory can't be created, it will panic.
func CreateDirOrPanic(dirName string) {
err := os.MkdirAll(dirName, 0755)
if err != nil {
log.WithError(err).Error("Error creating data directory")
panic(err)
}
}

// PathExists checks if the file or binary for the input path is a regular file
// and is executable. A regular file is one where no mode type bits are set.
func PathExists(path string) bool {
Expand Down
10 changes: 5 additions & 5 deletions cmd/devtools/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,26 +51,26 @@ func runInitialization(c *cobra.Command, args []string) {

// create the local config directories
localConfigPath := filepath.Join(instanceDir, LocalConfigDirName)
CreateDirOrPanic(localConfigPath)
cmd.CreateDirOrPanic(localConfigPath)
recreateLocalEnvFile(instanceDir, localConfigPath)
recreateCometbftAndSequencerGenesisData(localConfigPath)

// create the remote config directories
remoteConfigPath := filepath.Join(instanceDir, RemoteConfigDirName)
CreateDirOrPanic(remoteConfigPath)
cmd.CreateDirOrPanic(remoteConfigPath)
recreateRemoteEnvFile(instanceDir, remoteConfigPath)

// create the local bin directory for downloaded binaries
localBinPath := filepath.Join(instanceDir, BinariesDirName)
log.Info("Binary files for locally running a sequencer placed in: ", localBinPath)
CreateDirOrPanic(localBinPath)
cmd.CreateDirOrPanic(localBinPath)
for _, bin := range Binaries {
downloadAndUnpack(bin.Url, bin.Name, localBinPath)
}

// create the data directory for cometbft and sequencer
dataPath := filepath.Join(instanceDir, DataDirName)
CreateDirOrPanic(dataPath)
cmd.CreateDirOrPanic(dataPath)

initCometbft(instanceDir, DataDirName, BinariesDirName, LocalConfigDirName)

Expand Down Expand Up @@ -247,7 +247,7 @@ func extractTarGz(dest string, gzipStream io.Reader) error {
case tar.TypeDir:
// handle directory
if _, err := os.Stat(target); err != nil {
CreateDirOrPanic(target)
cmd.CreateDirOrPanic(target)
}
case tar.TypeReg:
// handle normal file
Expand Down
17 changes: 17 additions & 0 deletions cmd/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package cmd

import (
"os"

log "github.com/sirupsen/logrus"
)

// CreateDirOrPanic creates a directory with the given name with 0755 permissions.
// If the directory can't be created, it will panic.
func CreateDirOrPanic(dirName string) {
err := os.MkdirAll(dirName, 0755)
if err != nil {
log.WithError(err).Error("Error creating data directory")
panic(err)
}
}
64 changes: 62 additions & 2 deletions cmd/sequencer/createaccount.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package sequencer

import (
"os"
"path/filepath"

"github.com/astria/astria-cli-go/cmd"
"github.com/astria/astria-cli-go/internal/keys"
"github.com/astria/astria-cli-go/internal/sequencer"
"github.com/astria/astria-cli-go/internal/ui"
"github.com/pterm/pterm"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
Expand All @@ -21,17 +26,72 @@ transactions and blocks. The account will be created with a private key, public
func init() {
sequencerCmd.AddCommand(createaccountCmd)
createaccountCmd.Flags().Bool("json", false, "Output the account information in JSON format.")

createaccountCmd.Flags().Bool("insecure", false, "Print the account private key to terminal instead of storing securely.")
// user has multiple options for storing private key
createaccountCmd.Flags().Bool("keyfile", false, "Store the account private key in a keyfile.")
createaccountCmd.Flags().Bool("keyring", false, "Store the account private key in the system keyring.")

// you can't print private key AND store securely
createaccountCmd.MarkFlagsMutuallyExclusive("insecure", "keyring", "keyfile")
}

func createaccountCmdHandler(cmd *cobra.Command, args []string) {
printJSON := cmd.Flag("json").Value.String() == "true"
func createaccountCmdHandler(c *cobra.Command, _ []string) {
printJSON := c.Flag("json").Value.String() == "true"
isInsecure := c.Flag("insecure").Value.String() == "true"
useKeyfile := c.Flag("keyfile").Value.String() == "true"
useKeyring := c.Flag("keyring").Value.String() == "true"
if !isInsecure && !useKeyring && !useKeyfile {
// useKeyfile is the default if nothing is set
useKeyfile = true
}

account, err := sequencer.CreateAccount()
if err != nil {
log.WithError(err).Error("Error creating account")
panic(err)
}

if !isInsecure {
if useKeyfile {
pwIn := pterm.DefaultInteractiveTextInput.WithMask("*")
pw, _ := pwIn.Show("Your new account is locked with a password. Please give a password. Do not forget this password.\nPassword:")

ks, err := keys.NewEncryptedKeyStore(pw, account.Address, account.PrivateKey)
if err != nil {
log.WithError(err).Error("Error storing private key")
panic(err)
}
homePath, err := os.UserHomeDir()
if err != nil {
log.WithError(err).Error("Error getting home dir")
panic(err)
}
astriaDir := filepath.Join(homePath, ".astria")
keydir := filepath.Join(astriaDir, "keyfiles")
cmd.CreateDirOrPanic(keydir)

filename, err := keys.SaveKeystoreToFile(keydir, ks)
if err != nil {
log.WithError(err).Error("Error storing private key")
panic(err)
}

log.Infof("Storing private key in keyfile at %s", filename)
}
if useKeyring {
err = keys.StoreKeyring(account.Address, account.PrivateKeyString())
if err != nil {
log.WithError(err).Error("Error storing private key")
panic(err)
}
log.Infof("Private key for %s stored in keychain", account.Address)
}

// clear the private key. we don't want to print it since we are secure here
account.PrivateKey = nil
}

printer := ui.ResultsPrinter{
Data: account,
PrintJSON: printJSON,
Expand Down
70 changes: 70 additions & 0 deletions cmd/sequencer/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package sequencer

import (
"fmt"

"github.com/astria/astria-cli-go/internal/keys"
"github.com/astria/astria-cli-go/internal/sequencer"
"github.com/pterm/pterm"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

// GetPrivateKeyFromFlags retrieves the private key from the command flags.
// If the 'privkey' flag is set, it returns the value of that flag.
// If the 'keyring-address' flag is set, it calls the 'PrivateKeyFromKeyringAddress' function
// to retrieve the private key from the keyring.
// If the 'keyfile' flag is set, it calls the 'PrivateKeyFromKeyfile' function
// to retrieve the private key from the keyfile.
// If none of the flags are set or if the value of 'keyfile' is empty, it returns an error.
// NOTE - this requires the flags `keyfile`, `keyring-address`, and `privkey`
func GetPrivateKeyFromFlags(c *cobra.Command) (string, error) {
steezeburger marked this conversation as resolved.
Show resolved Hide resolved
keyfile := c.Flag("keyfile").Value.String()
keyringAddress := c.Flag("keyring-address").Value.String()
priv := c.Flag("privkey").Value.String()

// NOTE - this isn't very secure but we still support it
if priv != "" {
return priv, nil
}

// NOTE - this should trigger user's os keyring password prompt
if keyringAddress != "" {
return PrivateKeyFromKeyringAddress(keyringAddress)
}

if keyfile != "" {
return PrivateKeyFromKeyfile(keyfile)
}

return "", fmt.Errorf("no private key specified")
}

// PrivateKeyFromKeyfile retrieves the private key from the specified keyfile.
func PrivateKeyFromKeyfile(keyfile string) (string, error) {
kf, err := keys.ResolveKeyfilePath(keyfile)
if err != nil {
return "", err
}

pwIn := pterm.DefaultInteractiveTextInput.WithMask("*")
pw, _ := pwIn.Show("Account password:")

privkey, err := keys.DecryptKeyfile(kf, pw)
if err != nil {
log.WithError(err).Error("Error decrypting keyfile")
return "", err
}
account := sequencer.NewAccountFromPrivKey(privkey)
return account.PrivateKeyString(), nil
}

// PrivateKeyFromKeyringAddress retrieves the private key from the keyring for a given keyring address.
func PrivateKeyFromKeyringAddress(keyringAddress string) (string, error) {
key, err := keys.GetKeyring(keyringAddress)
if err != nil {
log.WithError(err).Error("Error getting private key from keyring")
return "", err
}
return key, nil
}
49 changes: 49 additions & 0 deletions cmd/sequencer/keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package sequencer

import (
"github.com/astria/astria-cli-go/cmd"
"github.com/astria/astria-cli-go/internal/keys"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

var setKeyCmd = &cobra.Command{
Use: "setkey [address] [private key]",
Short: "Set private key for an address in system keyring.",
Args: cobra.ExactArgs(2),
PreRun: cmd.SetLogLevel,
Run: setKeyCmdHandler,
}

func setKeyCmdHandler(cmd *cobra.Command, args []string) {
key := args[0]
val := args[1]

err := keys.StoreKeyring(key, val)
if err != nil {
panic(err)
}
}

var getKeyCmd = &cobra.Command{
Use: "getkey [address]",
Short: "Get private key for an address in system keyring.",
Args: cobra.ExactArgs(1),
PreRun: cmd.SetLogLevel,
Run: getKeyCmdHandler,
}

func getKeyCmdHandler(cmd *cobra.Command, args []string) {
key := args[0]

val, err := keys.GetKeyring(key)
if err != nil {
panic(err)
}
log.Infof("value: %s", val)
}

func init() {
sequencerCmd.AddCommand(setKeyCmd)
sequencerCmd.AddCommand(getKeyCmd)
}
22 changes: 12 additions & 10 deletions cmd/sequencer/transfer.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,29 +19,31 @@ var transferCmd = &cobra.Command{
func init() {
sequencerCmd.AddCommand(transferCmd)

transferCmd.Flags().String("privkey", "", "The private key of the account from which to transfer tokens.")
transferCmd.Flags().String("url", DefaultSequencerURL, "The URL of the sequencer.")
transferCmd.Flags().Bool("json", false, "Output in JSON format.")
transferCmd.Flags().String("url", DefaultSequencerURL, "The URL of the sequencer.")

err := transferCmd.MarkFlagRequired("privkey")
if err != nil {
log.WithError(err).Error("Error marking flag as required")
panic(err)
}
transferCmd.Flags().String("keyfile", "", "Path to secure keyfile for sender.")
transferCmd.Flags().String("keyring-address", "", "The address of the sender. Requires private key be stored in keyring.")
transferCmd.Flags().String("privkey", "", "The private key of the sender.")
transferCmd.MarkFlagsOneRequired("keyfile", "keyring-address", "privkey")
}

func transferCmdHandler(cmd *cobra.Command, args []string) {
printJSON := cmd.Flag("json").Value.String() == "true"

amount := args[0]
to := args[1]

url := cmd.Flag("url").Value.String()
from := cmd.Flag("privkey").Value.String()

priv, err := GetPrivateKeyFromFlags(cmd)
if err != nil {
log.WithError(err).Error("Could not get private key from flags")
panic(err)
}

opts := sequencer.TransferOpts{
SequencerURL: url,
FromKey: from,
FromKey: priv,
ToAddress: to,
Amount: amount,
}
Expand Down
Loading
Loading