cmd/lncli: add {encrypt,decrypt}debugpackage commands

This commit is contained in:
Oliver Gugger 2023-11-16 14:26:45 -06:00
parent b6abede4a3
commit d4f49cb04b
No known key found for this signature in database
GPG Key ID: 8E4256593F177720
3 changed files with 309 additions and 1 deletions

View File

@ -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
}

View File

@ -460,6 +460,8 @@ func main() {
channelBalanceCommand,
getInfoCommand,
getDebugInfoCommand,
encryptDebugPackageCommand,
decryptDebugPackageCommand,
getRecoveryInfoCommand,
pendingChannelsCommand,
sendPaymentCommand,

2
go.mod
View File

@ -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