190 lines
6.1 KiB
Go
190 lines
6.1 KiB
Go
package crypto
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"fmt"
|
|
|
|
"github.com/tendermint/crypto/bcrypt"
|
|
|
|
"github.com/tendermint/tendermint/crypto"
|
|
"github.com/tendermint/tendermint/crypto/armor"
|
|
cryptoAmino "github.com/tendermint/tendermint/crypto/encoding/amino"
|
|
"github.com/tendermint/tendermint/crypto/xsalsa20symmetric"
|
|
|
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
|
)
|
|
|
|
const (
|
|
blockTypePrivKey = "TENDERMINT PRIVATE KEY"
|
|
blockTypeKeyInfo = "TENDERMINT KEY INFO"
|
|
blockTypePubKey = "TENDERMINT PUBLIC KEY"
|
|
|
|
defaultAlgo = "secp256k1"
|
|
|
|
headerVersion = "version"
|
|
headerType = "type"
|
|
)
|
|
|
|
// Make bcrypt security parameter var, so 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
|
|
// parameter during a runtime vulnerability. The main security
|
|
// threat this then exposes would be something that changes this during
|
|
// runtime before the user creates their key. This vulnerability must
|
|
// succeed to update this to that same value before every subsequent call
|
|
// to the keys command in future startups / or the attacker must get access
|
|
// to the filesystem. However, with a similar threat model (changing
|
|
// variables in runtime), one can cause the user to sign a different tx
|
|
// than what they see, which is a significantly cheaper attack then breaking
|
|
// a bcrypt hash. (Recall that the nonce still exists to break rainbow tables)
|
|
// For further notes on security parameter choice, see README.md
|
|
var BcryptSecurityParameter = 12
|
|
|
|
//-----------------------------------------------------------------
|
|
// add armor
|
|
|
|
// Armor the InfoBytes
|
|
func ArmorInfoBytes(bz []byte) string {
|
|
header := map[string]string{
|
|
headerType: "Info",
|
|
headerVersion: "0.0.0",
|
|
}
|
|
return armor.EncodeArmor(blockTypeKeyInfo, header, bz)
|
|
}
|
|
|
|
// Armor the PubKeyBytes
|
|
func ArmorPubKeyBytes(bz []byte, algo string) string {
|
|
header := map[string]string{
|
|
headerVersion: "0.0.1",
|
|
}
|
|
if algo != "" {
|
|
header[headerType] = algo
|
|
}
|
|
return armor.EncodeArmor(blockTypePubKey, header, bz)
|
|
}
|
|
|
|
//-----------------------------------------------------------------
|
|
// remove armor
|
|
|
|
// Unarmor the InfoBytes
|
|
func UnarmorInfoBytes(armorStr string) ([]byte, error) {
|
|
bz, header, err := unarmorBytes(armorStr, blockTypeKeyInfo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if header[headerVersion] != "0.0.0" {
|
|
return nil, fmt.Errorf("unrecognized version: %v", header[headerVersion])
|
|
}
|
|
return bz, nil
|
|
}
|
|
|
|
// UnarmorPubKeyBytes returns the pubkey byte slice, a string of the algo type, and an error
|
|
func UnarmorPubKeyBytes(armorStr string) (bz []byte, algo string, err error) {
|
|
bz, header, err := unarmorBytes(armorStr, blockTypePubKey)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("couldn't unarmor bytes: %v", err)
|
|
}
|
|
|
|
switch header[headerVersion] {
|
|
case "0.0.0":
|
|
return bz, defaultAlgo, err
|
|
case "0.0.1":
|
|
if header[headerType] == "" {
|
|
header[headerType] = defaultAlgo
|
|
}
|
|
return bz, header[headerType], err
|
|
case "":
|
|
return nil, "", fmt.Errorf("header's version field is empty")
|
|
default:
|
|
err = fmt.Errorf("unrecognized version: %v", header[headerVersion])
|
|
return nil, "", err
|
|
}
|
|
}
|
|
|
|
func unarmorBytes(armorStr, blockType string) (bz []byte, header map[string]string, err error) {
|
|
bType, header, bz, err := armor.DecodeArmor(armorStr)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if bType != blockType {
|
|
err = fmt.Errorf("unrecognized armor type %q, expected: %q", bType, blockType)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
//-----------------------------------------------------------------
|
|
// encrypt/decrypt with armor
|
|
|
|
// Encrypt and armor the private key.
|
|
func EncryptArmorPrivKey(privKey crypto.PrivKey, passphrase string, algo string) string {
|
|
saltBytes, encBytes := encryptPrivKey(privKey, passphrase)
|
|
header := map[string]string{
|
|
"kdf": "bcrypt",
|
|
"salt": fmt.Sprintf("%X", saltBytes),
|
|
}
|
|
if algo != "" {
|
|
header[headerType] = algo
|
|
}
|
|
armorStr := armor.EncodeArmor(blockTypePrivKey, header, encBytes)
|
|
return armorStr
|
|
}
|
|
|
|
// encrypt the given privKey with the passphrase using a randomly
|
|
// generated salt and the xsalsa20 cipher. returns the salt and the
|
|
// encrypted priv key.
|
|
func encryptPrivKey(privKey crypto.PrivKey, passphrase string) (saltBytes []byte, encBytes []byte) {
|
|
saltBytes = crypto.CRandBytes(16)
|
|
key, err := bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), BcryptSecurityParameter)
|
|
if err != nil {
|
|
panic(sdkerrors.Wrap(err, "error generating bcrypt key from passphrase"))
|
|
}
|
|
key = crypto.Sha256(key) // get 32 bytes
|
|
privKeyBytes := privKey.Bytes()
|
|
return saltBytes, xsalsa20symmetric.EncryptSymmetric(privKeyBytes, key)
|
|
}
|
|
|
|
// UnarmorDecryptPrivKey returns the privkey byte slice, a string of the algo type, and an error
|
|
func UnarmorDecryptPrivKey(armorStr string, passphrase string) (privKey crypto.PrivKey, algo string, err error) {
|
|
blockType, header, encBytes, err := armor.DecodeArmor(armorStr)
|
|
if err != nil {
|
|
return privKey, "", err
|
|
}
|
|
if blockType != blockTypePrivKey {
|
|
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["salt"] == "" {
|
|
return privKey, "", fmt.Errorf("missing salt bytes")
|
|
}
|
|
saltBytes, err := hex.DecodeString(header["salt"])
|
|
if err != nil {
|
|
return privKey, "", fmt.Errorf("error decoding salt: %v", err.Error())
|
|
}
|
|
privKey, err = decryptPrivKey(saltBytes, encBytes, passphrase)
|
|
|
|
if header[headerType] == "" {
|
|
header[headerType] = defaultAlgo
|
|
}
|
|
return privKey, header[headerType], err
|
|
}
|
|
|
|
func decryptPrivKey(saltBytes []byte, encBytes []byte, passphrase string) (privKey crypto.PrivKey, err error) {
|
|
key, err := bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), BcryptSecurityParameter)
|
|
if err != nil {
|
|
return privKey, sdkerrors.Wrap(err, "error generating bcrypt key from passphrase")
|
|
}
|
|
key = crypto.Sha256(key) // Get 32 bytes
|
|
privKeyBytes, err := xsalsa20symmetric.DecryptSymmetric(encBytes, key)
|
|
if err != nil && err.Error() == "Ciphertext decryption failed" {
|
|
return privKey, sdkerrors.ErrWrongPassword
|
|
} else if err != nil {
|
|
return privKey, err
|
|
}
|
|
privKey, err = cryptoAmino.PrivKeyFromBytes(privKeyBytes)
|
|
return privKey, err
|
|
}
|