2015-12-01 19:44:58 +01:00
|
|
|
// 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.
|
|
|
|
|
Modernize the RPC server.
This is a rather monolithic commit that moves the old RPC server to
its own package (rpc/legacyrpc), introduces a new RPC server using
gRPC (rpc/rpcserver), and provides the ability to defer wallet loading
until request at a later time by an RPC (--noinitialload).
The legacy RPC server remains the default for now while the new gRPC
server is not enabled by default. Enabling the new server requires
setting a listen address (--experimenalrpclisten). This experimental
flag is used to effectively feature gate the server until it is ready
to use as a default. Both RPC servers can be run at the same time,
but require binding to different listen addresses.
In theory, with the legacy RPC server now living in its own package it
should become much easier to unit test the handlers. This will be
useful for any future changes to the package, as compatibility with
Core's wallet is still desired.
Type safety has also been improved in the legacy RPC server. Multiple
handler types are now used for methods that do and do not require the
RPC client as a dependency. This can statically help prevent nil
pointer dereferences, and was very useful for catching bugs during
refactoring.
To synchronize the wallet loading process between the main package
(the default) and through the gRPC WalletLoader service (with the
--noinitialload option), as well as increasing the loose coupling of
packages, a new wallet.Loader type has been added. All creating and
loading of existing wallets is done through a single Loader instance,
and callbacks can be attached to the instance to run after the wallet
has been opened. This is how the legacy RPC server is associated with
a loaded wallet, even after the wallet is loaded by a gRPC method in a
completely unrelated package.
Documentation for the new RPC server has been added to the
rpc/documentation directory. The documentation includes a
specification for the new RPC API, addresses how to make changes to
the server implementation, and provides short example clients in
several different languages.
Some of the new RPC methods are not implementated exactly as described
by the specification. These are considered bugs with the
implementation, not the spec. Known bugs are commented as such.
2015-06-01 21:57:50 +02:00
|
|
|
package prompt
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"bytes"
|
|
|
|
"encoding/hex"
|
|
|
|
"fmt"
|
|
|
|
"os"
|
|
|
|
"strings"
|
|
|
|
|
2017-04-28 01:31:27 +02:00
|
|
|
"github.com/btcsuite/btcutil/hdkeychain"
|
|
|
|
"github.com/btcsuite/btcwallet/internal/legacy/keystore"
|
2016-02-11 17:20:03 +01:00
|
|
|
"github.com/btcsuite/golangcrypto/ssh/terminal"
|
Modernize the RPC server.
This is a rather monolithic commit that moves the old RPC server to
its own package (rpc/legacyrpc), introduces a new RPC server using
gRPC (rpc/rpcserver), and provides the ability to defer wallet loading
until request at a later time by an RPC (--noinitialload).
The legacy RPC server remains the default for now while the new gRPC
server is not enabled by default. Enabling the new server requires
setting a listen address (--experimenalrpclisten). This experimental
flag is used to effectively feature gate the server until it is ready
to use as a default. Both RPC servers can be run at the same time,
but require binding to different listen addresses.
In theory, with the legacy RPC server now living in its own package it
should become much easier to unit test the handlers. This will be
useful for any future changes to the package, as compatibility with
Core's wallet is still desired.
Type safety has also been improved in the legacy RPC server. Multiple
handler types are now used for methods that do and do not require the
RPC client as a dependency. This can statically help prevent nil
pointer dereferences, and was very useful for catching bugs during
refactoring.
To synchronize the wallet loading process between the main package
(the default) and through the gRPC WalletLoader service (with the
--noinitialload option), as well as increasing the loose coupling of
packages, a new wallet.Loader type has been added. All creating and
loading of existing wallets is done through a single Loader instance,
and callbacks can be attached to the instance to run after the wallet
has been opened. This is how the legacy RPC server is associated with
a loaded wallet, even after the wallet is loaded by a gRPC method in a
completely unrelated package.
Documentation for the new RPC server has been added to the
rpc/documentation directory. The documentation includes a
specification for the new RPC API, addresses how to make changes to
the server implementation, and provides short example clients in
several different languages.
Some of the new RPC methods are not implementated exactly as described
by the specification. These are considered bugs with the
implementation, not the spec. Known bugs are commented as such.
2015-06-01 21:57:50 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
// 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
|
|
|
|
// reponse.
|
|
|
|
func promptListBool(reader *bufio.Reader, prefix string, defaultEntry string) (bool, error) {
|
|
|
|
// 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(reader *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([]byte(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
|
|
|
|
}
|
|
|
|
}
|