diff --git a/cmd/chantools/createwallet.go b/cmd/chantools/createwallet.go index 9a1d666..2ee162b 100644 --- a/cmd/chantools/createwallet.go +++ b/cmd/chantools/createwallet.go @@ -115,7 +115,7 @@ func (c *createWalletCommand) Execute(_ *cobra.Command, _ []string) error { // To automate things with chantools, we also offer reading the wallet // password from environment variables. - pw := []byte(strings.TrimSpace(os.Getenv(passwordEnvName))) + pw := []byte(strings.TrimSpace(os.Getenv(lnd.PasswordEnvName))) // Because we cannot differentiate between an empty and a non-existent // environment variable, we need a special character that indicates that @@ -131,11 +131,13 @@ func (c *createWalletCommand) Execute(_ *cobra.Command, _ []string) error { case len(pw) == 0: fmt.Printf("\n\nThe wallet password is used to encrypt the " + "wallet.db file itself and is unrelated to the seed.\n") - pw, err = passwordFromConsole("Input new wallet password: ") + pw, err = lnd.PasswordFromConsole("Input new wallet password: ") if err != nil { return err } - pw2, err := passwordFromConsole("Confirm new wallet password: ") + pw2, err := lnd.PasswordFromConsole( + "Confirm new wallet password: ", + ) if err != nil { return err } diff --git a/cmd/chantools/root.go b/cmd/chantools/root.go index 1ce9ea3..5dfae0e 100644 --- a/cmd/chantools/root.go +++ b/cmd/chantools/root.go @@ -1,14 +1,12 @@ package main import ( - "bufio" "bytes" "encoding/json" "fmt" "io/ioutil" "os" "strings" - "syscall" "time" "github.com/btcsuite/btcd/btcutil/hdkeychain" @@ -22,7 +20,6 @@ import ( "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/peer" "github.com/spf13/cobra" - "golang.org/x/crypto/ssh/terminal" ) const ( @@ -265,27 +262,6 @@ func readInput(input string) ([]byte, error) { return ioutil.ReadFile(input) } -func passwordFromConsole(userQuery string) ([]byte, error) { - // Read from terminal (if there is one). - if terminal.IsTerminal(int(syscall.Stdin)) { //nolint - fmt.Print(userQuery) - pw, err := terminal.ReadPassword(int(syscall.Stdin)) //nolint - if err != nil { - return nil, err - } - fmt.Println() - return pw, nil - } - - // Read from stdin as a fallback. - reader := bufio.NewReader(os.Stdin) - pw, err := reader.ReadBytes('\n') - if err != nil { - return nil, err - } - return pw, nil -} - func setupLogging() { setSubLogger("CHAN", log) addSubLogger("CHDB", channeldb.UseLogger) @@ -328,10 +304,6 @@ func setSubLogger(subsystem string, logger btclog.Logger, } } -func noConsole() ([]byte, error) { - return nil, fmt.Errorf("wallet db requires console access") -} - func newExplorerAPI(apiURL string) *btc.ExplorerAPI { // Override for testnet if default is used. if apiURL == defaultAPIURL && diff --git a/cmd/chantools/walletinfo.go b/cmd/chantools/walletinfo.go index 0d2c519..7715136 100644 --- a/cmd/chantools/walletinfo.go +++ b/cmd/chantools/walletinfo.go @@ -1,28 +1,19 @@ package main import ( - "errors" "fmt" - "os" - "strings" "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcwallet/snacl" "github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/wallet" "github.com/btcsuite/btcwallet/walletdb" _ "github.com/btcsuite/btcwallet/walletdb/bdb" "github.com/lightninglabs/chantools/lnd" "github.com/lightningnetwork/lnd/keychain" - "github.com/lightningnetwork/lnd/lncfg" - "github.com/lightningnetwork/lnd/lnwallet" "github.com/spf13/cobra" - "go.etcd.io/bbolt" ) const ( - passwordEnvName = "WALLET_PASSWORD" - walletInfoFormat = ` Identity Pubkey: %x BIP32 HD extended root key: %s @@ -38,19 +29,7 @@ Scope: m/%d'/%d' ) var ( - // Namespace from github.com/btcsuite/btcwallet/wallet/wallet.go. - waddrmgrNamespaceKey = []byte("waddrmgr") - - // Bucket names from github.com/btcsuite/btcwallet/waddrmgr/db.go. - mainBucketName = []byte("main") - masterPrivKeyName = []byte("mpriv") - cryptoPrivKeyName = []byte("cpriv") - masterHDPrivName = []byte("mhdpriv") - defaultAccount = uint32(waddrmgr.DefaultAccountNum) - openCallbacks = &waddrmgr.OpenCallbacks{ - ObtainSeed: noConsole, - ObtainPrivatePass: noConsole, - } + defaultAccount = uint32(waddrmgr.DefaultAccountNum) ) type walletInfoCommand struct { @@ -98,75 +77,24 @@ or simply press without entering a password when being prompted.`, } func (c *walletInfoCommand) Execute(_ *cobra.Command, _ []string) error { - var ( - publicWalletPw = lnwallet.DefaultPublicPassphrase - privateWalletPw = lnwallet.DefaultPrivatePassphrase - err error - ) - // Check that we have a wallet DB. if c.WalletDB == "" { return fmt.Errorf("wallet DB is required") } - // To automate things with chantools, we also offer reading the wallet - // password from environment variables. - pw := []byte(strings.TrimSpace(os.Getenv(passwordEnvName))) - - // Because we cannot differentiate between an empty and a non-existent - // environment variable, we need a special character that indicates that - // no password should be used. We use a single dash (-) for that as that - // would be too short for an explicit password anyway. - switch { - // The user indicated in the environment variable that no passphrase - // should be used. We don't set any value. - case string(pw) == "-": - - // The environment variable didn't contain anything, we'll read the - // passphrase from the terminal. - case len(pw) == 0: - pw, err = passwordFromConsole("Input wallet password: ") - if err != nil { - return err - } - if len(pw) > 0 { - publicWalletPw = pw - privateWalletPw = pw - } - - // There was a password in the environment, just use it directly. - default: - publicWalletPw = pw - privateWalletPw = pw - } - - // Try to load and open the wallet. - db, err := walletdb.Open( - "bdb", lncfg.CleanAndExpandPath(c.WalletDB), false, - lnd.DefaultOpenTimeout, + w, privateWalletPw, cleanup, err := lnd.OpenWallet( + c.WalletDB, chainParams, ) - if errors.Is(err, bbolt.ErrTimeout) { - return fmt.Errorf("error opening wallet database, make sure " + - "lnd is not running and holding the exclusive lock " + - "on the wallet") - } if err != nil { - return fmt.Errorf("error opening wallet database: %w", err) + return fmt.Errorf("error opening wallet file '%s': %w", + c.WalletDB, err) } - defer func() { _ = db.Close() }() - w, err := wallet.Open(db, publicWalletPw, openCallbacks, chainParams, 0) - if err != nil { - return err - } - - // Start and unlock the wallet. - w.Start() - defer w.Stop() - err = w.Unlock(privateWalletPw, nil) - if err != nil { - return err - } + defer func() { + if err := cleanup(); err != nil { + log.Errorf("error closing wallet: %v", err) + } + }() // Print the wallet info and if requested the root key. identityKey, scopeInfo, err := walletInfo(w, c.DumpAddrs) @@ -175,7 +103,9 @@ func (c *walletInfoCommand) Execute(_ *cobra.Command, _ []string) error { } rootKey := na if c.WithRootKey { - masterHDPrivKey, err := decryptRootKey(db, privateWalletPw) + masterHDPrivKey, err := lnd.DecryptWalletRootKey( + w.Database(), privateWalletPw, + ) if err != nil { return err } @@ -259,7 +189,7 @@ func walletInfo(w *wallet.Wallet, dumpAddrs bool) (*btcec.PublicKey, string, err = walletdb.View( w.Database(), func(tx walletdb.ReadTx) error { waddrmgrNs := tx.ReadBucket( - waddrmgrNamespaceKey, + lnd.WaddrmgrNamespaceKey, ) return mgr.ForEachAccountAddress( @@ -304,64 +234,3 @@ func printScopeInfo(name string, w *wallet.Wallet, return scopeInfo, nil } - -func decryptRootKey(db walletdb.DB, privPassphrase []byte) ([]byte, error) { - // Step 1: Load the encryption parameters and encrypted keys from the - // database. - var masterKeyPrivParams []byte - var cryptoKeyPrivEnc []byte - var masterHDPrivEnc []byte - err := walletdb.View(db, func(tx walletdb.ReadTx) error { - ns := tx.ReadBucket(waddrmgrNamespaceKey) - if ns == nil { - return fmt.Errorf("namespace '%s' does not exist", - waddrmgrNamespaceKey) - } - - mainBucket := ns.NestedReadBucket(mainBucketName) - if mainBucket == nil { - return fmt.Errorf("bucket '%s' does not exist", - mainBucketName) - } - - val := mainBucket.Get(masterPrivKeyName) - if val != nil { - masterKeyPrivParams = make([]byte, len(val)) - copy(masterKeyPrivParams, val) - } - val = mainBucket.Get(cryptoPrivKeyName) - if val != nil { - cryptoKeyPrivEnc = make([]byte, len(val)) - copy(cryptoKeyPrivEnc, val) - } - val = mainBucket.Get(masterHDPrivName) - if val != nil { - masterHDPrivEnc = make([]byte, len(val)) - copy(masterHDPrivEnc, val) - } - - return nil - }) - if err != nil { - return nil, err - } - - // Step 2: Unmarshal the master private key parameters and derive - // key from passphrase. - var masterKeyPriv snacl.SecretKey - if err := masterKeyPriv.Unmarshal(masterKeyPrivParams); err != nil { - return nil, err - } - if err := masterKeyPriv.DeriveKey(&privPassphrase); err != nil { - return nil, err - } - - // Step 3: Decrypt the keys in the correct order. - cryptoKeyPriv := &snacl.CryptoKey{} - cryptoKeyPrivBytes, err := masterKeyPriv.Decrypt(cryptoKeyPrivEnc) - if err != nil { - return nil, err - } - copy(cryptoKeyPriv[:], cryptoKeyPrivBytes) - return cryptoKeyPriv.Decrypt(masterHDPrivEnc) -} diff --git a/cmd/chantools/walletinfo_test.go b/cmd/chantools/walletinfo_test.go index ab4c180..a5a841d 100644 --- a/cmd/chantools/walletinfo_test.go +++ b/cmd/chantools/walletinfo_test.go @@ -3,6 +3,7 @@ package main import ( "testing" + "github.com/lightninglabs/chantools/lnd" "github.com/stretchr/testify/require" ) @@ -20,7 +21,7 @@ func TestWalletInfo(t *testing.T) { WithRootKey: true, } - t.Setenv(passwordEnvName, testPassPhrase) + t.Setenv(lnd.PasswordEnvName, testPassPhrase) err := info.Execute(nil, nil) require.NoError(t, err) diff --git a/lnd/aezeed.go b/lnd/aezeed.go index 589ef76..5c45266 100644 --- a/lnd/aezeed.go +++ b/lnd/aezeed.go @@ -2,6 +2,7 @@ package lnd import ( "bufio" + "errors" "fmt" "os" "regexp" @@ -11,20 +12,46 @@ import ( "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcwallet/snacl" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/wallet" + "github.com/btcsuite/btcwallet/walletdb" "github.com/lightningnetwork/lnd/aezeed" + "github.com/lightningnetwork/lnd/lncfg" + "github.com/lightningnetwork/lnd/lnwallet" + "go.etcd.io/bbolt" "golang.org/x/crypto/ssh/terminal" ) const ( MnemonicEnvName = "AEZEED_MNEMONIC" PassphraseEnvName = "AEZEED_PASSPHRASE" + PasswordEnvName = "WALLET_PASSWORD" ) var ( numberDotsRegex = regexp.MustCompile(`[\d.\-\n\r\t]*`) multipleSpaces = regexp.MustCompile(" [ ]+") + + openCallbacks = &waddrmgr.OpenCallbacks{ + ObtainSeed: noConsole, + ObtainPrivatePass: noConsole, + } + + // Namespace from github.com/btcsuite/btcwallet/wallet/wallet.go. + WaddrmgrNamespaceKey = []byte("waddrmgr") + + // Bucket names from github.com/btcsuite/btcwallet/waddrmgr/db.go. + mainBucketName = []byte("main") + masterPrivKeyName = []byte("mpriv") + cryptoPrivKeyName = []byte("cpriv") + masterHDPrivName = []byte("mhdpriv") ) +func noConsole() ([]byte, error) { + return nil, fmt.Errorf("wallet db requires console access") +} + // ReadAezeed reads an aezeed from the console or the environment variable. func ReadAezeed(params *chaincfg.Params) (*hdkeychain.ExtendedKey, time.Time, error) { @@ -130,3 +157,174 @@ func ReadPassphrase(verb string) ([]byte, error) { return passphraseBytes, nil } + +// PasswordFromConsole reads a password from the console or stdin. +func PasswordFromConsole(userQuery string) ([]byte, error) { + // Read from terminal (if there is one). + if terminal.IsTerminal(int(syscall.Stdin)) { //nolint + fmt.Print(userQuery) + pw, err := terminal.ReadPassword(int(syscall.Stdin)) //nolint + if err != nil { + return nil, err + } + fmt.Println() + return pw, nil + } + + // Read from stdin as a fallback. + reader := bufio.NewReader(os.Stdin) + pw, err := reader.ReadBytes('\n') + if err != nil { + return nil, err + } + return pw, nil +} + +// OpenWallet opens a lnd compatible wallet and returns it, along with the +// private wallet password. +func OpenWallet(walletDbPath string, + chainParams *chaincfg.Params) (*wallet.Wallet, []byte, func() error, + error) { + + var ( + publicWalletPw = lnwallet.DefaultPublicPassphrase + privateWalletPw = lnwallet.DefaultPrivatePassphrase + err error + ) + + // To automate things with chantools, we also offer reading the wallet + // password from environment variables. + pw := []byte(strings.TrimSpace(os.Getenv(PasswordEnvName))) + + // Because we cannot differentiate between an empty and a non-existent + // environment variable, we need a special character that indicates that + // no password should be used. We use a single dash (-) for that as that + // would be too short for an explicit password anyway. + switch { + // The user indicated in the environment variable that no passphrase + // should be used. We don't set any value. + case string(pw) == "-": + + // The environment variable didn't contain anything, we'll read the + // passphrase from the terminal. + case len(pw) == 0: + pw, err = PasswordFromConsole("Input wallet password: ") + if err != nil { + return nil, nil, nil, err + } + if len(pw) > 0 { + publicWalletPw = pw + privateWalletPw = pw + } + + // There was a password in the environment, just use it directly. + default: + publicWalletPw = pw + privateWalletPw = pw + } + + // Try to load and open the wallet. + db, err := walletdb.Open( + "bdb", lncfg.CleanAndExpandPath(walletDbPath), false, + DefaultOpenTimeout, + ) + if errors.Is(err, bbolt.ErrTimeout) { + return nil, nil, nil, fmt.Errorf("error opening wallet " + + "database, make sure lnd is not running and holding " + + "the exclusive lock on the wallet") + } + if err != nil { + return nil, nil, nil, fmt.Errorf("error opening wallet "+ + "database: %w", err) + } + + w, err := wallet.Open(db, publicWalletPw, openCallbacks, chainParams, 0) + if err != nil { + _ = db.Close() + return nil, nil, nil, fmt.Errorf("error opening wallet %w", err) + } + + // Start and unlock the wallet. + w.Start() + err = w.Unlock(privateWalletPw, nil) + if err != nil { + w.Stop() + _ = db.Close() + return nil, nil, nil, err + } + + cleanup := func() error { + w.Stop() + if err := db.Close(); err != nil { + return err + } + + return nil + } + + return w, privateWalletPw, cleanup, nil +} + +// DecryptWalletRootKey decrypts a lnd compatible wallet's root key. +func DecryptWalletRootKey(db walletdb.DB, + privatePassphrase []byte) ([]byte, error) { + + // Step 1: Load the encryption parameters and encrypted keys from the + // database. + var masterKeyPrivParams []byte + var cryptoKeyPrivEnc []byte + var masterHDPrivEnc []byte + err := walletdb.View(db, func(tx walletdb.ReadTx) error { + ns := tx.ReadBucket(WaddrmgrNamespaceKey) + if ns == nil { + return fmt.Errorf("namespace '%s' does not exist", + WaddrmgrNamespaceKey) + } + + mainBucket := ns.NestedReadBucket(mainBucketName) + if mainBucket == nil { + return fmt.Errorf("bucket '%s' does not exist", + mainBucketName) + } + + val := mainBucket.Get(masterPrivKeyName) + if val != nil { + masterKeyPrivParams = make([]byte, len(val)) + copy(masterKeyPrivParams, val) + } + val = mainBucket.Get(cryptoPrivKeyName) + if val != nil { + cryptoKeyPrivEnc = make([]byte, len(val)) + copy(cryptoKeyPrivEnc, val) + } + val = mainBucket.Get(masterHDPrivName) + if val != nil { + masterHDPrivEnc = make([]byte, len(val)) + copy(masterHDPrivEnc, val) + } + + return nil + }) + if err != nil { + return nil, err + } + + // Step 2: Unmarshal the master private key parameters and derive + // key from passphrase. + var masterKeyPriv snacl.SecretKey + if err := masterKeyPriv.Unmarshal(masterKeyPrivParams); err != nil { + return nil, err + } + if err := masterKeyPriv.DeriveKey(&privatePassphrase); err != nil { + return nil, err + } + + // Step 3: Decrypt the keys in the correct order. + cryptoKeyPriv := &snacl.CryptoKey{} + cryptoKeyPrivBytes, err := masterKeyPriv.Decrypt(cryptoKeyPrivEnc) + if err != nil { + return nil, err + } + copy(cryptoKeyPriv[:], cryptoKeyPrivBytes) + return cryptoKeyPriv.Decrypt(masterHDPrivEnc) +}