diff --git a/crypto/keys/keyring.go b/crypto/keys/keyring.go new file mode 100644 index 000000000000..ab5bf3191117 --- /dev/null +++ b/crypto/keys/keyring.go @@ -0,0 +1,558 @@ +package keys + +import ( + "bufio" + "crypto/hmac" + "crypto/sha256" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "sort" + "strings" + + "github.com/99designs/keyring" + "github.com/pkg/errors" + + "github.com/tendermint/crypto/bcrypt" + "github.com/tendermint/tendermint/crypto" + tmcrypto "github.com/tendermint/tendermint/crypto" + cryptoAmino "github.com/tendermint/tendermint/crypto/encoding/amino" + + "github.com/cosmos/cosmos-sdk/client/input" + "github.com/cosmos/cosmos-sdk/crypto/keys/hd" + "github.com/cosmos/cosmos-sdk/crypto/keys/keyerror" + "github.com/cosmos/cosmos-sdk/crypto/keys/mintkey" + "github.com/cosmos/cosmos-sdk/types" + + pdkdf2 "golang.org/x/crypto/pbkdf2" +) + +var _ Keybase = keyringKeybase{} + +// keyringKeybase implements the Keybase interface by using the Keyring library +// for account key persistence. +type keyringKeybase struct { + base baseKeybase + db keyring.Keyring +} + +var maxPassphraseEntryAttempts = 3 + +// NewKeyring creates a new instance of a keyring. +func NewKeyring(name string, dir string, userInput io.Reader) (Keybase, error) { + db, err := keyring.Open(lkbToKeyringConfig(name, dir, userInput, false)) + if err != nil { + return nil, err + } + + return newKeyringKeybase(db), nil +} + +// NewTestKeyring creates a new instance of a keyring for +// testing purposes that does not prompt users for password. +func NewTestKeyring(name string, dir string) (Keybase, error) { + db, err := keyring.Open(lkbToKeyringConfig(name, dir, nil, true)) + if err != nil { + return nil, err + } + + return newKeyringKeybase(db), nil +} + +// CreateMnemonic generates a new key and persists it to storage, encrypted +// using the provided password. It returns the generated mnemonic and the key Info. +// An error is returned if it fails to generate a key for the given algo type, +// or if another key is already stored under the same name. +func (kb keyringKeybase) CreateMnemonic( + name string, language Language, passwd string, algo SigningAlgo, +) (info Info, mnemonic string, err error) { + + return kb.base.CreateMnemonic(kb, name, language, passwd, algo) +} + +// CreateAccount converts a mnemonic to a private key and persists it, encrypted +// with the given password. +func (kb keyringKeybase) CreateAccount( + name, mnemonic, bip39Passwd, encryptPasswd string, account, index uint32, +) (Info, error) { + + return kb.base.CreateAccount(kb, name, mnemonic, bip39Passwd, encryptPasswd, account, index) +} + +// Derive computes a BIP39 seed from th mnemonic and bip39Passphrase. It creates +// a private key from the seed using the BIP44 params. +func (kb keyringKeybase) Derive( + name, mnemonic, bip39Passphrase, encryptPasswd string, params hd.BIP44Params, +) (info Info, err error) { + + return kb.base.Derive(kb, name, mnemonic, bip39Passphrase, encryptPasswd, params) +} + +// CreateLedger creates a new locally-stored reference to a Ledger keypair. +// It returns the created key info and an error if the Ledger could not be queried. +func (kb keyringKeybase) CreateLedger( + name string, algo SigningAlgo, hrp string, account, index uint32, +) (Info, error) { + + return kb.base.CreateLedger(kb, name, algo, hrp, account, index) +} + +// CreateOffline creates a new reference to an offline keypair. It returns the +// created key info. +func (kb keyringKeybase) CreateOffline(name string, pub tmcrypto.PubKey) (Info, error) { + return kb.base.writeOfflineKey(kb, name, pub), nil +} + +// CreateMulti creates a new reference to a multisig (offline) keypair. It +// returns the created key Info object. +func (kb keyringKeybase) CreateMulti(name string, pub tmcrypto.PubKey) (Info, error) { + return kb.base.writeMultisigKey(kb, name, pub), nil +} + +// List returns the keys from storage in alphabetical order. +func (kb keyringKeybase) List() ([]Info, error) { + var res []Info + keys, err := kb.db.Keys() + if err != nil { + return nil, err + } + + sort.Strings(keys) + + for _, key := range keys { + if strings.HasSuffix(key, infoSuffix) { + rawInfo, err := kb.db.Get(key) + if err != nil { + return nil, err + } + + if len(rawInfo.Data) == 0 { + return nil, keyerror.NewErrKeyNotFound(key) + } + + info, err := unmarshalInfo(rawInfo.Data) + if err != nil { + return nil, err + } + + res = append(res, info) + } + } + + return res, nil +} + +// Get returns the public information about one key. +func (kb keyringKeybase) Get(name string) (Info, error) { + key := infoKey(name) + + bs, err := kb.db.Get(string(key)) + if err != nil { + return nil, err + } + + if len(bs.Data) == 0 { + return nil, keyerror.NewErrKeyNotFound(name) + } + + return unmarshalInfo(bs.Data) +} + +// GetByAddress fetches a key by address and returns its public information. +func (kb keyringKeybase) GetByAddress(address types.AccAddress) (Info, error) { + ik, err := kb.db.Get(string(addrKey(address))) + if err != nil { + return nil, err + } + + if len(ik.Data) == 0 { + return nil, fmt.Errorf("key with address %s not found", address) + } + + bs, err := kb.db.Get(string(ik.Data)) + if err != nil { + return nil, err + } + + return unmarshalInfo(bs.Data) +} + +// Sign signs an arbitrary set of bytes with the named key. It returns an error +// if the key doesn't exist or the decryption fails. +func (kb keyringKeybase) Sign(name, passphrase string, msg []byte) (sig []byte, pub tmcrypto.PubKey, err error) { + info, err := kb.Get(name) + if err != nil { + return + } + + var priv tmcrypto.PrivKey + + switch i := info.(type) { + case localInfo: + if i.PrivKeyArmor == "" { + return nil, nil, fmt.Errorf("private key not available") + } + + priv, err = cryptoAmino.PrivKeyFromBytes([]byte(i.PrivKeyArmor)) + if err != nil { + return nil, nil, err + } + + case ledgerInfo: + return kb.base.SignWithLedger(info, msg) + + case offlineInfo, multiInfo: + return kb.base.DecodeSignature(info, msg) + } + + sig, err = priv.Sign(msg) + if err != nil { + return nil, nil, err + } + + return sig, priv.PubKey(), nil +} + +// ExportPrivateKeyObject exports an armored private key object. +func (kb keyringKeybase) ExportPrivateKeyObject(name string, passphrase string) (tmcrypto.PrivKey, error) { + info, err := kb.Get(name) + if err != nil { + return nil, err + } + + var priv tmcrypto.PrivKey + + switch linfo := info.(type) { + case localInfo: + if linfo.PrivKeyArmor == "" { + err = fmt.Errorf("private key not available") + return nil, err + } + + priv, err = cryptoAmino.PrivKeyFromBytes([]byte(linfo.PrivKeyArmor)) + if err != nil { + return nil, err + } + + case ledgerInfo, offlineInfo, multiInfo: + return nil, errors.New("only works on local private keys") + } + + return priv, nil +} + +// Export exports armored private key to the caller. +func (kb keyringKeybase) Export(name string) (armor string, err error) { + bz, err := kb.db.Get(string(infoKey(name))) + if err != nil { + return "", err + } + + if bz.Data == nil { + return "", fmt.Errorf("no key to export with name: %s", name) + } + + return mintkey.ArmorInfoBytes(bz.Data), nil +} + +// ExportPubKey returns public keys in ASCII armored format. It retrieves an Info +// object by its name and return the public key in a portable format. +func (kb keyringKeybase) ExportPubKey(name string) (armor string, err error) { + bz, err := kb.Get(name) + if err != nil { + return "", err + } + + if bz == nil { + return "", fmt.Errorf("no key to export with name: %s", name) + } + + return mintkey.ArmorPubKeyBytes(bz.GetPubKey().Bytes()), nil +} + +// Import imports armored private key. +func (kb keyringKeybase) Import(name string, armor string) error { + bz, _ := kb.Get(name) + + if bz != nil { + pubkey := bz.GetPubKey() + + if len(pubkey.Bytes()) > 0 { + return fmt.Errorf("cannot overwrite data for name: %s", name) + } + } + + infoBytes, err := mintkey.UnarmorInfoBytes(armor) + if err != nil { + return err + } + + info, err := unmarshalInfo(infoBytes) + if err != nil { + return err + } + + kb.writeInfo(name, info) + + err = kb.db.Set(keyring.Item{ + Key: string(addrKey(info.GetAddress())), + Data: infoKey(name), + }) + if err != nil { + return err + } + + return nil +} + +// ExportPrivKey returns a private key in ASCII armored format. An error is returned +// if the key does not exist or a wrong encryption passphrase is supplied. +func (kb keyringKeybase) ExportPrivKey(name, decryptPassphrase, encryptPassphrase string) (armor string, err error) { + priv, err := kb.ExportPrivateKeyObject(name, decryptPassphrase) + if err != nil { + return "", err + } + + return mintkey.EncryptArmorPrivKey(priv, encryptPassphrase), nil +} + +// ImportPrivKey imports a private key in ASCII armor format. An error is returned +// if a key with the same name exists or a wrong encryption passphrase is +// supplied. +func (kb keyringKeybase) ImportPrivKey(name, armor, passphrase string) error { + if kb.HasKey(name) { + return fmt.Errorf("cannot overwrite key: %s", name) + } + + privKey, err := mintkey.UnarmorDecryptPrivKey(armor, passphrase) + if err != nil { + return errors.Wrap(err, "failed to decrypt private key") + } + + // NOTE: The keyring keystore has no need for a passphrase. + kb.writeLocalKey(name, privKey, "") + return nil +} + +// HasKey returns whether the key exists in the keyring. +func (kb keyringKeybase) HasKey(name string) bool { + bz, _ := kb.Get(name) + return bz != nil +} + +// ImportPubKey imports an ASCII-armored public key. It will store a new Info +// object holding a public key only, i.e. it will not be possible to sign with +// it as it lacks the secret key. +func (kb keyringKeybase) ImportPubKey(name string, armor string) error { + bz, _ := kb.Get(name) + if bz != nil { + pubkey := bz.GetPubKey() + + if len(pubkey.Bytes()) > 0 { + return fmt.Errorf("cannot overwrite data for name: %s", name) + } + } + + pubBytes, err := mintkey.UnarmorPubKeyBytes(armor) + if err != nil { + return err + } + + pubKey, err := cryptoAmino.PubKeyFromBytes(pubBytes) + if err != nil { + return err + } + + kb.base.writeOfflineKey(kb, name, pubKey) + return nil +} + +// Delete removes key forever, but we must present the proper passphrase before +// deleting it (for security). It returns an error if the key doesn't exist or +// passphrases don't match. The passphrase is ignored when deleting references to +// offline and Ledger / HW wallet keys. +func (kb keyringKeybase) Delete(name, passphrase string, skipPass bool) error { + // verify we have the proper password before deleting + info, err := kb.Get(name) + if err != nil { + return err + } + + err = kb.db.Remove(string(addrKey(info.GetAddress()))) + if err != nil { + return err + } + + err = kb.db.Remove(string(infoKey(name))) + if err != nil { + return err + } + + return nil +} + +// Update changes the passphrase with which an already stored key is encrypted. +// The oldpass must be the current passphrase used for encryption, getNewpass is +// a function to get the passphrase to permanently replace the current passphrase. +func (kb keyringKeybase) Update(name, oldpass string, getNewpass func() (string, error)) error { + info, err := kb.Get(name) + if err != nil { + return err + } + + switch linfo := info.(type) { + case localInfo: + key, err := mintkey.UnarmorDecryptPrivKey(linfo.PrivKeyArmor, oldpass) + if err != nil { + return err + } + + newpass, err := getNewpass() + if err != nil { + return err + } + + kb.writeLocalKey(name, key, newpass) + return nil + + default: + return fmt.Errorf("locally stored key required; received: %v", reflect.TypeOf(info).String()) + } +} + +// CloseDB releases the lock and closes the storage backend. +func (kb keyringKeybase) CloseDB() {} + +func (kb keyringKeybase) writeLocalKey(name string, priv tmcrypto.PrivKey, _ string) Info { + // encrypt private key using keyring + pub := priv.PubKey() + info := newLocalInfo(name, pub, string(priv.Bytes())) + + kb.writeInfo(name, info) + return info +} + +func (kb keyringKeybase) writeInfo(name string, info Info) { + // write the info by key + key := infoKey(name) + serializedInfo := marshalInfo(info) + + err := kb.db.Set(keyring.Item{ + Key: string(key), + Data: serializedInfo, + }) + if err != nil { + panic(err) + } + + err = kb.db.Set(keyring.Item{ + Key: string(addrKey(info.GetAddress())), + Data: key, + }) + if err != nil { + panic(err) + } +} + +func lkbToKeyringConfig(name, dir string, buf io.Reader, test bool) keyring.Config { + if test { + return keyring.Config{ + AllowedBackends: []keyring.BackendType{"file"}, + ServiceName: name, + FileDir: dir, + FilePasswordFunc: fakePrompt, + } + } + + realPrompt := func(prompt string) (string, error) { + keyhashStored := false + keyhashFilePath := filepath.Join(dir, "keyhash") + + var keyhash []byte + + _, err := os.Stat(keyhashFilePath) + switch { + case err == nil: + keyhash, err = ioutil.ReadFile(keyhashFilePath) + if err != nil { + return "", fmt.Errorf("failed to read %s: %v", keyhashFilePath, err) + } + + keyhashStored = true + + case os.IsNotExist(err): + keyhashStored = false + + default: + return "", fmt.Errorf("failed to open %s: %v", keyhashFilePath, err) + } + + failureCounter := 0 + for { + failureCounter++ + if failureCounter > maxPassphraseEntryAttempts { + return "", fmt.Errorf("too many failed passphrase attempts") + } + + buf := bufio.NewReader(buf) + pass, err := input.GetPassword("Enter keyring passphrase:", buf) + if err != nil { + continue + } + + if keyhashStored { + if err := bcrypt.CompareHashAndPassword(keyhash, []byte(pass)); err != nil { + fmt.Fprintln(os.Stderr, "incorrect passphrase") + continue + } + return pass, nil + } + + reEnteredPass, err := input.GetPassword("Re-enter keyring passphrase:", buf) + if err != nil { + fmt.Fprintln(os.Stderr, err) + continue + } + + if pass != reEnteredPass { + fmt.Fprintln(os.Stderr, "passphrase do not match") + continue + } + + saltBytes := crypto.CRandBytes(16) + + //Create a password hash with MAC to differentiate old bcrypt.GeneratePassword from pdkdf2 + mac := hmac.New(sha256.New, []byte(pass)) + passwordHash := pdkdf2.Key([]byte(pass), saltBytes, 4096, 60, sha256.New) + mac.Write(passwordHash) + passwordHashMac := mac.Sum(nil) + + if err := ioutil.WriteFile(dir+"/keyhash", passwordHashMac, 0555); err != nil { + return "", err + } + + return pass, nil + } + } + + return keyring.Config{ + ServiceName: name, + FileDir: dir, + FilePasswordFunc: realPrompt, + } +} + +func fakePrompt(prompt string) (string, error) { + fmt.Fprintln(os.Stderr, "Fake prompt for passphase. Testing only") + return "test", nil +} + +func newKeyringKeybase(db keyring.Keyring) Keybase { + return keyringKeybase{ + db: db, + base: baseKeybase{}, + } +} diff --git a/crypto/keys/keyring_test.go b/crypto/keys/keyring_test.go new file mode 100644 index 000000000000..74385a14970a --- /dev/null +++ b/crypto/keys/keyring_test.go @@ -0,0 +1,343 @@ +// nolint: goconst +package keys + +import ( + "crypto/sha256" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/ed25519" + pdkdf2 "golang.org/x/crypto/pbkdf2" + + "github.com/cosmos/cosmos-sdk/crypto/keys/hd" + "github.com/cosmos/cosmos-sdk/tests" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func TestLazyKeyManagementKeyRing(t *testing.T) { + dir, cleanup := tests.NewTestCaseDir(t) + defer cleanup() + kb, err := NewTestKeyring("keybasename", dir) + require.NoError(t, err) + + algo := Secp256k1 + n1, n2, n3 := "personal", "business", "other" + p1, p2 := "1234", "really-secure!@#$" + + // Check empty state + l, err := kb.List() + require.Nil(t, err) + assert.Empty(t, l) + + _, _, err = kb.CreateMnemonic(n1, English, p1, Ed25519) + require.Error(t, err, "ed25519 keys are currently not supported by keybase") + + // create some keys + _, err = kb.Get(n1) + require.Error(t, err) + i, _, err := kb.CreateMnemonic(n1, English, p1, algo) + + require.NoError(t, err) + require.Equal(t, n1, i.GetName()) + _, _, err = kb.CreateMnemonic(n2, English, p2, algo) + require.NoError(t, err) + + // we can get these keys + i2, err := kb.Get(n2) + require.NoError(t, err) + _, err = kb.Get(n3) + require.NotNil(t, err) + _, err = kb.GetByAddress(accAddr(i2)) + require.NoError(t, err) + addr, err := sdk.AccAddressFromBech32("cosmos1yq8lgssgxlx9smjhes6ryjasmqmd3ts2559g0t") + require.NoError(t, err) + _, err = kb.GetByAddress(addr) + require.NotNil(t, err) + + // list shows them in order + keyS, err := kb.List() + require.NoError(t, err) + require.Equal(t, 2, len(keyS)) + // note these are in alphabetical order + require.Equal(t, n2, keyS[0].GetName()) + require.Equal(t, n1, keyS[1].GetName()) + require.Equal(t, i2.GetPubKey(), keyS[0].GetPubKey()) + + // deleting a key removes it + err = kb.Delete("bad name", "foo", false) + require.NotNil(t, err) + err = kb.Delete(n1, p1, false) + require.NoError(t, err) + keyS, err = kb.List() + require.NoError(t, err) + require.Equal(t, 1, len(keyS)) + _, err = kb.Get(n1) + require.Error(t, err) + + // create an offline key + o1 := "offline" + priv1 := ed25519.GenPrivKey() + pub1 := priv1.PubKey() + i, err = kb.CreateOffline(o1, pub1) + require.Nil(t, err) + require.Equal(t, pub1, i.GetPubKey()) + require.Equal(t, o1, i.GetName()) + keyS, err = kb.List() + require.NoError(t, err) + require.Equal(t, 2, len(keyS)) + + // delete the offline key + err = kb.Delete(o1, "", false) + require.NoError(t, err) + keyS, err = kb.List() + require.NoError(t, err) + require.Equal(t, 1, len(keyS)) + + // addr cache gets nuked - and test skip flag + err = kb.Delete(n2, "", true) + require.NoError(t, err) +} + +func TestLazySignVerifyKeyRing(t *testing.T) { + dir, cleanup := tests.NewTestCaseDir(t) + defer cleanup() + kb, err := NewTestKeyring("keybasename", dir) + require.NoError(t, err) + algo := Secp256k1 + + n1, n2, n3 := "some dude", "a dudette", "dude-ish" + p1, p2, p3 := "1234", "foobar", "foobar" + + // create two users and get their info + i1, _, err := kb.CreateMnemonic(n1, English, p1, algo) + require.Nil(t, err) + + i2, _, err := kb.CreateMnemonic(n2, English, p2, algo) + require.Nil(t, err) + + // Import a public key + armor, err := kb.ExportPubKey(n2) + require.Nil(t, err) + kb.ImportPubKey(n3, armor) + i3, err := kb.Get(n3) + require.NoError(t, err) + require.Equal(t, i3.GetName(), n3) + + // let's try to sign some messages + d1 := []byte("my first message") + d2 := []byte("some other important info!") + d3 := []byte("feels like I forgot something...") + + // try signing both data with both .. + s11, pub1, err := kb.Sign(n1, p1, d1) + require.Nil(t, err) + require.Equal(t, i1.GetPubKey(), pub1) + + s12, pub1, err := kb.Sign(n1, p1, d2) + require.Nil(t, err) + require.Equal(t, i1.GetPubKey(), pub1) + + s21, pub2, err := kb.Sign(n2, p2, d1) + require.Nil(t, err) + require.Equal(t, i2.GetPubKey(), pub2) + + s22, pub2, err := kb.Sign(n2, p2, d2) + require.Nil(t, err) + require.Equal(t, i2.GetPubKey(), pub2) + + // let's try to validate and make sure it only works when everything is proper + cases := []struct { + key crypto.PubKey + data []byte + sig []byte + valid bool + }{ + // proper matches + {i1.GetPubKey(), d1, s11, true}, + // change data, pubkey, or signature leads to fail + {i1.GetPubKey(), d2, s11, false}, + {i2.GetPubKey(), d1, s11, false}, + {i1.GetPubKey(), d1, s21, false}, + // make sure other successes + {i1.GetPubKey(), d2, s12, true}, + {i2.GetPubKey(), d1, s21, true}, + {i2.GetPubKey(), d2, s22, true}, + } + + for i, tc := range cases { + valid := tc.key.VerifyBytes(tc.data, tc.sig) + require.Equal(t, tc.valid, valid, "%d", i) + } + + // Now try to sign data with a secret-less key + _, _, err = kb.Sign(n3, p3, d3) + require.NotNil(t, err) +} + +func TestLazyExportImportKeyRing(t *testing.T) { + dir, cleanup := tests.NewTestCaseDir(t) + defer cleanup() + kb, err := NewTestKeyring("keybasename", dir) + require.NoError(t, err) + + info, _, err := kb.CreateMnemonic("john", English, "secretcpw", Secp256k1) + require.NoError(t, err) + require.Equal(t, info.GetName(), "john") + + john, err := kb.Get("john") + require.NoError(t, err) + require.Equal(t, info.GetName(), "john") + johnAddr := info.GetPubKey().Address() + + armor, err := kb.Export("john") + require.NoError(t, err) + + err = kb.Import("john2", armor) + require.NoError(t, err) + + john2, err := kb.Get("john2") + require.NoError(t, err) + + require.Equal(t, john.GetPubKey().Address(), johnAddr) + require.Equal(t, john.GetName(), "john") + require.Equal(t, john, john2) +} + +func TestLazyExportImportPubKeyKeyRing(t *testing.T) { + dir, cleanup := tests.NewTestCaseDir(t) + defer cleanup() + kb, err := NewTestKeyring("keybasename", dir) + require.NoError(t, err) + + // CreateMnemonic a private-public key pair and ensure consistency + notPasswd := "n9y25ah7" + info, _, err := kb.CreateMnemonic("john", English, notPasswd, Secp256k1) + require.Nil(t, err) + require.NotEqual(t, info, "") + require.Equal(t, info.GetName(), "john") + addr := info.GetPubKey().Address() + john, err := kb.Get("john") + require.NoError(t, err) + require.Equal(t, john.GetName(), "john") + require.Equal(t, john.GetPubKey().Address(), addr) + + // Export the public key only + armor, err := kb.ExportPubKey("john") + require.NoError(t, err) + // Import it under a different name + err = kb.ImportPubKey("john-pubkey-only", armor) + require.NoError(t, err) + // Ensure consistency + john2, err := kb.Get("john-pubkey-only") + require.NoError(t, err) + // Compare the public keys + require.True(t, john.GetPubKey().Equals(john2.GetPubKey())) + // Ensure the original key hasn't changed + john, err = kb.Get("john") + require.NoError(t, err) + require.Equal(t, john.GetPubKey().Address(), addr) + require.Equal(t, john.GetName(), "john") + + // Ensure keys cannot be overwritten + err = kb.ImportPubKey("john-pubkey-only", armor) + require.NotNil(t, err) +} + +func TestLazyExportPrivateKeyObjectKeyRing(t *testing.T) { + dir, cleanup := tests.NewTestCaseDir(t) + defer cleanup() + kb, err := NewTestKeyring("keybasename", dir) + require.NoError(t, err) + + info, _, err := kb.CreateMnemonic("john", English, "secretcpw", Secp256k1) + require.NoError(t, err) + require.Equal(t, info.GetName(), "john") + + // export private key object + exported, err := kb.ExportPrivateKeyObject("john", "secretcpw") + require.Nil(t, err, "%+v", err) + require.True(t, exported.PubKey().Equals(info.GetPubKey())) +} + +func TestLazyAdvancedKeyManagementKeyRing(t *testing.T) { + dir, cleanup := tests.NewTestCaseDir(t) + defer cleanup() + kb, err := NewTestKeyring("keybasename", dir) + require.NoError(t, err) + + algo := Secp256k1 + n1, n2 := "old-name", "new name" + p1 := "1234" + + // make sure key works with initial password + _, _, err = kb.CreateMnemonic(n1, English, p1, algo) + require.Nil(t, err, "%+v", err) + + _, err = kb.Export(n1 + ".notreal") + require.NotNil(t, err) + _, err = kb.Export(" " + n1) + require.NotNil(t, err) + _, err = kb.Export(n1 + " ") + require.NotNil(t, err) + _, err = kb.Export("") + require.NotNil(t, err) + exported, err := kb.Export(n1) + require.Nil(t, err, "%+v", err) + + // import succeeds + err = kb.Import(n2, exported) + require.NoError(t, err) + + // second import fails + err = kb.Import(n2, exported) + require.NotNil(t, err) +} + +func TestLazySeedPhraseKeyRing(t *testing.T) { + dir, cleanup := tests.NewTestCaseDir(t) + defer cleanup() + kb, err := NewTestKeyring("keybasename", dir) + require.NoError(t, err) + + algo := Secp256k1 + n1, n2 := "lost-key", "found-again" + p1, p2 := "1234", "foobar" + + // make sure key works with initial password + info, mnemonic, err := kb.CreateMnemonic(n1, English, p1, algo) + require.Nil(t, err, "%+v", err) + require.Equal(t, n1, info.GetName()) + assert.NotEmpty(t, mnemonic) + + // now, let us delete this key + err = kb.Delete(n1, p1, false) + require.Nil(t, err, "%+v", err) + _, err = kb.Get(n1) + require.NotNil(t, err) + + // let us re-create it from the mnemonic-phrase + params := *hd.NewFundraiserParams(0, sdk.CoinType, 0) + newInfo, err := kb.Derive(n2, mnemonic, DefaultBIP39Passphrase, p2, params) + require.NoError(t, err) + require.Equal(t, n2, newInfo.GetName()) + require.Equal(t, info.GetPubKey().Address(), newInfo.GetPubKey().Address()) + require.Equal(t, info.GetPubKey(), newInfo.GetPubKey()) +} + +func TestKeyDerivation(t *testing.T) { + passPhrase := "RandomPassPhrase" + saltBytes := crypto.CRandBytes(16) + + oldDerivedKey, err := bcrypt.GenerateFromPassword(saltBytes, []byte(passPhrase), 2) + assert.NotEmpty(t, oldDerivedKey) + require.NotNil(t, oldDerivedKey) + require.Equal(t, 60, len(oldDerivedKey)) + require.NoError(t, err) + + derivedKey := pdkdf2.Key([]byte(passPhrase), saltBytes, 10, 60, sha256.New) + assert.NotEmpty(t, derivedKey) + require.NotNil(t, derivedKey) + require.Equal(t, 60, len(derivedKey)) +} diff --git a/crypto/keys/mintkey/mintkey.go b/crypto/keys/mintkey/mintkey.go new file mode 100644 index 000000000000..575fb08dbcb4 --- /dev/null +++ b/crypto/keys/mintkey/mintkey.go @@ -0,0 +1,182 @@ +package mintkey + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "fmt" + + "github.com/cosmos/cosmos-sdk/crypto/keys/keyerror" + "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" + + cmn "github.com/tendermint/tendermint/libs/common" + + "github.com/tendermint/crypto/bcrypt" + pdkdf2 "golang.org/x/crypto/pbkdf2" +) + +const ( + blockTypePrivKey = "TENDERMINT PRIVATE KEY" + blockTypeKeyInfo = "TENDERMINT KEY INFO" + blockTypePubKey = "TENDERMINT PUBLIC KEY" +) + +// 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 { + return armorBytes(bz, blockTypeKeyInfo) +} + +// Armor the PubKeyBytes +func ArmorPubKeyBytes(bz []byte) string { + return armorBytes(bz, blockTypePubKey) +} + +func armorBytes(bz []byte, blockType string) string { + header := map[string]string{ + "type": "Info", + "version": "0.0.0", + } + return armor.EncodeArmor(blockType, header, bz) +} + +//----------------------------------------------------------------- +// remove armor + +// Unarmor the InfoBytes +func UnarmorInfoBytes(armorStr string) (bz []byte, err error) { + return unarmorBytes(armorStr, blockTypeKeyInfo) +} + +// Unarmor the PubKeyBytes +func UnarmorPubKeyBytes(armorStr string) (bz []byte, err error) { + return unarmorBytes(armorStr, blockTypePubKey) +} + +func unarmorBytes(armorStr, blockType string) (bz []byte, 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 + } + if header["version"] != "0.0.0" { + err = fmt.Errorf("unrecognized version: %v", header["version"]) + return + } + return +} + +//----------------------------------------------------------------- +// encrypt/decrypt with armor + +// Encrypt and armor the private key. +func EncryptArmorPrivKey(privKey crypto.PrivKey, passphrase string) string { + saltBytes, encBytes := encryptPrivKey(privKey, passphrase) + header := map[string]string{ + "kdf": "bcrypt", + "salt": fmt.Sprintf("%X", saltBytes), + } + 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 := pdkdf2.Key([]byte(passphrase), saltBytes, BcryptSecurityParameter, 60, sha256.New) + key = crypto.Sha256(key) // get 32 bytes + privKeyBytes := privKey.Bytes() + privKeyBytesHash := crypto.Sha256(privKeyBytes) + privKeyBytes = append(privKeyBytes, privKeyBytesHash...) // Add own hash to differentiate it from old implementation + return saltBytes, xsalsa20symmetric.EncryptSymmetric(privKeyBytes, key) +} + +// Unarmor and decrypt the private key. +func UnarmorDecryptPrivKey(armorStr string, passphrase string) (crypto.PrivKey, error) { + var privKey crypto.PrivKey + 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) + return privKey, err +} + +func decryptPrivKey(saltBytes []byte, encBytes []byte, passphrase string) (privKey crypto.PrivKey, err error) { + key := pdkdf2.Key([]byte(passphrase), saltBytes, BcryptSecurityParameter, 60, sha256.New) + key = crypto.Sha256(key) // Get 32 bytes + + privateBytes, err := decryptSymmetric(encBytes, key) + if err == nil || len(privateBytes) > 32 { + decryptedBytes := privateBytes[:len(privateBytes)-32] + decryptedBytesHash := privateBytes[len(privateBytes)-32:] //SHA-256 hash is 32 bytes + //If the decrypted hash doesn't match the privateBytes hash, then we are working with the old bcrypt algorithm + if !bytes.Equal(crypto.Sha256(decryptedBytes), decryptedBytesHash) { + decryptedBytes, err = decryptPrivKeyLegacy(saltBytes, encBytes, passphrase) + } + } else { + privateBytes, err = decryptPrivKeyLegacy(saltBytes, encBytes, passphrase) + } + + if err != nil { + return privKey, err + } + privKey, err = cryptoAmino.PrivKeyFromBytes(privateBytes) + return privKey, err +} + +func decryptPrivKeyLegacy(saltBytes []byte, encBytes []byte, passphrase string) (decryptedBytes []byte, err error) { + key, err := bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), BcryptSecurityParameter) + if err != nil { + cmn.Exit("error generating bcrypt key from passphrase: " + err.Error()) + } + return decryptSymmetric(encBytes, key) +} + +func decryptSymmetric(encBytes []byte, key []byte) (decryptedBytes []byte, err error) { + decryptedBytes, err = xsalsa20symmetric.DecryptSymmetric(encBytes, key) + if err != nil && err.Error() == "Ciphertext decryption failed" { + return decryptedBytes, keyerror.NewErrWrongPassword() + } else if err != nil { + return decryptedBytes, err + } + return decryptedBytes, nil +} diff --git a/crypto/keys/mintkey/mintkey_bench_test.go b/crypto/keys/mintkey/mintkey_bench_test.go new file mode 100644 index 000000000000..27b817037e49 --- /dev/null +++ b/crypto/keys/mintkey/mintkey_bench_test.go @@ -0,0 +1,27 @@ +package mintkey + +import ( + "crypto/sha256" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/tendermint/tendermint/crypto" + pdkdf2 "golang.org/x/crypto/pbkdf2" +) + +func BenchmarkBcryptGenerateFromPassword(b *testing.B) { + passphrase := []byte("passphrase") + for securityParam := 9; securityParam < 16; securityParam++ { + param := securityParam + b.Run(fmt.Sprintf("benchmark-security-param-%d", param), func(b *testing.B) { + saltBytes := crypto.CRandBytes(16) + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := pdkdf2.Key([]byte(passphrase), saltBytes, param, 24, sha256.New) + require.NotNil(b, key) + } + }) + } +} diff --git a/crypto/keys/mintkey/mintkey_test.go b/crypto/keys/mintkey/mintkey_test.go new file mode 100644 index 000000000000..53be95e50d03 --- /dev/null +++ b/crypto/keys/mintkey/mintkey_test.go @@ -0,0 +1,75 @@ +package mintkey_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tendermint/crypto/bcrypt" + "github.com/tendermint/tendermint/crypto" + armor "github.com/tendermint/tendermint/crypto/armor" + cryptoAmino "github.com/tendermint/tendermint/crypto/encoding/amino" + "github.com/tendermint/tendermint/crypto/secp256k1" + "github.com/tendermint/tendermint/crypto/xsalsa20symmetric" + + "github.com/cosmos/cosmos-sdk/crypto/keys" + "github.com/cosmos/cosmos-sdk/crypto/keys/mintkey" +) + +func TestArmorUnarmorPrivKey(t *testing.T) { + priv := secp256k1.GenPrivKey() + armor := mintkey.EncryptArmorPrivKey(priv, "passphrase") + _, err := mintkey.UnarmorDecryptPrivKey(armor, "wrongpassphrase") + require.Error(t, err) + decrypted, err := mintkey.UnarmorDecryptPrivKey(armor, "passphrase") + require.NoError(t, err) + require.True(t, priv.Equals(decrypted)) +} + +func TestArmorUnarmorPubKey(t *testing.T) { + // Select the encryption and storage for your cryptostore + cstore := keys.NewInMemory() + + // Add keys and see they return in alphabetical order + info, _, err := cstore.CreateMnemonic("Bob", keys.English, "passphrase", keys.Secp256k1) + require.NoError(t, err) + armor := mintkey.ArmorPubKeyBytes(info.GetPubKey().Bytes()) + pubBytes, err := mintkey.UnarmorPubKeyBytes(armor) + require.NoError(t, err) + pub, err := cryptoAmino.PubKeyFromBytes(pubBytes) + require.NoError(t, err) + require.True(t, pub.Equals(info.GetPubKey())) +} + +func TestPdkdf2Encryption(t *testing.T) { + priv := secp256k1.GenPrivKey() + armor := mintkey.EncryptArmorPrivKey(priv, "passphrase") + _, err := mintkey.UnarmorDecryptPrivKey(armor, "wrongpassphrase") + require.Error(t, err) + decrypted, err := mintkey.UnarmorDecryptPrivKey(armor, "passphrase") + require.NoError(t, err) + require.True(t, priv.Equals(decrypted)) + require.Equal(t, priv, decrypted) +} + +func TestBcryptLegacyEncryption(t *testing.T) { + priv := secp256k1.GenPrivKey() + + saltBytes := crypto.CRandBytes(16) + key, _ := bcrypt.GenerateFromPassword(saltBytes, []byte("passphrase"), 12) + key = crypto.Sha256(key) // get 32 bytes + privKeyBytes := priv.Bytes() + encBytes := xsalsa20symmetric.EncryptSymmetric(privKeyBytes, key) + header := map[string]string{ + "kdf": "bcrypt", + "salt": fmt.Sprintf("%X", saltBytes), + } + armorString := armor.EncodeArmor("TENDERMINT PRIVATE KEY", header, encBytes) + + _, err := mintkey.UnarmorDecryptPrivKey(armorString, "wrongpassphrase") + require.Error(t, err) + decrypted, err := mintkey.UnarmorDecryptPrivKey(armorString, "passphrase") + require.NoError(t, err) + require.True(t, priv.Equals(decrypted)) + require.Equal(t, priv, decrypted) +}