diff --git a/cmd/lncli/cmd_debug.go b/cmd/lncli/cmd_debug.go index c114dc9c0..2723e5785 100644 --- a/cmd/lncli/cmd_debug.go +++ b/cmd/lncli/cmd_debug.go @@ -1,8 +1,20 @@ package main import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "os" + + "github.com/andybalholm/brotli" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightningnetwork/lnd" + "github.com/lightningnetwork/lnd/lnencrypt" "github.com/lightningnetwork/lnd/lnrpc" "github.com/urfave/cli" + "google.golang.org/protobuf/proto" ) var getDebugInfoCommand = cli.Command{ @@ -27,3 +39,297 @@ func getDebugInfo(ctx *cli.Context) error { return nil } + +type DebugPackage struct { + EphemeralPubKey string `json:"ephemeral_public_key"` + EncryptedPayload string `json:"encrypted_payload"` +} + +var encryptDebugPackageCommand = cli.Command{ + Name: "encryptdebugpackage", + Category: "Debug", + Usage: "Collects a package of debug information and encrypts it.", + Description: ` + When requesting support with lnd, it's often required to submit a lot of + debug information to the developer in order to track down a problem. + This command will collect all the relevant information and encrypt it + using the provided public key. The resulting file can then be sent to + the developer for further analysis. + Because the file is encrypted, it is safe to send it over insecure + channels or upload it to a GitHub issue. + + Use 'lncli encryptdebugpackage 0xxxxxx... > package.txt' to write the + encrypted package to a file called package.txt. + `, + ArgsUsage: "pubkey [--output_file F]", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "pubkey", + Usage: "the public key to encrypt the information " + + "for (hex-encoded, e.g. 02aabb..), this " + + "should be provided to you by the issue " + + "tracker or developer you're requesting " + + "support from", + }, + cli.StringFlag{ + Name: "output_file", + Usage: "(optional) the file to write the encrypted " + + "package to; if not specified, the debug " + + "package is printed to stdout", + }, + }, + Action: actionDecorator(encryptDebugPackage), +} + +func encryptDebugPackage(ctx *cli.Context) error { + if ctx.NArg() == 0 && ctx.NumFlags() == 0 { + return cli.ShowCommandHelp(ctx, "encryptdebugpackage") + } + + var ( + args = ctx.Args() + pubKeyBytes []byte + err error + ) + switch { + case ctx.IsSet("pubkey"): + pubKeyBytes, err = hex.DecodeString(ctx.String("pubkey")) + case args.Present(): + pubKeyBytes, err = hex.DecodeString(args.First()) + } + if err != nil { + return fmt.Errorf("unable to decode pubkey argument: %w", err) + } + + pubKey, err := btcec.ParsePubKey(pubKeyBytes) + if err != nil { + return fmt.Errorf("unable to parse pubkey: %w", err) + } + + // Collect the information we want to send from the daemon. + payload, err := collectDebugPackageInfo(ctx) + if err != nil { + return fmt.Errorf("unable to collect debug package "+ + "information: %w", err) + } + + // We've collected the information we want to send, but before + // encrypting it, we want to compress it as much as possible to reduce + // the size of the final payload. + var ( + compressBuf bytes.Buffer + options = brotli.WriterOptions{ + Quality: brotli.BestCompression, + } + writer = brotli.NewWriterOptions(&compressBuf, options) + ) + _, err = writer.Write(payload) + if err != nil { + return fmt.Errorf("unable to compress payload: %w", err) + } + if err := writer.Close(); err != nil { + return fmt.Errorf("unable to compress payload: %w", err) + } + + // Now we have the full payload that we want to encrypt, so we'll create + // an ephemeral keypair to encrypt the payload with. + localKey, err := btcec.NewPrivateKey() + if err != nil { + return fmt.Errorf("unable to generate local key: %w", err) + } + + enc, err := lnencrypt.ECDHEncrypter(localKey, pubKey) + if err != nil { + return fmt.Errorf("unable to create encrypter: %w", err) + } + + var cipherBuf bytes.Buffer + err = enc.EncryptPayloadToWriter(compressBuf.Bytes(), &cipherBuf) + if err != nil { + return fmt.Errorf("unable to encrypt payload: %w", err) + } + + response := DebugPackage{ + EphemeralPubKey: hex.EncodeToString( + localKey.PubKey().SerializeCompressed(), + ), + EncryptedPayload: hex.EncodeToString( + cipherBuf.Bytes(), + ), + } + + // If the user specified an output file, we'll write the encrypted + // payload to that file. + if ctx.IsSet("output_file") { + fileName := lnd.CleanAndExpandPath(ctx.String("output_file")) + jsonBytes, err := json.Marshal(response) + if err != nil { + return fmt.Errorf("unable to encode JSON: %w", err) + } + + return os.WriteFile(fileName, jsonBytes, 0644) + } + + // Finally, we'll print out the final payload as a JSON if no output + // file was specified. + printJSON(response) + + return nil +} + +// collectDebugPackageInfo collects the information we want to send to the +// developer(s) from the daemon. +func collectDebugPackageInfo(ctx *cli.Context) ([]byte, error) { + ctxc := getContext() + client, cleanUp := getClient(ctx) + defer cleanUp() + + info, err := client.GetInfo(ctxc, &lnrpc.GetInfoRequest{}) + if err != nil { + return nil, fmt.Errorf("error getting info: %w", err) + } + debugInfo, err := client.GetDebugInfo( + ctxc, &lnrpc.GetDebugInfoRequest{}, + ) + if err != nil { + return nil, fmt.Errorf("error getting debug info: %w", err) + } + + var payloadBuf bytes.Buffer + addToBuf := func(msg proto.Message) error { + jsonBytes, err := lnrpc.ProtoJSONMarshalOpts.Marshal(msg) + if err != nil { + return fmt.Errorf("error encoding response: %w", err) + } + + payloadBuf.Write(jsonBytes) + + return nil + } + + if err := addToBuf(info); err != nil { + return nil, err + } + if err := addToBuf(debugInfo); err != nil { + return nil, err + } + + return payloadBuf.Bytes(), nil +} + +var decryptDebugPackageCommand = cli.Command{ + Name: "decryptdebugpackage", + Category: "Debug", + Usage: "Decrypts a package of debug information.", + Description: ` + Decrypt a debug package that was created with the encryptdebugpackage + command. Decryption requires the private key that corresponds to the + public key the package was encrypted to. + The command expects the encrypted package JSON to be provided on stdin. + If decryption is successful, the information will be printed to stdout. + + Use 'lncli decryptdebugpackage 0xxxxxx... < package.txt > decrypted.txt' + to read the encrypted package from a file called package.txt and to + write the decrypted content to a file called decrypted.txt. + `, + ArgsUsage: "privkey [--input_file F]", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "privkey", + Usage: "the hex encoded private key to decrypt the " + + "debug package", + }, + cli.StringFlag{ + Name: "input_file", + Usage: "(optional) the file to read the encrypted " + + "package from; if not specified, the debug " + + "package is read from stdin", + }, + }, + Action: actionDecorator(decryptDebugPackage), +} + +func decryptDebugPackage(ctx *cli.Context) error { + if ctx.NArg() == 0 && ctx.NumFlags() == 0 { + return cli.ShowCommandHelp(ctx, "decryptdebugpackage") + } + + var ( + args = ctx.Args() + privKeyBytes []byte + err error + ) + switch { + case ctx.IsSet("pubkey"): + privKeyBytes, err = hex.DecodeString(ctx.String("pubkey")) + case args.Present(): + privKeyBytes, err = hex.DecodeString(args.First()) + } + if err != nil { + return fmt.Errorf("unable to decode privkey argument: %w", err) + } + + privKey, _ := btcec.PrivKeyFromBytes(privKeyBytes) + + // Read the file from stdin and decode the JSON into a DebugPackage. + var pkg DebugPackage + if ctx.IsSet("input_file") { + fileName := lnd.CleanAndExpandPath(ctx.String("input_file")) + jsonBytes, err := os.ReadFile(fileName) + if err != nil { + return fmt.Errorf("unable to read file '%s': %w", + fileName, err) + } + + err = json.Unmarshal(jsonBytes, &pkg) + if err != nil { + return fmt.Errorf("unable to decode JSON: %w", err) + } + } else { + err = json.NewDecoder(os.Stdin).Decode(&pkg) + if err != nil { + return fmt.Errorf("unable to decode JSON: %w", err) + } + } + + // Decode the ephemeral public key and encrypted payload. + ephemeralPubKeyBytes, err := hex.DecodeString(pkg.EphemeralPubKey) + if err != nil { + return fmt.Errorf("unable to decode ephemeral public key: %w", + err) + } + encryptedPayloadBytes, err := hex.DecodeString(pkg.EncryptedPayload) + if err != nil { + return fmt.Errorf("unable to decode encrypted payload: %w", err) + } + + // Parse the ephemeral public key and create an encrypter. + ephemeralPubKey, err := btcec.ParsePubKey(ephemeralPubKeyBytes) + if err != nil { + return fmt.Errorf("unable to parse ephemeral public key: %w", + err) + } + enc, err := lnencrypt.ECDHEncrypter(privKey, ephemeralPubKey) + if err != nil { + return fmt.Errorf("unable to create encrypter: %w", err) + } + + // Decrypt the payload. + decryptedPayload, err := enc.DecryptPayloadFromReader( + bytes.NewReader(encryptedPayloadBytes), + ) + if err != nil { + return fmt.Errorf("unable to decrypt payload: %w", err) + } + + // Decompress the payload. + reader := brotli.NewReader(bytes.NewBuffer(decryptedPayload)) + decompressedPayload, err := io.ReadAll(reader) + if err != nil { + return fmt.Errorf("unable to decompress payload: %w", err) + } + + fmt.Println(string(decompressedPayload)) + + return nil +} diff --git a/cmd/lncli/main.go b/cmd/lncli/main.go index 801cb7562..deadbecdd 100644 --- a/cmd/lncli/main.go +++ b/cmd/lncli/main.go @@ -460,6 +460,8 @@ func main() { channelBalanceCommand, getInfoCommand, getDebugInfoCommand, + encryptDebugPackageCommand, + decryptDebugPackageCommand, getRecoveryInfoCommand, pendingChannelsCommand, sendPaymentCommand, diff --git a/go.mod b/go.mod index 83b160820..c29d4c659 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/lightningnetwork/lnd require ( github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82 github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 + github.com/andybalholm/brotli v1.0.3 github.com/btcsuite/btcd v0.23.5-0.20230905170901-80f5a0ffdf36 github.com/btcsuite/btcd/btcec/v2 v2.3.2 github.com/btcsuite/btcd/btcutil v1.1.4-0.20230904040416-d4f519f5dc05 @@ -75,7 +76,6 @@ require ( github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect github.com/aead/siphash v1.0.1 // indirect - github.com/andybalholm/brotli v1.0.3 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/btcsuite/btcwallet/wallet/txsizes v1.2.3 // indirect github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect