diff --git a/README.md b/README.md index 6b89460..d1f29a9 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,15 @@ you use it for anything serious. **WARNING 2**: This tool will query public block explorer APIs, your privacy might not be preserved. Use at your own risk. +## Installation + +To install this tool, make sure you have `go 1.13.x` (or later) and `make` +installed and run the following command: + +```bash +make install +``` + ## Overview ```text @@ -18,6 +27,7 @@ Usage: chantools [OPTIONS] Application Options: + --testnet Set to true if testnet parameters should be used. --apiurl= API URL to use (must be esplora compatible). (default: https://blockstream.info/api) --listchannels= The channel input is in the format of lncli's listchannels format. Specify '-' to read from stdin. --pendingchannels= The channel input is in the format of lncli's pendingchannels format. Specify '-' to read from stdin. @@ -28,11 +38,13 @@ Help Options: -h, --help Show this help message Available commands: - dumpchannels Dump all channel information from lnd's channel database - forceclose Force-close the last state that is in the channel.db provided - rescueclosed Try finding the private keys for funds that are in outputs of remotely force-closed channels - summary Compile a summary about the current state of channels - sweeptimelock Sweep the force-closed state after the time lock has expired + dumpbackup Dump the content of a channel.backup file. + dumpchannels Dump all channel information from lnd's channel database. + forceclose Force-close the last state that is in the channel.db provided. + rescueclosed Try finding the private keys for funds that are in outputs of remotely force-closed channels. + showrootkey Extract and show the BIP32 HD root key from the 24 word lnd aezeed. + summary Compile a summary about the current state of channels. + sweeptimelock Sweep the force-closed state after the time lock has expired. ``` ## summary command @@ -168,3 +180,36 @@ Example command: ```bash chantools dumpchannels --channeldb ~/.lnd/data/graph/mainnet/channel.db ``` + +## showrootkey command + +This command converts the 24 word lnd aezeed phrase and password to the BIP32 +HD root key that is used as the `rootkey` parameter in other commands of this +tool. + +Example command: + +```bash +chantools showrootkey +``` + +## dumpbackup command + +```text +Usage: + chantools [OPTIONS] dumpbackup [dumpbackup-OPTIONS] + +[dumpbackup command options] + --rootkey= BIP32 HD root key of the wallet that was used to create the backup. + --multi_file= The lnd channel.backup file to dump. +``` + +This command dumps all information that is inside a `channel.backup` file in a +human readable format. + +Example command: + +```bash +chantools dumpbackup --rootkey xprvxxxxxxxxxx \ + --multi_file ~/.lnd/data/chain/bitcoin/mainnet/channel.backup +``` \ No newline at end of file diff --git a/cmd_dumpbackup.go b/cmd_dumpbackup.go new file mode 100644 index 0000000..48286b5 --- /dev/null +++ b/cmd_dumpbackup.go @@ -0,0 +1,58 @@ +package chantools + +import ( + "net" + + "github.com/btcsuite/btcutil" + "github.com/davecgh/go-spew/spew" + "github.com/lightningnetwork/lnd/chanbackup" + "github.com/lightningnetwork/lnd/lnwire" +) + +type dumpMulti struct { + Version chanbackup.MultiBackupVersion + StaticBackups []dumpSingle +} + +// dumpSingle is the information we want to dump from an lnd channel backup. +// See `chanbackup.Single` for information about the fields. +type dumpSingle struct { + Version chanbackup.SingleBackupVersion + IsInitiator bool + ChainHash string + FundingOutpoint string + ShortChannelID lnwire.ShortChannelID + RemoteNodePub string + Addresses []net.Addr + Capacity btcutil.Amount + LocalChanCfg dumpChanCfg + RemoteChanCfg dumpChanCfg + ShaChainRootDesc dumpDescriptor +} + +func dumpChannelBackup(multi *chanbackup.Multi) error { + dumpSingles := make([]dumpSingle, len(multi.StaticBackups)) + for idx, single := range multi.StaticBackups { + dumpSingles[idx] = dumpSingle{ + Version: single.Version, + IsInitiator: single.IsInitiator, + ChainHash: single.ChainHash.String(), + FundingOutpoint: single.FundingOutpoint.String(), + ShortChannelID: single.ShortChannelID, + RemoteNodePub: pubKeyToString(single.RemoteNodePub), + Addresses: single.Addresses, + Capacity: single.Capacity, + LocalChanCfg: toDumpChanCfg(single.LocalChanCfg), + RemoteChanCfg: toDumpChanCfg(single.RemoteChanCfg), + ShaChainRootDesc: toDumpDescriptor( + single.ShaChainRootDesc, + ), + } + } + + spew.Dump(dumpMulti{ + Version: multi.Version, + StaticBackups: dumpSingles, + }) + return nil +} diff --git a/cmd_dumpchannels.go b/cmd_dumpchannels.go index 7914399..f25054c 100644 --- a/cmd_dumpchannels.go +++ b/cmd_dumpchannels.go @@ -3,22 +3,15 @@ package chantools import ( "bytes" "encoding/hex" - "fmt" - "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcutil" "github.com/davecgh/go-spew/spew" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/input" - "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwire" ) -const ( - lndInternalDerivationPath = "m/1017'/0'/%d'/0/%d" -) - // dumpInfo is the information we want to dump from an open channel in lnd's // channel DB. See `channeldb.OpenChannel` for information about the fields. type dumpInfo struct { @@ -48,22 +41,6 @@ type dumpInfo struct { RemoteShutdownScript lnwire.DeliveryAddress } -// dumpChanCfg is the information we want to dump from a channel configuration. -// See `channeldb.ChannelConfig` for more information about the fields. -type dumpChanCfg struct { - channeldb.ChannelConstraints - MultiSigKey dumpDescriptor - RevocationBasePoint dumpDescriptor - PaymentBasePoint dumpDescriptor - DelayBasePoint dumpDescriptor - HtlcBasePoint dumpDescriptor -} - -type dumpDescriptor struct { - Path string - Pubkey string -} - func dumpChannelInfo(chanDb *channeldb.DB) error { channels, err := chanDb.FetchAllChannels() if err != nil { @@ -124,27 +101,3 @@ func dumpChannelInfo(chanDb *channeldb.DB) error { spew.Dump(dumpChannels) return nil } - -func toDumpChanCfg(cfg channeldb.ChannelConfig) dumpChanCfg { - return dumpChanCfg{ - ChannelConstraints: cfg.ChannelConstraints, - MultiSigKey: toDumpDescriptor(cfg.MultiSigKey), - RevocationBasePoint: toDumpDescriptor(cfg.RevocationBasePoint), - PaymentBasePoint: toDumpDescriptor(cfg.PaymentBasePoint), - DelayBasePoint: toDumpDescriptor(cfg.DelayBasePoint), - HtlcBasePoint: toDumpDescriptor(cfg.HtlcBasePoint), - } -} - -func toDumpDescriptor(desc keychain.KeyDescriptor) dumpDescriptor { - return dumpDescriptor{ - Path: fmt.Sprintf( - lndInternalDerivationPath, desc.Family, desc.Index, - ), - Pubkey: pubKeyToString(desc.PubKey), - } -} - -func pubKeyToString(pubkey *btcec.PublicKey) string { - return hex.EncodeToString(pubkey.SerializeCompressed()) -} diff --git a/cmd_rescueclosed.go b/cmd_rescueclosed.go index 46fc039..3f4fdca 100644 --- a/cmd_rescueclosed.go +++ b/cmd_rescueclosed.go @@ -9,7 +9,6 @@ import ( "time" "github.com/btcsuite/btcd/btcec" - "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil/hdkeychain" "github.com/lightningnetwork/lnd/channeldb" @@ -18,8 +17,6 @@ import ( ) var ( - hardenedKeyStart = uint32(hdkeychain.HardenedKeyStart) - chainParams = &chaincfg.MainNetParams cacheSize = 2000 cache []*cacheEntry @@ -184,22 +181,6 @@ func fillCache(extendedKey *hdkeychain.ExtendedKey) error { return nil } -func deriveChildren(key *hdkeychain.ExtendedKey, path []uint32) ( - *hdkeychain.ExtendedKey, error) { - - var ( - currentKey = key - err error - ) - for _, pathPart := range path { - currentKey, err = currentKey.Child(pathPart) - if err != nil { - return nil, err - } - } - return currentKey, nil -} - func parseAddr(addr string) ([]byte, error) { // First parse address to get targetPubKeyHash from it later. targetAddr, err := btcutil.DecodeAddress(addr, chainParams) diff --git a/dump.go b/dump.go new file mode 100644 index 0000000..3b2a8cc --- /dev/null +++ b/dump.go @@ -0,0 +1,57 @@ +package chantools + +import ( + "encoding/hex" + "fmt" + + "github.com/btcsuite/btcd/btcec" + "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/keychain" +) + +const ( + lndInternalDerivationPath = "m/1017'/0'/%d'/0/%d" +) + +// dumpChanCfg is the information we want to dump from a channel configuration. +// See `channeldb.ChannelConfig` for more information about the fields. +type dumpChanCfg struct { + channeldb.ChannelConstraints + MultiSigKey dumpDescriptor + RevocationBasePoint dumpDescriptor + PaymentBasePoint dumpDescriptor + DelayBasePoint dumpDescriptor + HtlcBasePoint dumpDescriptor +} + +type dumpDescriptor struct { + Path string + Pubkey string +} + +func toDumpChanCfg(cfg channeldb.ChannelConfig) dumpChanCfg { + return dumpChanCfg{ + ChannelConstraints: cfg.ChannelConstraints, + MultiSigKey: toDumpDescriptor(cfg.MultiSigKey), + RevocationBasePoint: toDumpDescriptor(cfg.RevocationBasePoint), + PaymentBasePoint: toDumpDescriptor(cfg.PaymentBasePoint), + DelayBasePoint: toDumpDescriptor(cfg.DelayBasePoint), + HtlcBasePoint: toDumpDescriptor(cfg.HtlcBasePoint), + } +} + +func toDumpDescriptor(desc keychain.KeyDescriptor) dumpDescriptor { + return dumpDescriptor{ + Path: fmt.Sprintf( + lndInternalDerivationPath, desc.Family, desc.Index, + ), + Pubkey: pubKeyToString(desc.PubKey), + } +} + +func pubKeyToString(pubkey *btcec.PublicKey) string { + if pubkey == nil { + return "" + } + return hex.EncodeToString(pubkey.SerializeCompressed()) +} diff --git a/go.mod b/go.mod index f2dcd5f..301abb3 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/urfave/cli/v2 v2.0.0 // indirect gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40 // indirect gitlab.com/NebulousLabs/go-upnp v0.0.0-20181011194642-3a71999ed0d3 // indirect - golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 // indirect + golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect golang.org/x/sys v0.0.0-20191224085550-c709ea063b76 // indirect golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect diff --git a/hdkeychain.go b/hdkeychain.go new file mode 100644 index 0000000..85b49f9 --- /dev/null +++ b/hdkeychain.go @@ -0,0 +1,66 @@ +package chantools + +import ( + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcutil/hdkeychain" + "github.com/lightningnetwork/lnd/keychain" +) + +const ( + hardenedKeyStart = uint32(hdkeychain.HardenedKeyStart) +) + +func deriveChildren(key *hdkeychain.ExtendedKey, path []uint32) ( + *hdkeychain.ExtendedKey, error) { + + var ( + currentKey = key + err error + ) + for _, pathPart := range path { + currentKey, err = currentKey.Child(pathPart) + if err != nil { + return nil, err + } + } + return currentKey, nil +} + +type channelBackupEncryptionRing struct { + extendedKey *hdkeychain.ExtendedKey + chainParams *chaincfg.Params +} + +func (r *channelBackupEncryptionRing) DeriveNextKey(_ keychain.KeyFamily) ( + keychain.KeyDescriptor, error) { + + return keychain.KeyDescriptor{}, nil +} + +func (r *channelBackupEncryptionRing) DeriveKey(keyLoc keychain.KeyLocator) ( + keychain.KeyDescriptor, error) { + + var empty = keychain.KeyDescriptor{} + keyBackup, err := deriveChildren(r.extendedKey, []uint32{ + hardenedKeyStart + uint32(keychain.BIP0043Purpose), + hardenedKeyStart + r.chainParams.HDCoinType, + hardenedKeyStart + uint32(keyLoc.Family), + 0, + keyLoc.Index, + }) + if err != nil { + return empty, err + } + + backupPubKey, err := keyBackup.ECPubKey() + if err != nil { + return empty, err + } + return keychain.KeyDescriptor{ + KeyLocator: keychain.KeyLocator{ + Family: keyLoc.Family, + Index: keyLoc.Index, + }, + PubKey: backupPubKey, + }, nil +} diff --git a/main.go b/main.go index 0c9fc3b..be2aafd 100644 --- a/main.go +++ b/main.go @@ -1,13 +1,21 @@ package chantools import ( + "bufio" "fmt" + "os" "path" + "strings" + "syscall" + "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcutil/hdkeychain" "github.com/jessevdk/go-flags" + "github.com/lightningnetwork/lnd/aezeed" "github.com/lightningnetwork/lnd/build" + "github.com/lightningnetwork/lnd/chanbackup" "github.com/lightningnetwork/lnd/channeldb" + "golang.org/x/crypto/ssh/terminal" ) const ( @@ -15,6 +23,7 @@ const ( ) type config struct { + Testnet bool `long:"testnet" description:"Set to true if testnet parameters should be used."` ApiUrl string `long:"apiurl" description:"API URL to use (must be esplora compatible)."` ListChannels string `long:"listchannels" description:"The channel input is in the format of lncli's listchannels format. Specify '-' to read from stdin."` PendingChannels string `long:"pendingchannels" description:"The channel input is in the format of lncli's pendingchannels format. Specify '-' to read from stdin."` @@ -28,6 +37,7 @@ var ( cfg = &config{ ApiUrl: defaultApiUrl, } + chainParams = &chaincfg.MainNetParams ) func Main() error { @@ -37,27 +47,32 @@ func Main() error { parser := flags.NewParser(cfg, flags.Default) _, _ = parser.AddCommand( "summary", "Compile a summary about the current state of "+ - "channels", "", &summaryCommand{}, + "channels.", "", &summaryCommand{}, ) _, _ = parser.AddCommand( "rescueclosed", "Try finding the private keys for funds that "+ - "are in outputs of remotely force-closed channels", "", + "are in outputs of remotely force-closed channels.", "", &rescueClosedCommand{}, ) _, _ = parser.AddCommand( "forceclose", "Force-close the last state that is in the "+ - "channel.db provided", "", - &forceCloseCommand{}, + "channel.db provided.", "", &forceCloseCommand{}, ) _, _ = parser.AddCommand( "sweeptimelock", "Sweep the force-closed state after the time "+ - "lock has expired", "", - &sweepTimeLockCommand{}, + "lock has expired.", "", &sweepTimeLockCommand{}, ) _, _ = parser.AddCommand( "dumpchannels", "Dump all channel information from lnd's "+ - "channel database", "", - &dumpChannelsCommand{}, + "channel database.", "", &dumpChannelsCommand{}, + ) + _, _ = parser.AddCommand( + "showrootkey", "Extract and show the BIP32 HD root key from "+ + "the 24 word lnd aezeed.", "", &showRootKeyCommand{}, + ) + _, _ = parser.AddCommand( + "dumpbackup", "Dump the content of a channel.backup file.", "", + &dumpBackupCommand{}, ) _, err := parser.Parse() @@ -184,7 +199,7 @@ type dumpChannelsCommand struct { func (c *dumpChannelsCommand) Execute(_ []string) error { // Check that we have a channel DB. if c.ChannelDB == "" { - return fmt.Errorf("rescue DB is required") + return fmt.Errorf("channel DB is required") } db, err := channeldb.Open(path.Dir(c.ChannelDB)) if err != nil { @@ -193,6 +208,97 @@ func (c *dumpChannelsCommand) Execute(_ []string) error { return dumpChannelInfo(db) } +type showRootKeyCommand struct{} + +func (c *showRootKeyCommand) Execute(_ []string) error { + // We'll now prompt the user to enter in their 24-word mnemonic. + fmt.Printf("Input your 24-word mnemonic separated by spaces: ") + reader := bufio.NewReader(os.Stdin) + mnemonicStr, err := reader.ReadString('\n') + if err != nil { + return err + } + + // We'll trim off extra spaces, and ensure the mnemonic is all + // lower case, then populate our request. + mnemonicStr = strings.TrimSpace(mnemonicStr) + mnemonicStr = strings.ToLower(mnemonicStr) + + cipherSeedMnemonic := strings.Split(mnemonicStr, " ") + + fmt.Println() + + if len(cipherSeedMnemonic) != 24 { + return fmt.Errorf("wrong cipher seed mnemonic "+ + "length: got %v words, expecting %v words", + len(cipherSeedMnemonic), 24) + } + + // Additionally, the user may have a passphrase, that will also + // need to be provided so the daemon can properly decipher the + // cipher seed. + fmt.Printf("Input your cipher seed passphrase (press enter if " + + "your seed doesn't have a passphrase): ") + passphrase, err := terminal.ReadPassword(syscall.Stdin) + if err != nil { + return err + } + + var mnemonic aezeed.Mnemonic + copy(mnemonic[:], cipherSeedMnemonic[:]) + + // If we're unable to map it back into the ciphertext, then either the + // mnemonic is wrong, or the passphrase is wrong. + cipherSeed, err := mnemonic.ToCipherSeed(passphrase) + if err != nil { + return err + } + rootKey, err := hdkeychain.NewMaster(cipherSeed.Entropy[:], chainParams) + if err != nil { + return fmt.Errorf("failed to derive master extended key") + } + fmt.Printf("\nYour BIP32 HD root key is: %s\n", rootKey.String()) + return nil +} + +type dumpBackupCommand struct { + RootKey string `long:"rootkey" description:"BIP32 HD root key of the wallet that was used to create the backup."` + MultiFile string `long:"multi_file" description:"The lnd channel.backup file to dump."` +} + +func (c *dumpBackupCommand) Execute(_ []string) error { + setupChainParams(cfg) + + // Check that root key is valid. + if c.RootKey == "" { + return fmt.Errorf("root key is required") + } + extendedKey, err := hdkeychain.NewKeyFromString(c.RootKey) + if err != nil { + return fmt.Errorf("error parsing root key: %v", err) + } + + // Check that we have a backup file. + if c.MultiFile == "" { + return fmt.Errorf("backup file is required") + } + multiFile := chanbackup.NewMultiFile(c.MultiFile) + multi, err := multiFile.ExtractMulti(&channelBackupEncryptionRing{ + extendedKey: extendedKey, + chainParams: chainParams, + }) + if err != nil { + return fmt.Errorf("could not extract multi file: %v", err) + } + return dumpChannelBackup(multi) +} + +func setupChainParams(cfg *config) { + if cfg.Testnet { + chainParams = &chaincfg.TestNet3Params + } +} + func setupLogging() { logWriter.RegisterSubLogger("CHAN", log) err := logWriter.InitLogRotator("./results/chantools.log", 10, 3)