diff --git a/crypto/armor.go b/crypto/armor.go index b65854262387..4366c90239fe 100644 --- a/crypto/armor.go +++ b/crypto/armor.go @@ -8,6 +8,7 @@ import ( "github.com/cometbft/cometbft/crypto" "golang.org/x/crypto/openpgp/armor" //nolint:staticcheck + "golang.org/x/crypto/scrypt" errorsmod "cosmossdk.io/errors" @@ -25,8 +26,9 @@ const ( defaultAlgo = "secp256k1" - headerVersion = "version" - headerType = "type" + headerVersion = "version" + headerType = "type" + headerKDFParams = "kdfparams" ) // BcryptSecurityParameter is security parameter var, and it can be changed within the lcd test. @@ -44,6 +46,80 @@ const ( // For further notes on security parameter choice, see README.md var BcryptSecurityParameter uint32 = 12 +var ( + KDFBcrypt = "bcrypt" + KDFScrypt = "scrypt" + DefaultKDF = KDFScrypt +) + +// TODO: verify that these parameters are reasonable +var ( + ScryptDefaultN int = 1 << 15 + ScryptDefaultR int = 8 + ScryptDefaultP int = 1 + ScryptDefaultDKLen int = 32 +) + +// Scrypt params +type scryptParams struct { + N int `json:"n"` + R int `json:"r"` + P int `json:"p"` + DKLen int `json:"dklen"` +} + +func ScryptDefaultParams() scryptParams { + return scryptParams{ + N: ScryptDefaultN, + R: ScryptDefaultR, + P: ScryptDefaultP, + DKLen: ScryptDefaultDKLen, + } +} + +func (p scryptParams) String() string { + return fmt.Sprintf("n=%d,r=%d,p=%d,dklen=%d", p.N, p.R, p.P, p.DKLen) +} + +func parseScryptParams(params string) (scryptParams, error) { + var p scryptParams + _, err := fmt.Sscanf(params, "n=%d,r=%d,p=%d,dklen=%d", &p.N, &p.R, &p.P, &p.DKLen) + if err != nil { + return p, fmt.Errorf("invalid scrypt params: %v %s", err, params) + } + return p, nil +} + +func deriveKey(header map[string]string, passphrase string) ([]byte, error) { + kdf := header["kdf"] + salt, err := hex.DecodeString(header["salt"]) + if err != nil { + return nil, fmt.Errorf("error decoding salt: %v", err.Error()) + } + + if kdf == KDFBcrypt { + key, err := bcrypt.GenerateFromPassword(salt, []byte(passphrase), BcryptSecurityParameter) + if err != nil { + return nil, errorsmod.Wrap(err, "error generating bcrypt key from passphrase") + } + key = crypto.Sha256(key) // get 32 bytes + return key, nil + } else if kdf == KDFScrypt { + params, err := parseScryptParams(header[headerKDFParams]) + if err != nil { + return nil, errorsmod.Wrap(err, "error parsing scrypt params") + } + + key, err := scrypt.Key([]byte(passphrase), salt, params.N, params.R, params.P, params.DKLen) + if err != nil { + return nil, errorsmod.Wrap(err, "error generating scrypt key from passphrase") + } + return key, nil + } + + return nil, fmt.Errorf("unrecognized KDF type: %s", kdf) +} + //----------------------------------------------------------------- // add armor @@ -124,17 +200,21 @@ func unarmorBytes(armorStr, blockType string) (bz []byte, header map[string]stri return } -//----------------------------------------------------------------- +// ----------------------------------------------------------------- // encrypt/decrypt with armor // Encrypt and armor the private key. func EncryptArmorPrivKey(privKey cryptotypes.PrivKey, passphrase string, algo string) string { - saltBytes, encBytes := encryptPrivKey(privKey, passphrase) header := map[string]string{ - "kdf": "bcrypt", - "salt": fmt.Sprintf("%X", saltBytes), + "kdf": DefaultKDF, } + if header["kdf"] == KDFScrypt { + header[headerKDFParams] = ScryptDefaultParams().String() + } + + header, encBytes := encryptPrivKey(privKey, header, passphrase) + if algo != "" { header[headerType] = algo } @@ -147,17 +227,16 @@ func EncryptArmorPrivKey(privKey cryptotypes.PrivKey, passphrase string, algo st // encrypt the given privKey with the passphrase using a randomly // generated salt and the xsalsa20 cipher. returns the salt and the // encrypted priv key. -func encryptPrivKey(privKey cryptotypes.PrivKey, passphrase string) (saltBytes []byte, encBytes []byte) { - saltBytes = crypto.CRandBytes(16) - key, err := bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), BcryptSecurityParameter) +func encryptPrivKey(privKey cryptotypes.PrivKey, header map[string]string, passphrase string) (updatedHeader map[string]string, encBytes []byte) { + header["salt"] = fmt.Sprintf("%X", crypto.CRandBytes(16)) + key, err := deriveKey(header, passphrase) if err != nil { - panic(errorsmod.Wrap(err, "error generating bcrypt key from passphrase")) + panic(errorsmod.Wrap(err, "error deriving key from passphrase")) } - key = crypto.Sha256(key) // get 32 bytes privKeyBytes := legacy.Cdc.MustMarshal(privKey) - return saltBytes, xsalsa20symmetric.EncryptSymmetric(privKeyBytes, key) + return header, xsalsa20symmetric.EncryptSymmetric(privKeyBytes, key) } // UnarmorDecryptPrivKey returns the privkey byte slice, a string of the algo type, and an error @@ -171,20 +250,7 @@ func UnarmorDecryptPrivKey(armorStr string, passphrase string) (privKey cryptoty return privKey, "", fmt.Errorf("unrecognized armor type: %v", blockType) } - if header["kdf"] != "bcrypt" { - return privKey, "", fmt.Errorf("unrecognized KDF type: %v", header["kdf"]) - } - - if header["salt"] == "" { - return privKey, "", fmt.Errorf("missing salt bytes") - } - - saltBytes, err := hex.DecodeString(header["salt"]) - if err != nil { - return privKey, "", fmt.Errorf("error decoding salt: %v", err.Error()) - } - - privKey, err = decryptPrivKey(saltBytes, encBytes, passphrase) + privKey, err = decryptPrivKey(encBytes, header, passphrase) if header[headerType] == "" { header[headerType] = defaultAlgo @@ -193,14 +259,12 @@ func UnarmorDecryptPrivKey(armorStr string, passphrase string) (privKey cryptoty return privKey, header[headerType], err } -func decryptPrivKey(saltBytes []byte, encBytes []byte, passphrase string) (privKey cryptotypes.PrivKey, err error) { - key, err := bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), BcryptSecurityParameter) +func decryptPrivKey(encBytes []byte, header map[string]string, passphrase string) (privKey cryptotypes.PrivKey, err error) { + key, err := deriveKey(header, passphrase) if err != nil { - return privKey, errorsmod.Wrap(err, "error generating bcrypt key from passphrase") + return nil, err } - key = crypto.Sha256(key) // Get 32 bytes - privKeyBytes, err := xsalsa20symmetric.DecryptSymmetric(encBytes, key) if err != nil && err == xsalsa20symmetric.ErrCiphertextDecrypt { return privKey, sdkerrors.ErrWrongPassword diff --git a/crypto/armor_test.go b/crypto/armor_test.go index 60ed5a495aee..416e899f37a6 100644 --- a/crypto/armor_test.go +++ b/crypto/armor_test.go @@ -11,8 +11,10 @@ import ( "github.com/cometbft/cometbft/crypto/xsalsa20symmetric" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/crypto/scrypt" "cosmossdk.io/depinject" + "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec/legacy" "github.com/cosmos/cosmos-sdk/crypto" @@ -26,7 +28,63 @@ import ( "github.com/cosmos/cosmos-sdk/types" ) -func TestArmorUnarmorPrivKey(t *testing.T) { +func TestScryptArmorUnarmorPrivKey(t *testing.T) { + crypto.DefaultKDF = crypto.KDFScrypt + priv := secp256k1.GenPrivKey() + armored := crypto.EncryptArmorPrivKey(priv, "passphrase", "") + _, _, err := crypto.UnarmorDecryptPrivKey(armored, "wrongpassphrase") + require.Error(t, err) + decrypted, algo, err := crypto.UnarmorDecryptPrivKey(armored, "passphrase") + require.NoError(t, err) + require.Equal(t, string(hd.Secp256k1Type), algo) + require.True(t, priv.Equals(decrypted)) + + // empty string + decrypted, algo, err = crypto.UnarmorDecryptPrivKey("", "passphrase") + require.Error(t, err) + require.True(t, errors.Is(io.EOF, err)) + require.Nil(t, decrypted) + require.Empty(t, algo) + + // wrong key type + armored = crypto.ArmorPubKeyBytes(priv.PubKey().Bytes(), "") + _, _, err = crypto.UnarmorDecryptPrivKey(armored, "passphrase") + require.Error(t, err) + require.Contains(t, err.Error(), "unrecognized armor type") + + // armor key manually + params := crypto.ScryptDefaultParams() + encryptPrivKeyFn := func(privKey cryptotypes.PrivKey, passphrase string) (saltBytes []byte, encBytes []byte) { + saltBytes = cmtcrypto.CRandBytes(16) + key, err := scrypt.Key([]byte(passphrase), saltBytes, params.N, params.R, params.P, params.DKLen) + require.NoError(t, err) + + privKeyBytes := legacy.Cdc.Amino.MustMarshalBinaryBare(privKey) + return saltBytes, xsalsa20symmetric.EncryptSymmetric(privKeyBytes, key) + } + saltBytes, encBytes := encryptPrivKeyFn(priv, "passphrase") + + // wrong kdf header + headerWrongKdf := map[string]string{ + "kdf": "wrong", + "kdfparams": params.String(), + "salt": fmt.Sprintf("%X", saltBytes), + "type": "secp256k", + } + armored = crypto.EncodeArmor("TENDERMINT PRIVATE KEY", headerWrongKdf, encBytes) + _, _, err = crypto.UnarmorDecryptPrivKey(armored, "passphrase") + require.Error(t, err) + require.Equal(t, "unrecognized KDF type: wrong", err.Error()) + + // fix kdf and try again + headerWrongKdf["kdf"] = "scrypt" + armored = crypto.EncodeArmor("TENDERMINT PRIVATE KEY", headerWrongKdf, encBytes) + _, _, err = crypto.UnarmorDecryptPrivKey(armored, "passphrase") + require.NoError(t, err) +} + +func TestBcryptArmorUnarmorPrivKey(t *testing.T) { + crypto.DefaultKDF = crypto.KDFBcrypt priv := secp256k1.GenPrivKey() armored := crypto.EncryptArmorPrivKey(priv, "passphrase", "") _, _, err := crypto.UnarmorDecryptPrivKey(armored, "wrongpassphrase") @@ -72,7 +130,8 @@ func TestArmorUnarmorPrivKey(t *testing.T) { require.Equal(t, "unrecognized KDF type: wrong", err.Error()) } -func TestArmorUnarmorPubKey(t *testing.T) { +func TestScryptArmorUnarmorPubKey(t *testing.T) { + crypto.DefaultKDF = crypto.KDFScrypt // Select the encryption and storage for your cryptostore var cdc codec.Codec err := depinject.Inject(configurator.NewAppConfig(), &cdc) @@ -142,7 +201,88 @@ func TestArmorUnarmorPubKey(t *testing.T) { require.Equal(t, "unrecognized version: unknown", err.Error()) } -func TestArmorInfoBytes(t *testing.T) { +func TestBcryptArmorUnarmorPubKey(t *testing.T) { + crypto.DefaultKDF = crypto.KDFBcrypt + // Select the encryption and storage for your cryptostore + var cdc codec.Codec + err := depinject.Inject(configurator.NewAppConfig(), &cdc) + require.NoError(t, err) + + cstore := keyring.NewInMemory(cdc) + + // Add keys and see they return in alphabetical order + k, _, err := cstore.NewMnemonic("Bob", keyring.English, types.FullFundraiserPath, keyring.DefaultBIP39Passphrase, hd.Secp256k1) + require.NoError(t, err) + key, err := k.GetPubKey() + require.NoError(t, err) + armored := crypto.ArmorPubKeyBytes(legacy.Cdc.Amino.MustMarshalBinaryBare(key), "") + pubBytes, algo, err := crypto.UnarmorPubKeyBytes(armored) + require.NoError(t, err) + pub, err := legacy.PubKeyFromBytes(pubBytes) + require.NoError(t, err) + require.Equal(t, string(hd.Secp256k1Type), algo) + require.True(t, pub.Equals(key)) + + armored = crypto.ArmorPubKeyBytes(legacy.Cdc.Amino.MustMarshalBinaryBare(key), "unknown") + pubBytes, algo, err = crypto.UnarmorPubKeyBytes(armored) + require.NoError(t, err) + pub, err = legacy.PubKeyFromBytes(pubBytes) + require.NoError(t, err) + require.Equal(t, "unknown", algo) + require.True(t, pub.Equals(key)) + + armored, err = cstore.ExportPrivKeyArmor("Bob", "passphrase") + require.NoError(t, err) + _, _, err = crypto.UnarmorPubKeyBytes(armored) + require.Error(t, err) + require.Equal(t, `couldn't unarmor bytes: unrecognized armor type "TENDERMINT PRIVATE KEY", expected: "TENDERMINT PUBLIC KEY"`, err.Error()) + + // armor pubkey manually + header := map[string]string{ + "version": "0.0.0", + "type": "unknown", + } + armored = crypto.EncodeArmor("TENDERMINT PUBLIC KEY", header, pubBytes) + _, algo, err = crypto.UnarmorPubKeyBytes(armored) + require.NoError(t, err) + // return secp256k1 if version is 0.0.0 + require.Equal(t, "secp256k1", algo) + + // missing version header + header = map[string]string{ + "type": "unknown", + } + armored = crypto.EncodeArmor("TENDERMINT PUBLIC KEY", header, pubBytes) + bz, algo, err := crypto.UnarmorPubKeyBytes(armored) + require.Nil(t, bz) + require.Empty(t, algo) + require.Error(t, err) + require.Equal(t, "header's version field is empty", err.Error()) + + // unknown version header + header = map[string]string{ + "type": "unknown", + "version": "unknown", + } + armored = crypto.EncodeArmor("TENDERMINT PUBLIC KEY", header, pubBytes) + bz, algo, err = crypto.UnarmorPubKeyBytes(armored) + require.Nil(t, bz) + require.Empty(t, algo) + require.Error(t, err) + require.Equal(t, "unrecognized version: unknown", err.Error()) +} + +func TestScryptArmorInfoBytes(t *testing.T) { + crypto.DefaultKDF = crypto.KDFScrypt + bs := []byte("test") + armoredString := crypto.ArmorInfoBytes(bs) + unarmoredBytes, err := crypto.UnarmorInfoBytes(armoredString) + require.NoError(t, err) + require.True(t, bytes.Equal(bs, unarmoredBytes)) +} + +func TestBcryptArmorInfoBytes(t *testing.T) { + crypto.DefaultKDF = crypto.KDFBcrypt bs := []byte("test") armoredString := crypto.ArmorInfoBytes(bs) unarmoredBytes, err := crypto.UnarmorInfoBytes(armoredString) @@ -150,7 +290,26 @@ func TestArmorInfoBytes(t *testing.T) { require.True(t, bytes.Equal(bs, unarmoredBytes)) } -func TestUnarmorInfoBytesErrors(t *testing.T) { +func TestScryptUnarmorInfoBytesErrors(t *testing.T) { + crypto.DefaultKDF = crypto.KDFScrypt + unarmoredBytes, err := crypto.UnarmorInfoBytes("") + require.Error(t, err) + require.True(t, errors.Is(io.EOF, err)) + require.Nil(t, unarmoredBytes) + + header := map[string]string{ + "type": "Info", + "version": "0.0.1", + } + unarmoredBytes, err = crypto.UnarmorInfoBytes(crypto.EncodeArmor( + "TENDERMINT KEY INFO", header, []byte("plain-text"))) + require.Error(t, err) + require.Equal(t, "unrecognized version: 0.0.1", err.Error()) + require.Nil(t, unarmoredBytes) +} + +func TestBcryptUnarmorInfoBytesErrors(t *testing.T) { + crypto.DefaultKDF = crypto.KDFBcrypt unarmoredBytes, err := crypto.UnarmorInfoBytes("") require.Error(t, err) require.True(t, errors.Is(io.EOF, err)) @@ -183,7 +342,21 @@ func BenchmarkBcryptGenerateFromPassword(b *testing.B) { } } -func TestArmor(t *testing.T) { +func TestScryptArmor(t *testing.T) { + crypto.DefaultKDF = crypto.KDFScrypt + blockType := "MINT TEST" + data := []byte("somedata") + armorStr := crypto.EncodeArmor(blockType, nil, data) + + // Decode armorStr and test for equivalence. + blockType2, _, data2, err := crypto.DecodeArmor(armorStr) + require.Nil(t, err, "%+v", err) + assert.Equal(t, blockType, blockType2) + assert.Equal(t, data, data2) +} + +func TestBcryptArmor(t *testing.T) { + crypto.DefaultKDF = crypto.KDFBcrypt blockType := "MINT TEST" data := []byte("somedata") armorStr := crypto.EncodeArmor(blockType, nil, data) diff --git a/crypto/keyring/keyring.go b/crypto/keyring/keyring.go index a1e09ae4c9b3..e632b3c48b4f 100644 --- a/crypto/keyring/keyring.go +++ b/crypto/keyring/keyring.go @@ -46,7 +46,7 @@ const ( passKeyringPrefix = "keyring-%s" // temporary pass phrase for exporting a key during a key rename - passPhrase = "temp" + tempPassphrase = "temp" ) var ( @@ -454,7 +454,7 @@ func (ks keystore) Rename(oldName, newName string) error { return errorsmod.Wrap(ErrKeyAlreadyExists, fmt.Sprintf("rename failed, %s", newName)) } - armor, err := ks.ExportPrivKeyArmor(oldName, passPhrase) + armor, err := ks.ExportPrivKeyArmor(oldName, tempPassphrase) if err != nil { return err } @@ -463,7 +463,7 @@ func (ks keystore) Rename(oldName, newName string) error { return err } - if err := ks.ImportPrivKey(newName, armor, passPhrase); err != nil { + if err := ks.ImportPrivKey(newName, armor, tempPassphrase); err != nil { return err }