mirror of https://github.com/guggero/chantools
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.
229 lines
5.9 KiB
Go
229 lines
5.9 KiB
Go
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 (
|
|
ErrAddrNotFound = 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",
|
|
ErrAddrNotFound, 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)
|
|
}
|