github.com/cosmos/cosmos-sdk@v0.50.10/crypto/armor.go (about)

     1  package crypto
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/hex"
     6  	"fmt"
     7  	"io"
     8  
     9  	"github.com/cometbft/cometbft/crypto"
    10  	"golang.org/x/crypto/argon2"
    11  	"golang.org/x/crypto/chacha20poly1305"
    12  	"golang.org/x/crypto/openpgp/armor" //nolint:staticcheck //TODO: remove this dependency
    13  
    14  	errorsmod "cosmossdk.io/errors"
    15  
    16  	"github.com/cosmos/cosmos-sdk/codec/legacy"
    17  	"github.com/cosmos/cosmos-sdk/crypto/keys/bcrypt"
    18  	cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
    19  	"github.com/cosmos/cosmos-sdk/crypto/xsalsa20symmetric"
    20  	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
    21  )
    22  
    23  const (
    24  	blockTypePrivKey = "TENDERMINT PRIVATE KEY"
    25  	blockTypeKeyInfo = "TENDERMINT KEY INFO"
    26  	blockTypePubKey  = "TENDERMINT PUBLIC KEY"
    27  
    28  	defaultAlgo = "secp256k1"
    29  
    30  	headerVersion = "version"
    31  	headerType    = "type"
    32  )
    33  
    34  var (
    35  	kdfHeader = "kdf"
    36  	kdfBcrypt = "bcrypt"
    37  	kdfArgon2 = "argon2"
    38  )
    39  
    40  const (
    41  	argon2Time    = 1
    42  	argon2Memory  = 64 * 1024
    43  	argon2Threads = 4
    44  )
    45  
    46  // BcryptSecurityParameter is security parameter var, and it can be changed within the lcd test.
    47  // Making the bcrypt security parameter a var shouldn't be a security issue:
    48  // One can't verify an invalid key by maliciously changing the bcrypt
    49  // parameter during a runtime vulnerability. The main security
    50  // threat this then exposes would be something that changes this during
    51  // runtime before the user creates their key. This vulnerability must
    52  // succeed to update this to that same value before every subsequent call
    53  // to the keys command in future startups / or the attacker must get access
    54  // to the filesystem. However, with a similar threat model (changing
    55  // variables in runtime), one can cause the user to sign a different tx
    56  // than what they see, which is a significantly cheaper attack then breaking
    57  // a bcrypt hash. (Recall that the nonce still exists to break rainbow tables)
    58  // For further notes on security parameter choice, see README.md
    59  var BcryptSecurityParameter uint32 = 12
    60  
    61  //-----------------------------------------------------------------
    62  // add armor
    63  
    64  // Armor the InfoBytes
    65  func ArmorInfoBytes(bz []byte) string {
    66  	header := map[string]string{
    67  		headerType:    "Info",
    68  		headerVersion: "0.0.0",
    69  	}
    70  
    71  	return EncodeArmor(blockTypeKeyInfo, header, bz)
    72  }
    73  
    74  // Armor the PubKeyBytes
    75  func ArmorPubKeyBytes(bz []byte, algo string) string {
    76  	header := map[string]string{
    77  		headerVersion: "0.0.1",
    78  	}
    79  	if algo != "" {
    80  		header[headerType] = algo
    81  	}
    82  
    83  	return EncodeArmor(blockTypePubKey, header, bz)
    84  }
    85  
    86  //-----------------------------------------------------------------
    87  // remove armor
    88  
    89  // Unarmor the InfoBytes
    90  func UnarmorInfoBytes(armorStr string) ([]byte, error) {
    91  	bz, header, err := unarmorBytes(armorStr, blockTypeKeyInfo)
    92  	if err != nil {
    93  		return nil, err
    94  	}
    95  
    96  	if header[headerVersion] != "0.0.0" {
    97  		return nil, fmt.Errorf("unrecognized version: %v", header[headerVersion])
    98  	}
    99  
   100  	return bz, nil
   101  }
   102  
   103  // UnarmorPubKeyBytes returns the pubkey byte slice, a string of the algo type, and an error
   104  func UnarmorPubKeyBytes(armorStr string) (bz []byte, algo string, err error) {
   105  	bz, header, err := unarmorBytes(armorStr, blockTypePubKey)
   106  	if err != nil {
   107  		return nil, "", fmt.Errorf("couldn't unarmor bytes: %v", err)
   108  	}
   109  
   110  	switch header[headerVersion] {
   111  	case "0.0.0":
   112  		return bz, defaultAlgo, err
   113  	case "0.0.1":
   114  		if header[headerType] == "" {
   115  			header[headerType] = defaultAlgo
   116  		}
   117  
   118  		return bz, header[headerType], err
   119  	case "":
   120  		return nil, "", fmt.Errorf("header's version field is empty")
   121  	default:
   122  		err = fmt.Errorf("unrecognized version: %v", header[headerVersion])
   123  		return nil, "", err
   124  	}
   125  }
   126  
   127  func unarmorBytes(armorStr, blockType string) (bz []byte, header map[string]string, err error) {
   128  	bType, header, bz, err := DecodeArmor(armorStr)
   129  	if err != nil {
   130  		return
   131  	}
   132  
   133  	if bType != blockType {
   134  		err = fmt.Errorf("unrecognized armor type %q, expected: %q", bType, blockType)
   135  		return
   136  	}
   137  
   138  	return
   139  }
   140  
   141  //-----------------------------------------------------------------
   142  // encrypt/decrypt with armor
   143  
   144  // Encrypt and armor the private key.
   145  func EncryptArmorPrivKey(privKey cryptotypes.PrivKey, passphrase, algo string) string {
   146  	saltBytes, encBytes := encryptPrivKey(privKey, passphrase)
   147  	header := map[string]string{
   148  		kdfHeader: kdfArgon2,
   149  		"salt":    fmt.Sprintf("%X", saltBytes),
   150  	}
   151  
   152  	if algo != "" {
   153  		header[headerType] = algo
   154  	}
   155  
   156  	armorStr := EncodeArmor(blockTypePrivKey, header, encBytes)
   157  
   158  	return armorStr
   159  }
   160  
   161  func encryptPrivKey(privKey cryptotypes.PrivKey, passphrase string) (saltBytes, encBytes []byte) {
   162  	saltBytes = crypto.CRandBytes(16)
   163  
   164  	key := argon2.IDKey([]byte(passphrase), saltBytes, argon2Time, argon2Memory, argon2Threads, chacha20poly1305.KeySize)
   165  	privKeyBytes := legacy.Cdc.MustMarshal(privKey)
   166  
   167  	aead, err := chacha20poly1305.New(key)
   168  	if err != nil {
   169  		panic(errorsmod.Wrap(err, "error generating cypher from key"))
   170  	}
   171  
   172  	nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+len(privKeyBytes)+aead.Overhead()) // Nonce is fixed to maintain consistency, each key is generated  at every encryption using a random salt.
   173  
   174  	encBytes = aead.Seal(nil, nonce, privKeyBytes, nil)
   175  
   176  	return saltBytes, encBytes
   177  }
   178  
   179  // UnarmorDecryptPrivKey returns the privkey byte slice, a string of the algo type, and an error
   180  func UnarmorDecryptPrivKey(armorStr, passphrase string) (privKey cryptotypes.PrivKey, algo string, err error) {
   181  	blockType, header, encBytes, err := DecodeArmor(armorStr)
   182  	if err != nil {
   183  		return privKey, "", err
   184  	}
   185  
   186  	if blockType != blockTypePrivKey {
   187  		return privKey, "", fmt.Errorf("unrecognized armor type: %v", blockType)
   188  	}
   189  
   190  	if header[kdfHeader] != kdfBcrypt && header[kdfHeader] != kdfArgon2 {
   191  		return privKey, "", fmt.Errorf("unrecognized KDF type: %v", header[kdfHeader])
   192  	}
   193  
   194  	if header["salt"] == "" {
   195  		return privKey, "", fmt.Errorf("missing salt bytes")
   196  	}
   197  
   198  	saltBytes, err := hex.DecodeString(header["salt"])
   199  	if err != nil {
   200  		return privKey, "", fmt.Errorf("error decoding salt: %v", err.Error())
   201  	}
   202  
   203  	privKey, err = decryptPrivKey(saltBytes, encBytes, passphrase, header[kdfHeader])
   204  
   205  	if header[headerType] == "" {
   206  		header[headerType] = defaultAlgo
   207  	}
   208  
   209  	return privKey, header[headerType], err
   210  }
   211  
   212  func decryptPrivKey(saltBytes, encBytes []byte, passphrase, kdf string) (privKey cryptotypes.PrivKey, err error) {
   213  	// Key derivation
   214  	var (
   215  		key          []byte
   216  		privKeyBytes []byte
   217  	)
   218  
   219  	// Since the argon2 key derivation and chacha encryption was implemented together, it is not possible to have mixed kdf and encryption algorithms
   220  	switch kdf {
   221  	case kdfArgon2:
   222  		key = argon2.IDKey([]byte(passphrase), saltBytes, argon2Time, argon2Memory, argon2Threads, chacha20poly1305.KeySize)
   223  
   224  		aead, err := chacha20poly1305.New(key)
   225  		if err != nil {
   226  			return privKey, errorsmod.Wrap(err, "Error generating aead cypher for key.")
   227  		} else if len(encBytes) < aead.NonceSize() {
   228  			return privKey, errorsmod.Wrap(nil, "Encrypted bytes length is smaller than aead nonce size.")
   229  		}
   230  		nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+len(privKeyBytes)+aead.Overhead())
   231  		privKeyBytes, err = aead.Open(nil, nonce, encBytes, nil) // Decrypt the message and check it wasn't tampered with.
   232  		if err != nil {
   233  			return privKey, sdkerrors.ErrWrongPassword
   234  		}
   235  	case kdfBcrypt:
   236  		key, err = bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), BcryptSecurityParameter)
   237  		if err != nil {
   238  			return privKey, errorsmod.Wrap(err, "Error generating bcrypt cypher for key.")
   239  		}
   240  		key = crypto.Sha256(key) // Get 32 bytes
   241  		privKeyBytes, err = xsalsa20symmetric.DecryptSymmetric(encBytes, key)
   242  
   243  		if err == xsalsa20symmetric.ErrCiphertextDecrypt {
   244  			return privKey, sdkerrors.ErrWrongPassword
   245  		}
   246  	default:
   247  		return privKey, errorsmod.Wrap(nil, fmt.Sprintf("Unrecognized key derivation function (kdf) header: %s.", kdf))
   248  	}
   249  
   250  	if err != nil {
   251  		return privKey, err
   252  	}
   253  
   254  	return legacy.PrivKeyFromBytes(privKeyBytes)
   255  }
   256  
   257  //-----------------------------------------------------------------
   258  // encode/decode with armor
   259  
   260  func EncodeArmor(blockType string, headers map[string]string, data []byte) string {
   261  	buf := new(bytes.Buffer)
   262  	w, err := armor.Encode(buf, blockType, headers)
   263  	if err != nil {
   264  		panic(fmt.Errorf("could not encode ascii armor: %s", err))
   265  	}
   266  	_, err = w.Write(data)
   267  	if err != nil {
   268  		panic(fmt.Errorf("could not encode ascii armor: %s", err))
   269  	}
   270  	err = w.Close()
   271  	if err != nil {
   272  		panic(fmt.Errorf("could not encode ascii armor: %s", err))
   273  	}
   274  	return buf.String()
   275  }
   276  
   277  func DecodeArmor(armorStr string) (blockType string, headers map[string]string, data []byte, err error) {
   278  	buf := bytes.NewBufferString(armorStr)
   279  	block, err := armor.Decode(buf)
   280  	if err != nil {
   281  		return "", nil, nil, err
   282  	}
   283  	data, err = io.ReadAll(block.Body)
   284  	if err != nil {
   285  		return "", nil, nil, err
   286  	}
   287  	return block.Type, block.Header, data, nil
   288  }