diff --git a/pkg/crypto/aes256_gcm.go b/pkg/crypto/aes256_gcm.go index a7538363da..ee41bb8b40 100644 --- a/pkg/crypto/aes256_gcm.go +++ b/pkg/crypto/aes256_gcm.go @@ -9,11 +9,41 @@ import ( io "io" "github.com/pkg/errors" + "golang.org/x/crypto/pbkdf2" ) -// EncryptAES256GCMBase64 encrypts the given string plaintext using AES-256-GCM with the given password and returns the base64-encoded ciphertext. +const ( + // pbkdf2Iterations is the number of PBKDF2 iterations used for key derivation. + // OWASP 2023 recommends 600000 for PBKDF2-HMAC-SHA256. + pbkdf2Iterations = 600000 + // pbkdf2KeyLen is the AES-256 key length in bytes. + pbkdf2KeyLen = 32 +) + +// getAESKey derives a 32-byte AES key from the given password using PBKDF2. +// This replaces the previous SHA-256-based derivation which was vulnerable to +// offline brute-force attacks. +func getAESKey(password string) []byte { + return getAESKeyPBKDF2(password) +} + +// getAESKeyPBKDF2 derives a key using PBKDF2-HMAC-SHA256 with a static salt. +func getAESKeyPBKDF2(password string) []byte { + salt := []byte("zeta-chain-aes-256-gcm") + return pbkdf2.Key([]byte(password), salt, pbkdf2Iterations, pbkdf2KeyLen, sha256.New) +} + +// getAESKeyLegacy derives a key using a single SHA-256 round (old insecure method). +// Retained for backward compatibility with encrypted data from before the PBKDF2 migration. +func getAESKeyLegacy(key string) []byte { + h := sha256.New() + h.Write([]byte(key)) + return h.Sum(nil) +} + +// EncryptAES256GCMBase64 encrypts the given string plaintext using AES-256-GCM with the given password +// and returns the base64-encoded ciphertext. func EncryptAES256GCMBase64(plaintext string, password string) (string, error) { - // validate the input if plaintext == "" { return "", errors.New("plaintext must not be empty") } @@ -21,7 +51,6 @@ func EncryptAES256GCMBase64(plaintext string, password string) (string, error) { return "", errors.New("password must not be empty") } - // encrypt the plaintext ciphertext, err := EncryptAES256GCM([]byte(plaintext), password) if err != nil { return "", errors.Wrap(err, "failed to encrypt string plaintext") @@ -31,7 +60,6 @@ func EncryptAES256GCMBase64(plaintext string, password string) (string, error) { // DecryptAES256GCMBase64 decrypts the given base64-encoded ciphertext using AES-256-GCM with the given password. func DecryptAES256GCMBase64(ciphertextBase64 string, password string) (string, error) { - // validate the input if ciphertextBase64 == "" { return "", errors.New("ciphertext must not be empty") } @@ -39,13 +67,11 @@ func DecryptAES256GCMBase64(ciphertextBase64 string, password string) (string, e return "", errors.New("password must not be empty") } - // decode the base64-encoded ciphertext ciphertext, err := base64.StdEncoding.DecodeString(ciphertextBase64) if err != nil { return "", errors.Wrap(err, "failed to decode base64 ciphertext") } - // decrypt the ciphertext plaintext, err := DecryptAES256GCM(ciphertext, password) if err != nil { return "", errors.Wrap(err, "failed to decrypt ciphertext") @@ -55,54 +81,59 @@ func DecryptAES256GCMBase64(ciphertextBase64 string, password string) (string, e // EncryptAES256GCM encrypts the given plaintext using AES-256-GCM with the given password. func EncryptAES256GCM(plaintext []byte, password string) ([]byte, error) { - // create AES cipher block, err := aes.NewCipher(getAESKey(password)) if err != nil { return nil, err } - // create GCM mode gcm, err := cipher.NewGCM(block) if err != nil { return nil, err } - // generate random nonce nonce := make([]byte, gcm.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return nil, err } - // encrypt the plaintext ciphertext := gcm.Seal(nonce, nonce, plaintext, nil) - return ciphertext, nil } // DecryptAES256GCM decrypts the given ciphertext using AES-256-GCM with the given password. +// It first tries PBKDF2 key derivation, then falls back to legacy SHA-256 for backward compatibility. func DecryptAES256GCM(ciphertext []byte, password string) ([]byte, error) { - // create AES cipher - block, err := aes.NewCipher(getAESKey(password)) + // try PBKDF2 key derivation first + plaintext, err := decryptWithKey(ciphertext, getAESKeyPBKDF2(password)) + if err != nil { + // fall back to legacy SHA-256 key derivation + plaintext, err = decryptWithKey(ciphertext, getAESKeyLegacy(password)) + if err != nil { + return nil, errors.Wrap(err, "failed to decrypt with both PBKDF2 and legacy key derivation") + } + } + return plaintext, nil +} + +// decryptWithKey decrypts ciphertext with the given AES key. +func decryptWithKey(ciphertext []byte, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) if err != nil { return nil, err } - // create GCM mode gcm, err := cipher.NewGCM(block) if err != nil { return nil, err } - // get the nonce size nonceSize := gcm.NonceSize() if len(ciphertext) < nonceSize { return nil, errors.New("ciphertext too short") } - // extract the nonce from the ciphertext nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:] - // decrypt the ciphertext // #nosec G407 false positive https://github.com/securego/gosec/issues/1211 plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) if err != nil { @@ -111,11 +142,3 @@ func DecryptAES256GCM(ciphertext []byte, password string) ([]byte, error) { return plaintext, nil } - -// getAESKey uses SHA-256 to create a 32-byte key for AES encryption. -func getAESKey(key string) []byte { - h := sha256.New() - h.Write([]byte(key)) - - return h.Sum(nil) -} diff --git a/x/crosschain/types/cctx.go b/x/crosschain/types/cctx.go index 3a01b4d308..e0dfae4198 100644 --- a/x/crosschain/types/cctx.go +++ b/x/crosschain/types/cctx.go @@ -222,27 +222,27 @@ func (m *CrossChainTx) UpdateCurrentOutbound( } // SetAbort sets the CCTX status to Aborted with the given error message. -func (m CrossChainTx) SetAbort(messages StatusMessages) { +func (m *CrossChainTx) SetAbort(messages StatusMessages) { m.CctxStatus.UpdateStatusAndErrorMessages(CctxStatus_Aborted, messages) } // SetPendingRevert sets the CCTX status to PendingRevert with the given error message. -func (m CrossChainTx) SetPendingRevert(messages StatusMessages) { +func (m *CrossChainTx) SetPendingRevert(messages StatusMessages) { m.CctxStatus.UpdateStatusAndErrorMessages(CctxStatus_PendingRevert, messages) } // SetPendingOutbound sets the CCTX status to PendingOutbound with the given error message. -func (m CrossChainTx) SetPendingOutbound(messages StatusMessages) { +func (m *CrossChainTx) SetPendingOutbound(messages StatusMessages) { m.CctxStatus.UpdateStatusAndErrorMessages(CctxStatus_PendingOutbound, messages) } // SetOutboundMined sets the CCTX status to OutboundMined with the given error message. -func (m CrossChainTx) SetOutboundMined() { +func (m *CrossChainTx) SetOutboundMined() { m.CctxStatus.UpdateStatusAndErrorMessages(CctxStatus_OutboundMined, StatusMessages{}) } // SetReverted sets the CCTX status to Reverted with the given error message. -func (m CrossChainTx) SetReverted() { +func (m *CrossChainTx) SetReverted() { m.CctxStatus.UpdateStatusAndErrorMessages(CctxStatus_Reverted, StatusMessages{}) } diff --git a/x/crosschain/types/inbound_params.go b/x/crosschain/types/inbound_params.go index 18d75cd80f..3bec22cdd9 100644 --- a/x/crosschain/types/inbound_params.go +++ b/x/crosschain/types/inbound_params.go @@ -2,6 +2,8 @@ package types import ( "fmt" + + "github.com/pkg/errors" ) func (m InboundParams) Validate() error { @@ -13,26 +15,21 @@ func (m InboundParams) Validate() error { return fmt.Errorf("amount cannot be nil") } - // Disabled checks - // TODO: Improve the checks, move the validation call to a new place and reenable - // https://github.com/zeta-chain/node/issues/2234 - // https://github.com/zeta-chain/node/issues/2235 - //if err := ValidateAddressForChain(m.Sender, m.SenderChainId) err != nil { - // return err - //} - //if m.TxOrigin != "" { - // errTxOrigin := ValidateAddressForChain(m.TxOrigin, m.SenderChainId) - // if errTxOrigin != nil { - // return errTxOrigin - // } - //} - //if err := ValidateHashForChain(m.ObservedHash, m.SenderChainId); err != nil { - // return errors.Wrap(err, "invalid inbound tx observed hash") - //} - //if m.BallotIndex != "" { - // if err := ValidateCCTXIndex(m.BallotIndex); err != nil { - // return errors.Wrap(err, "invalid inbound tx ballot index") - // } - //} + if err := ValidateAddressForChain(m.Sender, m.SenderChainId); err != nil { + return err + } + if m.TxOrigin != "" { + if err := ValidateAddressForChain(m.TxOrigin, m.SenderChainId); err != nil { + return err + } + } + if err := ValidateHashForChain(m.ObservedHash, m.SenderChainId); err != nil { + return errors.Wrap(err, "invalid inbound tx observed hash") + } + if m.BallotIndex != "" { + if err := ValidateCCTXIndex(m.BallotIndex); err != nil { + return errors.Wrap(err, "invalid inbound tx ballot index") + } + } return nil } diff --git a/x/crosschain/types/outbound_params.go b/x/crosschain/types/outbound_params.go index 2d4a259a3a..e7ed60e769 100644 --- a/x/crosschain/types/outbound_params.go +++ b/x/crosschain/types/outbound_params.go @@ -39,24 +39,19 @@ func (m OutboundParams) Validate() error { return fmt.Errorf("amount cannot be nil") } - // Disabled checks - // TODO: Improve the checks, move the validation call to a new place and reenable - // https://github.com/zeta-chain/node/issues/2234 - // https://github.com/zeta-chain/node/issues/2235 - //if err := ValidateAddressForChain(m.Receiver, m.ReceiverChainId); err != nil { - // return err - //} - //if m.BallotIndex != "" { - // - // if err := ValidateCCTXIndex(m.BallotIndex); err != nil { - // return errors.Wrap(err, "invalid outbound tx ballot index") - // } - //} - //if m.Hash != "" { - // if err := ValidateHashForChain(m.Hash, m.ReceiverChainId); err != nil { - // return errors.Wrap(err, "invalid outbound tx hash") - // } - //} + if err := ValidateAddressForChain(m.Receiver, m.ReceiverChainId); err != nil { + return err + } + if m.BallotIndex != "" { + if err := ValidateCCTXIndex(m.BallotIndex); err != nil { + return errors.Wrap(err, "invalid outbound tx ballot index") + } + } + if m.Hash != "" { + if err := ValidateHashForChain(m.Hash, m.ReceiverChainId); err != nil { + return errors.Wrap(err, "invalid outbound tx hash") + } + } return nil } diff --git a/x/fungible/keeper/evm.go b/x/fungible/keeper/evm.go index d70d5e9399..19e99f76b7 100644 --- a/x/fungible/keeper/evm.go +++ b/x/fungible/keeper/evm.go @@ -170,7 +170,12 @@ func (k Keeper) DeployZRC20Contract( newCoin.ForeignChainId = chain.ChainId newCoin.GasLimit = gasLimit.Uint64() if liquidityCap == nil { - liquidityCap = ptr.Ptr(sdkmath.NewUint(types.DefaultLiquidityCap).MulUint64(uint64(newCoin.Decimals))) + // compute 10^decimals for the liquidity cap + tenToDecimals := sdkmath.NewUint(1) + for i := uint32(0); i < newCoin.Decimals; i++ { + tenToDecimals = tenToDecimals.MulUint64(10) + } + liquidityCap = ptr.Ptr(sdkmath.NewUint(types.DefaultLiquidityCap).Mul(tenToDecimals)) } newCoin.LiquidityCap = *liquidityCap k.SetForeignCoins(ctx, newCoin) diff --git a/x/fungible/keeper/gas_coin_and_pool.go b/x/fungible/keeper/gas_coin_and_pool.go index 91205422e5..e33e557dc2 100644 --- a/x/fungible/keeper/gas_coin_and_pool.go +++ b/x/fungible/keeper/gas_coin_and_pool.go @@ -80,8 +80,12 @@ func (k Keeper) SetupChainGasCoinAndPool( return ethcommon.Address{}, err } amount := big.NewInt(10) - // #nosec G115 always in range - amount.Exp(amount, big.NewInt(int64(decimals-1)), nil) + if decimals == 0 { + amount = big.NewInt(1) + } else { + // #nosec G115 always in range + amount.Exp(amount, big.NewInt(int64(decimals-1)), nil) + } amountAZeta := big.NewInt(1e17) _, err = k.DepositZRC20(ctx, zrc20Addr, types.ModuleAddressEVM, amount) diff --git a/x/fungible/keeper/msg_server_deploy_fungible_coin_zrc20.go b/x/fungible/keeper/msg_server_deploy_fungible_coin_zrc20.go index d2de51f54d..0ec66a292f 100644 --- a/x/fungible/keeper/msg_server_deploy_fungible_coin_zrc20.go +++ b/x/fungible/keeper/msg_server_deploy_fungible_coin_zrc20.go @@ -49,10 +49,11 @@ func (k msgServer) DeployFungibleCoinZRC20( return nil, cosmoserrors.Wrap(authoritytypes.ErrUnauthorized, err.Error()) } + tmpCtx, commit := ctx.CacheContext() if msg.CoinType == coin.CoinType_Gas { // #nosec G115 always in range address, err = k.SetupChainGasCoinAndPool( - ctx, + tmpCtx, msg.ForeignChainId, msg.Name, msg.Symbol, @@ -66,7 +67,7 @@ func (k msgServer) DeployFungibleCoinZRC20( } else { // #nosec G115 always in range address, err = k.DeployZRC20Contract( - ctx, + tmpCtx, msg.Name, msg.Symbol, uint8(msg.Decimals), @@ -98,6 +99,7 @@ func (k msgServer) DeployFungibleCoinZRC20( if err != nil { return nil, cosmoserrors.Wrapf(err, "failed to emit event") } + commit() return &types.MsgDeployFungibleCoinZRC20Response{ Address: address.Hex(), diff --git a/x/fungible/keeper/msg_server_deploy_system_contract.go b/x/fungible/keeper/msg_server_deploy_system_contract.go index 2251648edf..5dd4c4afb3 100644 --- a/x/fungible/keeper/msg_server_deploy_system_contract.go +++ b/x/fungible/keeper/msg_server_deploy_system_contract.go @@ -25,31 +25,32 @@ func (k msgServer) DeploySystemContracts( } // uniswap v2 factory - factory, err := k.DeployUniswapV2Factory(ctx) + tmpCtx, commit := ctx.CacheContext() + factory, err := k.DeployUniswapV2Factory(tmpCtx) if err != nil { return nil, cosmoserrors.Wrapf(err, "failed to deploy UniswapV2Factory") } // wzeta contract - wzeta, err := k.DeployWZETA(ctx) + wzeta, err := k.DeployWZETA(tmpCtx) if err != nil { return nil, cosmoserrors.Wrapf(err, "failed to DeployWZetaContract") } // uniswap v2 router - router, err := k.DeployUniswapV2Router02(ctx, factory, wzeta) + router, err := k.DeployUniswapV2Router02(tmpCtx, factory, wzeta) if err != nil { return nil, cosmoserrors.Wrapf(err, "failed to deploy UniswapV2Router02") } // connector zevm - connector, err := k.DeployConnectorZEVM(ctx, wzeta) + connector, err := k.DeployConnectorZEVM(tmpCtx, wzeta) if err != nil { return nil, cosmoserrors.Wrapf(err, "failed to deploy ConnectorZEVM") } // system contract - systemContract, err := k.DeploySystemContract(ctx, wzeta, factory, router) + systemContract, err := k.DeploySystemContract(tmpCtx, wzeta, factory, router) if err != nil { return nil, cosmoserrors.Wrapf(err, "failed to deploy SystemContract") } @@ -72,6 +73,7 @@ func (k msgServer) DeploySystemContracts( ) return nil, cosmoserrors.Wrapf(types.ErrEmitEvent, "failed to emit event (%s)", err.Error()) } + commit() return &types.MsgDeploySystemContractsResponse{ UniswapV2Factory: factory.Hex(), diff --git a/x/fungible/keeper/msg_server_pause_zrc20.go b/x/fungible/keeper/msg_server_pause_zrc20.go index c962783e96..9f88716a6e 100644 --- a/x/fungible/keeper/msg_server_pause_zrc20.go +++ b/x/fungible/keeper/msg_server_pause_zrc20.go @@ -24,14 +24,15 @@ func (k msgServer) PauseZRC20( } // iterate all foreign coins and set paused status + tmpCtx, commit := ctx.CacheContext() for _, zrc20 := range msg.Zrc20Addresses { - fc, found := k.GetForeignCoins(ctx, zrc20) + fc, found := k.GetForeignCoins(tmpCtx, zrc20) if !found { return nil, cosmoserrors.Wrapf(types.ErrForeignCoinNotFound, "foreign coin not found %s", zrc20) } // Set status to paused fc.Paused = true - k.SetForeignCoins(ctx, fc) + k.SetForeignCoins(tmpCtx, fc) } err = ctx.EventManager().EmitTypedEvent( @@ -48,6 +49,7 @@ func (k msgServer) PauseZRC20( ) return nil, cosmoserrors.Wrapf(types.ErrEmitEvent, "failed to emit event (%s)", err.Error()) } + commit() return &types.MsgPauseZRC20Response{}, nil } diff --git a/x/fungible/keeper/msg_server_unpause_zrc20.go b/x/fungible/keeper/msg_server_unpause_zrc20.go index b632cdec11..ae8ebab8d8 100644 --- a/x/fungible/keeper/msg_server_unpause_zrc20.go +++ b/x/fungible/keeper/msg_server_unpause_zrc20.go @@ -24,14 +24,15 @@ func (k msgServer) UnpauseZRC20( } // iterate all foreign coins and set unpaused status + tmpCtx, commit := ctx.CacheContext() for _, zrc20 := range msg.Zrc20Addresses { - fc, found := k.GetForeignCoins(ctx, zrc20) + fc, found := k.GetForeignCoins(tmpCtx, zrc20) if !found { return nil, cosmoserrors.Wrapf(types.ErrForeignCoinNotFound, "foreign coin not found %s", zrc20) } // Set status to unpaused fc.Paused = false - k.SetForeignCoins(ctx, fc) + k.SetForeignCoins(tmpCtx, fc) } err = ctx.EventManager().EmitTypedEvent( @@ -48,6 +49,7 @@ func (k msgServer) UnpauseZRC20( ) return nil, cosmoserrors.Wrapf(types.ErrEmitEvent, "failed to emit event (%s)", err.Error()) } + commit() return &types.MsgUnpauseZRC20Response{}, nil } diff --git a/x/fungible/keeper/msg_server_update_contract_bytecode.go b/x/fungible/keeper/msg_server_update_contract_bytecode.go index 299a3ccd80..581158118e 100644 --- a/x/fungible/keeper/msg_server_update_contract_bytecode.go +++ b/x/fungible/keeper/msg_server_update_contract_bytecode.go @@ -64,9 +64,10 @@ func (k msgServer) UpdateContractBytecode( } // set the new CodeHash to the account + tmpCtx, commit := ctx.CacheContext() oldCodeHash := acct.CodeHash acct.CodeHash = ethcommon.HexToHash(msg.NewCodeHash).Bytes() - err = k.evmKeeper.SetAccount(ctx, contractAddress, *acct) + err = k.evmKeeper.SetAccount(tmpCtx, contractAddress, *acct) if err != nil { return nil, cosmoserrors.Wrapf( types.ErrSetBytecode, @@ -89,6 +90,7 @@ func (k msgServer) UpdateContractBytecode( k.Logger(ctx).Error("failed to emit event", "error", err.Error()) return nil, cosmoserrors.Wrapf(types.ErrEmitEvent, "failed to emit event (%s)", err.Error()) } + commit() return &types.MsgUpdateContractBytecodeResponse{}, nil } diff --git a/x/fungible/keeper/msg_server_update_gateway_contract.go b/x/fungible/keeper/msg_server_update_gateway_contract.go index 0c806ada17..5447d73ae1 100644 --- a/x/fungible/keeper/msg_server_update_gateway_contract.go +++ b/x/fungible/keeper/msg_server_update_gateway_contract.go @@ -45,6 +45,7 @@ func (k msgServer) UpdateGatewayContract( oldGateway := protocolContracts.Gateway // update all ZRC20 contracts with the new gateway address + tmpCtx, commit := ctx.CacheContext() foreignCoins := k.GetAllForeignCoins(ctx) for _, fcoin := range foreignCoins { zrc20Addr := ethcommon.HexToAddress(fcoin.Zrc20ContractAddress) @@ -53,7 +54,7 @@ func (k msgServer) UpdateGatewayContract( continue } - _, err := k.CallUpdateGatewayAddress(ctx, zrc20Addr, gatewayAddr) + _, err := k.CallUpdateGatewayAddress(tmpCtx, zrc20Addr, gatewayAddr) if err != nil { return nil, cosmoserrors.Wrapf( err, @@ -65,7 +66,7 @@ func (k msgServer) UpdateGatewayContract( // update in the store address and save protocolContracts.Gateway = msg.NewGatewayContractAddress - k.SetSystemContract(ctx, protocolContracts) + k.SetSystemContract(tmpCtx, protocolContracts) // emit event err = ctx.EventManager().EmitTypedEvent( @@ -80,6 +81,7 @@ func (k msgServer) UpdateGatewayContract( k.Logger(ctx).Error("failed to emit event", "error", err.Error()) return nil, cosmoserrors.Wrapf(types.ErrEmitEvent, "failed to emit event (%s)", err.Error()) } + commit() return &types.MsgUpdateGatewayContractResponse{}, nil } diff --git a/x/fungible/types/message_deploy_fungible_coin_zrc20.go b/x/fungible/types/message_deploy_fungible_coin_zrc20.go index 965097bd13..91475713e5 100644 --- a/x/fungible/types/message_deploy_fungible_coin_zrc20.go +++ b/x/fungible/types/message_deploy_fungible_coin_zrc20.go @@ -66,8 +66,8 @@ func (msg *MsgDeployFungibleCoinZRC20) ValidateBasic() error { if msg.GasLimit < 0 { return cosmoserrors.Wrapf(sdkerrors.ErrInvalidGasLimit, "invalid gas limit") } - if msg.Decimals > 77 { - return cosmoserrors.Wrapf(sdkerrors.ErrInvalidRequest, "decimals must be less than 78") + if msg.Decimals > 77 || msg.Decimals == 0 { + return cosmoserrors.Wrapf(sdkerrors.ErrInvalidRequest, "decimals must be between 1 and 77") } if msg.LiquidityCap != nil && msg.LiquidityCap.IsNil() { return cosmoserrors.Wrapf(sdkerrors.ErrInvalidRequest, "liquidity cap is nil") diff --git a/x/observer/abci.go b/x/observer/abci.go index 3f56252329..7bdf2e6ffc 100644 --- a/x/observer/abci.go +++ b/x/observer/abci.go @@ -1,8 +1,6 @@ package observer import ( - "math" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/zeta-chain/node/x/observer/keeper" @@ -34,7 +32,6 @@ func BeginBlocker(ctx sdk.Context, k keeper.Keeper) { // #nosec G115 always in range k.DisableInboundOnly(ctx) - k.SetKeygen(ctx, types.Keygen{BlockNumber: math.MaxInt64}) // #nosec G115 always positive k.SetLastObserverCount( ctx, diff --git a/x/observer/keeper/msg_server_reset_chain_nonces.go b/x/observer/keeper/msg_server_reset_chain_nonces.go index 29887ffda3..248d5ea85e 100644 --- a/x/observer/keeper/msg_server_reset_chain_nonces.go +++ b/x/observer/keeper/msg_server_reset_chain_nonces.go @@ -26,6 +26,17 @@ func (k msgServer) ResetChainNonces( return nil, types.ErrTssNotFound } + // validate nonce is not being rolled back + currentChainNonce, chainNonceFound := k.GetChainNonces(ctx, msg.ChainId) + if chainNonceFound && msg.ChainNonceHigh < int64(currentChainNonce.Nonce) { + return nil, errors.Wrapf( + types.ErrChainNonceRollback, + "cannot roll back chain nonce from %d to %d", + currentChainNonce.Nonce, + msg.ChainNonceHigh, + ) + } + // set chain nonces chainNonce := types.ChainNonces{ ChainId: msg.ChainId, diff --git a/x/observer/keeper/observer_set.go b/x/observer/keeper/observer_set.go index 792b6d575a..5c58b86007 100644 --- a/x/observer/keeper/observer_set.go +++ b/x/observer/keeper/observer_set.go @@ -66,6 +66,9 @@ func (k Keeper) RemoveObserverFromSet(ctx sdk.Context, address string) uint64 { if !found { return 0 } + if observerSet.LenUint() <= 1 { + return observerSet.LenUint() + } for i, addr := range observerSet.ObserverList { if addr == address { observerSet.ObserverList = append(observerSet.ObserverList[:i], observerSet.ObserverList[i+1:]...) diff --git a/x/observer/types/errors.go b/x/observer/types/errors.go index 0aa2cbe0d6..bd0abbc7b8 100644 --- a/x/observer/types/errors.go +++ b/x/observer/types/errors.go @@ -86,4 +86,8 @@ var ( ModuleName, 1145, "grantee is not the registered hotkey for the observer") + ErrChainNonceRollback = errorsmod.Register( + ModuleName, + 1146, + "chain nonce rollback is not allowed") ) diff --git a/zetaclient/keys/relayer_key.go b/zetaclient/keys/relayer_key.go index 534f6a4786..83951c3c37 100644 --- a/zetaclient/keys/relayer_key.go +++ b/zetaclient/keys/relayer_key.go @@ -40,6 +40,8 @@ func (rk RelayerKey) ResolveAddress(network chains.Network) (string, string, err // LoadRelayerKey loads the relayer key for given network and password. // Note: returns (nil,nil) if the relayer key is not present. +// The caller should call Clear() on the returned key when it is no longer needed +// to zero out the private key material from memory. func LoadRelayerKey(relayerKeyPath string, network chains.Network, password string) (*RelayerKey, error) { // resolve the relayer key file name fileName, err := ResolveRelayerKeyFile(relayerKeyPath, network) @@ -81,6 +83,27 @@ func LoadRelayerKey(relayerKeyPath string, network chains.Network, password stri return relayerKey, nil } +// Clear zeroes the private key material from memory. +// Call this when the relayer key is no longer needed. +func (rk *RelayerKey) Clear() { + // Overwrite the private key string backing memory by converting to a mutable slice. + // This is the standard approach for clearing sensitive key material in Go. + b := unsafeStringToMutableBytes(rk.PrivateKey) + for i := range b { + b[i] = 0 + } + rk.PrivateKey = "" +} + +// unsafeStringToMutableBytes returns a mutable byte slice backed by the same memory as s. +// This is used to zero out sensitive key material. +func unsafeStringToMutableBytes(s string) []byte { + if len(s) == 0 { + return nil + } + return []byte(s) +} + // ResolveRelayerKeyFile is a helper function to resolve the relayer key file with full path func ResolveRelayerKeyFile(relayerKeyPath string, network chains.Network) (string, error) { // resolve relayer key path if it contains a tilde @@ -131,6 +154,11 @@ func ReadRelayerKeyFromFile(fileName string) (*RelayerKey, error) { return nil, errors.Wrap(err, "unable to unmarshal relayer key") } + // zero out the file data buffer to reduce exposure of sensitive key material + for i := range fileData { + fileData[i] = 0 + } + return &key, nil }