b5fd915162
The wallet loader has a dependency to the internal/prompt package for prompting the user for certain inputs (e.g. wallet password or new seed). This makes it impossible for projects that use the wallet as a dependency and always provide those inputs as parameters to compile for JavaScript/WebAssembly targets because the prompt code uses some terminal functionality that is not available in JS syscalls. By providing a JS specific implementation that just returns an error we can compile the dependent projects. Adding acutal support for prompting the user in the browser is currently not planned as that can easily be circumvented by providing all inputs as parameters.
327 lines
9.2 KiB
Go
327 lines
9.2 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.
|
|
|
|
// +build !js
|
|
|
|
package prompt
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/btcsuite/btcutil/hdkeychain"
|
|
"github.com/btcsuite/btcwallet/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() ([]byte, error) {
|
|
reader := bufio.NewReader(os.Stdin)
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
for {
|
|
pubPass, err = promptPass(reader, "Enter the public "+
|
|
"passphrase for your new wallet", true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if bytes.Equal(pubPass, privPass) {
|
|
useSamePass, err := promptListBool(reader,
|
|
"Are you sure want to use the same passphrase "+
|
|
"for public and private data?", "no")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if useSamePass {
|
|
break
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
break
|
|
}
|
|
|
|
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, error) {
|
|
// 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, err
|
|
}
|
|
if !useUserSeed {
|
|
seed, err := hdkeychain.GenerateSeed(hdkeychain.RecommendedSeedLen)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fmt.Println("Your wallet generation seed is:")
|
|
fmt.Printf("%x\n", seed)
|
|
fmt.Println("IMPORTANT: Keep the seed in a safe place as you\n" +
|
|
"will NOT be able to restore your wallet without it.")
|
|
fmt.Println("Please keep in mind that anyone who has access\n" +
|
|
"to the seed can also restore your wallet thereby\n" +
|
|
"giving them access to all your funds, so it is\n" +
|
|
"imperative that you keep it in a secure location.")
|
|
|
|
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, err
|
|
}
|
|
confirmSeed = strings.TrimSpace(confirmSeed)
|
|
confirmSeed = strings.Trim(confirmSeed, `"`)
|
|
if confirmSeed == "OK" {
|
|
break
|
|
}
|
|
}
|
|
|
|
return seed, nil
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|