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 }