You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
chantools/cmd/chantools/walletinfo.go

301 lines
8.0 KiB
Go

package main
import (
"fmt"
"os"
"strings"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcwallet/snacl"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/wallet"
"github.com/btcsuite/btcwallet/walletdb"
"github.com/guggero/chantools/lnd"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/spf13/cobra"
// This is required to register bdb as a valid walletdb driver. In the
// init function of the package, it registers itself. The import is used
// to activate the side effects w/o actually binding the package name to
// a file-level variable.
_ "github.com/btcsuite/btcwallet/walletdb/bdb"
)
const (
passwordEnvName = "WALLET_PASSWORD"
walletInfoFormat = `
Identity Pubkey: %x
BIP32 HD extended root key: %s
Wallet scopes:
%s
`
keyScopeformat = `
Scope: m/%d'/%d'
Number of internal %s addresses: %d
Number of external %s addresses: %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,
}
)
type walletInfoCommand struct {
WalletDB string
WithRootKey bool
cmd *cobra.Command
}
func newWalletInfoCommand() *cobra.Command {
cc := &walletInfoCommand{}
cc.cmd = &cobra.Command{
Use: "walletinfo",
Short: "Shows info about an lnd wallet.db file and optionally " +
"extracts the BIP32 HD root key",
Long: `Shows some basic information about an lnd wallet.db file,
like the node identity the wallet belongs to, how many on-chain addresses are
used and, if enabled with --withrootkey the BIP32 HD root key of the wallet. The
latter can be useful to recover funds from a wallet if the wallet password is
still known but the seed was lost. **The 24 word seed phrase itself cannot be
extracted** because it is hashed into the extended HD root key before storing it
in the wallet.db.`,
Example: `chantools walletinfo --withrootkey \
--walletdb ~/.lnd/data/chain/bitcoin/mainnet/wallet.db`,
RunE: cc.Execute,
}
cc.cmd.Flags().StringVar(
&cc.WalletDB, "walletdb", "", "lnd wallet.db file to dump the "+
"contents from",
)
cc.cmd.Flags().BoolVar(
&cc.WithRootKey, "withrootkey", false, "print BIP32 HD root "+
"key of wallet to standard out",
)
return cc.cmd
}
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,
)
if err != nil {
return fmt.Errorf("error opening wallet database: %v", 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
}
// Print the wallet info and if requested the root key.
identityKey, scopeInfo, err := walletInfo(w)
if err != nil {
return err
}
rootKey := "n/a"
if c.WithRootKey {
masterHDPrivKey, err := decryptRootKey(db, privateWalletPw)
if err != nil {
return err
}
rootKey = string(masterHDPrivKey)
}
result := fmt.Sprintf(
walletInfoFormat, identityKey.SerializeCompressed(), rootKey,
scopeInfo,
)
fmt.Printf(result)
// For the tests, also log as trace level which is disabled by default.
log.Tracef(result)
return nil
}
func walletInfo(w *wallet.Wallet) (*btcec.PublicKey, string, error) {
keyRing := keychain.NewBtcWalletKeyRing(w, chainParams.HDCoinType)
idPrivKey, err := keyRing.DerivePrivKey(keychain.KeyDescriptor{
KeyLocator: keychain.KeyLocator{
Family: keychain.KeyFamilyNodeKey,
Index: 0,
},
})
if err != nil {
return nil, "", fmt.Errorf("unable to open key ring for coin "+
"type %d: %v", chainParams.HDCoinType, err)
}
// Collect information about the different addresses in use.
scopeNp2wkh, err := printScopeInfo(
"np2wkh", w, w.Manager.ScopesForExternalAddrType(
waddrmgr.NestedWitnessPubKey,
),
)
if err != nil {
return nil, "", err
}
scopeP2wkh, err := printScopeInfo(
"p2wkh", w, w.Manager.ScopesForExternalAddrType(
waddrmgr.WitnessPubKey,
),
)
if err != nil {
return nil, "", err
}
return idPrivKey.PubKey(), scopeNp2wkh + scopeP2wkh, nil
}
func printScopeInfo(name string, w *wallet.Wallet,
scopes []waddrmgr.KeyScope) (string, error) {
scopeInfo := ""
for _, scope := range scopes {
props, err := w.AccountProperties(scope, defaultAccount)
if err != nil {
return "", fmt.Errorf("error fetching account "+
"properties: %v", err)
}
scopeInfo += fmt.Sprintf(
keyScopeformat, scope.Purpose, scope.Coin, name,
props.InternalKeyCount, name, props.ExternalKeyCount,
)
}
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)
}