Skip to content
This repository was archived by the owner on Apr 15, 2024. It is now read-only.

Commit

Permalink
feat: support bip39 passphrases
Browse files Browse the repository at this point in the history
  • Loading branch information
rach-id committed Nov 6, 2023
1 parent dc24dd4 commit b84665e
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 18 deletions.
66 changes: 53 additions & 13 deletions cmd/blobstream/keys/evm/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@ func Add(serviceName string) *cobra.Command {
}
}(s, logger)

bip39Passphrase, err := GetBIP39Passphrase()
if err != nil {
return err
}

if bip39Passphrase != "" {
fmt.Println("\nThe provided passphrase will be the 25th word in your mnemonic. Make sure to save it as you won't be able to recover your accounts without it.")
}

passphrase := config.EVMPassphrase
// if the passphrase is not specified as a flag, ask for it.
if passphrase == "" {
Expand All @@ -92,9 +101,6 @@ func Add(serviceName string) *cobra.Command {
}
}

fmt.Printf("\nThe provided password is **not** BIP39 passphrase but the store encryption.\n" +
"The account can be retrieved using the mnemonic only, without using this password.\n\n")

// read entropy seed straight from tmcrypto.Rand and convert to mnemonic
entropySeed, err := bip39.NewEntropy(mnemonicEntropySize)
if err != nil {
Expand All @@ -108,7 +114,7 @@ func Add(serviceName string) *cobra.Command {

// get the private key using an empty passphrase so that only the mnemonic
// is enough to recover the account
ethPrivKey, err := MnemonicToPrivateKey(mnemonic, "")
ethPrivKey, err := MnemonicToPrivateKey(mnemonic, bip39Passphrase)
if err != nil {
return err
}
Expand All @@ -121,8 +127,13 @@ func Add(serviceName string) *cobra.Command {
logger.Info("account created successfully", "address", account.Address.String())

fmt.Println("\n\n**Important** write this mnemonic phrase in a safe place." +
"\nIt is the only way to recover your account if you ever forget your password.")
fmt.Printf("\n%s\n\n", mnemonic)
"\nIt is the only way to recover your account if you ever forget your storage password.")

if bip39Passphrase == "" {
fmt.Printf("\n%s\n\n", mnemonic)
} else {
fmt.Printf("\n%s <your_bip39_passphrase>\n\n", mnemonic)
}
return nil
},
}
Expand Down Expand Up @@ -462,8 +473,10 @@ func ImportMnemonic(serviceName string) *cobra.Command {
return errors.New("invalid mnemonic")
}

fmt.Printf("\n\nThe provided password is **not** BIP39 passphrase but the store encryption.\n" +
"The account can be retrieved using the mnemonic only, without using this password.\n\n")
bip39Passphrase, err := GetBIP39Passphrase()
if err != nil {
return err
}

// get the passphrase to use for the seed
passphrase := config.EVMPassphrase
Expand All @@ -477,7 +490,7 @@ func ImportMnemonic(serviceName string) *cobra.Command {

logger.Info("importing account")

ethPrivKey, err := MnemonicToPrivateKey(mnemonic, passphrase)
ethPrivKey, err := MnemonicToPrivateKey(mnemonic, bip39Passphrase)
if err != nil {
return err
}
Expand Down Expand Up @@ -624,11 +637,38 @@ func GetNewPassphrase() (string, error) {
var err error
var bzPassphrase []byte
for {
fmt.Print("please provide the account new passphrase: ")
fmt.Print("\nplease provide the account new passphrase (Note: this is for the store encryption and not the BIP39 passphrase. This means that you can recover your account without providing it): ")
bzPassphrase, err = term.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
return "", err
}
fmt.Print("\nenter the same passphrase again: ")
bzPassphraseConfirm, err := term.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
return "", err
}
if bytes.Equal(bzPassphrase, bzPassphraseConfirm) {
fmt.Println()
break
}
fmt.Print("\npassphrase and confirmation mismatch.\n")
}
return string(bzPassphrase), nil
}

