261 lines
8.6 KiB
Go
261 lines
8.6 KiB
Go
package crypto_test
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"testing"
|
|
|
|
cmtcrypto "github.com/cometbft/cometbft/crypto"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"cosmossdk.io/core/address"
|
|
"cosmossdk.io/depinject"
|
|
"cosmossdk.io/log"
|
|
|
|
"github.com/cosmos/cosmos-sdk/codec"
|
|
addresscodec "github.com/cosmos/cosmos-sdk/codec/address"
|
|
"github.com/cosmos/cosmos-sdk/codec/legacy"
|
|
"github.com/cosmos/cosmos-sdk/crypto"
|
|
"github.com/cosmos/cosmos-sdk/crypto/hd"
|
|
"github.com/cosmos/cosmos-sdk/crypto/keyring"
|
|
"github.com/cosmos/cosmos-sdk/crypto/keys/bcrypt"
|
|
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
|
|
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
|
|
"github.com/cosmos/cosmos-sdk/crypto/xsalsa20symmetric"
|
|
_ "github.com/cosmos/cosmos-sdk/runtime"
|
|
"github.com/cosmos/cosmos-sdk/testutil/configurator"
|
|
"github.com/cosmos/cosmos-sdk/types"
|
|
)
|
|
|
|
func TestArmorUnarmorPrivKey(t *testing.T) {
|
|
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
|
|
encryptPrivKeyFn := func(privKey cryptotypes.PrivKey, passphrase string) (saltBytes, encBytes []byte) {
|
|
saltBytes = cmtcrypto.CRandBytes(16)
|
|
key, err := bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), crypto.BcryptSecurityParameter)
|
|
require.NoError(t, err)
|
|
key = cmtcrypto.Sha256(key) // get 32 bytes
|
|
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",
|
|
"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())
|
|
}
|
|
|
|
func TestArmorUnarmorPubKey(t *testing.T) {
|
|
// Select the encryption and storage for your cryptostore
|
|
var cdc codec.Codec
|
|
|
|
err := depinject.Inject(depinject.Configs(
|
|
configurator.NewAppConfig(),
|
|
depinject.Supply(log.NewNopLogger(),
|
|
func() address.Codec { return addresscodec.NewBech32Codec("cosmos") },
|
|
func() address.ValidatorAddressCodec { return addresscodec.NewBech32Codec("cosmosvaloper") },
|
|
func() address.ConsensusAddressCodec { return addresscodec.NewBech32Codec("cosmosvalcons") },
|
|
),
|
|
), &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 TestArmorInfoBytes(t *testing.T) {
|
|
bs := []byte("test")
|
|
armoredString := crypto.ArmorInfoBytes(bs)
|
|
unarmoredBytes, err := crypto.UnarmorInfoBytes(armoredString)
|
|
require.NoError(t, err)
|
|
require.True(t, bytes.Equal(bs, unarmoredBytes))
|
|
}
|
|
|
|
func TestUnarmorInfoBytesErrors(t *testing.T) {
|
|
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 BenchmarkBcryptGenerateFromPassword(b *testing.B) {
|
|
passphrase := []byte("passphrase")
|
|
for securityParam := uint32(9); securityParam < 16; securityParam++ {
|
|
param := securityParam
|
|
b.Run(fmt.Sprintf("benchmark-security-param-%d", param), func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
saltBytes := cmtcrypto.CRandBytes(16)
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_, err := bcrypt.GenerateFromPassword(saltBytes, passphrase, param)
|
|
require.Nil(b, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestArmor(t *testing.T) {
|
|
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 TestBcryptLegacyEncryption(t *testing.T) {
|
|
privKey := secp256k1.GenPrivKey()
|
|
saltBytes := cmtcrypto.CRandBytes(16)
|
|
passphrase := "passphrase"
|
|
privKeyBytes := legacy.Cdc.MustMarshal(privKey)
|
|
|
|
// Bcrypt + Aead
|
|
headerBcrypt := map[string]string{
|
|
"kdf": "bcrypt",
|
|
"salt": fmt.Sprintf("%X", saltBytes),
|
|
}
|
|
keyBcrypt, _ := bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), 12) // Legacy key generation
|
|
keyBcrypt = cmtcrypto.Sha256(keyBcrypt)
|
|
|
|
// bcrypt + xsalsa20symmetric
|
|
encBytesBcryptXsalsa20symetric := xsalsa20symmetric.EncryptSymmetric(privKeyBytes, keyBcrypt)
|
|
|
|
type testCase struct {
|
|
description string
|
|
armor string
|
|
}
|
|
|
|
for _, scenario := range []testCase{
|
|
{
|
|
description: "Argon2 + Aead",
|
|
armor: crypto.EncryptArmorPrivKey(privKey, "passphrase", ""),
|
|
},
|
|
{
|
|
description: "Bcrypt + xsalsa20symmetric",
|
|
armor: crypto.EncodeArmor("TENDERMINT PRIVATE KEY", headerBcrypt, encBytesBcryptXsalsa20symetric),
|
|
},
|
|
} {
|
|
t.Run(scenario.description, func(t *testing.T) {
|
|
_, _, err := crypto.UnarmorDecryptPrivKey(scenario.armor, "wrongpassphrase")
|
|
require.Error(t, err)
|
|
decryptedPrivKey, _, err := crypto.UnarmorDecryptPrivKey(scenario.armor, "passphrase")
|
|
require.NoError(t, err)
|
|
require.True(t, privKey.Equals(decryptedPrivKey))
|
|
})
|
|
}
|
|
|
|
// Test wrong kdf header
|
|
headerWithoutKdf := map[string]string{
|
|
"kdf": "wrongKdf",
|
|
"salt": fmt.Sprintf("%X", saltBytes),
|
|
}
|
|
|
|
_, _, err := crypto.UnarmorDecryptPrivKey(crypto.EncodeArmor("TENDERMINT PRIVATE KEY", headerWithoutKdf, encBytesBcryptXsalsa20symetric), "passphrase")
|
|
require.Error(t, err)
|
|
require.Equal(t, "unrecognized KDF type: wrongKdf", err.Error())
|
|
}
|