Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
842 changes: 611 additions & 231 deletions .secrets.baseline

Large diffs are not rendered by default.

102 changes: 96 additions & 6 deletions client/app/privkey.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package app

import (
"encoding/json"
"os"
"strings"

"github.com/cometbft/cometbft/crypto"
cmtjson "github.com/cometbft/cometbft/libs/json"
"github.com/cometbft/cometbft/privval"
"github.com/ethereum/go-ethereum/accounts/keystore"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"

"github.com/piplabs/story/lib/errors"
"github.com/piplabs/story/lib/k1util"
Expand All @@ -15,16 +18,39 @@ import (
// loadPrivVal returns a privval.FilePV by loading either a CometBFT priv validator key or an Ethereum keystore file.
func loadPrivVal(cfg Config) (*privval.FilePV, error) {
cmtFile := cfg.Comet.PrivValidatorKeyFile()
encPrivKeyFile := cfg.EncPrivKeyFile()
cmtExists := exists(cmtFile)
encPrivExists := exists(encPrivKeyFile)

if !cmtExists {
return nil, errors.New("no cometBFT priv validator key file exists", "comet_file", cmtFile)
if !cmtExists && !encPrivExists {
return nil, errors.New("no cometBFT priv validator key file exists", "comet_file", cmtFile, "enc_priv_key_file", encPrivKeyFile)
}

var key crypto.PrivKey
key, err := loadCometFilePV(cmtFile)
if err != nil {
return nil, err
var (
key crypto.PrivKey
err error
)
if encPrivExists {
password, err := InputPassword(
PasswordPromptText,
"",
false,
ValidatePasswordInput,
)
if err != nil {
return nil, errors.Wrap(err, "error occurred while input password")
}

pv, err := LoadEncryptedPrivKey(password, encPrivKeyFile)
if err != nil {
return nil, err
}
key = pv.PrivKey
} else {
key, err = loadCometFilePV(cmtFile)
if err != nil {
return nil, err
}
}

state, err := loadCometPVState(cfg.Comet.PrivValidatorStateFile())
Expand Down Expand Up @@ -98,3 +124,67 @@ func exists(file string) bool {
_, err := os.Stat(file)
return !os.IsNotExist(err)
}

// EncryptedKeyRepresentation defines an internal representation of encrypted validator key.
type EncryptedKeyRepresentation struct {
Crypto map[string]interface{} `json:"crypto"` //nolint:revive // This is from Prysm.
Version uint `json:"version"`
Name string `json:"name"`
}

func EncryptAndStoreKey(key privval.FilePVKey, password, filePath string) error {
encodedKey, err := cmtjson.MarshalIndent(key, "", "\t")
if err != nil {
return errors.Wrap(err, "failed to marshal key for encryption")
}

encryptor := keystorev4.New()
encryptedKey, err := encryptor.Encrypt(encodedKey, password)
if err != nil {
return errors.Wrap(err, "could not encrypt key")
}

encKeyRepr := EncryptedKeyRepresentation{
Crypto: encryptedKey,
Version: encryptor.Version(),
Name: encryptor.Name(),
}

data, err := json.MarshalIndent(encKeyRepr, "", "\t")
if err != nil {
return errors.Wrap(err, "failed to marshal encrypted key")
}

if err := os.WriteFile(filePath, data, 0600); err != nil {
return errors.Wrap(err, "failed to write enc_priv_key.json file")
}

return nil
}

func LoadEncryptedPrivKey(password, encPrivKeyFile string) (privval.FilePVKey, error) {
data, err := os.ReadFile(encPrivKeyFile)
if err != nil {
return privval.FilePVKey{}, errors.Wrap(err, "failed to read enc_priv_key.json file")
}

var encKeyRepr EncryptedKeyRepresentation
if err := json.Unmarshal(data, &encKeyRepr); err != nil {
return privval.FilePVKey{}, errors.Wrap(err, "failed to unmarshal enc_priv_key.json data")
}

decryptor := keystorev4.New()
decryptedKey, err := decryptor.Decrypt(encKeyRepr.Crypto, password)
if err != nil && strings.Contains(err.Error(), "invalid checksum") {
return privval.FilePVKey{}, errors.Wrap(err, "wrong password for wallet entered")
} else if err != nil {
return privval.FilePVKey{}, errors.Wrap(err, "could not decrypt key")
}

var key privval.FilePVKey
if err := cmtjson.Unmarshal(decryptedKey, &key); err != nil {
return privval.FilePVKey{}, errors.Wrap(err, "failed to unmarshal decrypted key")
}

return key, nil
}
63 changes: 63 additions & 0 deletions client/app/privkey_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package app_test

import (
"path/filepath"
"testing"

k1 "github.com/cometbft/cometbft/crypto/secp256k1"
"github.com/cometbft/cometbft/privval"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/piplabs/story/client/app"
)

func setupTestEnv(t *testing.T) (string, string, string) {
t.Helper()

stateFileDir := filepath.Join(t.TempDir(), "stateFileDir")
encFileDir := filepath.Join(t.TempDir(), "encFileDir")
password := "testpassword"

return stateFileDir, encFileDir, password
}

func TestEncryptAndDecrypt_Success(t *testing.T) {
stateFileDir, encFileDir, password := setupTestEnv(t)

pv := privval.NewFilePV(k1.GenPrivKey(), "", stateFileDir)

// Encryption
err := app.EncryptAndStoreKey(pv.Key, password, encFileDir)
require.NoError(t, err)

// Decryption
loadedKey, err := app.LoadEncryptedPrivKey(password, encFileDir)
require.NoError(t, err)

assert.Equal(t, pv.Key, loadedKey, "The decrypted key must match the original.")
}

func TestLoadEncryptedPrivKey_WrongPassword(t *testing.T) {
stateFileDir, encFileDir, password := setupTestEnv(t)
wrongPassword := "wrongpassword"

pv := privval.NewFilePV(k1.GenPrivKey(), "", stateFileDir)

// Encryption
err := app.EncryptAndStoreKey(pv.Key, password, encFileDir)
require.NoError(t, err)

// Decrypt with wrong password
_, err = app.LoadEncryptedPrivKey(wrongPassword, encFileDir)
require.Error(t, err)
assert.Contains(t, err.Error(), "wrong password for wallet entered")
}

func TestLoadEncryptedPrivKey_FileNotFound(t *testing.T) {
_, encFileDir, password := setupTestEnv(t)

_, err := app.LoadEncryptedPrivKey(password, encFileDir)
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to read enc_priv_key.json file")
}
110 changes: 110 additions & 0 deletions client/app/prompt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//nolint:revive,wrapcheck // This file is taken from Prysm
package app

import (
"fmt"
"os"
"strings"

"golang.org/x/crypto/ssh/terminal"

"github.com/logrusorgru/aurora"

"github.com/piplabs/story/lib/errors"
)

const (
// Constants for passwords.
minPasswordLength = 8

// NewKeyPasswordPromptText for key creation.
NewKeyPasswordPromptText = "New key password"
// PasswordPromptText for wallet unlocking.
PasswordPromptText = "Key password"
// ConfirmPasswordPromptText for confirming a key password.
ConfirmPasswordPromptText = "Confirm password"
)

var (
au = aurora.NewAurora(true)

errPasswordWeak = errors.New("password must have at least 8 characters")
)

// PasswordReaderFunc takes in a *file and returns a password using the terminal package.
func passwordReaderFunc(file *os.File) ([]byte, error) {
pass, err := terminal.ReadPassword(int(file.Fd()))

return pass, err
}

// PasswordReader has passwordReaderFunc as the default but can be changed for testing purposes.
var PasswordReader = passwordReaderFunc

// PasswordPrompt prompts the user for a password, that repeatedly requests the password until it qualifies the
// passed in validation function.
func PasswordPrompt(promptText string, validateFunc func(string) error) (string, error) {
var responseValid bool
var response string
for !responseValid {
fmt.Printf("%s: ", au.Bold(promptText))
bytePassword, err := PasswordReader(os.Stdin)
if err != nil {
return "", err
}
response = strings.TrimRight(string(bytePassword), "\r\n")
if err := validateFunc(response); err != nil {
fmt.Printf("\nEntry not valid: %s\n", au.BrightRed(err))
} else {
fmt.Println("")
responseValid = true
}
}

return response, nil
}

// InputPassword with a custom validator along capabilities of confirming the password.
func InputPassword(
promptText, confirmText string,
shouldConfirmPassword bool,
passwordValidator func(input string) error,
) (string, error) {
if strings.Contains(strings.ToLower(promptText), "new wallet") {
fmt.Println("Password requirements: at least 8 characters")
}
var hasValidPassword bool
var password string
var err error
for !hasValidPassword {
password, err = PasswordPrompt(promptText, passwordValidator)
if err != nil {
return "", errors.Wrap(err, "could not read password")
}
if shouldConfirmPassword {
passwordConfirmation, err := PasswordPrompt(confirmText, passwordValidator)
if err != nil {
return "", errors.Wrap(err, "could not read password confirmation")
}
if password != passwordConfirmation {
fmt.Println(au.BrightRed("Passwords do not match"))
continue
}
hasValidPassword = true
} else {
return password, nil
}
}

return password, nil
}

// ValidatePasswordInput validates a strong password input for new accounts,
// including a min length.
func ValidatePasswordInput(input string) error {
if len(input) < minPasswordLength {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do more weak criteria, e.g. at least one number.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. This validation is the same one from Prysm. We could add more.

return errPasswordWeak
}

return nil
}
42 changes: 38 additions & 4 deletions client/cmd/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import (

"cosmossdk.io/math"

cmtos "github.com/cometbft/cometbft/libs/os"
stypes "github.com/cosmos/cosmos-sdk/x/staking/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/spf13/cobra"
"github.com/spf13/pflag"

Expand All @@ -26,9 +28,6 @@ import (
"github.com/piplabs/story/lib/k1util"
"github.com/piplabs/story/lib/netconf"
"github.com/piplabs/story/lib/tracer"

// Used for ABI embedding of the staking contract.
_ "embed"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not used

)

func bindRunFlags(cmd *cobra.Command, cfg *config.Config) {
Expand Down Expand Up @@ -61,9 +60,11 @@ func bindInitFlags(flags *pflag.FlagSet, cfg *InitConfig) {
flags.BoolVar(&cfg.SeedMode, "seed-mode", false, "Enable seed mode")
flags.StringVar(&cfg.PersistentPeers, "persistent-peers", "", "Override the persistent peers (comma-separated)")
flags.StringVar(&cfg.Moniker, "moniker", "", "Declare a custom moniker for your node")
flags.BoolVar(&cfg.EncryptPrivKey, "encrypt-priv-key", false, "Encrypt the validator's private key")
}

func bindValidatorBaseFlags(cmd *cobra.Command, cfg *baseConfig) {
libcmd.BindHomeFlag(cmd.Flags(), &cfg.HomeDir)
cmd.Flags().StringVar(&cfg.RPC, "rpc", "https://mainnet.storyrpc.io", "RPC URL to connect to the network")
cmd.Flags().StringVar(&cfg.Explorer, "explorer", "https://storyscan.xyz", "URL of the blockchain explorer")
cmd.Flags().Int64Var(&cfg.ChainID, "chain-id", 1514, "Chain ID to use for the transaction")
Expand Down Expand Up @@ -154,11 +155,16 @@ func bindValidatorKeyExportFlags(cmd *cobra.Command, cfg *exportKeyConfig) {
cmd.Flags().StringVar(&cfg.EvmKeyFile, "evm-key-path", defaultEVMKeyFilePath, "Path to save the exported EVM private key")
}

func bindValidatorGenPrivKeyJSONFlags(cmd *cobra.Command, cfg *genPrivKeyJSONConfig) {
func bindKeyGenPrivKeyJSONFlags(cmd *cobra.Command, cfg *genPrivKeyJSONConfig) {
bindValidatorKeyFlags(cmd, &cfg.ValidatorKeyFile)
bindValidatorBaseFlags(cmd, &cfg.baseConfig)
}

func bindKeyShowEncryptedFlags(cmd *cobra.Command, cfg *showEncryptedConfig) {
bindValidatorBaseFlags(cmd, &cfg.baseConfig)
cmd.Flags().BoolVar(&cfg.ShowPrivate, "show-private", false, "Show private key")
}

func bindValidatorKeyFlags(cmd *cobra.Command, keyFilePath *string) {
defaultKeyFilePath := filepath.Join(config.DefaultHomeDir(), "config", "priv_validator_key.json")
cmd.Flags().StringVar(keyFilePath, "keyfile", defaultKeyFilePath, "Path to the Tendermint key file")
Expand Down Expand Up @@ -492,6 +498,34 @@ func validateGenPrivKeyJSONFlags(cfg *genPrivKeyJSONConfig) error {
return nil
}

func validateEncryptFlags(cfg *baseConfig) error {
if cmtos.FileExists(cfg.EncPrivKeyFile()) {
return errors.New("already encrypted private key exists")
}

loadEnv()
pk := os.Getenv("PRIVATE_KEY")
if pk == "" {
return errors.New("no private key is provided")
}

if _, err := crypto.HexToECDSA(pk); err != nil {
return errors.New("invalid secp256k1 private key")
}

cfg.PrivateKey = pk

return nil
}

func validateShowEncryptedFlags(cfg *showEncryptedConfig) error {
if !cmtos.FileExists(cfg.EncPrivKeyFile()) {
return errors.New("no encrypted private key file")
}

return nil
}

func validateValidatorUnjailFlags(ctx context.Context, cmd *cobra.Command, cfg *unjailConfig) error {
if err := validateFlags(cmd, []string{}); err != nil {
return err
Expand Down
Loading
Loading