func GetBIP39Passphrase() (string, error) {
var err error
var bzPassphrase []byte
for {
fmt.Print("\nplease provide the BIP39 passphrase (leave empty if you don't want to set a BIP39 passphrase, i.e. 25th mnemonic word): ")
bzPassphrase, err = term.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
return "", err
}
if len(string(bzPassphrase)) > 100 {
fmt.Println("\n\nThe BIP39 passphrase cannot have more than 100 characters! Please try again.")
continue
}
fmt.Print("\nenter the same passphrase again: ")
bzPassphraseConfirm, err := term.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
Expand All @@ -644,8 +684,8 @@ func GetNewPassphrase() (string, error) {
}

// MnemonicToPrivateKey derives a private key from the provided mnemonic.
// It uses the Ledger derivation path, geth.LegacyLedgerBaseDerivationPath, i.e. m/44'/60'/0'/0, to generate
// the first private key.
// It uses the default derivation path, geth.DefaultBaseDerivationPath, i.e. m/44'/60'/0'/0, to generate
// the first private key. The generated account is of path m/44'/60'/0'/0/0.
func MnemonicToPrivateKey(mnemonic string, passphrase string) (*ecdsa.PrivateKey, error) {
// create the master key
seed, err := bip39.NewSeedWithErrorChecking(mnemonic, passphrase)
Expand All @@ -656,7 +696,7 @@ func MnemonicToPrivateKey(mnemonic string, passphrase string) (*ecdsa.PrivateKey
secret, chainCode := hd.ComputeMastersFromSeed(seed)

// derive the first private key from the master key
key, err := hd.DerivePrivateKeyForPath(secret, chainCode, accounts.LegacyLedgerBaseDerivationPath.String())
key, err := hd.DerivePrivateKeyForPath(secret, chainCode, accounts.DefaultBaseDerivationPath.String())
if err != nil {
return nil, err
}
Expand Down
22 changes: 17 additions & 5 deletions cmd/blobstream/keys/evm/evm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,32 @@ import (
"github.com/stretchr/testify/assert"
)

// TestMnemonicToPrivateKey tests the generation of private keys using mnemonics.
// The test vectors were generated and verified using a Ledger Nano X with Ethereum accounts.
func TestMnemonicToPrivateKey(t *testing.T) {
tests := []struct {
name string
mnemonic string
passphrase string
expectedError bool
expectedResult string
expectedAddress string
}{
{
name: "Valid Mnemonic and Passphrase",
mnemonic: "rescue any open drink foster thing scale country embark stable segment stem portion ostrich spoon hat debate diesel morning galaxy weird firm capital census",
name: "Valid Mnemonic with passphrase",
mnemonic: "eight moment square film same crystal trophy diagram awkward defense crazy garlic exile rabbit coast truck foam broken shed attract bamboo drum dry cage",
passphrase: "abcd",
expectedError: false,
expectedResult: "cb4851012ea2e0421fee67c496b1ae43f0f863903f4e2b57459d3f49f365e926",
expectedAddress: "0x082d835d29b0519e55401084Ef60fC3D720b62b6",
expectedResult: "5dfb97434a8a31cca1d1c2c6b6b9cf09b4946823331ec434894f204acf79d850",
expectedAddress: "0x6Ca3653B3B50892e051Da60b1E14540f2f7EBdBF",
},
{
name: "Valid Mnemonic without passphrase",
mnemonic: "eight moment square film same crystal trophy diagram awkward defense crazy garlic exile rabbit coast truck foam broken shed attract bamboo drum dry cage",
passphrase: "",
expectedError: false,
expectedResult: "4252916c6e7f80dc96928c66a885be5a362790ad2fb3552ab781cd9112aef3a2",
expectedAddress: "0x33bb23EB923C284fC76D93C26aFd1FdCAf770Ea2",
},
{
name: "Invalid Mnemonic",
Expand All @@ -32,7 +44,7 @@ func TestMnemonicToPrivateKey(t *testing.T) {

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
privateKey, err := evm.MnemonicToPrivateKey(test.mnemonic, "1234")
privateKey, err := evm.MnemonicToPrivateKey(test.mnemonic, test.passphrase)

if test.expectedError {
assert.Error(t, err)
Expand Down

0 comments on commit b84665e

Please sign in to comment.