lbcwallet/walletsetup.go

651 lines
19 KiB
Go
Raw Normal View History

/*
* Copyright (c) 2014 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package main
import (
"bufio"
2014-10-20 17:29:23 +02:00
"bytes"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/btcsuite/btcwallet/legacy/keystore"
"github.com/btcsuite/btcwallet/txstore"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/wallet"
"github.com/btcsuite/btcwallet/walletdb"
_ "github.com/btcsuite/btcwallet/walletdb/bdb"
"github.com/btcsuite/golangcrypto/ssh/terminal"
)
var (
// waddrmgrNamespaceKey is the namespace key for the waddrmgr package.
waddrmgrNamespaceKey = []byte("waddrmgr")
)
// networkDir returns the directory name of a network directory to hold wallet
// files.
func networkDir(dataDir string, chainParams *chaincfg.Params) string {
netname := chainParams.Name
// For now, we must always name the testnet data directory as "testnet"
// and not "testnet3" or any other version, as the chaincfg testnet3
// paramaters will likely be switched to being named "testnet3" in the
// future. This is done to future proof that change, and an upgrade
// plan to move the testnet3 data directory can be worked out later.
if chainParams.Net == wire.TestNet3 {
netname = "testnet"
}
return filepath.Join(dataDir, netname)
}
// promptSeed is used to prompt for the wallet seed which maybe required during
// upgrades.
func promptSeed() ([]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
}
}
// promptPrivPassPhrase is used to prompt for the private passphrase which maybe
// required during upgrades.
func promptPrivPassPhrase() ([]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
}
}
// promptConsoleList 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 promptConsoleList(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
}
}
}
}
// promptConsoleListBool 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 promptConsoleListBool(reader *bufio.Reader, prefix string, defaultEntry string) (bool, error) {
// Setup the valid responses.
valid := []string{"n", "no", "y", "yes"}
response, err := promptConsoleList(reader, prefix, valid, defaultEntry)
if err != nil {
return false, err
}
return response == "yes" || response == "y", nil
}
// promptConsolePass 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.
2014-10-20 17:29:23 +02:00
func promptConsolePass(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)
2014-10-20 17:29:23 +02:00
pass, err := terminal.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
2014-10-20 17:29:23 +02:00
return nil, err
}
2014-10-20 17:29:23 +02:00
fmt.Print("\n")
pass = bytes.TrimSpace(pass)
if len(pass) == 0 {
continue
}
if !confirm {
return pass, nil
}
fmt.Print("Confirm passphrase: ")
2014-10-20 17:29:23 +02:00
confirm, err := terminal.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
2014-10-20 17:29:23 +02:00
return nil, err
}
2014-10-20 17:29:23 +02:00
fmt.Print("\n")
confirm = bytes.TrimSpace(confirm)
if !bytes.Equal(pass, confirm) {
fmt.Println("The entered passphrases do not match")
continue
}
return pass, nil
}
}
// promptConsolePrivatePass 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.
2014-10-20 17:29:23 +02:00
func promptConsolePrivatePass(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 promptConsolePass(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 := promptConsolePass(reader, "Enter the private "+
"passphrase for your existing wallet", false)
if err != nil {
2014-10-20 17:29:23 +02:00
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
}
2014-10-20 17:29:23 +02:00
return nil, err
}
return privPass, nil
}
}
// promptConsolePublicPass 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.
2014-10-20 17:29:23 +02:00
func promptConsolePublicPass(reader *bufio.Reader, privPass []byte, cfg *config) ([]byte, error) {
pubPass := []byte(defaultPubPassphrase)
usePubPass, err := promptConsoleListBool(reader, "Do you want "+
"to add an additional layer of encryption for public "+
"data?", "no")
if err != nil {
2014-10-20 17:29:23 +02:00
return nil, err
}
if !usePubPass {
return pubPass, nil
}
2014-10-20 17:29:23 +02:00
walletPass := []byte(cfg.WalletPass)
if !bytes.Equal(walletPass, pubPass) {
useExisting, err := promptConsoleListBool(reader, "Use the "+
"existing configured public passphrase for encryption "+
"of public data?", "no")
if err != nil {
2014-10-20 17:29:23 +02:00
return nil, err
}
if useExisting {
2014-10-20 17:29:23 +02:00
return walletPass, nil
}
}
for {
pubPass, err = promptConsolePass(reader, "Enter the public "+
"passphrase for your new wallet", true)
if err != nil {
2014-10-20 17:29:23 +02:00
return nil, err
}
2014-10-20 17:29:23 +02:00
if bytes.Equal(pubPass, privPass) {
useSamePass, err := promptConsoleListBool(reader,
"Are you sure want to use the same passphrase "+
"for public and private data?", "no")
if err != nil {
2014-10-20 17:29:23 +02:00
return nil, err
}
if useSamePass {
break
}
continue
}
break
}
fmt.Println("NOTE: Use the --walletpass option to configure your " +
"public passphrase.")
return pubPass, nil
}
// promptConsoleSeed 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 promptConsoleSeed(reader *bufio.Reader) ([]byte, error) {
// Ascertain the wallet generation seed.
useUserSeed, err := promptConsoleListBool(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
}
}
// convertLegacyKeystore converts all of the addresses in the passed legacy
// key store to the new waddrmgr.Manager format. Both the legacy keystore and
// the new manager must be unlocked.
func convertLegacyKeystore(legacyKeyStore *keystore.Store, manager *waddrmgr.Manager) error {
netParams := legacyKeyStore.Net()
blockStamp := waddrmgr.BlockStamp{
Height: 0,
Hash: *netParams.GenesisHash,
}
for _, walletAddr := range legacyKeyStore.ActiveAddresses() {
switch addr := walletAddr.(type) {
case keystore.PubKeyAddress:
privKey, err := addr.PrivKey()
if err != nil {
fmt.Printf("WARN: Failed to obtain private key "+
"for address %v: %v\n", addr.Address(),
err)
continue
}
wif, err := btcutil.NewWIF((*btcec.PrivateKey)(privKey),
netParams, addr.Compressed())
if err != nil {
fmt.Printf("WARN: Failed to create wallet "+
"import format for address %v: %v\n",
addr.Address(), err)
continue
}
_, err = manager.ImportPrivateKey(wif, &blockStamp)
if err != nil {
fmt.Printf("WARN: Failed to import private "+
"key for address %v: %v\n",
addr.Address(), err)
continue
}
case keystore.ScriptAddress:
_, err := manager.ImportScript(addr.Script(), &blockStamp)
if err != nil {
fmt.Printf("WARN: Failed to import "+
"pay-to-script-hash script for "+
"address %v: %v\n", addr.Address(), err)
continue
}
default:
fmt.Printf("WARN: Skipping unrecognized legacy "+
"keystore type: %T\n", addr)
continue
}
}
return nil
}
// createWallet prompts the user for information needed to generate a new wallet
// and generates the wallet accordingly. The new wallet will reside at the
// provided path.
func createWallet(cfg *config) error {
// When there is a legacy keystore, open it now to ensure any errors
// don't end up exiting the process after the user has spent time
// entering a bunch of information.
netDir := networkDir(cfg.DataDir, activeNet.Params)
keystorePath := filepath.Join(netDir, keystore.Filename)
var legacyKeyStore *keystore.Store
if fileExists(keystorePath) {
var err error
legacyKeyStore, err = keystore.OpenDir(netDir)
if err != nil {
return err
}
}
// Start by prompting for the private passphrase. When there is an
// existing keystore, the user will be promped for that passphrase,
// otherwise they will be prompted for a new one.
reader := bufio.NewReader(os.Stdin)
privPass, err := promptConsolePrivatePass(reader, legacyKeyStore)
if err != nil {
return err
}
// Ascertain the public passphrase. This will either be a value
// specified by the user or the default hard-coded public passphrase if
// the user does not want the additional public data encryption.
pubPass, err := promptConsolePublicPass(reader, privPass, cfg)
if err != nil {
return err
}
// Ascertain the wallet generation seed. This will either be an
// automatically generated value the user has already confirmed or a
// value the user has entered which has already been validated.
seed, err := promptConsoleSeed(reader)
if err != nil {
return err
}
// Create the wallet.
dbPath := filepath.Join(netDir, walletDbName)
fmt.Println("Creating the wallet...")
// Create the wallet database backed by bolt db.
db, err := walletdb.Create("bdb", dbPath)
if err != nil {
return err
}
// Create the address manager.
namespace, err := db.Namespace(waddrmgrNamespaceKey)
if err != nil {
return err
}
manager, err := waddrmgr.Create(namespace, seed, []byte(pubPass),
[]byte(privPass), activeNet.Params, nil)
if err != nil {
return err
}
// Import the addresses in the legacy keystore to the new wallet if
// any exist.
if legacyKeyStore != nil {
fmt.Println("Importing addresses from existing wallet...")
if err := manager.Unlock([]byte(privPass)); err != nil {
return err
}
if err := convertLegacyKeystore(legacyKeyStore, manager); err != nil {
return err
}
legacyKeyStore.Lock()
legacyKeyStore = nil
// Remove the legacy key store.
if err := os.Remove(keystorePath); err != nil {
fmt.Printf("WARN: Failed to remove legacy wallet "+
"from'%s'\n", keystorePath)
}
}
manager.Close()
fmt.Println("The wallet has been created successfully.")
return nil
}
// createSimulationWallet is intended to be called from the rpcclient
// and used to create a wallet for actors involved in simulations.
func createSimulationWallet(cfg *config) error {
// Simulation wallet password is 'password'.
privPass := []byte("password")
// Public passphrase is the default.
pubPass := []byte(defaultPubPassphrase)
// Generate a random seed.
seed, err := hdkeychain.GenerateSeed(hdkeychain.RecommendedSeedLen)
if err != nil {
return err
}
netDir := networkDir(cfg.DataDir, activeNet.Params)
// Create the wallet.
dbPath := filepath.Join(netDir, walletDbName)
fmt.Println("Creating the wallet...")
// Create the wallet database backed by bolt db.
db, err := walletdb.Create("bdb", dbPath)
if err != nil {
return err
}
defer db.Close()
// Create the address manager.
waddrmgrNamespace, err := db.Namespace(waddrmgrNamespaceKey)
if err != nil {
return err
}
manager, err := waddrmgr.Create(waddrmgrNamespace, seed, []byte(pubPass),
[]byte(privPass), activeNet.Params, nil)
if err != nil {
return err
}
manager.Close()
fmt.Println("The wallet has been created successfully.")
return nil
}
// openDb opens and returns a *walletdb.DB (boltdb here) given the
// directory and dbname
func openDb(directory string, dbname string) (*walletdb.DB, error) {
dbPath := filepath.Join(directory, dbname)
// Ensure that the network directory exists.
if err := checkCreateDir(directory); err != nil {
return nil, err
}
// Open the database using the boltdb backend.
db, err := walletdb.Open("bdb", dbPath)
if err != nil {
return nil, err
}
return &db, nil
}
// openWaddrmgr returns an address manager given a database, namespace,
// public pass and the chain params
// It prompts for seed and private passphrase required in case of upgrades
func openWaddrmgr(db *walletdb.DB, namespaceKey []byte, pass string,
chainParams *chaincfg.Params) (*waddrmgr.Manager, error) {
// Get the namespace for the address manager.
namespace, err := (*db).Namespace(namespaceKey)
if err != nil {
return nil, err
}
config := &waddrmgr.Options{
ObtainSeed: promptSeed,
ObtainPrivatePass: promptPrivPassPhrase,
}
// Open address manager and transaction store.
// var txs *txstore.Store
return waddrmgr.Open(namespace, []byte(pass),
chainParams, config)
}
// openWallet returns a wallet. The function handles opening an existing wallet
// database, the address manager and the transaction store and uses the values
// to open a wallet.Wallet
func openWallet() (*wallet.Wallet, error) {
netdir := networkDir(cfg.DataDir, activeNet.Params)
db, err := openDb(netdir, walletDbName)
if err != nil {
log.Errorf("%v", err)
return nil, err
}
var txs *txstore.Store
mgr, err := openWaddrmgr(db, waddrmgrNamespaceKey, cfg.WalletPass,
activeNet.Params)
if err == nil {
txs, err = txstore.OpenDir(netdir)
}
if err != nil {
// Special case: if the address manager was successfully read
// (mgr != nil) but the transaction store was not, create a
// new txstore and write it out to disk. Write an unsynced
// manager back to disk so on future opens, the empty txstore
// is not considered fully synced.
if mgr == nil {
log.Errorf("%v", err)
return nil, err
}
txs = txstore.New(netdir)
txs.MarkDirty()
err = txs.WriteIfDirty()
if err != nil {
log.Errorf("%v", err)
return nil, err
}
mgr.SetSyncedTo(nil)
}
walletConfig := &wallet.Config{
Db: db,
TxStore: txs,
Waddrmgr: mgr,
ChainParams: activeNet.Params,
}
log.Infof("Opened wallet files") // TODO: log balance? last sync height?
w := wallet.Open(walletConfig)
return w, nil
}