prompt.go raw

   1  package prompt
   2  
   3  import (
   4  	"bufio"
   5  	"bytes"
   6  	"encoding/hex"
   7  	"fmt"
   8  	"os"
   9  	"strings"
  10  	
  11  	"github.com/btcsuite/golangcrypto/ssh/terminal"
  12  	
  13  	"github.com/p9c/p9/pkg/util/hdkeychain"
  14  	"github.com/p9c/p9/pkg/util/legacy/keystore"
  15  )
  16  
  17  // ProvideSeed is used to prompt for the wallet seed which maybe required during upgrades.
  18  func ProvideSeed() ([]byte, error) {
  19  	reader := bufio.NewReader(os.Stdin)
  20  	for {
  21  		fmt.Print("Enter existing wallet seed: ")
  22  		seedStr, e := reader.ReadString('\n')
  23  		if e != nil {
  24  			return nil, e
  25  		}
  26  		seedStr = strings.TrimSpace(strings.ToLower(seedStr))
  27  		seed, e := hex.DecodeString(seedStr)
  28  		if e != nil || len(seed) < hdkeychain.MinSeedBytes ||
  29  			len(seed) > hdkeychain.MaxSeedBytes {
  30  			E.F(
  31  				"Invalid seed specified.  "+
  32  					"Must be a hexadecimal value that is at least %d bits and at"+
  33  					" most %d bits", hdkeychain.MinSeedBytes*8,
  34  				hdkeychain.MaxSeedBytes*8,
  35  			)
  36  			continue
  37  		}
  38  		return seed, nil
  39  	}
  40  }
  41  
  42  // ProvidePrivPassphrase is used to prompt for the private passphrase which maybe required during upgrades.
  43  func ProvidePrivPassphrase() ([]byte, error) {
  44  	prompt := "enter the private passphrase for your wallet: "
  45  	for {
  46  		fmt.Print(prompt)
  47  		pass, e := terminal.ReadPassword(int(os.Stdin.Fd()))
  48  		if e != nil {
  49  			return nil, e
  50  		}
  51  		fmt.Print("\n")
  52  		pass = bytes.TrimSpace(pass)
  53  		if len(pass) == 0 {
  54  			continue
  55  		}
  56  		return pass, nil
  57  	}
  58  }
  59  
  60  // promptList prompts the user with the given prefix, list of valid responses, and default list entry to use. The
  61  // function will repeat the prompt to the user until they enter a valid response.
  62  func promptList(reader *bufio.Reader, prefix string, validResponses []string, defaultEntry string) (string, error) {
  63  	// Setup the prompt according to the parameters.
  64  	validStrings := strings.Join(validResponses, "/")
  65  	var prompt string
  66  	if defaultEntry != "" {
  67  		prompt = fmt.Sprintf(
  68  			"%s (%s) [%s]: ", prefix, validStrings,
  69  			defaultEntry,
  70  		)
  71  	} else {
  72  		prompt = fmt.Sprintf("%s (%s): ", prefix, validStrings)
  73  	}
  74  	// Prompt the user until one of the valid responses is given.
  75  	for {
  76  		fmt.Print(prompt)
  77  		reply, e := reader.ReadString('\n')
  78  		if e != nil {
  79  			return "", e
  80  		}
  81  		reply = strings.TrimSpace(strings.ToLower(reply))
  82  		if reply == "" {
  83  			reply = defaultEntry
  84  		}
  85  		for _, validResponse := range validResponses {
  86  			if reply == validResponse {
  87  				return reply, nil
  88  			}
  89  		}
  90  	}
  91  }
  92  
  93  // promptListBool prompts the user for a boolean (yes/no) with the given prefix. The function will repeat the prompt to
  94  // the user until they enter a valid reponse.
  95  func promptListBool(reader *bufio.Reader, prefix string, defaultEntry string) (bool, error) {
  96  	// Setup the valid responses.
  97  	valid := []string{"n", "no", "y", "yes"}
  98  	response, e := promptList(reader, prefix, valid, defaultEntry)
  99  	if e != nil {
 100  		return false, e
 101  	}
 102  	return response == "yes" || response == "y", nil
 103  }
 104  
 105  // promptPass prompts the user for a passphrase with the given prefix. The function will ask the user to confirm the
 106  // passphrase and will repeat the prompts until they enter a matching response.
 107  func promptPass(reader *bufio.Reader, prefix string, confirm bool) ([]byte, error) {
 108  	// Prompt the user until they enter a passphrase.
 109  	prompt := fmt.Sprintf("%s: ", prefix)
 110  	for {
 111  		fmt.Print(prompt)
 112  		pass, e := terminal.ReadPassword(int(os.Stdin.Fd()))
 113  		if e != nil {
 114  			return nil, e
 115  		}
 116  		fmt.Print("\n")
 117  		pass = bytes.TrimSpace(pass)
 118  		if len(pass) == 0 {
 119  			continue
 120  		}
 121  		if !confirm {
 122  			return pass, nil
 123  		}
 124  		fmt.Print("Confirm passphrase: ")
 125  		confirm, e := terminal.ReadPassword(int(os.Stdin.Fd()))
 126  		if e != nil {
 127  			return nil, e
 128  		}
 129  		fmt.Print("\n")
 130  		confirm = bytes.TrimSpace(confirm)
 131  		if !bytes.Equal(pass, confirm) {
 132  			E.Ln("The entered passphrases do not match")
 133  			continue
 134  		}
 135  		return pass, nil
 136  	}
 137  }
 138  
 139  // PrivatePass prompts the user for a private passphrase with varying behavior depending on whether the passed legacy
 140  // keystore exists. When it does, the user is prompted for the existing passphrase which is then used to unlock it.
 141  //
 142  // On the other hand, when the legacy keystore is nil, the user is prompted for a new private passphrase. All prompts
 143  // are repeated until the user enters a valid response.
 144  func PrivatePass(reader *bufio.Reader, legacyKeyStore *keystore.Store) ([]byte, error) {
 145  	// When there is not an existing legacy wallet, simply prompt the user for a new private passphase and return it.
 146  	if legacyKeyStore == nil {
 147  		return promptPass(
 148  			reader,
 149  			"Creating new wallet\n\nEnter the private passphrase for your new wallet", true,
 150  		)
 151  	}
 152  	// At this point, there is an existing legacy wallet, so prompt the user for the existing private passphrase and
 153  	// ensure it properly unlocks the legacy wallet so all of the addresses can later be imported.
 154  	W.Ln(
 155  		"You have an existing legacy wallet.  " +
 156  			"All addresses from your existing legacy wallet will be imported into" +
 157  			" the new wallet format.",
 158  	)
 159  	for {
 160  		privPass, e := promptPass(reader, "Enter the private passphrase for your existing wallet", false)
 161  		if e != nil {
 162  			return nil, e
 163  		}
 164  		// Keep prompting the user until the passphrase is correct.
 165  		if e := legacyKeyStore.Unlock(privPass); E.Chk(e) {
 166  			if e == keystore.ErrWrongPassphrase {
 167  				continue
 168  			}
 169  			return nil, e
 170  		}
 171  		return privPass, nil
 172  	}
 173  }
 174  
 175  // PublicPass prompts the user whether they want to add an additional layer of encryption to the wallet. When the user
 176  // answers yes and there is already a public passphrase provided via the passed config, it prompts them whether or not
 177  // to use that configured passphrase.
 178  //
 179  // It will also detect when the same passphrase is used for the private and public passphrase and prompt the user if
 180  // they are sure they want to use the same passphrase for both. Finally, all prompts are repeated until the user enters
 181  // a valid response.
 182  func PublicPass(
 183  	reader *bufio.Reader, privPass []byte,
 184  	defaultPubPassphrase, configPubPassphrase []byte,
 185  ) ([]byte, error) {
 186  	pubPass := defaultPubPassphrase
 187  	usePubPass, e := promptListBool(
 188  		reader, "Do you want "+
 189  			"to add an additional layer of encryption for public "+
 190  			"data?", "no",
 191  	)
 192  	if e != nil {
 193  		return nil, e
 194  	}
 195  	if !usePubPass {
 196  		return pubPass, nil
 197  	}
 198  	if !bytes.Equal(configPubPassphrase, pubPass) {
 199  		var useExisting bool
 200  		useExisting, e = promptListBool(
 201  			reader, "Use the "+
 202  				"existing configured public passphrase for encryption "+
 203  				"of public data?", "no",
 204  		)
 205  		if e != nil {
 206  			return nil, e
 207  		}
 208  		if useExisting {
 209  			return configPubPassphrase, nil
 210  		}
 211  	}
 212  	for {
 213  		pubPass, e = promptPass(
 214  			reader, "Enter the public "+
 215  				"passphrase for your new wallet", true,
 216  		)
 217  		if e != nil {
 218  			return nil, e
 219  		}
 220  		if bytes.Equal(pubPass, privPass) {
 221  			useSamePass, e := promptListBool(
 222  				reader,
 223  				"Are you sure want to use the same passphrase "+
 224  					"for public and private data?", "no",
 225  			)
 226  			if e != nil {
 227  				return nil, e
 228  			}
 229  			if useSamePass {
 230  				break
 231  			}
 232  			continue
 233  		}
 234  		break
 235  	}
 236  	fmt.Println(
 237  		"NOTE: Use the --walletpass opt to configure your " +
 238  			"public passphrase.",
 239  	)
 240  	return pubPass, nil
 241  }
 242  
 243  // Seed prompts the user whether they want to use an existing wallet generation seed.
 244  //
 245  // When the user answers no, a seed will be generated and displayed to the user along with prompting them for
 246  // confirmation.
 247  //
 248  // When the user answers yes, a the user is prompted for it.
 249  //
 250  // All prompts are repeated until the user enters a valid response.
 251  func Seed(reader *bufio.Reader) ([]byte, error) {
 252  	// Ascertain the wallet generation seed.
 253  	useUserSeed, e := promptListBool(
 254  		reader, "Do you have an "+
 255  			"existing wallet seed you want to use?", "no",
 256  	)
 257  	if e != nil {
 258  		return nil, e
 259  	}
 260  	if !useUserSeed {
 261  		seed, e := hdkeychain.GenerateSeed(hdkeychain.RecommendedSeedLen)
 262  		if e != nil {
 263  			return nil, e
 264  		}
 265  		fmt.Println("\nYour wallet generation seed is:")
 266  		fmt.Printf("\n%x\n\n", seed)
 267  		fmt.Print(
 268  			"IMPORTANT: Keep the seed in a safe place as you will NOT be" +
 269  				" able to restore your wallet without it.\n\n",
 270  		)
 271  		fmt.Print(
 272  			"Please keep in mind that anyone who has access to the seed" +
 273  				" can also restore your wallet thereby giving them access to all your funds, so it is imperative that you keep it in a secure location.\n\n",
 274  		)
 275  		for {
 276  			fmt.Print(
 277  				`Once you have stored the seed in a safe ` +
 278  					`and secure location, enter "OK" to continue: `,
 279  			)
 280  			confirmSeed, e := reader.ReadString('\n')
 281  			if e != nil {
 282  				return nil, e
 283  			}
 284  			confirmSeed = strings.TrimSpace(confirmSeed)
 285  			confirmSeed = strings.Trim(confirmSeed, `"`)
 286  			if confirmSeed == "OK" {
 287  				break
 288  			}
 289  		}
 290  		return seed, nil
 291  	}
 292  	for {
 293  		fmt.Print("Enter existing wallet seed: ")
 294  		seedStr, e := reader.ReadString('\n')
 295  		if e != nil {
 296  			return nil, e
 297  		}
 298  		seedStr = strings.TrimSpace(strings.ToLower(seedStr))
 299  		seed, e := hex.DecodeString(seedStr)
 300  		if e != nil || len(seed) < hdkeychain.MinSeedBytes ||
 301  			len(seed) > hdkeychain.MaxSeedBytes {
 302  			E.F(
 303  				"Invalid seed specified.  Must be a "+
 304  					"hexadecimal value that is at least %d bits and "+
 305  					"at most %d bits\n", hdkeychain.MinSeedBytes*8,
 306  				hdkeychain.MaxSeedBytes*8,
 307  			)
 308  			continue
 309  		}
 310  		return seed, nil
 311  	}
 312  }
 313