diff --git a/Gopkg.lock b/Gopkg.lock index 169893877a54..3817fd238e9a 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -48,12 +48,19 @@ revision = "d4cc87b860166d00d6b5b9e0d3b3d71d6088d4d4" [[projects]] - digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec" + digest = "1:e8a3550c8786316675ff54ad6f09d265d129c9d986919af7f541afba50d87ce2" + name = "github.com/cosmos/go-bip39" + packages = ["."] + pruneopts = "UT" + revision = "52158e4697b87de16ed390e1bdaf813e581008fa" + +[[projects]] + digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39" name = "github.com/davecgh/go-spew" packages = ["spew"] pruneopts = "UT" - revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73" - version = "v1.1.1" + revision = "346938d642f2ec3594ed81d874461961cd0faa76" + version = "v1.1.0" [[projects]] digest = "1:c7644c73a3d23741fdba8a99b1464e021a224b7e205be497271a8003a15ca41b" @@ -390,8 +397,7 @@ version = "v1.2.1" [[projects]] - branch = "master" - digest = "1:f2ffd421680b0a3f7887501b3c6974bcf19217ecd301d0e2c9b681940ec363d5" + digest = "1:b3cfb8d82b1601a846417c3f31c03a7961862cb2c98dcf0959c473843e6d9a2b" name = "github.com/syndtr/goleveldb" packages = [ "leveldb", @@ -408,7 +414,7 @@ "leveldb/util", ] pruneopts = "UT" - revision = "ae2bd5eed72d46b28834ec3f60db3a3ebedd8dbd" + revision = "c4c61651e9e37fa117f53c5a906d3b63090d8445" [[projects]] digest = "1:605b6546f3f43745695298ec2d342d3e952b6d91cdf9f349bea9315f677d759f" @@ -524,10 +530,10 @@ version = "v0.1.0" [[projects]] - branch = "master" - digest = "1:27507554c6d4f060d8d700c31c624a43d3a92baa634e178ddc044bdf7d13b44a" + digest = "1:aaff04fa01d9b824fde6799759cc597b3ac3671b9ad31924c28b6557d0ee5284" name = "golang.org/x/crypto" packages = [ + "bcrypt", "blowfish", "chacha20poly1305", "curve25519", @@ -544,7 +550,8 @@ "salsa20/salsa", ] pruneopts = "UT" - revision = "e3636079e1a4c1f337f212cc5cd2aca108f6c900" + revision = "3764759f34a542a3aef74d6b02e35be7ab893bba" + source = "https://github.com/tendermint/crypto" [[projects]] digest = "1:d36f55a999540d29b6ea3c2ea29d71c76b1d9853fdcd3e5c5cb4836f2ba118f1" @@ -563,15 +570,14 @@ revision = "292b43bbf7cb8d35ddf40f8d5100ef3837cced3f" [[projects]] - branch = "master" - digest = "1:8bc8ecef1d63576cfab4d08b44a1f255dd67e5b019b7a44837d62380f266a91c" + digest = "1:4bd75b1a219bc590b05c976bbebf47f4e993314ebb5c7cbf2efe05a09a184d54" name = "golang.org/x/sys" packages = [ "cpu", "unix", ] pruneopts = "UT" - revision = "e4b3c5e9061176387e7cea65e4dc5853801f3fb7" + revision = "4e1fef5609515ec7a2cee7b5de30ba6d9b438cbf" [[projects]] digest = "1:a2ab62866c75542dd18d2b069fec854577a20211d7c0ea6ae746072a1dccdd18" @@ -597,12 +603,11 @@ version = "v0.3.0" [[projects]] - branch = "master" - digest = "1:1e6b0176e8c5dd8ff551af65c76f8b73a99bcf4d812cedff1b91711b7df4804c" + digest = "1:077c1c599507b3b3e9156d17d36e1e61928ee9b53a5b420f10f28ebd4a0b275c" name = "google.golang.org/genproto" packages = ["googleapis/rpc/status"] pruneopts = "UT" - revision = "c7e5094acea1ca1b899e2259d80a6b0f882f81f8" + revision = "383e8b2c3b9e36c4076b235b32537292176bae20" [[projects]] digest = "1:2dab32a43451e320e49608ff4542fdfc653c95dcc35d0065ec9c6c3dd540ed74" @@ -653,6 +658,7 @@ "github.com/bartekn/go-bip39", "github.com/bgentry/speakeasy", "github.com/btcsuite/btcd/btcec", + "github.com/cosmos/go-bip39", "github.com/golang/protobuf/proto", "github.com/gorilla/mux", "github.com/mattn/go-isatty", @@ -699,7 +705,7 @@ "github.com/tendermint/tendermint/types", "github.com/tendermint/tendermint/version", "github.com/zondax/ledger-goclient", - "golang.org/x/crypto/blowfish", + "golang.org/x/crypto/bcrypt", ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index b63234f4603d..05140857dd47 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -59,22 +59,51 @@ name = "github.com/tendermint/tendermint" version = "=0.25.0" +## deps without releases: + +[[constraint]] + name = "golang.org/x/crypto" + source = "https://github.com/tendermint/crypto" + revision = "3764759f34a542a3aef74d6b02e35be7ab893bba" + [[constraint]] - name = "github.com/bartekn/go-bip39" - revision = "a05967ea095d81c8fe4833776774cfaff8e5036c" + name = "github.com/cosmos/go-bip39" + revision = "52158e4697b87de16ed390e1bdaf813e581008fa" [[constraint]] name = "github.com/zondax/ledger-goclient" version = "=v0.1.0" +## transitive deps, with releases: + +[[override]] + name = "github.com/davecgh/go-spew" + version = "=v1.1.0" + [[constraint]] name = "github.com/rakyll/statik" version = "=v0.1.4" +[[constraint]] + name = "github.com/mitchellh/go-homedir" + version = "1.0.0" + +## transitive deps, without releases: +# + +[[override]] + name = "github.com/syndtr/goleveldb" + revision = "c4c61651e9e37fa117f53c5a906d3b63090d8445" + +[[override]] + name = "golang.org/x/sys" + revision = "4e1fef5609515ec7a2cee7b5de30ba6d9b438cbf" + +[[override]] + name = "google.golang.org/genproto" + revision = "383e8b2c3b9e36c4076b235b32537292176bae20" + [prune] go-tests = true unused-packages = true -[[constraint]] - name = "github.com/mitchellh/go-homedir" - version = "1.0.0" diff --git a/client/input.go b/client/input.go index a456f1b92a3f..46c838e2e4e4 100644 --- a/client/input.go +++ b/client/input.go @@ -7,7 +7,7 @@ import ( "strings" "github.com/bgentry/speakeasy" - isatty "github.com/mattn/go-isatty" + "github.com/mattn/go-isatty" "github.com/pkg/errors" ) @@ -44,13 +44,8 @@ func GetPassword(prompt string, buf *bufio.Reader) (pass string, err error) { // GetSeed will request a seed phrase from stdin and trims off // leading/trailing spaces -func GetSeed(prompt string, buf *bufio.Reader) (seed string, err error) { - if inputIsTty() { - fmt.Println(prompt) - } - seed, err = readLineFromBuf(buf) - seed = strings.TrimSpace(seed) - return +func GetSeed(prompt string, buf *bufio.Reader) (string, error) { + return GetString(prompt, buf) } // GetCheckPassword will prompt for a password twice to verify they @@ -133,5 +128,6 @@ func readLineFromBuf(buf *bufio.Reader) (string, error) { // PrintPrefixed prints a string with > prefixed for use in prompts. func PrintPrefixed(msg string) { - fmt.Printf("> %s\n", msg) + msg = fmt.Sprintf("> %s\n", msg) + fmt.Fprint(os.Stderr, msg) } diff --git a/client/keys/mnemonic.go b/client/keys/mnemonic.go new file mode 100644 index 000000000000..33270a087414 --- /dev/null +++ b/client/keys/mnemonic.go @@ -0,0 +1,78 @@ +package keys + +import ( + "crypto/sha256" + "fmt" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/spf13/cobra" + + bip39 "github.com/bartekn/go-bip39" +) + +const ( + flagUserEntropy = "unsafe-entropy" + + mnemonicEntropySize = 256 +) + +func mnemonicKeyCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "mnemonic", + Short: "Compute the bip39 mnemonic for some input entropy", + Long: "Create a bip39 mnemonic, sometimes called a seed phrase, by reading from the system entropy. To pass your own entropy, use --unsafe-entropy", + RunE: runMnemonicCmd, + } + cmd.Flags().Bool(flagUserEntropy, false, "Prompt the user to supply their own entropy, instead of relying on the system") + return cmd +} + +func runMnemonicCmd(cmd *cobra.Command, args []string) error { + flags := cmd.Flags() + + userEntropy, _ := flags.GetBool(flagUserEntropy) + + var entropySeed []byte + + if userEntropy { + // prompt the user to enter some entropy + buf := client.BufferStdin() + inputEntropy, err := client.GetString("> WARNING: Generate at least 256-bits of entropy and enter the results here:", buf) + if err != nil { + return err + } + if len(inputEntropy) < 43 { + return fmt.Errorf("256-bits is 43 characters in Base-64, and 100 in Base-6. You entered %v, and probably want more", len(inputEntropy)) + } + conf, err := client.GetConfirmation( + fmt.Sprintf("> Input length: %d", len(inputEntropy)), + buf) + if err != nil { + return err + } + if !conf { + return nil + } + + // hash input entropy to get entropy seed + hashedEntropy := sha256.Sum256([]byte(inputEntropy)) + entropySeed = hashedEntropy[:] + printStep() + } else { + // read entropy seed straight from crypto.Rand + var err error + entropySeed, err = bip39.NewEntropy(mnemonicEntropySize) + if err != nil { + return err + } + } + + mnemonic, err := bip39.NewMnemonic(entropySeed[:]) + if err != nil { + return err + } + + fmt.Println(mnemonic) + + return nil +} diff --git a/client/keys/new.go b/client/keys/new.go new file mode 100644 index 000000000000..3408eb9d0061 --- /dev/null +++ b/client/keys/new.go @@ -0,0 +1,182 @@ +package keys + +import ( + "bufio" + "fmt" + "os" + + "github.com/bartekn/go-bip39" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/crypto/keys" + "github.com/cosmos/cosmos-sdk/crypto/keys/hd" +) + +const ( + flagNewDefault = "default" + flagBIP44Path = "bip44-path" +) + +func newKeyCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "new", + Short: "Interactive command to derive a new private key, encrypt it, and save to disk", + Long: `Derive a new private key using an interactive command that will prompt you for each input. +Optionally specify a bip39 mnemonic, a bip39 passphrase to further secure the mnemonic, +and a bip32 HD path to derive a specific account. The key will be stored under the given name +and encrypted with the given password. The only input that is required is the encryption password.`, + Args: cobra.ExactArgs(1), + RunE: runNewCmd, + } + cmd.Flags().Bool(flagNewDefault, false, "Skip the prompts and just use the default values for everything") + cmd.Flags().Bool(client.FlagUseLedger, false, "Store a local reference to a private key on a Ledger device") + cmd.Flags().String(flagBIP44Path, "44'/118'/0'/0/0", "BIP44 path from which to derive a private key") + return cmd +} + +/* +input + - bip39 mnemonic + - bip39 passphrase + - bip44 path + - local encryption password +output + - armor encrypted private key (saved to file) +*/ +// nolint: gocyclo +func runNewCmd(cmd *cobra.Command, args []string) error { + name := args[0] + kb, err := GetKeyBase() + if err != nil { + return err + } + + buf := client.BufferStdin() + _, err = kb.Get(name) + if err == nil { + // account exists, ask for user confirmation + if response, err := client.GetConfirmation( + fmt.Sprintf("> override the existing name %s", name), buf); err != nil || !response { + return err + } + } + + flags := cmd.Flags() + useDefaults, _ := flags.GetBool(flagNewDefault) + bipFlag := flags.Lookup(flagBIP44Path) + + bip44Params, err := getBIP44ParamsAndPath(bipFlag.Value.String(), bipFlag.Changed || useDefaults) + if err != nil { + return err + } + + // if we're using ledger, only thing we need is the path. + // generate key and we're done. + if viper.GetBool(client.FlagUseLedger) { + + algo := keys.Secp256k1 // SigningAlgo(viper.GetString(flagType)) + path := bip44Params.DerivationPath() // ccrypto.DerivationPath{44, 118, account, 0, index} + info, err := kb.CreateLedger(name, path, algo) + if err != nil { + return err + } + printCreate(info, "") + return nil + } + + // get the mnemonic + var mnemonic string + if !useDefaults { + mnemonic, err = client.GetString("> Enter your bip39 mnemonic, or hit enter to generate one.", buf) + if err != nil { + return err + } + } + + if len(mnemonic) == 0 { + // read entropy seed straight from crypto.Rand and convert to mnemonic + entropySeed, err := bip39.NewEntropy(mnemonicEntropySize) + if err != nil { + return err + } + mnemonic, err = bip39.NewMnemonic(entropySeed[:]) + if err != nil { + return err + } + } + + // get bip39 passphrase + var bip39Passphrase string + if !useDefaults { + printStep() + printPrefixed("Enter your bip39 passphrase. This is combined with the mnemonic to derive the seed") + bip39Passphrase, err = client.GetString("> Most users should just hit enter to use the default, \"\"", buf) + if err != nil { + return err + } + + // if they use one, make them re-enter it + if len(bip39Passphrase) != 0 { + p2, err := client.GetString("Repeat the passphrase:", buf) + if err != nil { + return err + } + if bip39Passphrase != p2 { + return errors.New("passphrases don't match") + } + } + } + + // get the encryption password + printStep() + encryptPassword, err := client.GetCheckPassword( + "> Enter a passphrase to encrypt your key to disk:", + "> Repeat the passphrase:", buf) + if err != nil { + return err + } + + info, err := kb.Derive(name, mnemonic, bip39Passphrase, encryptPassword, *bip44Params) + if err != nil { + return err + } + _ = info + + return nil +} + +func getBIP44ParamsAndPath(path string, flagSet bool) (*hd.BIP44Params, error) { + buf := bufio.NewReader(os.Stdin) + bip44Path := path + + // if it wasnt set in the flag, give it a chance to overide interactively + if !flagSet { + printStep() + + var err error + bip44Path, err = client.GetString(fmt.Sprintf("> Enter your bip44 path. Default is %s\n", path), buf) + if err != nil { + return nil, err + } + if len(bip44Path) == 0 { + bip44Path = path + } + } + + bip44params, err := hd.NewParamsFromPath(bip44Path) + if err != nil { + return nil, err + } + return bip44params, nil +} + +func printPrefixed(msg string) { + fmt.Printf("> %s\n", msg) +} + +func printStep() { + printPrefixed("-------------------------------------") +} diff --git a/client/keys/root.go b/client/keys/root.go index a7a7d2e6fc60..b10cd2b550c1 100644 --- a/client/keys/root.go +++ b/client/keys/root.go @@ -19,6 +19,8 @@ func Commands() *cobra.Command { needs to sign with a private key.`, } cmd.AddCommand( + mnemonicKeyCommand(), + newKeyCommand(), addKeyCommand(), listKeysCmd, showKeysCmd(), diff --git a/client/lcd/lcd_test.go b/client/lcd/lcd_test.go index 94174398d66c..7aea16d868c6 100644 --- a/client/lcd/lcd_test.go +++ b/client/lcd/lcd_test.go @@ -10,20 +10,20 @@ import ( "testing" "time" - "github.com/cosmos/cosmos-sdk/client/rpc" - "github.com/cosmos/cosmos-sdk/client/tx" - p2p "github.com/tendermint/tendermint/p2p" - "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - cryptoKeys "github.com/cosmos/cosmos-sdk/crypto/keys" + p2p "github.com/tendermint/tendermint/p2p" ctypes "github.com/tendermint/tendermint/rpc/core/types" client "github.com/cosmos/cosmos-sdk/client" keys "github.com/cosmos/cosmos-sdk/client/keys" + "github.com/cosmos/cosmos-sdk/client/rpc" + "github.com/cosmos/cosmos-sdk/client/tx" "github.com/cosmos/cosmos-sdk/codec" + cryptoKeys "github.com/cosmos/cosmos-sdk/crypto/keys" + "github.com/cosmos/cosmos-sdk/crypto/keys/mintkey" tests "github.com/cosmos/cosmos-sdk/tests" sdk "github.com/cosmos/cosmos-sdk/types" version "github.com/cosmos/cosmos-sdk/version" @@ -35,7 +35,7 @@ import ( ) func init() { - cryptoKeys.BcryptSecurityParameter = 1 + mintkey.BcryptSecurityParameter = 1 version.Version = os.Getenv("VERSION") } diff --git a/crypto/keys/bcrypt/base64.go b/crypto/keys/bcrypt/base64.go deleted file mode 100644 index fc3116090818..000000000000 --- a/crypto/keys/bcrypt/base64.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2011 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package bcrypt - -import "encoding/base64" - -const alphabet = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" - -var bcEncoding = base64.NewEncoding(alphabet) - -func base64Encode(src []byte) []byte { - n := bcEncoding.EncodedLen(len(src)) - dst := make([]byte, n) - bcEncoding.Encode(dst, src) - for dst[n-1] == '=' { - n-- - } - return dst[:n] -} - -func base64Decode(src []byte) ([]byte, error) { - numOfEquals := 4 - (len(src) % 4) - for i := 0; i < numOfEquals; i++ { - src = append(src, '=') - } - - dst := make([]byte, bcEncoding.DecodedLen(len(src))) - n, err := bcEncoding.Decode(dst, src) - if err != nil { - return nil, err - } - return dst[:n], nil -} diff --git a/crypto/keys/bcrypt/bcrypt.go b/crypto/keys/bcrypt/bcrypt.go deleted file mode 100644 index e24120bfbed4..000000000000 --- a/crypto/keys/bcrypt/bcrypt.go +++ /dev/null @@ -1,297 +0,0 @@ -package bcrypt - -// MODIFIED BY TENDERMINT TO EXPOSE NONCE -// Copyright 2011 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package bcrypt implements Provos and Mazières's bcrypt adaptive hashing -// algorithm. See http://www.usenix.org/event/usenix99/provos/provos.pdf - -// The code is a port of Provos and Mazières's C implementation. -import ( - "crypto/subtle" - "errors" - "fmt" - "strconv" - - "golang.org/x/crypto/blowfish" -) - -const ( - // the minimum allowable cost as passed in to GenerateFromPassword - MinCost int = 4 - // the maximum allowable cost as passed in to GenerateFromPassword - MaxCost int = 31 - // the cost that will actually be set if a cost below MinCost is passed into GenerateFromPassword - DefaultCost int = 10 -) - -// The error returned from CompareHashAndPassword when a password and hash do -// not match. -var ErrMismatchedHashAndPassword = errors.New("crypto/bcrypt: hashedPassword is not the hash of the given password") - -// The error returned from CompareHashAndPassword when a hash is too short to -// be a bcrypt hash. -var ErrHashTooShort = errors.New("crypto/bcrypt: hashedSecret too short to be a bcrypted password") - -// The error returned from CompareHashAndPassword when a hash was created with -// a bcrypt algorithm newer than this implementation. -type HashVersionTooNewError byte - -func (hv HashVersionTooNewError) Error() string { - return fmt.Sprintf("crypto/bcrypt: bcrypt algorithm version '%c' requested is newer than current version '%c'", byte(hv), majorVersion) -} - -// The error returned from CompareHashAndPassword when a hash starts with something other than '$' -type InvalidHashPrefixError byte - -// Format error -func (ih InvalidHashPrefixError) Error() string { - return fmt.Sprintf("crypto/bcrypt: bcrypt hashes must start with '$', but hashedSecret started with '%c'", byte(ih)) -} - -// Invalid bcrypt cost -type InvalidCostError int - -func (ic InvalidCostError) Error() string { - return fmt.Sprintf("crypto/bcrypt: cost %d is outside allowed range (%d,%d)", int(ic), int(MinCost), int(MaxCost)) // nolint: unconvert -} - -const ( - majorVersion = '2' - minorVersion = 'a' - maxSaltSize = 16 - maxCryptedHashSize = 23 - encodedSaltSize = 22 - encodedHashSize = 31 - minHashSize = 59 -) - -// magicCipherData is an IV for the 64 Blowfish encryption calls in -// bcrypt(). It's the string "OrpheanBeholderScryDoubt" in big-endian bytes. -var magicCipherData = []byte{ - 0x4f, 0x72, 0x70, 0x68, - 0x65, 0x61, 0x6e, 0x42, - 0x65, 0x68, 0x6f, 0x6c, - 0x64, 0x65, 0x72, 0x53, - 0x63, 0x72, 0x79, 0x44, - 0x6f, 0x75, 0x62, 0x74, -} - -type hashed struct { - hash []byte - salt []byte - cost int // allowed range is MinCost to MaxCost - major byte - minor byte -} - -// GenerateFromPassword returns the bcrypt hash of the password at the given -// cost. If the cost given is less than MinCost, the cost will be set to -// DefaultCost, instead. Use CompareHashAndPassword, as defined in this package, -// to compare the returned hashed password with its cleartext version. -func GenerateFromPassword(salt []byte, password []byte, cost int) ([]byte, error) { - if len(salt) != maxSaltSize { - return nil, fmt.Errorf("salt len must be %v", maxSaltSize) - } - p, err := newFromPassword(salt, password, cost) - if err != nil { - return nil, err - } - return p.Hash(), nil -} - -// CompareHashAndPassword compares a bcrypt hashed password with its possible -// plaintext equivalent. Returns nil on success, or an error on failure. -func CompareHashAndPassword(hashedPassword, password []byte) error { - p, err := newFromHash(hashedPassword) - if err != nil { - return err - } - - otherHash, err := bcrypt(password, p.cost, p.salt) - if err != nil { - return err - } - - otherP := &hashed{otherHash, p.salt, p.cost, p.major, p.minor} - if subtle.ConstantTimeCompare(p.Hash(), otherP.Hash()) == 1 { - return nil - } - - return ErrMismatchedHashAndPassword -} - -// Cost returns the hashing cost used to create the given hashed -// password. When, in the future, the hashing cost of a password system needs -// to be increased in order to adjust for greater computational power, this -// function allows one to establish which passwords need to be updated. -func Cost(hashedPassword []byte) (int, error) { - p, err := newFromHash(hashedPassword) - if err != nil { - return 0, err - } - return p.cost, nil -} - -func newFromPassword(salt []byte, password []byte, cost int) (*hashed, error) { - if cost < MinCost { - cost = DefaultCost - } - p := new(hashed) - p.major = majorVersion - p.minor = minorVersion - - err := checkCost(cost) - if err != nil { - return nil, err - } - p.cost = cost - - p.salt = base64Encode(salt) - hash, err := bcrypt(password, p.cost, p.salt) - if err != nil { - return nil, err - } - p.hash = hash - return p, err -} - -func newFromHash(hashedSecret []byte) (*hashed, error) { - if len(hashedSecret) < minHashSize { - return nil, ErrHashTooShort - } - p := new(hashed) - n, err := p.decodeVersion(hashedSecret) - if err != nil { - return nil, err - } - hashedSecret = hashedSecret[n:] - n, err = p.decodeCost(hashedSecret) - if err != nil { - return nil, err - } - hashedSecret = hashedSecret[n:] - - // The "+2" is here because we'll have to append at most 2 '=' to the salt - // when base64 decoding it in expensiveBlowfishSetup(). - p.salt = make([]byte, encodedSaltSize, encodedSaltSize+2) - copy(p.salt, hashedSecret[:encodedSaltSize]) - - hashedSecret = hashedSecret[encodedSaltSize:] - p.hash = make([]byte, len(hashedSecret)) - copy(p.hash, hashedSecret) - - return p, nil -} - -func bcrypt(password []byte, cost int, salt []byte) ([]byte, error) { - cipherData := make([]byte, len(magicCipherData)) - copy(cipherData, magicCipherData) - - c, err := expensiveBlowfishSetup(password, uint32(cost), salt) - if err != nil { - return nil, err - } - - for i := 0; i < 24; i += 8 { - for j := 0; j < 64; j++ { - c.Encrypt(cipherData[i:i+8], cipherData[i:i+8]) - } - } - - // Bug compatibility with C bcrypt implementations. We only encode 23 of - // the 24 bytes encrypted. - hsh := base64Encode(cipherData[:maxCryptedHashSize]) - return hsh, nil -} - -func expensiveBlowfishSetup(key []byte, cost uint32, salt []byte) (*blowfish.Cipher, error) { - - csalt, err := base64Decode(salt) - if err != nil { - return nil, err - } - - // Bug compatibility with C bcrypt implementations. They use the trailing - // NULL in the key string during expansion. - ckey := append(key, 0) - - c, err := blowfish.NewSaltedCipher(ckey, csalt) - if err != nil { - return nil, err - } - - var i, rounds uint64 - rounds = 1 << cost - for i = 0; i < rounds; i++ { - blowfish.ExpandKey(ckey, c) - blowfish.ExpandKey(csalt, c) - } - - return c, nil -} - -func (p *hashed) Hash() []byte { - arr := make([]byte, 60) - arr[0] = '$' - arr[1] = p.major - n := 2 - if p.minor != 0 { - arr[2] = p.minor - n = 3 - } - arr[n] = '$' - n++ - copy(arr[n:], []byte(fmt.Sprintf("%02d", p.cost))) - n += 2 - arr[n] = '$' - n++ - copy(arr[n:], p.salt) - n += encodedSaltSize - copy(arr[n:], p.hash) - n += encodedHashSize - return arr[:n] -} - -func (p *hashed) decodeVersion(sbytes []byte) (int, error) { - if sbytes[0] != '$' { - return -1, InvalidHashPrefixError(sbytes[0]) - } - if sbytes[1] > majorVersion { - return -1, HashVersionTooNewError(sbytes[1]) - } - p.major = sbytes[1] - n := 3 - if sbytes[2] != '$' { - p.minor = sbytes[2] - n++ - } - return n, nil -} - -// sbytes should begin where decodeVersion left off. -func (p *hashed) decodeCost(sbytes []byte) (int, error) { - cost, err := strconv.Atoi(string(sbytes[0:2])) - if err != nil { - return -1, err - } - err = checkCost(cost) - if err != nil { - return -1, err - } - p.cost = cost - return 3, nil -} - -func (p *hashed) String() string { - return fmt.Sprintf("&{hash: %#v, salt: %#v, cost: %d, major: %c, minor: %c}", string(p.hash), p.salt, p.cost, p.major, p.minor) -} - -func checkCost(cost int) error { - if cost < MinCost || cost > MaxCost { - return InvalidCostError(cost) - } - return nil -} diff --git a/crypto/keys/bip39/wordcodec.go b/crypto/keys/bip39/wordcodec.go deleted file mode 100644 index 074d1393c028..000000000000 --- a/crypto/keys/bip39/wordcodec.go +++ /dev/null @@ -1,66 +0,0 @@ -package bip39 - -import ( - "strings" - - "github.com/bartekn/go-bip39" -) - -// ValidSentenceLen defines the mnemonic sentence lengths supported by this BIP 39 library. -type ValidSentenceLen uint8 - -const ( - // FundRaiser is the sentence length used during the cosmos fundraiser (12 words). - FundRaiser ValidSentenceLen = 12 - // Size of the checksum employed for the fundraiser - FundRaiserChecksumSize = 4 - // FreshKey is the sentence length used for newly created keys (24 words). - FreshKey ValidSentenceLen = 24 - // Size of the checksum employed for new keys - FreshKeyChecksumSize = 8 -) - -// NewMnemonic will return a string consisting of the mnemonic words for -// the given sentence length. -func NewMnemonic(len ValidSentenceLen) (words []string, err error) { - // len = (entropySize + checksum) / 11 - var entropySize int - switch len { - case FundRaiser: - // entropySize = 128 - entropySize = int(len)*11 - FundRaiserChecksumSize - case FreshKey: - // entropySize = 256 - entropySize = int(len)*11 - FreshKeyChecksumSize - } - var entropy []byte - entropy, err = bip39.NewEntropy(entropySize) - if err != nil { - return - } - var mnemonic string - mnemonic, err = bip39.NewMnemonic(entropy) - if err != nil { - return - } - words = strings.Split(mnemonic, " ") - return -} - -// MnemonicToSeed creates a BIP 39 seed from the passed mnemonic (with an empty BIP 39 password). -// This method does not validate the mnemonics checksum. -func MnemonicToSeed(mne string) (seed []byte) { - // we do not checksum here... - seed = bip39.NewSeed(mne, "") - return -} - -// MnemonicToSeedWithErrChecking returns the same seed as MnemonicToSeed. -// It creates a BIP 39 seed from the passed mnemonic (with an empty BIP 39 password). -// -// Different from MnemonicToSeed it validates the checksum. -// For details on the checksum see the BIP 39 spec. -func MnemonicToSeedWithErrChecking(mne string) (seed []byte, err error) { - seed, err = bip39.NewSeedWithErrorChecking(mne, "") - return -} diff --git a/crypto/keys/bip39/wordcodec_test.go b/crypto/keys/bip39/wordcodec_test.go deleted file mode 100644 index a821239d7b40..000000000000 --- a/crypto/keys/bip39/wordcodec_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package bip39 - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestWordCodec_NewMnemonic(t *testing.T) { - _, err := NewMnemonic(FundRaiser) - require.NoError(t, err, "unexpected error generating fundraiser mnemonic") - - _, err = NewMnemonic(FreshKey) - require.NoError(t, err, "unexpected error generating new 24-word mnemonic") -} diff --git a/crypto/keys/hd/fundraiser_test.go b/crypto/keys/hd/fundraiser_test.go index 84de09758d59..5e3cf06f3f46 100644 --- a/crypto/keys/hd/fundraiser_test.go +++ b/crypto/keys/hd/fundraiser_test.go @@ -7,9 +7,10 @@ import ( "io/ioutil" "testing" - "github.com/bartekn/go-bip39" "github.com/stretchr/testify/require" + "github.com/cosmos/go-bip39" + "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/secp256k1" ) diff --git a/crypto/keys/hd/hdpath.go b/crypto/keys/hd/hdpath.go index ef2e6f783a2a..112abe0b662a 100644 --- a/crypto/keys/hd/hdpath.go +++ b/crypto/keys/hd/hdpath.go @@ -22,7 +22,6 @@ import ( "strings" "github.com/btcsuite/btcd/btcec" - "github.com/tendermint/tendermint/crypto/secp256k1" ) // BIP44Prefix is the parts of the BIP32 HD path that are fixed by what we used during the fundraiser. @@ -55,6 +54,77 @@ func NewParams(purpose, coinType, account uint32, change bool, addressIdx uint32 } } +// Parse the BIP44 path and unmarshal into the struct. +// nolint: gocyclo +func NewParamsFromPath(path string) (*BIP44Params, error) { + spl := strings.Split(path, "/") + if len(spl) != 5 { + return nil, fmt.Errorf("path length is wrong. Expected 5, got %d", len(spl)) + } + + if spl[0] != "44'" { + return nil, fmt.Errorf("first field in path must be 44', got %v", spl[0]) + } + + if !isHardened(spl[1]) || !isHardened(spl[2]) { + return nil, + fmt.Errorf("second and third field in path must be hardened (ie. contain the suffix ', got %v and %v", spl[1], spl[2]) + } + if isHardened(spl[3]) || isHardened(spl[4]) { + return nil, + fmt.Errorf("fourth and fifth field in path must not be hardened (ie. not contain the suffix ', got %v and %v", spl[3], spl[4]) + } + + purpose, err := hardenedInt(spl[0]) + if err != nil { + return nil, err + } + coinType, err := hardenedInt(spl[1]) + if err != nil { + return nil, err + } + account, err := hardenedInt(spl[2]) + if err != nil { + return nil, err + } + change, err := hardenedInt(spl[3]) + if err != nil { + return nil, err + } + if !(change == 0 || change == 1) { + return nil, fmt.Errorf("change field can only be 0 or 1") + } + + addressIdx, err := hardenedInt(spl[4]) + if err != nil { + return nil, err + } + + return &BIP44Params{ + purpose: purpose, + coinType: coinType, + account: account, + change: change > 0, + addressIdx: addressIdx, + }, nil +} + +func hardenedInt(field string) (uint32, error) { + field = strings.TrimSuffix(field, "'") + i, err := strconv.Atoi(field) + if err != nil { + return 0, err + } + if i < 0 { + return 0, fmt.Errorf("fields must not be negative. got %d", i) + } + return uint32(i), nil +} + +func isHardened(field string) bool { + return strings.HasSuffix(field, "'") +} + // NewFundraiserParams creates a BIP 44 parameter object from the params: // m / 44' / 118' / account' / 0 / address_index // The fixed parameters (purpose', coin_type', and change) are determined by what was used in the fundraiser. @@ -62,6 +132,21 @@ func NewFundraiserParams(account uint32, addressIdx uint32) *BIP44Params { return NewParams(44, 118, account, false, addressIdx) } +// Return the BIP44 fields as an array. +func (p BIP44Params) DerivationPath() []uint32 { + change := uint32(0) + if p.change { + change = 1 + } + return []uint32{ + p.purpose, + p.coinType, + p.account, + change, + p.addressIdx, + } +} + func (p BIP44Params) String() string { var changeStr string if p.change { @@ -128,10 +213,15 @@ func derivePrivateKey(privKeyBytes [32]byte, chainCode [32]byte, index uint32, h data = append([]byte{byte(0)}, privKeyBytes[:]...) } else { // this can't return an error: - pubkey := secp256k1.PrivKeySecp256k1(privKeyBytes).PubKey() + _, ecPub := btcec.PrivKeyFromBytes(btcec.S256(), privKeyBytes[:]) + pubkeyBytes := ecPub.SerializeCompressed() + data = pubkeyBytes + /* By using btcec, we can remove the dependency on tendermint/crypto/secp256k1 + pubkey := secp256k1.PrivKeySecp256k1(privKeyBytes).PubKey() public := pubkey.(secp256k1.PubKeySecp256k1) data = public[:] + */ } data = append(data, uint32ToBytes(index)...) data2, chainCode2 := i64(chainCode[:], data) diff --git a/crypto/keys/hd/hdpath_test.go b/crypto/keys/hd/hdpath_test.go index 58398655f0b5..f310fc3555cb 100644 --- a/crypto/keys/hd/hdpath_test.go +++ b/crypto/keys/hd/hdpath_test.go @@ -3,9 +3,20 @@ package hd import ( "encoding/hex" "fmt" - "github.com/cosmos/cosmos-sdk/crypto/keys/bip39" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cosmos/go-bip39" ) +var defaultBIP39Passphrase = "" + +// return bip39 seed with empty passphrase +func mnemonicToSeed(mnemonic string) []byte { + return bip39.NewSeed(mnemonic, defaultBIP39Passphrase) +} + //nolint func ExampleStringifyPathParams() { path := NewParams(44, 0, 0, false, 0) @@ -13,10 +24,57 @@ func ExampleStringifyPathParams() { // Output: 44'/0'/0'/0/0 } +func TestParamsFromPath(t *testing.T) { + goodCases := []struct { + params *BIP44Params + path string + }{ + {&BIP44Params{44, 0, 0, false, 0}, "44'/0'/0'/0/0"}, + {&BIP44Params{44, 1, 0, false, 0}, "44'/1'/0'/0/0"}, + {&BIP44Params{44, 0, 1, false, 0}, "44'/0'/1'/0/0"}, + {&BIP44Params{44, 0, 0, true, 0}, "44'/0'/0'/1/0"}, + {&BIP44Params{44, 0, 0, false, 1}, "44'/0'/0'/0/1"}, + {&BIP44Params{44, 1, 1, true, 1}, "44'/1'/1'/1/1"}, + {&BIP44Params{44, 118, 52, true, 41}, "44'/118'/52'/1/41"}, + } + + for i, c := range goodCases { + params, err := NewParamsFromPath(c.path) + errStr := fmt.Sprintf("%d %v", i, c) + assert.NoError(t, err, errStr) + assert.EqualValues(t, c.params, params, errStr) + assert.Equal(t, c.path, c.params.String()) + } + + badCases := []struct { + path string + }{ + {"43'/0'/0'/0/0"}, // doesnt start with 44 + {"44'/1'/0'/0/0/5"}, // too many fields + {"44'/0'/1'/0"}, // too few fields + {"44'/0'/0'/2/0"}, // change field can only be 0/1 + {"44/0'/0'/0/0"}, // first field needs ' + {"44'/0/0'/0/0"}, // second field needs ' + {"44'/0'/0/0/0"}, // third field needs ' + {"44'/0'/0'/0'/0"}, // fourth field must not have ' + {"44'/0'/0'/0/0'"}, // fifth field must not have ' + {"44'/-1'/0'/0/0"}, // no negatives + {"44'/0'/0'/-1/0"}, // no negatives + } + + for i, c := range badCases { + params, err := NewParamsFromPath(c.path) + errStr := fmt.Sprintf("%d %v", i, c) + assert.Nil(t, params, errStr) + assert.Error(t, err, errStr) + } + +} + //nolint func ExampleSomeBIP32TestVecs() { - seed := bip39.MnemonicToSeed("barrel original fuel morning among eternal " + + seed := mnemonicToSeed("barrel original fuel morning among eternal " + "filter ball stove pluck matrix mechanic") master, ch := ComputeMastersFromSeed(seed) fmt.Println("keys from fundraiser test-vector (cosmos, bitcoin, ether)") @@ -35,14 +93,14 @@ func ExampleSomeBIP32TestVecs() { fmt.Println("keys generated via https://coinomi.com/recovery-phrase-tool.html") fmt.Println() - seed = bip39.MnemonicToSeed( + seed = mnemonicToSeed( "advice process birth april short trust crater change bacon monkey medal garment " + "gorilla ranch hour rival razor call lunar mention taste vacant woman sister") master, ch = ComputeMastersFromSeed(seed) priv, _ = DerivePrivateKeyForPath(master, ch, "44'/1'/1'/0/4") fmt.Println(hex.EncodeToString(priv[:])) - seed = bip39.MnemonicToSeed("idea naive region square margin day captain habit " + + seed = mnemonicToSeed("idea naive region square margin day captain habit " + "gun second farm pact pulse someone armed") master, ch = ComputeMastersFromSeed(seed) priv, _ = DerivePrivateKeyForPath(master, ch, "44'/0'/0'/0/420") @@ -53,7 +111,7 @@ func ExampleSomeBIP32TestVecs() { fmt.Println() // bip32 path: m/0/7 - seed = bip39.MnemonicToSeed("monitor flock loyal sick object grunt duty ride develop assault harsh history") + seed = mnemonicToSeed("monitor flock loyal sick object grunt duty ride develop assault harsh history") master, ch = ComputeMastersFromSeed(seed) priv, _ = DerivePrivateKeyForPath(master, ch, "0/7") fmt.Println(hex.EncodeToString(priv[:])) diff --git a/crypto/keys/keybase.go b/crypto/keys/keybase.go index 99632e764268..ddcd0357e1a3 100644 --- a/crypto/keys/keybase.go +++ b/crypto/keys/keybase.go @@ -6,11 +6,15 @@ import ( "os" "strings" + "github.com/pkg/errors" + + "github.com/cosmos/go-bip39" + "github.com/cosmos/cosmos-sdk/crypto" - "github.com/cosmos/cosmos-sdk/crypto/keys/bip39" "github.com/cosmos/cosmos-sdk/crypto/keys/hd" + "github.com/cosmos/cosmos-sdk/crypto/keys/mintkey" "github.com/cosmos/cosmos-sdk/types" - "github.com/pkg/errors" + tmcrypto "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/encoding/amino" "github.com/tendermint/tendermint/crypto/secp256k1" @@ -46,6 +50,14 @@ const ( infoSuffix = "info" ) +const ( + // used for deriving seed from mnemonic + defaultBIP39Passphrase = "" + + // bits of entropy to draw when creating a mnemonic + defaultEntropySize = 256 +) + var ( // ErrUnsupportedSigningAlgo is raised when the caller tries to use a // different signing scheme than secp256k1. @@ -85,12 +97,17 @@ func (kb dbKeybase) CreateMnemonic(name string, language Language, passwd string } // default number of words (24): - mnemonicS, err := bip39.NewMnemonic(bip39.FreshKey) + // this generates a mnemonic directly from the number of words by reading system entropy. + entropy, err := bip39.NewEntropy(defaultEntropySize) + if err != nil { + return + } + mnemonic, err = bip39.NewMnemonic(entropy) if err != nil { return } - mnemonic = strings.Join(mnemonicS, " ") - seed := bip39.MnemonicToSeed(mnemonic) + + seed := bip39.NewSeed(mnemonic, defaultBIP39Passphrase) info, err = kb.persistDerivedKey(seed, passwd, name, hd.FullFundraiserPath) return } @@ -102,7 +119,7 @@ func (kb dbKeybase) CreateKey(name, mnemonic, passwd string) (info Info, err err err = fmt.Errorf("recovering only works with 12 word (fundraiser) or 24 word mnemonics, got: %v words", len(words)) return } - seed, err := bip39.MnemonicToSeedWithErrChecking(mnemonic) + seed, err := bip39.NewSeedWithErrorChecking(mnemonic, defaultBIP39Passphrase) if err != nil { return } @@ -119,7 +136,7 @@ func (kb dbKeybase) CreateFundraiserKey(name, mnemonic, passwd string) (info Inf err = fmt.Errorf("recovering only works with 12 word (fundraiser), got: %v words", len(words)) return } - seed, err := bip39.MnemonicToSeedWithErrChecking(mnemonic) + seed, err := bip39.NewSeedWithErrorChecking(mnemonic, defaultBIP39Passphrase) if err != nil { return } @@ -127,12 +144,12 @@ func (kb dbKeybase) CreateFundraiserKey(name, mnemonic, passwd string) (info Inf return } -func (kb dbKeybase) Derive(name, mnemonic, passwd string, params hd.BIP44Params) (info Info, err error) { - seed, err := bip39.MnemonicToSeedWithErrChecking(mnemonic) +func (kb dbKeybase) Derive(name, mnemonic, bip39Passphrase, encryptPasswd string, params hd.BIP44Params) (info Info, err error) { + seed, err := bip39.NewSeedWithErrorChecking(mnemonic, bip39Passphrase) if err != nil { return } - info, err = kb.persistDerivedKey(seed, passwd, name, params.String()) + info, err = kb.persistDerivedKey(seed, encryptPasswd, name, params.String()) return } @@ -229,7 +246,7 @@ func (kb dbKeybase) Sign(name, passphrase string, msg []byte) (sig []byte, pub t err = fmt.Errorf("private key not available") return } - priv, err = unarmorDecryptPrivKey(linfo.PrivKeyArmor, passphrase) + priv, err = mintkey.UnarmorDecryptPrivKey(linfo.PrivKeyArmor, passphrase) if err != nil { return nil, nil, err } @@ -279,7 +296,7 @@ func (kb dbKeybase) ExportPrivateKeyObject(name string, passphrase string) (tmcr err = fmt.Errorf("private key not available") return nil, err } - priv, err = unarmorDecryptPrivKey(linfo.PrivKeyArmor, passphrase) + priv, err = mintkey.UnarmorDecryptPrivKey(linfo.PrivKeyArmor, passphrase) if err != nil { return nil, err } @@ -296,7 +313,7 @@ func (kb dbKeybase) Export(name string) (armor string, err error) { if bz == nil { return "", fmt.Errorf("no key to export with name %s", name) } - return armorInfoBytes(bz), nil + return mintkey.ArmorInfoBytes(bz), nil } // ExportPubKey returns public keys in ASCII armored format. @@ -311,7 +328,7 @@ func (kb dbKeybase) ExportPubKey(name string) (armor string, err error) { if err != nil { return } - return armorPubKeyBytes(info.GetPubKey().Bytes()), nil + return mintkey.ArmorPubKeyBytes(info.GetPubKey().Bytes()), nil } func (kb dbKeybase) Import(name string, armor string) (err error) { @@ -319,7 +336,7 @@ func (kb dbKeybase) Import(name string, armor string) (err error) { if len(bz) > 0 { return errors.New("Cannot overwrite data for name " + name) } - infoBytes, err := unarmorInfoBytes(armor) + infoBytes, err := mintkey.UnarmorInfoBytes(armor) if err != nil { return } @@ -335,7 +352,7 @@ func (kb dbKeybase) ImportPubKey(name string, armor string) (err error) { if len(bz) > 0 { return errors.New("Cannot overwrite data for name " + name) } - pubBytes, err := unarmorPubKeyBytes(armor) + pubBytes, err := mintkey.UnarmorPubKeyBytes(armor) if err != nil { return } @@ -360,7 +377,7 @@ func (kb dbKeybase) Delete(name, passphrase string) error { switch info.(type) { case localInfo: linfo := info.(localInfo) - _, err = unarmorDecryptPrivKey(linfo.PrivKeyArmor, passphrase) + _, err = mintkey.UnarmorDecryptPrivKey(linfo.PrivKeyArmor, passphrase) if err != nil { return err } @@ -394,7 +411,7 @@ func (kb dbKeybase) Update(name, oldpass string, getNewpass func() (string, erro switch info.(type) { case localInfo: linfo := info.(localInfo) - key, err := unarmorDecryptPrivKey(linfo.PrivKeyArmor, oldpass) + key, err := mintkey.UnarmorDecryptPrivKey(linfo.PrivKeyArmor, oldpass) if err != nil { return err } @@ -411,7 +428,7 @@ func (kb dbKeybase) Update(name, oldpass string, getNewpass func() (string, erro func (kb dbKeybase) writeLocalKey(priv tmcrypto.PrivKey, name, passphrase string) Info { // encrypt private key using passphrase - privArmor := encryptArmorPrivKey(priv, passphrase) + privArmor := mintkey.EncryptArmorPrivKey(priv, passphrase) // make Info pub := priv.PubKey() info := newLocalInfo(name, pub, privArmor) diff --git a/crypto/keys/keybase_test.go b/crypto/keys/keybase_test.go index 3273c229afe6..cafa2382a598 100644 --- a/crypto/keys/keybase_test.go +++ b/crypto/keys/keybase_test.go @@ -4,9 +4,12 @@ import ( "fmt" "testing" - "github.com/cosmos/cosmos-sdk/crypto/keys/hd" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/crypto/keys/hd" + "github.com/cosmos/cosmos-sdk/crypto/keys/mintkey" + "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/ed25519" @@ -15,7 +18,7 @@ import ( ) func init() { - BcryptSecurityParameter = 1 + mintkey.BcryptSecurityParameter = 1 } // TestKeyManagement makes sure we can manipulate these keys well @@ -342,7 +345,7 @@ func TestSeedPhrase(t *testing.T) { // let us re-create it from the mnemonic-phrase params := *hd.NewFundraiserParams(0, 0) - newInfo, err := cstore.Derive(n2, mnemonic, p2, params) + newInfo, err := cstore.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()) diff --git a/crypto/keys/mintkey.go b/crypto/keys/mintkey/mintkey.go similarity index 79% rename from crypto/keys/mintkey.go rename to crypto/keys/mintkey/mintkey.go index 70e1bc44e259..80377920fe46 100644 --- a/crypto/keys/mintkey.go +++ b/crypto/keys/mintkey/mintkey.go @@ -1,16 +1,17 @@ -package keys +package mintkey import ( "encoding/hex" "fmt" - cmn "github.com/tendermint/tendermint/libs/common" + "golang.org/x/crypto/bcrypt" - "github.com/cosmos/cosmos-sdk/crypto/keys/bcrypt" "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/armor" "github.com/tendermint/tendermint/crypto/encoding/amino" "github.com/tendermint/tendermint/crypto/xsalsa20symmetric" + + cmn "github.com/tendermint/tendermint/libs/common" ) const ( @@ -34,11 +35,16 @@ const ( // TODO: Consider increasing default var BcryptSecurityParameter = 12 -func armorInfoBytes(bz []byte) string { +//----------------------------------------------------------------- +// add armor + +// Armor the InfoBytes +func ArmorInfoBytes(bz []byte) string { return armorBytes(bz, blockTypeKeyInfo) } -func armorPubKeyBytes(bz []byte) string { +// Armor the PubKeyBytes +func ArmorPubKeyBytes(bz []byte) string { return armorBytes(bz, blockTypePubKey) } @@ -50,11 +56,16 @@ func armorBytes(bz []byte, blockType string) string { return armor.EncodeArmor(blockType, header, bz) } -func unarmorInfoBytes(armorStr string) (bz []byte, err error) { +//----------------------------------------------------------------- +// remove armor + +// Unarmor the InfoBytes +func UnarmorInfoBytes(armorStr string) (bz []byte, err error) { return unarmorBytes(armorStr, blockTypeKeyInfo) } -func unarmorPubKeyBytes(armorStr string) (bz []byte, err error) { +// Unarmor the PubKeyBytes +func UnarmorPubKeyBytes(armorStr string) (bz []byte, err error) { return unarmorBytes(armorStr, blockTypePubKey) } @@ -74,7 +85,11 @@ func unarmorBytes(armorStr, blockType string) (bz []byte, err error) { return } -func encryptArmorPrivKey(privKey crypto.PrivKey, passphrase string) string { +//----------------------------------------------------------------- +// 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", @@ -84,7 +99,22 @@ func encryptArmorPrivKey(privKey crypto.PrivKey, passphrase string) string { return armorStr } -func unarmorDecryptPrivKey(armorStr string, passphrase string) (crypto.PrivKey, error) { +// encrypt the given privKey with the passphrase using a randomly +// generated salt and the xsalsa20 cipher. returns the salt and the +// encrypted priv key. +func encryptPrivKey(privKey crypto.PrivKey, passphrase string) (saltBytes []byte, encBytes []byte) { + saltBytes = crypto.CRandBytes(16) + key, err := bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), BcryptSecurityParameter) + if err != nil { + cmn.Exit("Error generating bcrypt key from passphrase: " + err.Error()) + } + key = crypto.Sha256(key) // get 32 bytes + privKeyBytes := privKey.Bytes() + 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 { @@ -107,17 +137,6 @@ func unarmorDecryptPrivKey(armorStr string, passphrase string) (crypto.PrivKey, return privKey, err } -func encryptPrivKey(privKey crypto.PrivKey, passphrase string) (saltBytes []byte, encBytes []byte) { - saltBytes = crypto.CRandBytes(16) - key, err := bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), BcryptSecurityParameter) - if err != nil { - cmn.Exit("Error generating bcrypt key from passphrase: " + err.Error()) - } - key = crypto.Sha256(key) // Get 32 bytes - privKeyBytes := privKey.Bytes() - return saltBytes, xsalsa20symmetric.EncryptSymmetric(privKeyBytes, key) -} - func decryptPrivKey(saltBytes []byte, encBytes []byte, passphrase string) (privKey crypto.PrivKey, err error) { key, err := bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), BcryptSecurityParameter) if err != nil { diff --git a/crypto/keys/types.go b/crypto/keys/types.go index c5e97d5fba31..f5194748a01f 100644 --- a/crypto/keys/types.go +++ b/crypto/keys/types.go @@ -26,8 +26,12 @@ type Keybase interface { CreateKey(name, mnemonic, passwd string) (info Info, err error) // CreateFundraiserKey takes a mnemonic and derives, a password CreateFundraiserKey(name, mnemonic, passwd string) (info Info, err error) - // Derive derives a key from the passed mnemonic using a BIP44 path. - Derive(name, mnemonic, passwd string, params hd.BIP44Params) (Info, error) + // Compute a BIP39 seed from th mnemonic and bip39Passwd. + // Derive private key from the seed using the BIP44 params. + // Encrypt the key to disk using encryptPasswd. + // See https://github.com/cosmos/cosmos-sdk/issues/2095 + Derive(name, mnemonic, bip39Passwd, + encryptPasswd string, params hd.BIP44Params) (Info, error) // Create, store, and return a new Ledger key reference CreateLedger(name string, path ccrypto.DerivationPath, algo SigningAlgo) (info Info, err error)