Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: bcrypt key derivation to aead #509

Merged
merged 27 commits into from
Apr 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1c798d6
Added Aead encryption and Argon2 key derivation to armor
bizk Mar 16, 2023
afc9266
Added Argon2 key derivation to keyring
bizk Mar 16, 2023
55acbdc
Merge branch 'cosmos:main' into feat/bcrypt-key-derivation-to-aead
bizk Mar 16, 2023
5f0fc46
Fixed potential bug in variable not being propertly assigned
bizk Mar 16, 2023
9bda388
Switched aead library to x/crypto
bizk Mar 17, 2023
f36d013
fix different lint errors
bizk Mar 17, 2023
f1de6ba
fix lint issues
bizk Mar 20, 2023
185c764
Merge branch 'main' into feat/bcrypt-key-derivation-to-aead
bizk Mar 20, 2023
451c90b
Modified decrtyption logic and fixed typo
bizk Mar 21, 2023
d18cea8
Merge branch 'feat/bcrypt-key-derivation-to-aead' of github.com:Zonda…
bizk Mar 22, 2023
6f03c47
Fixed comments, and added error handling messages
bizk Mar 23, 2023
9abbbc1
Merge branch 'main' into feat/bcrypt-key-derivation-to-aead
bizk Mar 23, 2023
7b1fe33
reopen pr
bizk Mar 23, 2023
673d6fd
fixed coments
bizk Mar 24, 2023
7f1a3d7
fixed comments
bizk Mar 28, 2023
63ac151
added changelog
bizk Mar 28, 2023
f69f8a4
Merge branch 'main' into feat/bcrypt-key-derivation-to-aead
bizk Mar 28, 2023
76756e8
Added improvements over crypto/armor.go
bizk Apr 3, 2023
2a0ad85
merged with master
bizk Apr 3, 2023
0364bff
added error default value for kdf switch case and error handling
bizk Apr 3, 2023
421ce8c
Merge branch 'main' of github.com:Zondax/cosmos-sdk into feat/bcrypt-…
bizk Apr 8, 2023
d0a1d97
failing test fix
bizk Apr 8, 2023
0fa9606
referted keyring changes since it was out of scope and generated bugs
bizk Apr 10, 2023
6ed018d
rollbakc changes on keyring_test
bizk Apr 10, 2023
59c3de6
reducing scope of the PR to thiecket issue
bizk Apr 11, 2023
8f25ac3
added missing error message for unarmor when having a bad passphrase
bizk Apr 11, 2023
6a866ab
Merge branch 'main' into feat/bcrypt-key-derivation-to-aead
bizk Apr 11, 2023
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Ref: https://keepachangelog.com/en/1.0.0/

### Improvements

