decred.org/dcrwallet/v3@v3.1.0/internal/prompt/prompt.go (about)

     1  // Copyright (c) 2015-2016 The btcsuite developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  package prompt
     6  
     7  import (
     8  	"bufio"
     9  	"bytes"
    10  	"encoding/hex"
    11  	"fmt"
    12  	"io"
    13  	"os"
    14  	"strings"
    15  	"unicode"
    16  
    17  	"decred.org/dcrwallet/v3/errors"
    18  	"decred.org/dcrwallet/v3/walletseed"
    19  	"github.com/decred/dcrd/hdkeychain/v3"
    20  	"golang.org/x/term"
    21  )
    22  
    23  // ProvideSeed is used to prompt for the wallet seed which maybe required during
    24  // upgrades.
    25  func ProvideSeed() ([]byte, error) {
    26  	reader := bufio.NewReader(os.Stdin)
    27  	for {
    28  		fmt.Print("Enter existing wallet seed: ")
    29  		seedStr, err := reader.ReadString('\n')
    30  		if err != nil {
    31  			return nil, err
    32  		}
    33  		seedStr = strings.TrimSpace(strings.ToLower(seedStr))
    34  
    35  		seed, err := hex.DecodeString(seedStr)
    36  		if err != nil || len(seed) < hdkeychain.MinSeedBytes ||
    37  			len(seed) > hdkeychain.MaxSeedBytes {
    38  
    39  			fmt.Printf("Invalid seed specified.  Must be a "+
    40  				"hexadecimal value that is at least %d bits and "+
    41  				"at most %d bits\n", hdkeychain.MinSeedBytes*8,
    42  				hdkeychain.MaxSeedBytes*8)
    43  			continue
    44  		}
    45  
    46  		return seed, nil
    47  	}
    48  }
    49  
    50  // ProvidePrivPassphrase is used to prompt for the private passphrase which
    51  // maybe required during upgrades.
    52  func ProvidePrivPassphrase() ([]byte, error) {
    53  	prompt := "Enter the private passphrase of your wallet: "
    54  	for {
    55  		fmt.Print(prompt)
    56  		pass, err := term.ReadPassword(int(os.Stdin.Fd()))
    57  		if err != nil {
    58  			return nil, err
    59  		}
    60  		fmt.Print("\n")
    61  		pass = bytes.TrimSpace(pass)
    62  		if len(pass) == 0 {
    63  			continue
    64  		}
    65  
    66  		return pass, nil
    67  	}
    68  }
    69  
    70  // promptList prompts the user with the given prefix, list of valid responses,
    71  // and default list entry to use.  The function will repeat the prompt to the
    72  // user until they enter a valid response.
    73  func promptList(reader *bufio.Reader, prefix string, validResponses []string, defaultEntry string) (string, error) {
    74  	// Setup the prompt according to the parameters.
    75  	validStrings := strings.Join(validResponses, "/")
    76  	var prompt string
    77  	if defaultEntry != "" {
    78  		prompt = fmt.Sprintf("%s (%s) [%s]: ", prefix, validStrings,
    79  			defaultEntry)
    80  	} else {
    81  		prompt = fmt.Sprintf("%s (%s): ", prefix, validStrings)
    82  	}
    83  
    84  	// Prompt the user until one of the valid responses is given.
    85  	for {
    86  		fmt.Print(prompt)
    87  		reply, err := reader.ReadString('\n')
    88  		if err != nil {
    89  			return "", err
    90  		}
    91  		reply = strings.TrimSpace(strings.ToLower(reply))
    92  		if reply == "" {
    93  			reply = defaultEntry
    94  		}
    95  
    96  		for _, validResponse := range validResponses {
    97  			if reply == validResponse {
    98  				return reply, nil
    99  			}
   100  		}
   101  	}
   102  }
   103  
   104  // promptListBool prompts the user for a boolean (yes/no) with the given prefix.
   105  // The function will repeat the prompt to the user until they enter a valid
   106  // response.
   107  func promptListBool(reader *bufio.Reader, prefix string, defaultEntry string) (bool, error) {
   108  	// Setup the valid responses.
   109  	valid := []string{"n", "no", "y", "yes"}
   110  	response, err := promptList(reader, prefix, valid, defaultEntry)
   111  	if err != nil {
   112  		return false, err
   113  	}
   114  	return response == "yes" || response == "y", nil
   115  }
   116  
   117  // PassPrompt prompts the user for a passphrase with the given prefix.  The
   118  // function will ask the user to confirm the passphrase and will repeat the
   119  // prompts until they enter a matching response.
   120  func PassPrompt(reader *bufio.Reader, prefix string, confirm bool) ([]byte, error) {
   121  	// Prompt the user until they enter a passphrase.
   122  	prompt := fmt.Sprintf("%s: ", prefix)
   123  	for {
   124  		fmt.Print(prompt)
   125  		var pass []byte
   126  		var err error
   127  		fd := int(os.Stdin.Fd())
   128  		if term.IsTerminal(fd) {
   129  			pass, err = term.ReadPassword(fd)
   130  		} else {
   131  			pass, err = reader.ReadBytes('\n')
   132  			if errors.Is(err, io.EOF) {
   133  				err = nil
   134  			}
   135  		}
   136  		if err != nil {
   137  			return nil, err
   138  		}
   139  		fmt.Print("\n")
   140  		pass = bytes.TrimSpace(pass)
   141  		if len(pass) == 0 {
   142  			continue
   143  		}
   144  
   145  		if !confirm {
   146  			return pass, nil
   147  		}
   148  
   149  		fmt.Print("Confirm passphrase: ")
   150  		var confirm []byte
   151  		if term.IsTerminal(fd) {
   152  			confirm, err = term.ReadPassword(fd)
   153  		} else {
   154  			confirm, err = reader.ReadBytes('\n')
   155  			if errors.Is(err, io.EOF) {
   156  				err = nil
   157  			}
   158  		}
   159  		if err != nil {
   160  			return nil, err
   161  		}
   162  		fmt.Print("\n")
   163  		confirm = bytes.TrimSpace(confirm)
   164  		if !bytes.Equal(pass, confirm) {
   165  			fmt.Println("The entered passphrases do not match")
   166  			continue
   167  		}
   168  
   169  		return pass, nil
   170  	}
   171  }
   172  
   173  // PrivatePass prompts the user for a private passphrase.  All prompts are
   174  // repeated until the user enters a valid response.
   175  func PrivatePass(reader *bufio.Reader, configPass []byte) ([]byte, error) {
   176  	if len(configPass) > 0 {
   177  		useExisting, err := promptListBool(reader, "Use the "+
   178  			"existing configured private passphrase for "+
   179  			"wallet encryption?", "no")
   180  		if err != nil {
   181  			return nil, err
   182  		}
   183  		if useExisting {
   184  			return configPass, nil
   185  		}
   186  	}
   187  	return PassPrompt(reader, "Enter the private passphrase for your new wallet", true)
   188  }
   189  
   190  // PublicPass prompts the user whether they want to add an additional layer of
   191  // encryption to the wallet.  When the user answers yes and there is already a
   192  // public passphrase provided via the passed config, it prompts them whether or
   193  // not to use that configured passphrase.  It will also detect when the same
   194  // passphrase is used for the private and public passphrase and prompt the user
   195  // if they are sure they want to use the same passphrase for both.  Finally, all
   196  // prompts are repeated until the user enters a valid response.
   197  func PublicPass(reader *bufio.Reader, privPass []byte,
   198  	defaultPubPassphrase, configPubPass []byte) ([]byte, error) {
   199  
   200  	pubPass := defaultPubPassphrase
   201  	usePubPass, err := promptListBool(reader, "Do you want "+
   202  		"to add an additional layer of encryption for public "+
   203  		"data?", "no")
   204  	if err != nil {
   205  		return nil, err
   206  	}
   207  
   208  	if !usePubPass {
   209  		return pubPass, nil
   210  	}
   211  
   212  	if len(configPubPass) != 0 && !bytes.Equal(configPubPass, pubPass) {
   213  		useExisting, err := promptListBool(reader, "Use the "+
   214  			"existing configured public passphrase for encryption "+
   215  			"of public data?", "no")
   216  		if err != nil {
   217  			return nil, err
   218  		}
   219  
   220  		if useExisting {
   221  			return configPubPass, nil
   222  		}
   223  	}
   224  
   225  	for {
   226  		pubPass, err = PassPrompt(reader, "Enter the public "+
   227  			"passphrase for your new wallet", true)
   228  		if err != nil {
   229  			return nil, err
   230  		}
   231  
   232  		if bytes.Equal(pubPass, privPass) {
   233  			useSamePass, err := promptListBool(reader,
   234  				"Are you sure want to use the same passphrase "+
   235  					"for public and private data?", "no")
   236  			if err != nil {
   237  				return nil, err
   238  			}
   239  
   240  			if useSamePass {
   241  				break
   242  			}
   243  
   244  			continue
   245  		}
   246  
   247  		break
   248  	}
   249  
   250  	fmt.Println("NOTE: Use the --walletpass option to configure your " +
   251  		"public passphrase.")
   252  	return pubPass, nil
   253  }
   254  
   255  // Seed prompts the user whether they want to use an existing wallet generation
   256  // seed.  When the user answers no, a seed will be generated and displayed to
   257  // the user along with prompting them for confirmation.  When the user answers
   258  // yes, a the user is prompted for it.  All prompts are repeated until the user
   259  // enters a valid response. The bool returned indicates if the wallet was
   260  // restored from a given seed or not.
   261  func Seed(reader *bufio.Reader) (seed []byte, imported bool, err error) {
   262  	// Ascertain the wallet generation seed.
   263  	useUserSeed, err := promptListBool(reader, "Do you have an "+
   264  		"existing wallet seed you want to use?", "no")
   265  	if err != nil {
   266  		return nil, false, err
   267  	}
   268  	if !useUserSeed {
   269  		seed, err := hdkeychain.GenerateSeed(hdkeychain.RecommendedSeedLen)
   270  		if err != nil {
   271  			return nil, false, err
   272  		}
   273  
   274  		seedStrSplit := walletseed.EncodeMnemonicSlice(seed)
   275  
   276  		fmt.Println("Your wallet generation seed is:")
   277  		for i := 0; i < hdkeychain.RecommendedSeedLen+1; i++ {
   278  			fmt.Printf("%v ", seedStrSplit[i])
   279  
   280  			if (i+1)%6 == 0 {
   281  				fmt.Printf("\n")
   282  			}
   283  		}
   284  
   285  		fmt.Printf("\n\nHex: %x\n", seed)
   286  		fmt.Println("IMPORTANT: Keep the seed in a safe place as you\n" +
   287  			"will NOT be able to restore your wallet without it.")
   288  		fmt.Println("Please keep in mind that anyone who has access\n" +
   289  			"to the seed can also restore your wallet thereby\n" +
   290  			"giving them access to all your funds, so it is\n" +
   291  			"imperative that you keep it in a secure location.")
   292  
   293  		for {
   294  			fmt.Print(`Once you have stored the seed in a safe ` +
   295  				`and secure location, enter "OK" to continue: `)
   296  			confirmSeed, err := reader.ReadString('\n')
   297  			if err != nil {
   298  				return nil, false, err
   299  			}
   300  			confirmSeed = strings.TrimSpace(confirmSeed)
   301  			confirmSeed = strings.Trim(confirmSeed, `"`)
   302  			if strings.EqualFold("OK", confirmSeed) {
   303  				break
   304  			}
   305  		}
   306  
   307  		return seed, false, nil
   308  	}
   309  
   310  	for {
   311  		fmt.Print("Enter existing wallet seed " +
   312  			"(follow seed words with additional blank line): ")
   313  
   314  		// Use scanner instead of buffio.Reader so we can choose choose
   315  		// more complicated ending condition rather than just a single
   316  		// newline.
   317  		var seedStr string
   318  		scanner := bufio.NewScanner(reader)
   319  		for firstline := true; scanner.Scan(); {
   320  			line := scanner.Text()
   321  			if line == "" {
   322  				break
   323  			}
   324  			if firstline {
   325  				_, err := hex.DecodeString(line)
   326  				if err == nil {
   327  					seedStr = line
   328  					break
   329  				}
   330  				firstline = false
   331  			}
   332  			seedStr += " " + line
   333  		}
   334  		seedStrTrimmed := strings.TrimSpace(seedStr)
   335  		seedStrTrimmed = collapseSpace(seedStrTrimmed)
   336  		wordCount := strings.Count(seedStrTrimmed, " ") + 1
   337  
   338  		var seed []byte
   339  		if wordCount == 1 {
   340  			if len(seedStrTrimmed)%2 != 0 {
   341  				seedStrTrimmed = "0" + seedStrTrimmed
   342  			}
   343  			seed, err = hex.DecodeString(seedStrTrimmed)
   344  			if err != nil {
   345  				fmt.Printf("Input error: %v\n", err.Error())
   346  			}
   347  		} else {
   348  			seed, err = walletseed.DecodeUserInput(seedStrTrimmed)
   349  			if err != nil {
   350  				fmt.Printf("Input error: %v\n", err.Error())
   351  			}
   352  		}
   353  		if err != nil || len(seed) < hdkeychain.MinSeedBytes ||
   354  			len(seed) > hdkeychain.MaxSeedBytes {
   355  			fmt.Printf("Invalid seed specified.  Must be a "+
   356  				"word seed (usually 33 words) using the PGP wordlist or "+
   357  				"hexadecimal value that is at least %d bits and "+
   358  				"at most %d bits\n", hdkeychain.MinSeedBytes*8,
   359  				hdkeychain.MaxSeedBytes*8)
   360  			continue
   361  		}
   362  
   363  		fmt.Printf("\nSeed input successful. \nHex: %x\n", seed)
   364  
   365  		return seed, true, nil
   366  	}
   367  }
   368  
   369  // collapseSpace takes a string and replaces any repeated areas of whitespace
   370  // with a single space character.
   371  func collapseSpace(in string) string {
   372  	whiteSpace := false
   373  	out := ""
   374  	for _, c := range in {
   375  		if unicode.IsSpace(c) {
   376  			if !whiteSpace {
   377  				out = out + " "
   378  			}
   379  			whiteSpace = true
   380  		} else {
   381  			out = out + string(c)
   382  			whiteSpace = false
   383  		}
   384  	}
   385  	return out
   386  }