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) }