From 508e2eafcbde6099f86dde4ac0492a7dd43000ac Mon Sep 17 00:00:00 2001 From: Jim McDonald Date: Fri, 30 Oct 2020 12:30:57 +0000 Subject: [PATCH] Refuse weak passphrases without explicit flag. --- README.md | 6 +++++ cmd/accountcreate.go | 5 ++++ cmd/accountimport.go | 5 ++-- cmd/passphrases.go | 4 ++- cmd/root.go | 4 +++ cmd/walletcreate.go | 2 ++ cmd/walletexport.go | 2 ++ go.mod | 2 ++ go.sum | 2 ++ util/passphrase.go | 31 +++++++++++++++++++++ util/passphrase_test.go | 60 +++++++++++++++++++++++++++++++++++++++++ 11 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 util/passphrase.go create mode 100644 util/passphrase_test.go diff --git a/README.md b/README.md index 1305197..54ec60d 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,12 @@ If set, the `--debug` argument will output additional information about the oper Commands will have an exit status of 0 on success and 1 on failure. The specific definition of success is specified in the help for each command. +## Passphrase strength + +`ethdo` will by default not allow creation or export of accounts or wallets with weak passphrases. If a weak pasphrase is used then `ethdo` will refuse to continue. + +If a weak passphrase is required, `ethdo` can be supplied with the `--allow-weak-passphrases` option which will force it to accept any passphrase, even if it is considered weak. + ## Rules for account passphrases Account passphrases are used in various places in `ethdo`. Where they are used, the following rules apply: diff --git a/cmd/accountcreate.go b/cmd/accountcreate.go index 5c19194..2fe4b6e 100644 --- a/cmd/accountcreate.go +++ b/cmd/accountcreate.go @@ -21,6 +21,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" + "github.com/wealdtech/ethdo/util" e2wallet "github.com/wealdtech/go-eth2-wallet" e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" ) @@ -61,8 +62,12 @@ In quiet mode this will return 0 if the account is created successfully, otherwi outputIf(debug, fmt.Sprintf("Distributed account has %d/%d threshold", viper.GetUint32("signing-threshold"), viper.GetUint32("participants"))) ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) defer cancel() + if getOptionalPassphrase() != "" { + assert(util.AcceptablePassphrase(getPassphrase()), "supplied passphrase is weak; use a stronger one or run with the --allow-weak-passphrases flag") + } account, err = distributedCreator.CreateDistributedAccount(ctx, accountName, viper.GetUint32("participants"), viper.GetUint32("signing-threshold"), []byte(getOptionalPassphrase())) } else { + assert(util.AcceptablePassphrase(getPassphrase()), "supplied passphrase is weak; use a stronger one or run with the --allow-weak-passphrases flag") if viper.GetString("path") != "" { // Want a pathed account creator, isCreator := wallet.(e2wtypes.WalletPathedAccountCreator) diff --git a/cmd/accountimport.go b/cmd/accountimport.go index ca27163..0f193b4 100644 --- a/cmd/accountimport.go +++ b/cmd/accountimport.go @@ -20,6 +20,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" + "github.com/wealdtech/ethdo/util" "github.com/wealdtech/go-bytesutil" e2wallet "github.com/wealdtech/go-eth2-wallet" e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" @@ -38,8 +39,8 @@ In quiet mode this will return 0 if the account is imported successfully, otherw Run: func(cmd *cobra.Command, args []string) { assert(!remote, "account import not available with remote wallets") assert(viper.GetString("account") != "", "--account is required") - passphrase := getPassphrase() assert(accountImportKey != "", "--key is required") + assert(util.AcceptablePassphrase(getPassphrase()), "supplied passphrase is weak; use a stronger one or run with the --allow-weak-passphrases flag") ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) defer cancel() @@ -64,7 +65,7 @@ In quiet mode this will return 0 if the account is imported successfully, otherw _, accountName, err := e2wallet.WalletAndAccountNames(viper.GetString("account")) errCheck(err, "Failed to obtain account name") - account, err := w.(e2wtypes.WalletAccountImporter).ImportAccount(ctx, accountName, key, []byte(passphrase)) + account, err := w.(e2wtypes.WalletAccountImporter).ImportAccount(ctx, accountName, key, []byte(getPassphrase())) errCheck(err, "Failed to create account") pubKey, err := bestPublicKey(account) diff --git a/cmd/passphrases.go b/cmd/passphrases.go index abae6f6..2c48af5 100644 --- a/cmd/passphrases.go +++ b/cmd/passphrases.go @@ -13,7 +13,9 @@ package cmd -import "github.com/spf13/viper" +import ( + "github.com/spf13/viper" +) // getStorePassphrases() fetches the store passphrase supplied by the user. func getStorePassphrase() string { diff --git a/cmd/root.go b/cmd/root.go index a76fe2f..b770dbe 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -206,6 +206,10 @@ func init() { if err := viper.BindPFlag("server-ca-cert", RootCmd.PersistentFlags().Lookup("server-ca-cert")); err != nil { panic(err) } + RootCmd.PersistentFlags().Bool("allow-weak-passphrases", false, "allow passphrases that use common words, are short, or generally considered weak") + if err := viper.BindPFlag("allow-weak-passphrases", RootCmd.PersistentFlags().Lookup("allow-weak-passphrases")); err != nil { + panic(err) + } } // initConfig reads in config file and ENV variables if set. diff --git a/cmd/walletcreate.go b/cmd/walletcreate.go index c620106..a4e8d8c 100644 --- a/cmd/walletcreate.go +++ b/cmd/walletcreate.go @@ -24,6 +24,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" bip39 "github.com/tyler-smith/go-bip39" + "github.com/wealdtech/ethdo/util" distributed "github.com/wealdtech/go-eth2-wallet-distributed" keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4" hd "github.com/wealdtech/go-eth2-wallet-hd/v2" @@ -58,6 +59,7 @@ In quiet mode this will return 0 if the wallet is created successfully, otherwis os.Exit(_exitFailure) } assert(getWalletPassphrase() != "", "--walletpassphrase is required for hierarchical deterministic wallets") + assert(util.AcceptablePassphrase(getWalletPassphrase()), "supplied passphrase is weak; use a stronger one or run with the --allow-weak-passphrases flag") err = walletCreateHD(ctx, viper.GetString("wallet"), getWalletPassphrase(), viper.GetString("mnemonic")) case "distributed": assert(viper.GetString("mnemonic") == "", "--mnemonic is not allowed with distributed wallets") diff --git a/cmd/walletexport.go b/cmd/walletexport.go index 34c1b86..b69e466 100644 --- a/cmd/walletexport.go +++ b/cmd/walletexport.go @@ -20,6 +20,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" + "github.com/wealdtech/ethdo/util" types "github.com/wealdtech/go-eth2-wallet-types/v2" ) @@ -40,6 +41,7 @@ In quiet mode this will return 0 if the wallet is able to be exported, otherwise assert(viper.GetString("remote") == "", "wallet export not available with remote wallets") assert(viper.GetString("wallet") != "", "--wallet is required") assert(walletExportPassphrase != "", "--exportpassphrase is required") + assert(util.AcceptablePassphrase(walletExportPassphrase), "supplied passphrase is weak; use a stronger one or run with the --allow-weak-passphrases flag") wallet, err := walletFromPath(ctx, viper.GetString("wallet")) errCheck(err, "Failed to access wallet") diff --git a/go.mod b/go.mod index c8739bd..0fae227 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/magiconair/properties v1.8.4 // indirect github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/mapstructure v1.3.3 // indirect + github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d github.com/pelletier/go-toml v1.8.1 // indirect github.com/pkg/errors v0.9.1 github.com/prysmaticlabs/ethereumapis v0.0.0-20201003171600-a72e5f77d233 @@ -24,6 +25,7 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.7.1 + github.com/stretchr/testify v1.6.1 github.com/tyler-smith/go-bip39 v1.0.2 github.com/wealdtech/eth2-signer-api v1.6.0 github.com/wealdtech/go-bytesutil v1.1.1 diff --git a/go.sum b/go.sum index 06341c7..2d6a463 100644 --- a/go.sum +++ b/go.sum @@ -241,6 +241,8 @@ github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d h1:AREM5mwr4u1ORQBMvzfzBgpsctsbQikCVpvC+tX285E= +github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= diff --git a/util/passphrase.go b/util/passphrase.go new file mode 100644 index 0000000..7d38e01 --- /dev/null +++ b/util/passphrase.go @@ -0,0 +1,31 @@ +// Copyright © 2020 Weald Technology Trading +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + zxcvbn "github.com/nbutton23/zxcvbn-go" + "github.com/spf13/viper" +) + +// minPassphraseScore is the minimum acceptable passphrase score. +const minPassphraseScore = 2 + +// AcceptablePassphrase returns true if the passphrase is acceptable. +func AcceptablePassphrase(passphrase string) bool { + if viper.GetBool("allow-weak-passphrases") { + return true + } + res := zxcvbn.PasswordStrength(passphrase, nil) + return res.Score >= minPassphraseScore +} diff --git a/util/passphrase_test.go b/util/passphrase_test.go new file mode 100644 index 0000000..eb6bf07 --- /dev/null +++ b/util/passphrase_test.go @@ -0,0 +1,60 @@ +// Copyright © 2020 Weald Technology Trading +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util_test + +import ( + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + "github.com/wealdtech/ethdo/util" +) + +func TestAcceptablePassphrase(t *testing.T) { + tests := []struct { + name string + input string + allowWeak bool + expected bool + }{ + { + name: "Empty", + input: ``, + expected: false, + }, + { + name: "Simple", + input: `password`, + expected: false, + }, + { + name: "AllowedWeak", + input: `password`, + allowWeak: true, + expected: true, + }, + { + name: "Complex", + input: `Hu[J"yKH{z&-;[]'7T*Dm1:t`, + expected: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + viper.Set("allow-weak-passphrases", test.allowWeak) + require.Equal(t, test.expected, util.AcceptablePassphrase(test.input)) + }) + } +}