Skip to content

Commit

Permalink
Feature: store private key in keyring or keyfile (#64)
Browse files Browse the repository at this point in the history
* store private key in os keyring

* --insecure flag for createaccount

* improve secure flag

* createaccount support keyfile

* cleanup

* working transfer with privkey, keyring, and keyfile

* handle error

* test file decryption

* remove quotes that implied we aren't secure.

was quoting because we're in the if branch that is ran when no `--insecure` flag exists, which means we're "secure"

* capture all args for nicer just run usage

* update just command comment

* use astria-dusk-5 as chain id so remote will work

* can only pass in one type of key flag
  • Loading branch information
steezeburger authored May 2, 2024
1 parent 89d32e3 commit 2c0f7ee
Show file tree
Hide file tree
Showing 22 changed files with 750 additions and 49 deletions.
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) {
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)
}
24 changes: 13 additions & 11 deletions cmd/sequencer/transfer.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,30 +19,32 @@ 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.")
// add chainId
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")
transferCmd.MarkFlagsMutuallyExclusive("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

0 comments on commit 2c0f7ee

Please sign in to comment.