* (crypto) [#3129](https://github.com/cosmos/cosmos-sdk/pull/3129) New armor and keyring key derivation uses aead and encryption uses chacha20poly
* (x/slashing) [#15580](https://github.com/cosmos/cosmos-sdk/pull/15580) Refactor the validator's missed block signing window to be a chunked bitmap instead of a "logical" bitmap, significantly reducing the storage footprint.
* [#15448](https://github.com/cosmos/cosmos-sdk/pull/15448) Automatically populate the block timestamp for historical queries. In contexts where the block timestamp is needed for previous states, the timestamp will now be set. Note, when querying against a node it must be re-synced in order to be able to automatically populate the block timestamp. Otherwise, the block timestamp will be populated for heights going forward once upgraded.
* (x/gov) [#15554](https://github.com/cosmos/cosmos-sdk/pull/15554) Add proposal result log in `active_proposal` event. When a proposal passes but fails to execute, the proposal result is logged in the `active_proposal` event.
Expand Down
90 changes: 69 additions & 21 deletions crypto/armor.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import (
"io"

"github.com/cometbft/cometbft/crypto"
"golang.org/x/crypto/openpgp/armor" //nolint:staticcheck // TODO: remove this dependency
"golang.org/x/crypto/argon2"
"golang.org/x/crypto/openpgp/armor" //nolint:staticcheck

errorsmod "cosmossdk.io/errors"
"golang.org/x/crypto/chacha20poly1305"

"github.com/cosmos/cosmos-sdk/codec/legacy"
"github.com/cosmos/cosmos-sdk/crypto/keys/bcrypt"
Expand All @@ -29,6 +31,19 @@ const (
headerType = "type"
)

var (
kdfHeader = "kdf"
kdfBcrypt = "bcrypt"
kdfArgon2 = "argon2"
)

const (
Argon2Time = 1
Argon2Memory = 64 * 1024
Argon2Threads = 4
Argon2KeyLen = 32
)

// BcryptSecurityParameter is security parameter var, and it can be changed within the lcd test.
// Making the bcrypt security parameter a var shouldn't be a security issue:
// One can't verify an invalid key by maliciously changing the bcrypt
Expand Down Expand Up @@ -131,8 +146,8 @@ func unarmorBytes(armorStr, blockType string) (bz []byte, header map[string]stri
func EncryptArmorPrivKey(privKey cryptotypes.PrivKey, passphrase, algo string) string {
saltBytes, encBytes := encryptPrivKey(privKey, passphrase)
header := map[string]string{
"kdf": "bcrypt",
"salt": fmt.Sprintf("%X", saltBytes),
kdfHeader: kdfArgon2,
"salt": fmt.Sprintf("%X", saltBytes),
}

if algo != "" {
Expand All @@ -149,15 +164,20 @@ func EncryptArmorPrivKey(privKey cryptotypes.PrivKey, passphrase, algo string) s
// encrypted priv key.
func encryptPrivKey(privKey cryptotypes.PrivKey, passphrase string) (saltBytes, encBytes []byte) {
saltBytes = crypto.CRandBytes(16)
key, err := bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), BcryptSecurityParameter)

key := argon2.IDKey([]byte(passphrase), saltBytes, Argon2Time, Argon2Memory, Argon2Threads, Argon2KeyLen)
privKeyBytes := legacy.Cdc.MustMarshal(privKey)

aead, err := chacha20poly1305.New(key)
if err != nil {
panic(errorsmod.Wrap(err, "error generating bcrypt key from passphrase"))
panic(errorsmod.Wrap(err, "error generating cypher from key"))
}

key = crypto.Sha256(key) // get 32 bytes
privKeyBytes := legacy.Cdc.MustMarshal(privKey)
nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+len(privKeyBytes)+aead.Overhead())

encBytes = aead.Seal(nonce, nonce, privKeyBytes, nil)

return saltBytes, xsalsa20symmetric.EncryptSymmetric(privKeyBytes, key)
return saltBytes, encBytes
}

// UnarmorDecryptPrivKey returns the privkey byte slice, a string of the algo type, and an error
Expand All @@ -171,8 +191,8 @@ func UnarmorDecryptPrivKey(armorStr, passphrase string) (privKey cryptotypes.Pri
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[kdfHeader] != kdfBcrypt && header[kdfHeader] != kdfArgon2 {
return privKey, "", fmt.Errorf("unrecognized KDF type: %v", header[kdfHeader])
}

if header["salt"] == "" {
Expand All @@ -184,7 +204,7 @@ func UnarmorDecryptPrivKey(armorStr, passphrase string) (privKey cryptotypes.Pri
return privKey, "", fmt.Errorf("error decoding salt: %v", err.Error())
}

privKey, err = decryptPrivKey(saltBytes, encBytes, passphrase)
privKey, err = decryptPrivKey(saltBytes, encBytes, passphrase, header[kdfHeader])

if header[headerType] == "" {
header[headerType] = defaultAlgo
Expand All @@ -193,18 +213,46 @@ func UnarmorDecryptPrivKey(armorStr, passphrase string) (privKey cryptotypes.Pri
return privKey, header[headerType], err
}

func decryptPrivKey(saltBytes, encBytes []byte, passphrase string) (privKey cryptotypes.PrivKey, err error) {
key, err := bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), BcryptSecurityParameter)
if err != nil {
return privKey, errorsmod.Wrap(err, "error generating bcrypt key from passphrase")
}
func decryptPrivKey(saltBytes []byte, encBytes []byte, passphrase string, kdf string) (privKey cryptotypes.PrivKey, err error) {
// Key derivation
var (
key []byte
privKeyBytes []byte
)

// Since the argon2 key derivation and chacha encryption was implemented together, it is not possible to have mixed kdf and encryption algorithms
switch kdf {
case kdfArgon2:
key = argon2.IDKey([]byte(passphrase), saltBytes, Argon2Time, Argon2Memory, Argon2Threads, Argon2KeyLen)

aead, err := chacha20poly1305.New(key)
if err != nil {
return privKey, errorsmod.Wrap(err, "Error generating aead cypher for key.")
} else if len(encBytes) < aead.NonceSize() {
return privKey, errorsmod.Wrap(nil, "Encrypted bytes length is smaller than aead nonce size.")
}

nonce, privKeyBytesEncrypted := encBytes[:aead.NonceSize()], encBytes[aead.NonceSize():] // Split nonce and ciphertext.
privKeyBytes, err = aead.Open(nil, nonce, privKeyBytesEncrypted, nil) // Decrypt the message and check it wasn't tampered with.
if err != nil {
return privKey, sdkerrors.ErrWrongPassword
}
case kdfBcrypt:
key, err = bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), BcryptSecurityParameter)
if err != nil {
return privKey, errorsmod.Wrap(err, "Error generating bcrypt cypher for key.")
}
key = crypto.Sha256(key) // Get 32 bytes
privKeyBytes, err = xsalsa20symmetric.DecryptSymmetric(encBytes, key)

key = crypto.Sha256(key) // Get 32 bytes
if err == xsalsa20symmetric.ErrCiphertextDecrypt {
return privKey, sdkerrors.ErrWrongPassword
}
default:
return privKey, errorsmod.Wrap(nil, fmt.Sprintf("Unrecognized key derivation function (kdf) header: %s.", kdf))
}

privKeyBytes, err := xsalsa20symmetric.DecryptSymmetric(encBytes, key)
if err != nil && err == xsalsa20symmetric.ErrCiphertextDecrypt {
return privKey, sdkerrors.ErrWrongPassword
} else if err != nil {
if err != nil {
return privKey, err
}

Expand Down
57 changes: 55 additions & 2 deletions crypto/armor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import (
"testing"

cmtcrypto "github.com/cometbft/cometbft/crypto"
"github.com/cometbft/cometbft/crypto/xsalsa20symmetric"
"github.com/cosmos/cosmos-sdk/crypto/xsalsa20symmetric"

_ "github.com/cosmos/cosmos-sdk/runtime"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

Expand All @@ -21,7 +23,6 @@ import (
"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/runtime"
"github.com/cosmos/cosmos-sdk/testutil/configurator"
"github.com/cosmos/cosmos-sdk/types"
)
Expand Down Expand Up @@ -194,3 +195,55 @@ func TestArmor(t *testing.T) {
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())
}