554 lines
14 KiB
Go
554 lines
14 KiB
Go
package keys
|
|
|
|
import (
|
|
"bufio"
|
|
"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"
|
|
)
|
|
|
|
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)
|
|
passwordHash, err := bcrypt.GenerateFromPassword(saltBytes, []byte(pass), 2)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
continue
|
|
}
|
|
|
|
if err := ioutil.WriteFile(dir+"/keyhash", passwordHash, 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{},
|
|
}
|
|
}
|