From 0e856460a73be612a4ef5e2c751706c9897ded36 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 16 Nov 2022 16:31:31 +0100 Subject: [PATCH] rescuetweakedkey: add new command rescuetweakedkey --- cmd/chantools/rescuetweakedkey.go | 228 +++++++++++++++++++++++++ cmd/chantools/rescuetweakedkey_test.go | 63 +++++++ cmd/chantools/root.go | 1 + doc/chantools.md | 1 + doc/chantools_rescuetweakedkey.md | 44 +++++ doc/chantools_walletinfo.md | 1 + 6 files changed, 338 insertions(+) create mode 100644 cmd/chantools/rescuetweakedkey.go create mode 100644 cmd/chantools/rescuetweakedkey_test.go create mode 100644 doc/chantools_rescuetweakedkey.md diff --git a/cmd/chantools/rescuetweakedkey.go b/cmd/chantools/rescuetweakedkey.go new file mode 100644 index 0000000..c2531e4 --- /dev/null +++ b/cmd/chantools/rescuetweakedkey.go @@ -0,0 +1,228 @@ +package main + +import ( + "bytes" + "fmt" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/guggero/chantools/lnd" + "github.com/spf13/cobra" +) + +var ( + AddrNotFoundErr = fmt.Errorf("address not found") +) + +type rescueTweakedKeyCommand struct { + Path string + TargetAddr string + NumTries uint64 + + rootKey *rootKey + cmd *cobra.Command +} + +func newRescueTweakedKeyCommand() *cobra.Command { + cc := &rescueTweakedKeyCommand{} + cc.cmd = &cobra.Command{ + Use: "rescuetweakedkey", + Short: "Attempt to rescue funds locked in an address with a " + + "key that was affected by a specific bug in lnd", + Long: `There very likely is no reason to run this command +unless you exactly know why or were told by the author of this tool to use it. +`, + Example: `chantools rescuetweakedkey \ + --path "m/1017'/0'/5'/0/0'" \ + --targetaddr bc1pxxxxxxx`, + RunE: cc.Execute, + } + cc.cmd.Flags().StringVar( + &cc.Path, "path", "", "BIP32 derivation path to derive the "+ + "starting key from; must start with \"m/\"", + ) + cc.cmd.Flags().StringVar( + &cc.TargetAddr, "targetaddr", "", "address the funds are "+ + "locked in", + ) + cc.cmd.Flags().Uint64Var( + &cc.NumTries, "numtries", 10_000_000, "the number of "+ + "mutations to try", + ) + + cc.rootKey = newRootKey(cc.cmd, "deriving starting key") + + return cc.cmd +} + +func (c *rescueTweakedKeyCommand) Execute(_ *cobra.Command, _ []string) error { + extendedKey, err := c.rootKey.read() + if err != nil { + return fmt.Errorf("error reading root key: %w", err) + } + + if c.Path == "" { + return fmt.Errorf("path is required") + } + + childKey, _, _, err := lnd.DeriveKey(extendedKey, c.Path, chainParams) + if err != nil { + return fmt.Errorf("could not derive key: %w", err) + } + + startKey, err := childKey.ECPrivKey() + if err != nil { + return fmt.Errorf("error deriving private key: %w", err) + } + + targetAddr, err := lnd.ParseAddress(c.TargetAddr, chainParams) + if err != nil { + return fmt.Errorf("error parsing target addr: %w", err) + } + + return testPattern(startKey, targetAddr, c.NumTries) +} + +func testPattern(startKey *btcec.PrivateKey, targetAddr btcutil.Address, + max uint64) error { + + currentKey := copyPrivKey(startKey) + for idx := uint64(0); idx <= max; idx++ { + match, err := pubKeyMatchesAddr(currentKey.PubKey(), targetAddr) + if err != nil { + return fmt.Errorf("error matching key to address: %w", + err) + } + + if match { + log.Infof("Success! Found private key %x for "+ + "address %v\n", currentKey.Serialize(), + targetAddr) + return nil + } + + mutateWithTweak(currentKey) + + match, err = pubKeyMatchesAddr(currentKey.PubKey(), targetAddr) + if err != nil { + return fmt.Errorf("error matching key to address: %w", + err) + } + + if match { + log.Infof("Success! Found private key %x for "+ + "address %v\n", currentKey.Serialize(), + targetAddr) + return nil + } + + keyCopy := copyPrivKey(currentKey) + mutateWithSign(keyCopy) + + match, err = pubKeyMatchesAddr(keyCopy.PubKey(), targetAddr) + if err != nil { + return fmt.Errorf("error matching key to address: %w", + err) + } + + if match { + log.Infof("Success! Found private key %x for "+ + "address %v\n", keyCopy.Serialize(), + targetAddr) + return nil + } + + if idx != 0 && idx%5000 == 0 { + fmt.Printf("Tested %d of %d mutations\n", idx, max) + } + } + + match, err := pubKeyMatchesAddr(currentKey.PubKey(), targetAddr) + if err != nil { + return fmt.Errorf("error matching key to address: %w", err) + } + + if match { + log.Infof("Success! Found private key %x for address %v\n", + currentKey.Serialize(), targetAddr) + return nil + } + + return fmt.Errorf("%w: key for address %v not found after %d attempts", + AddrNotFoundErr, targetAddr.String(), max) +} + +func pubKeyMatchesAddr(pubKey *btcec.PublicKey, addr btcutil.Address) (bool, + error) { + + switch typedAddr := addr.(type) { + case *btcutil.AddressWitnessPubKeyHash: + hash160 := btcutil.Hash160(pubKey.SerializeCompressed()) + + return bytes.Equal(hash160, typedAddr.WitnessProgram()), nil + + case *btcutil.AddressTaproot: + taprootKey := txscript.ComputeTaprootKeyNoScript(pubKey) + + return bytes.Equal( + schnorr.SerializePubKey(taprootKey), + typedAddr.WitnessProgram(), + ), nil + + default: + return false, fmt.Errorf("unsupported address type <%T>", + typedAddr) + } +} + +func copyPrivKey(privKey *btcec.PrivateKey) *btcec.PrivateKey { + privKeyCopy := *privKey + return &btcec.PrivateKey{ + Key: privKeyCopy.Key, + } +} + +func mutateWithSign(privKey *btcec.PrivateKey) { + privKeyScalar := &privKey.Key + pub := privKey.PubKey() + + // Step 5. + // + // Negate d if P.y is odd. + pubKeyBytes := pub.SerializeCompressed() + if pubKeyBytes[0] == secp256k1.PubKeyFormatCompressedOdd { + privKeyScalar.Negate() + } +} + +func mutateWithTweak(privKey *btcec.PrivateKey) { + // If the corresponding public key has an odd y coordinate, then we'll + // negate the private key as specified in BIP 341. + privKeyScalar := &privKey.Key + pubKeyBytes := privKey.PubKey().SerializeCompressed() + if pubKeyBytes[0] == secp256k1.PubKeyFormatCompressedOdd { + privKeyScalar.Negate() + } + + // Next, we'll compute the tap tweak hash that commits to the internal + // key and the merkle script root. We'll snip off the extra parity byte + // from the compressed serialization and use that directly. + schnorrKeyBytes := pubKeyBytes[1:] + tapTweakHash := chainhash.TaggedHash( + chainhash.TagTapTweak, schnorrKeyBytes, []byte{}, + ) + + // Map the private key to a ModNScalar which is needed to perform + // operation mod the curve order. + var tweakScalar btcec.ModNScalar + tweakScalar.SetBytes((*[32]byte)(tapTweakHash)) + + // Now that we have the private key in its may negated form, we'll add + // the script root as a tweak. As we're using a ModNScalar all + // operations are already normalized mod the curve order. + _ = privKeyScalar.Add(&tweakScalar) +} diff --git a/cmd/chantools/rescuetweakedkey_test.go b/cmd/chantools/rescuetweakedkey_test.go new file mode 100644 index 0000000..5aea7ca --- /dev/null +++ b/cmd/chantools/rescuetweakedkey_test.go @@ -0,0 +1,63 @@ +package main + +import ( + "encoding/hex" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/stretchr/testify/require" +) + +var ( + privKeyBytes, _ = hex.DecodeString( + "571e2fc5e99f91596f7561da9f605cbf2e2342a166593eef041862b6a8b7" + + "4f35", + ) + pubKeyOrigBytes, _ = hex.DecodeString( + "032ec305fb12642fd3b1091d1cba88ebb7b1a8dbc256b35789b7e223a1b3" + + "75f0b7", + ) + pubKeyNegBytes, _ = hex.DecodeString( + "022ec305fb12642fd3b1091d1cba88ebb7b1a8dbc256b35789b7e223a1b3" + + "75f0b7", + ) + pubKeyNegTweakBytes, _ = hex.DecodeString( + "0322b5c94ec4dc3a8843edc7448a0aad389d43e0f8d1b35b546dd1aad70f" + + "b2c45b", + ) + pubKeyNegTweakTweakBytes, _ = hex.DecodeString( + "03f4cd1ff9efa8198e33e5a110dc690c1472d56c01287893c2f8ed55f61e" + + "a767d1", + ) +) + +func TestTweak(t *testing.T) { + privKey, pubKey := btcec.PrivKeyFromBytes(privKeyBytes) + require.Equal(t, pubKeyOrigBytes, pubKey.SerializeCompressed()) + + privKeyCopy := copyPrivKey(privKey) + require.Equal(t, privKey, privKeyCopy) + + mutateWithSign(privKeyCopy) + require.NotEqual(t, privKey, privKeyCopy) + require.Equalf( + t, pubKeyNegBytes, privKeyCopy.PubKey().SerializeCompressed(), + "%x", privKeyCopy.PubKey().SerializeCompressed(), + ) + + mutateWithTweak(privKeyCopy) + require.NotEqual(t, privKey, privKeyCopy) + require.Equalf( + t, pubKeyNegTweakBytes, + privKeyCopy.PubKey().SerializeCompressed(), + "%x", privKeyCopy.PubKey().SerializeCompressed(), + ) + + mutateWithTweak(privKeyCopy) + require.NotEqual(t, privKey, privKeyCopy) + require.Equalf( + t, pubKeyNegTweakTweakBytes, + privKeyCopy.PubKey().SerializeCompressed(), + "%x", privKeyCopy.PubKey().SerializeCompressed(), + ) +} diff --git a/cmd/chantools/root.go b/cmd/chantools/root.go index 9c12d09..e63720c 100644 --- a/cmd/chantools/root.go +++ b/cmd/chantools/root.go @@ -97,6 +97,7 @@ func main() { newRemoveChannelCommand(), newRescueClosedCommand(), newRescueFundingCommand(), + newRescueTweakedKeyCommand(), newShowRootKeyCommand(), newSignRescueFundingCommand(), newSummaryCommand(), diff --git a/doc/chantools.md b/doc/chantools.md index 959d0b4..699b98d 100644 --- a/doc/chantools.md +++ b/doc/chantools.md @@ -35,6 +35,7 @@ Complete documentation is available at https://github.com/guggero/chantools/. * [chantools removechannel](chantools_removechannel.md) - Remove a single channel from the given channel DB * [chantools rescueclosed](chantools_rescueclosed.md) - Try finding the private keys for funds that are in outputs of remotely force-closed channels * [chantools rescuefunding](chantools_rescuefunding.md) - Rescue funds locked in a funding multisig output that never resulted in a proper channel; this is the command the initiator of the channel needs to run +* [chantools rescuetweakedkey](chantools_rescuetweakedkey.md) - Attempt to rescue funds locked in an address with a key that was affected by a specific bug in lnd * [chantools showrootkey](chantools_showrootkey.md) - Extract and show the BIP32 HD root key from the 24 word lnd aezeed * [chantools signrescuefunding](chantools_signrescuefunding.md) - Rescue funds locked in a funding multisig output that never resulted in a proper channel; this is the command the remote node (the non-initiator) of the channel needs to run * [chantools summary](chantools_summary.md) - Compile a summary about the current state of channels diff --git a/doc/chantools_rescuetweakedkey.md b/doc/chantools_rescuetweakedkey.md new file mode 100644 index 0000000..b80f4cd --- /dev/null +++ b/doc/chantools_rescuetweakedkey.md @@ -0,0 +1,44 @@ +## chantools rescuetweakedkey + +Attempt to rescue funds locked in an address with a key that was affected by a specific bug in lnd + +### Synopsis + +There very likely is no reason to run this command +unless you exactly know why or were told by the author of this tool to use it. + + +``` +chantools rescuetweakedkey [flags] +``` + +### Examples + +``` +chantools rescuetweakedkey \ + --path "m/1017'/0'/5'/0/0'" \ + --targetaddr bc1pxxxxxxx +``` + +### Options + +``` + --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag + -h, --help help for rescuetweakedkey + --numtries uint the number of mutations to try (default 10000000) + --path string BIP32 derivation path to derive the starting key from; must start with "m/" + --rootkey string BIP32 HD root key of the wallet to use for deriving starting key; leave empty to prompt for lnd 24 word aezeed + --targetaddr string address the funds are locked in +``` + +### Options inherited from parent commands + +``` + -r, --regtest Indicates if regtest parameters should be used + -t, --testnet Indicates if testnet parameters should be used +``` + +### SEE ALSO + +* [chantools](chantools.md) - Chantools helps recover funds from lightning channels + diff --git a/doc/chantools_walletinfo.md b/doc/chantools_walletinfo.md index 31e9203..4916674 100644 --- a/doc/chantools_walletinfo.md +++ b/doc/chantools_walletinfo.md @@ -26,6 +26,7 @@ chantools walletinfo --withrootkey \ ### Options ``` + --dumpaddrs print all addresses, including private keys -h, --help help for walletinfo --walletdb string lnd wallet.db file to dump the contents from --withrootkey print BIP32 HD root key of wallet to standard out