Skip to content

Commit 8ed9401

Browse files
authored
Merge pull request #400 from Zondax/feat/bcrypt-api-pbkdf2
feat: Migrated bcrypt.GeneratePassword to pdkdf2 and added tests
2 parents 2f4654e + 240b7ad commit 8ed9401

File tree

3 files changed

+82
-19
lines changed

3 files changed

+82
-19
lines changed

crypto/armor.go

+41-13
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package crypto
22

33
import (
44
"bytes"
5+
"crypto/sha256"
56
"encoding/hex"
67
"fmt"
78
"io"
@@ -16,6 +17,8 @@ import (
1617
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
1718
"github.com/cosmos/cosmos-sdk/crypto/xsalsa20symmetric"
1819
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
20+
21+
pdkdf2 "golang.org/x/crypto/pbkdf2"
1922
)
2023

2124
const (
@@ -149,13 +152,12 @@ func EncryptArmorPrivKey(privKey cryptotypes.PrivKey, passphrase string, algo st
149152
// encrypted priv key.
150153
func encryptPrivKey(privKey cryptotypes.PrivKey, passphrase string) (saltBytes []byte, encBytes []byte) {
151154
saltBytes = crypto.CRandBytes(16)
152-
key, err := bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), BcryptSecurityParameter)
153-
if err != nil {
154-
panic(errorsmod.Wrap(err, "error generating bcrypt key from passphrase"))
155-
}
156-
155+
key := pdkdf2.Key([]byte(passphrase), saltBytes, int(BcryptSecurityParameter), 60, sha256.New)
157156
key = crypto.Sha256(key) // get 32 bytes
157+
158158
privKeyBytes := legacy.Cdc.MustMarshal(privKey)
159+
privKeyBytesHash := crypto.Sha256(privKeyBytes)
160+
privKeyBytes = append(privKeyBytes, privKeyBytesHash...) // Add own hash to differentiate it from old implementation
159161

160162
return saltBytes, xsalsa20symmetric.EncryptSymmetric(privKeyBytes, key)
161163
}
@@ -194,21 +196,47 @@ func UnarmorDecryptPrivKey(armorStr string, passphrase string) (privKey cryptoty
194196
}
195197

196198
func decryptPrivKey(saltBytes []byte, encBytes []byte, passphrase string) (privKey cryptotypes.PrivKey, err error) {
197-
key, err := bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), BcryptSecurityParameter)
199+
key := pdkdf2.Key([]byte(passphrase), saltBytes, int(BcryptSecurityParameter), 60, sha256.New)
200+
201+
var privKeyBytes []byte
202+
203+
decryptedPrivBytes, err := decryptSymmetric(encBytes, key)
204+
if err == nil && len(decryptedPrivBytes) > 32 {
205+
privBytes := decryptedPrivBytes[:len(decryptedPrivBytes)-32]
206+
privBytesHash := decryptedPrivBytes[len(decryptedPrivBytes)-32:] // SHA-256 hash is 32 bytes
207+
// If the decrypted hash doesn't match the privateBytes hash, then we are working with the old bcrypt algorithm
208+
if !bytes.Equal(crypto.Sha256(privBytes), privBytesHash) {
209+
privBytes, err = legacyDecryptPrivKey(saltBytes, encBytes, passphrase)
210+
}
211+
privKeyBytes = privBytes
212+
} else {
213+
privKeyBytes, err = legacyDecryptPrivKey(saltBytes, encBytes, passphrase)
214+
}
215+
198216
if err != nil {
199-
return privKey, errorsmod.Wrap(err, "error generating bcrypt key from passphrase")
217+
return privKey, err
200218
}
201219

202-
key = crypto.Sha256(key) // Get 32 bytes
220+
return legacy.PrivKeyFromBytes(privKeyBytes)
221+
}
203222

204-
privKeyBytes, err := xsalsa20symmetric.DecryptSymmetric(encBytes, key)
205-
if err != nil && err == xsalsa20symmetric.ErrCiphertextDecrypt {
206-
return privKey, sdkerrors.ErrWrongPassword
223+
func decryptSymmetric(encBytes []byte, key []byte) (privKeyBytes []byte, err error) {
224+
key = crypto.Sha256(key)
225+
privKeyBytes, err = xsalsa20symmetric.DecryptSymmetric(encBytes, key)
226+
if err != nil && err.Error() == "ciphertext decryption failed" {
227+
return privKeyBytes, sdkerrors.ErrWrongPassword
207228
} else if err != nil {
208-
return privKey, err
229+
return privKeyBytes, err
209230
}
231+
return privKeyBytes, nil
232+
}
210233

211-
return legacy.PrivKeyFromBytes(privKeyBytes)
234+
func legacyDecryptPrivKey(saltBytes []byte, encBytes []byte, passphrase string) (decryptedBytes []byte, err error) {
235+
key, err := bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), BcryptSecurityParameter)
236+
if err != nil {
237+
return decryptedBytes, errorsmod.Wrap(err, "error generating bcrypt key from passphrase")
238+
}
239+
return decryptSymmetric(encBytes, key)
212240
}
213241

