cosmos-sdk/crypto/keyring/keyring.go

852 lines
22 KiB
Go

package keyring
import (
"bufio"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"github.com/99designs/keyring"
bip39 "github.com/cosmos/go-bip39"
"github.com/pkg/errors"
"github.com/tendermint/crypto/bcrypt"
tmcrypto "github.com/tendermint/tendermint/crypto"
"github.com/cosmos/cosmos-sdk/client/input"
"github.com/cosmos/cosmos-sdk/codec/legacy"
"github.com/cosmos/cosmos-sdk/crypto"
"github.com/cosmos/cosmos-sdk/crypto/hd"
"github.com/cosmos/cosmos-sdk/crypto/ledger"
"github.com/cosmos/cosmos-sdk/crypto/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
// Backend options for Keyring
const (
BackendFile = "file"
BackendOS = "os"
BackendKWallet = "kwallet"
BackendPass = "pass"
BackendTest = "test"
BackendMemory = "memory"
)
const (
keyringFileDirName = "keyring-file"
keyringTestDirName = "keyring-test"
passKeyringPrefix = "keyring-%s"
)
var (
_ Keyring = &keystore{}
maxPassphraseEntryAttempts = 3
)
// Keyring exposes operations over a backend supported by github.com/99designs/keyring.
type Keyring interface {
// List all keys.
List() ([]Info, error)
// Supported signing algorithms for Keyring and Ledger respectively.
SupportedAlgorithms() (SigningAlgoList, SigningAlgoList)
// Key and KeyByAddress return keys by uid and address respectively.
Key(uid string) (Info, error)
KeyByAddress(address sdk.Address) (Info, error)
// Delete and DeleteByAddress remove keys from the keyring.
Delete(uid string) error
DeleteByAddress(address sdk.Address) error
// NewMnemonic generates a new mnemonic, derives a hierarchical deterministic key from it, and
// persists the key to storage. Returns the generated mnemonic and the key Info.
// It returns an error if it fails to generate a key for the given algo type, or if
// another key is already stored under the same name or address.
//
// A passphrase set to the empty string will set the passphrase to the DefaultBIP39Passphrase value.
NewMnemonic(uid string, language Language, hdPath, bip39Passphrase string, algo SignatureAlgo) (Info, string, error)
// NewAccount converts a mnemonic to a private key and BIP-39 HD Path and persists it.
// It fails if there is an existing key Info with the same address.
NewAccount(uid, mnemonic, bip39Passphrase, hdPath string, algo SignatureAlgo) (Info, error)
// SaveLedgerKey retrieves a public key reference from a Ledger device and persists it.
SaveLedgerKey(uid string, algo SignatureAlgo, hrp string, coinType, account, index uint32) (Info, error)
// SavePubKey stores a public key and returns the persisted Info structure.
SavePubKey(uid string, pubkey types.PubKey, algo hd.PubKeyType) (Info, error)
// SaveMultisig stores and returns a new multsig (offline) key reference.
SaveMultisig(uid string, pubkey types.PubKey) (Info, error)
Signer
Importer
Exporter
}
// UnsafeKeyring exposes unsafe operations such as unsafe unarmored export in
// addition to those that are made available by the Keyring interface.
type UnsafeKeyring interface {
Keyring
UnsafeExporter
}
// Signer is implemented by key stores that want to provide signing capabilities.
type Signer interface {
// Sign sign byte messages with a user key.
Sign(uid string, msg []byte) ([]byte, types.PubKey, error)
// SignByAddress sign byte messages with a user key providing the address.
SignByAddress(address sdk.Address, msg []byte) ([]byte, types.PubKey, error)
}
// Importer is implemented by key stores that support import of public and private keys.
type Importer interface {
// ImportPrivKey imports ASCII armored passphrase-encrypted private keys.
ImportPrivKey(uid, armor, passphrase string) error
// ImportPubKey imports ASCII armored public keys.
ImportPubKey(uid string, armor string) error
}
// LegacyInfoImporter is implemented by key stores that support import of Info types.
type LegacyInfoImporter interface {
// ImportInfo import a keyring.Info into the current keyring.
// It is used to migrate multisig, ledger, and public key Info structure.
ImportInfo(oldInfo Info) error
}
// Exporter is implemented by key stores that support export of public and private keys.
type Exporter interface {
// Export public key
ExportPubKeyArmor(uid string) (string, error)
ExportPubKeyArmorByAddress(address sdk.Address) (string, error)
// ExportPrivKeyArmor returns a private key in ASCII armored format.
// It returns an error if the key does not exist or a wrong encryption passphrase is supplied.
ExportPrivKeyArmor(uid, encryptPassphrase string) (armor string, err error)
ExportPrivKeyArmorByAddress(address sdk.Address, encryptPassphrase string) (armor string, err error)
}
// UnsafeExporter is implemented by key stores that support unsafe export
// of private keys' material.
type UnsafeExporter interface {
// UnsafeExportPrivKeyHex returns a private key in unarmored hex format
UnsafeExportPrivKeyHex(uid string) (string, error)
}
// Option overrides keyring configuration options.
type Option func(options *Options)
// Options define the options of the Keyring.
type Options struct {
// supported signing algorithms for keyring
SupportedAlgos SigningAlgoList
// supported signing algorithms for Ledger
SupportedAlgosLedger SigningAlgoList
}
// NewInMemory creates a transient keyring useful for testing
// purposes and on-the-fly key generation.
// Keybase options can be applied when generating this new Keybase.
func NewInMemory(opts ...Option) Keyring {
return newKeystore(keyring.NewArrayKeyring(nil), opts...)
}
// New creates a new instance of a keyring.
// Keyring ptions can be applied when generating the new instance.
// Available backends are "os", "file", "kwallet", "memory", "pass", "test".
func New(
appName, backend, rootDir string, userInput io.Reader, opts ...Option,
) (Keyring, error) {
var (
db keyring.Keyring
err error
)
switch backend {
case BackendMemory:
return NewInMemory(opts...), err
case BackendTest:
db, err = keyring.Open(newTestBackendKeyringConfig(appName, rootDir))
case BackendFile:
db, err = keyring.Open(newFileBackendKeyringConfig(appName, rootDir, userInput))
case BackendOS:
db, err = keyring.Open(newOSBackendKeyringConfig(appName, rootDir, userInput))
case BackendKWallet:
db, err = keyring.Open(newKWalletBackendKeyringConfig(appName, rootDir, userInput))
case BackendPass:
db, err = keyring.Open(newPassBackendKeyringConfig(appName, rootDir, userInput))
default:
return nil, fmt.Errorf("unknown keyring backend %v", backend)
}
if err != nil {
return nil, err
}
return newKeystore(db, opts...), nil
}
type keystore struct {
db keyring.Keyring
options Options
}
func newKeystore(kr keyring.Keyring, opts ...Option) keystore {
// Default options for keybase
options := Options{
SupportedAlgos: SigningAlgoList{hd.Secp256k1},
SupportedAlgosLedger: SigningAlgoList{hd.Secp256k1},
}
for _, optionFn := range opts {
optionFn(&options)
}
return keystore{kr, options}
}
func (ks keystore) ExportPubKeyArmor(uid string) (string, error) {
bz, err := ks.Key(uid)
if err != nil {
return "", err
}
if bz == nil {
return "", fmt.Errorf("no key to export with name: %s", uid)
}
return crypto.ArmorPubKeyBytes(legacy.Cdc.MustMarshal(bz.GetPubKey()), string(bz.GetAlgo())), nil
}
func (ks keystore) ExportPubKeyArmorByAddress(address sdk.Address) (string, error) {
info, err := ks.KeyByAddress(address)
if err != nil {
return "", err
}
return ks.ExportPubKeyArmor(info.GetName())
}
func (ks keystore) ExportPrivKeyArmor(uid, encryptPassphrase string) (armor string, err error) {
priv, err := ks.ExportPrivateKeyObject(uid)
if err != nil {
return "", err
}
info, err := ks.Key(uid)
if err != nil {
return "", err
}
return crypto.EncryptArmorPrivKey(priv, encryptPassphrase, string(info.GetAlgo())), nil
}
// ExportPrivateKeyObject exports an armored private key object.
func (ks keystore) ExportPrivateKeyObject(uid string) (types.PrivKey, error) {
info, err := ks.Key(uid)
if err != nil {
return nil, err
}
var priv types.PrivKey
switch linfo := info.(type) {
case localInfo:
if linfo.PrivKeyArmor == "" {
err = fmt.Errorf("private key not available")
return nil, err
}
priv, err = legacy.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
}
func (ks keystore) ExportPrivKeyArmorByAddress(address sdk.Address, encryptPassphrase string) (armor string, err error) {
byAddress, err := ks.KeyByAddress(address)
if err != nil {
return "", err
}
return ks.ExportPrivKeyArmor(byAddress.GetName(), encryptPassphrase)
}
func (ks keystore) ImportPrivKey(uid, armor, passphrase string) error {
if _, err := ks.Key(uid); err == nil {
return fmt.Errorf("cannot overwrite key: %s", uid)
}
privKey, algo, err := crypto.UnarmorDecryptPrivKey(armor, passphrase)
if err != nil {
return errors.Wrap(err, "failed to decrypt private key")
}
_, err = ks.writeLocalKey(uid, privKey, hd.PubKeyType(algo))
if err != nil {
return err
}
return nil
}
func (ks keystore) ImportPubKey(uid string, armor string) error {
if _, err := ks.Key(uid); err == nil {
return fmt.Errorf("cannot overwrite key: %s", uid)
}
pubBytes, algo, err := crypto.UnarmorPubKeyBytes(armor)
if err != nil {
return err
}
pubKey, err := legacy.PubKeyFromBytes(pubBytes)
if err != nil {
return err
}
_, err = ks.writeOfflineKey(uid, pubKey, hd.PubKeyType(algo))
if err != nil {
return err
}
return nil
}
// ImportInfo implements Importer.MigrateInfo.
func (ks keystore) ImportInfo(oldInfo Info) error {
if _, err := ks.Key(oldInfo.GetName()); err == nil {
return fmt.Errorf("cannot overwrite key: %s", oldInfo.GetName())
}
return ks.writeInfo(oldInfo)
}
func (ks keystore) Sign(uid string, msg []byte) ([]byte, types.PubKey, error) {
info, err := ks.Key(uid)
if err != nil {
return nil, nil, err
}
var priv types.PrivKey
switch i := info.(type) {
case localInfo:
if i.PrivKeyArmor == "" {
return nil, nil, fmt.Errorf("private key not available")
}
priv, err = legacy.PrivKeyFromBytes([]byte(i.PrivKeyArmor))
if err != nil {
return nil, nil, err
}
case ledgerInfo:
return SignWithLedger(info, msg)
case offlineInfo, multiInfo:
return nil, info.GetPubKey(), errors.New("cannot sign with offline keys")
}
sig, err := priv.Sign(msg)
if err != nil {
return nil, nil, err
}
return sig, priv.PubKey(), nil
}
func (ks keystore) SignByAddress(address sdk.Address, msg []byte) ([]byte, types.PubKey, error) {
key, err := ks.KeyByAddress(address)
if err != nil {
return nil, nil, err
}
return ks.Sign(key.GetName(), msg)
}
func (ks keystore) SaveLedgerKey(uid string, algo SignatureAlgo, hrp string, coinType, account, index uint32) (Info, error) {
if !ks.options.SupportedAlgosLedger.Contains(algo) {
return nil, ErrUnsupportedSigningAlgo
}
hdPath := hd.NewFundraiserParams(account, coinType, index)
priv, _, err := ledger.NewPrivKeySecp256k1(*hdPath, hrp)
if err != nil {
return nil, err
}
return ks.writeLedgerKey(uid, priv.PubKey(), *hdPath, algo.Name())
}
func (ks keystore) writeLedgerKey(name string, pub types.PubKey, path hd.BIP44Params, algo hd.PubKeyType) (Info, error) {
info := newLedgerInfo(name, pub, path, algo)
if err := ks.writeInfo(info); err != nil {
return nil, err
}
return info, nil
}
func (ks keystore) SaveMultisig(uid string, pubkey types.PubKey) (Info, error) {
return ks.writeMultisigKey(uid, pubkey)
}
func (ks keystore) SavePubKey(uid string, pubkey types.PubKey, algo hd.PubKeyType) (Info, error) {
return ks.writeOfflineKey(uid, pubkey, algo)
}
func (ks keystore) DeleteByAddress(address sdk.Address) error {
info, err := ks.KeyByAddress(address)
if err != nil {
return err
}
err = ks.Delete(info.GetName())
if err != nil {
return err
}
return nil
}
func (ks keystore) Delete(uid string) error {
info, err := ks.Key(uid)
if err != nil {
return err
}
err = ks.db.Remove(addrHexKeyAsString(info.GetAddress()))
if err != nil {
return err
}
err = ks.db.Remove(infoKey(uid))
if err != nil {
return err
}
return nil
}
func (ks keystore) KeyByAddress(address sdk.Address) (Info, error) {
ik, err := ks.db.Get(addrHexKeyAsString(address))
if err != nil {
return nil, wrapKeyNotFound(err, fmt.Sprint("key with address", address, "not found"))
}
if len(ik.Data) == 0 {
return nil, wrapKeyNotFound(err, fmt.Sprint("key with address", address, "not found"))
}
return ks.key(string(ik.Data))
}
func wrapKeyNotFound(err error, msg string) error {
if err == keyring.ErrKeyNotFound {
return sdkerrors.Wrap(sdkerrors.ErrKeyNotFound, msg)
}
return err
}
func (ks keystore) List() ([]Info, error) {
var res []Info
keys, err := ks.db.Keys()
if err != nil {
return nil, err
}
sort.Strings(keys)
for _, key := range keys {
if strings.HasSuffix(key, infoSuffix) {
rawInfo, err := ks.db.Get(key)
if err != nil {
return nil, err
}
if len(rawInfo.Data) == 0 {
return nil, sdkerrors.Wrap(sdkerrors.ErrKeyNotFound, key)
}
info, err := unmarshalInfo(rawInfo.Data)
if err != nil {
return nil, err
}
res = append(res, info)
}
}
return res, nil
}
func (ks keystore) NewMnemonic(uid string, language Language, hdPath, bip39Passphrase string, algo SignatureAlgo) (Info, string, error) {
if language != English {
return nil, "", ErrUnsupportedLanguage
}
if !ks.isSupportedSigningAlgo(algo) {
return nil, "", ErrUnsupportedSigningAlgo
}
// Default number of words (24): This generates a mnemonic directly from the
// number of words by reading system entropy.
entropy, err := bip39.NewEntropy(defaultEntropySize)
if err != nil {
return nil, "", err
}
mnemonic, err := bip39.NewMnemonic(entropy)
if err != nil {
return nil, "", err
}
if bip39Passphrase == "" {
bip39Passphrase = DefaultBIP39Passphrase
}
info, err := ks.NewAccount(uid, mnemonic, bip39Passphrase, hdPath, algo)
if err != nil {
return nil, "", err
}
return info, mnemonic, nil
}
func (ks keystore) NewAccount(name string, mnemonic string, bip39Passphrase string, hdPath string, algo SignatureAlgo) (Info, error) {
if !ks.isSupportedSigningAlgo(algo) {
return nil, ErrUnsupportedSigningAlgo
}
// create master key and derive first key for keyring
derivedPriv, err := algo.Derive()(mnemonic, bip39Passphrase, hdPath)
if err != nil {
return nil, err
}
privKey := algo.Generate()(derivedPriv)
// check if the a key already exists with the same address and return an error
// if found
address := sdk.AccAddress(privKey.PubKey().Address())
if _, err := ks.KeyByAddress(address); err == nil {
return nil, fmt.Errorf("account with address %s already exists in keyring, delete the key first if you want to recreate it", address)
}
return ks.writeLocalKey(name, privKey, algo.Name())
}
func (ks keystore) isSupportedSigningAlgo(algo SignatureAlgo) bool {
return ks.options.SupportedAlgos.Contains(algo)
}
func (ks keystore) key(infoKey string) (Info, error) {
bs, err := ks.db.Get(infoKey)
if err != nil {
return nil, wrapKeyNotFound(err, infoKey)
}
if len(bs.Data) == 0 {
return nil, sdkerrors.Wrap(sdkerrors.ErrKeyNotFound, infoKey)
}
return unmarshalInfo(bs.Data)
}
func (ks keystore) Key(uid string) (Info, error) {
return ks.key(infoKey(uid))
}
// SupportedAlgorithms returns the keystore Options' supported signing algorithm.
// for the keyring and Ledger.
func (ks keystore) SupportedAlgorithms() (SigningAlgoList, SigningAlgoList) {
return ks.options.SupportedAlgos, ks.options.SupportedAlgosLedger
}
// SignWithLedger signs a binary message with the ledger device referenced by an Info object
// and returns the signed bytes and the public key. It returns an error if the device could
// not be queried or it returned an error.
func SignWithLedger(info Info, msg []byte) (sig []byte, pub types.PubKey, err error) {
switch info.(type) {
case *ledgerInfo, ledgerInfo:
default:
return nil, nil, errors.New("not a ledger object")
}
path, err := info.GetPath()
if err != nil {
return
}
priv, err := ledger.NewPrivKeySecp256k1Unsafe(*path)
if err != nil {
return
}
sig, err = priv.Sign(msg)
if err != nil {
return nil, nil, err
}
return sig, priv.PubKey(), nil
}
func newOSBackendKeyringConfig(appName, dir string, buf io.Reader) keyring.Config {
return keyring.Config{
ServiceName: appName,
FileDir: dir,
KeychainTrustApplication: true,
FilePasswordFunc: newRealPrompt(dir, buf),
}
}
func newTestBackendKeyringConfig(appName, dir string) keyring.Config {
return keyring.Config{
AllowedBackends: []keyring.BackendType{keyring.FileBackend},
ServiceName: appName,
FileDir: filepath.Join(dir, keyringTestDirName),
FilePasswordFunc: func(_ string) (string, error) {
return "test", nil
},
}
}
func newKWalletBackendKeyringConfig(appName, _ string, _ io.Reader) keyring.Config {
return keyring.Config{
AllowedBackends: []keyring.BackendType{keyring.KWalletBackend},
ServiceName: "kdewallet",
KWalletAppID: appName,
KWalletFolder: "",
}
}
func newPassBackendKeyringConfig(appName, _ string, _ io.Reader) keyring.Config {
prefix := fmt.Sprintf(passKeyringPrefix, appName)
return keyring.Config{
AllowedBackends: []keyring.BackendType{keyring.PassBackend},
ServiceName: appName,
PassPrefix: prefix,
}
}
func newFileBackendKeyringConfig(name, dir string, buf io.Reader) keyring.Config {
fileDir := filepath.Join(dir, keyringFileDirName)
return keyring.Config{
AllowedBackends: []keyring.BackendType{keyring.FileBackend},
ServiceName: name,
FileDir: fileDir,
FilePasswordFunc: newRealPrompt(fileDir, buf),
}
}
func newRealPrompt(dir string, buf io.Reader) func(string) (string, error) {
return 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 {
// NOTE: LGTM.io reports a false positive alert that states we are printing the password,
// but we only log the error.
//
// lgtm [go/clear-text-logging]
fmt.Fprintln(os.Stderr, err)
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 {
// NOTE: LGTM.io reports a false positive alert that states we are printing the password,
// but we only log the error.
//
// lgtm [go/clear-text-logging]
fmt.Fprintln(os.Stderr, err)
continue
}
if pass != reEnteredPass {
fmt.Fprintln(os.Stderr, "passphrase do not match")
continue
}
saltBytes := tmcrypto.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
}
}
}
func (ks keystore) writeLocalKey(name string, priv types.PrivKey, algo hd.PubKeyType) (Info, error) {
// encrypt private key using keyring
pub := priv.PubKey()
info := newLocalInfo(name, pub, string(legacy.Cdc.MustMarshal(priv)), algo)
if err := ks.writeInfo(info); err != nil {
return nil, err
}
return info, nil
}
func (ks keystore) writeInfo(info Info) error {
key := infoKeyBz(info.GetName())
serializedInfo := marshalInfo(info)
exists, err := ks.existsInDb(info)
if err != nil {
return err
}
if exists {
return errors.New("public key already exist in keybase")
}
err = ks.db.Set(keyring.Item{
Key: string(key),
Data: serializedInfo,
})
if err != nil {
return err
}
err = ks.db.Set(keyring.Item{
Key: addrHexKeyAsString(info.GetAddress()),
Data: key,
})
if err != nil {
return err
}
return nil
}
// existsInDb returns true if key is in DB. Error is returned only when we have error
// different thant ErrKeyNotFound
func (ks keystore) existsInDb(info Info) (bool, error) {
if _, err := ks.db.Get(addrHexKeyAsString(info.GetAddress())); err == nil {
return true, nil // address lookup succeeds - info exists
} else if err != keyring.ErrKeyNotFound {
return false, err // received unexpected error - returns error
}
if _, err := ks.db.Get(infoKey(info.GetName())); err == nil {
return true, nil // uid lookup succeeds - info exists
} else if err != keyring.ErrKeyNotFound {
return false, err // received unexpected error - returns
}
// both lookups failed, info does not exist
return false, nil
}
func (ks keystore) writeOfflineKey(name string, pub types.PubKey, algo hd.PubKeyType) (Info, error) {
info := newOfflineInfo(name, pub, algo)
err := ks.writeInfo(info)
if err != nil {
return nil, err
}
return info, nil
}
func (ks keystore) writeMultisigKey(name string, pub types.PubKey) (Info, error) {
info, err := NewMultiInfo(name, pub)
if err != nil {
return nil, err
}
if err = ks.writeInfo(info); err != nil {
return nil, err
}
return info, nil
}
type unsafeKeystore struct {
keystore
}
// NewUnsafe returns a new keyring that provides support for unsafe operations.
func NewUnsafe(kr Keyring) UnsafeKeyring {
// The type assertion is against the only keystore
// implementation that is currently provided.
ks := kr.(keystore)
return unsafeKeystore{ks}
}
// UnsafeExportPrivKeyHex exports private keys in unarmored hexadecimal format.
func (ks unsafeKeystore) UnsafeExportPrivKeyHex(uid string) (privkey string, err error) {
priv, err := ks.ExportPrivateKeyObject(uid)
if err != nil {
return "", err
}
return hex.EncodeToString(priv.Bytes()), nil
}
func addrHexKeyAsString(address sdk.Address) string {
return fmt.Sprintf("%s.%s", hex.EncodeToString(address.Bytes()), addressSuffix)
}