lbcwallet/internal/prompt/prompt.go

332 lines
9.3 KiB
Go

// Copyright (c) 2015-2016 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
//go:build !js
// +build !js
package prompt
import (
"bufio"
"bytes"
"encoding/hex"
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/lbryio/lbcutil/hdkeychain"
"github.com/lbryio/lbcwallet/internal/legacy/keystore"
"golang.org/x/crypto/ssh/terminal"
)
// ProvideSeed is used to prompt for the wallet seed which maybe required during
// upgrades.
func ProvideSeed() func() ([]byte, error) {
return func() ([]byte, error) {
return provideSeed(bufio.NewReader(os.Stdin))
}
}
func provideSeed(reader *bufio.Reader) ([]byte, error) {
for {
fmt.Print("Enter existing wallet seed: ")
seedStr, err := reader.ReadString('\n')
if err != nil {
return nil, err
}
seedStr = strings.TrimSpace(strings.ToLower(seedStr))
seed, err := hex.DecodeString(seedStr)
if err != nil || len(seed) < hdkeychain.MinSeedBytes ||
len(seed) > hdkeychain.MaxSeedBytes {
fmt.Printf("Invalid seed specified. Must be a "+
"hexadecimal value that is at least %d bits and "+
"at most %d bits\n", hdkeychain.MinSeedBytes*8,
hdkeychain.MaxSeedBytes*8)
continue
}
return seed, nil
}
}
// ProvidePrivPassphrase is used to prompt for the private passphrase which
// maybe required during upgrades.
func ProvidePrivPassphrase() ([]byte, error) {
prompt := "Enter the private passphrase of your wallet: "
for {
fmt.Print(prompt)
pass, err := terminal.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
return nil, err
}
fmt.Print("\n")
pass = bytes.TrimSpace(pass)
if len(pass) == 0 {
continue
}
return pass, nil
}
}
// promptList prompts the user with the given prefix, list of valid responses,
// and default list entry to use. The function will repeat the prompt to the
// user until they enter a valid response.
func promptList(reader *bufio.Reader, prefix string, validResponses []string, defaultEntry string) (string, error) {
// Setup the prompt according to the parameters.
validStrings := strings.Join(validResponses, "/")
var prompt string
if defaultEntry != "" {
prompt = fmt.Sprintf("%s (%s) [%s]: ", prefix, validStrings,
defaultEntry)
} else {
prompt = fmt.Sprintf("%s (%s): ", prefix, validStrings)
}
// Prompt the user until one of the valid responses is given.
for {
fmt.Print(prompt)
reply, err := reader.ReadString('\n')
if err != nil {
return "", err
}
reply = strings.TrimSpace(strings.ToLower(reply))
if reply == "" {
reply = defaultEntry
}
for _, validResponse := range validResponses {
if reply == validResponse {
return reply, nil
}
}
}
}
// promptListBool prompts the user for a boolean (yes/no) with the given prefix.
// The function will repeat the prompt to the user until they enter a valid
// response.
func promptListBool(reader *bufio.Reader, prefix string,
defaultEntry string) (bool, error) { // nolint:unparam
// Setup the valid responses.
valid := []string{"n", "no", "y", "yes"}
response, err := promptList(reader, prefix, valid, defaultEntry)
if err != nil {
return false, err
}
return response == "yes" || response == "y", nil
}
// promptBirhdayUnixTimeStamp prompts the user a Unix timestamp in second.
func promptUnixTimestamp(reader *bufio.Reader, prefix string,
defaultEntry string) (time.Time, error) { // nolint:unparam
prompt := fmt.Sprintf("%s [%s]: ", prefix, defaultEntry)
for {
fmt.Print(prompt)
reply, err := reader.ReadString('\n')
if err != nil {
return time.Time{}, err
}
reply = strings.TrimSpace(strings.ToLower(reply))
if reply == "" {
reply = defaultEntry
}
ts, err := strconv.ParseInt(reply, 10, 64)
if err != nil {
fmt.Print(prompt)
continue
}
return time.Unix(ts, 0), nil
}
}
// promptPass prompts the user for a passphrase with the given prefix. The
// function will ask the user to confirm the passphrase and will repeat the
// prompts until they enter a matching response.
func promptPass(_ *bufio.Reader, prefix string, confirm bool) ([]byte, error) {
// Prompt the user until they enter a passphrase.
prompt := fmt.Sprintf("%s: ", prefix)
for {
fmt.Print(prompt)
pass, err := terminal.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
return nil, err
}
fmt.Print("\n")
pass = bytes.TrimSpace(pass)
if len(pass) == 0 {
continue
}
if !confirm {
return pass, nil
}
fmt.Print("Confirm passphrase: ")
confirm, err := terminal.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
return nil, err
}
fmt.Print("\n")
confirm = bytes.TrimSpace(confirm)
if !bytes.Equal(pass, confirm) {
fmt.Println("The entered passphrases do not match")
continue
}
return pass, nil
}
}
func birthday(reader *bufio.Reader) (time.Time, error) {
prompt := "Enter the birthday of the seed in Unix timestamp " +
"(the walllet will scan the chain from this time)"
return promptUnixTimestamp(reader, prompt, "0")
}
// PrivatePass prompts the user for a private passphrase with varying behavior
// depending on whether the passed legacy keystore exists. When it does, the
// user is prompted for the existing passphrase which is then used to unlock it.
// On the other hand, when the legacy keystore is nil, the user is prompted for
// a new private passphrase. All prompts are repeated until the user enters a
// valid response.
func PrivatePass(reader *bufio.Reader, legacyKeyStore *keystore.Store) ([]byte, error) {
// When there is not an existing legacy wallet, simply prompt the user
// for a new private passphase and return it.
if legacyKeyStore == nil {
return promptPass(reader, "Enter the private "+
"passphrase for your new wallet", true)
}
// At this point, there is an existing legacy wallet, so prompt the user
// for the existing private passphrase and ensure it properly unlocks
// the legacy wallet so all of the addresses can later be imported.
fmt.Println("You have an existing legacy wallet. All addresses from " +
"your existing legacy wallet will be imported into the new " +
"wallet format.")
for {
privPass, err := promptPass(reader, "Enter the private "+
"passphrase for your existing wallet", false)
if err != nil {
return nil, err
}
// Keep prompting the user until the passphrase is correct.
if err := legacyKeyStore.Unlock(privPass); err != nil {
if err == keystore.ErrWrongPassphrase {
fmt.Println(err)
continue
}
return nil, err
}
return privPass, nil
}
}
// PublicPass prompts the user whether they want to add an additional layer of
// encryption to the wallet. When the user answers yes and there is already a
// public passphrase provided via the passed config, it prompts them whether or
// not to use that configured passphrase. It will also detect when the same
// passphrase is used for the private and public passphrase and prompt the user
// if they are sure they want to use the same passphrase for both. Finally, all
// prompts are repeated until the user enters a valid response.
func PublicPass(reader *bufio.Reader, privPass []byte,
defaultPubPassphrase, configPubPassphrase []byte) ([]byte, error) {
pubPass := defaultPubPassphrase
usePubPass, err := promptListBool(reader, "Do you want "+
"to add an additional layer of encryption for public "+
"data?", "no")
if err != nil {
return nil, err
}
if !usePubPass {
return pubPass, nil
}
if !bytes.Equal(configPubPassphrase, pubPass) {
useExisting, err := promptListBool(reader, "Use the "+
"existing configured public passphrase for encryption "+
"of public data?", "no")
if err != nil {
return nil, err
}
if useExisting {
return configPubPassphrase, nil
}
}
pubPass, err = provideSeed(reader)
if err != nil {
return nil, err
}
fmt.Println("NOTE: Use the --walletpass option to configure your " +
"public passphrase.")
return pubPass, nil
}
// Seed prompts the user whether they want to use an existing wallet generation
// seed. When the user answers no, a seed will be generated and displayed to
// the user along with prompting them for confirmation. When the user answers
// yes, a the user is prompted for it. All prompts are repeated until the user
// enters a valid response.
func Seed(reader *bufio.Reader) ([]byte, time.Time, error) {
bday := time.Now()
// Ascertain the wallet generation seed.
useUserSeed, err := promptListBool(reader, "Do you have an "+
"existing wallet seed you want to use?", "no")
if err != nil {
return nil, bday, err
}
if !useUserSeed {
seed, err := hdkeychain.GenerateSeed(hdkeychain.RecommendedSeedLen)
if err != nil {
return nil, bday, err
}
fmt.Printf("Your wallet generation seed is: %x\n", seed)
fmt.Printf("\nIMPORTANT: Keep the seed in a safe place as you " +
"will NOT be able to restore your wallet without it.\n\n")
for {
fmt.Print(`Once you have stored the seed in a safe ` +
`and secure location, enter "OK" to continue: `)
confirmSeed, err := reader.ReadString('\n')
if err != nil {
return nil, bday, err
}
confirmSeed = strings.TrimSpace(confirmSeed)
confirmSeed = strings.Trim(confirmSeed, `"`)
if confirmSeed == "OK" {
break
}
}
return seed, bday, nil
}
seed, err := provideSeed(reader)
if err != nil {
return nil, bday, err
}
bday, err = birthday(reader)
if err != nil {
return nil, bday, err
}
return seed, bday, nil
}