214242
//-----------------------------------------------------------------

crypto/armor_test.go

+40-5
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@ package crypto_test
22

33
import (
44
"bytes"
5+
"crypto/sha256"
56
"errors"
67
"fmt"
78
"io"
89
"testing"
910

1011
cmtcrypto "github.com/cometbft/cometbft/crypto"
12+
"github.com/cometbft/cometbft/crypto/armor"
1113
"github.com/cometbft/cometbft/crypto/xsalsa20symmetric"
1214
"github.com/stretchr/testify/assert"
1315
"github.com/stretchr/testify/require"
16+
pdkdf2 "golang.org/x/crypto/pbkdf2"
1417

1518
"cosmossdk.io/depinject"
1619
"github.com/cosmos/cosmos-sdk/codec"
@@ -167,17 +170,16 @@ func TestUnarmorInfoBytesErrors(t *testing.T) {
167170
require.Nil(t, unarmoredBytes)
168171
}
169172

170-
func BenchmarkBcryptGenerateFromPassword(b *testing.B) {
173+
func BenchmarkBcryptPdkdf2(b *testing.B) {
171174
passphrase := []byte("passphrase")
172175
for securityParam := uint32(9); securityParam < 16; securityParam++ {
173-
param := securityParam
174-
b.Run(fmt.Sprintf("benchmark-security-param-%d", param), func(b *testing.B) {
176+
b.Run(fmt.Sprintf("benchmark-security-param-%d", securityParam), func(b *testing.B) {
175177
b.ReportAllocs()
176178
saltBytes := cmtcrypto.CRandBytes(16)
177179
b.ResetTimer()
178180
for i := 0; i < b.N; i++ {
179-
_, err := bcrypt.GenerateFromPassword(saltBytes, passphrase, param)
180-
require.Nil(b, err)
181+
key := pdkdf2.Key(passphrase, saltBytes, int(securityParam), 60, sha256.New)
182+
require.NotNil(b, key)
181183
}
182184
})
183185
}
@@ -194,3 +196,36 @@ func TestArmor(t *testing.T) {
194196
assert.Equal(t, blockType, blockType2)
195197
assert.Equal(t, data, data2)
196198
}
199+
200+
func TestBcryptLegacyEncryption(t *testing.T) {
201+
privKey := secp256k1.GenPrivKey()
202+
saltBytes := cmtcrypto.CRandBytes(16)
203+
passphrase := "passphrase"
204+
205+
// Old encryption method
206+
key, _ := bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), 12) // Legacy Ley gemeratopm
207+
key = cmtcrypto.Sha256(key) // get 32 bytes
208+
privKeyBytes := legacy.Cdc.MustMarshal(privKey)
209+
encBytes := xsalsa20symmetric.EncryptSymmetric(privKeyBytes, key)
210+
header := map[string]string{
211+
"kdf": "bcrypt",
212+
"salt": fmt.Sprintf("%X", saltBytes),
213+
}
214+
armorString := armor.EncodeArmor("TENDERMINT PRIVATE KEY", header, encBytes)
215+
216+
_, _, err := crypto.UnarmorDecryptPrivKey(armorString, "wrongpassphrase")
217+
require.Error(t, err)
218+
decrypted, algo, err := crypto.UnarmorDecryptPrivKey(armorString, passphrase)
219+
require.NoError(t, err)
220+
require.Equal(t, string(hd.Secp256k1Type), algo)
221+
require.True(t, privKey.Equals(decrypted))
222+
223+
// Latest encryption method
224+
armored := crypto.EncryptArmorPrivKey(privKey, passphrase, "")
225+
_, _, err = crypto.UnarmorDecryptPrivKey(armored, "wrongpassphrase")
226+
require.Error(t, err)
227+
decrypted, algo, err = crypto.UnarmorDecryptPrivKey(armored, passphrase)
228+
require.NoError(t, err)
229+
require.Equal(t, string(hd.Secp256k1Type), algo)
230+
require.True(t, privKey.Equals(decrypted))
231+
}

server/grpc/server_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ func (s *IntegrationTestSuite) TestGRPCServer_Reflection() {
115115
// so that we can always assert that given a reflection server it is
116116
// possible to fully query all the methods, without having any context
117117
// on the proto registry
118-
rc := grpcreflect.NewClient(ctx, stub)
118+
rc := grpcreflect.NewClientV1Alpha(ctx, stub)
119119

120120
services, err := rc.ListServices()
121121
s.Require().NoError(err)

0 commit comments

Comments
 (0)