Skip to content

Commit

Permalink
Merge pull request lightningnetwork#8188 from guggero/debug-rpcs
Browse files Browse the repository at this point in the history
rpcserver+lncli: add ability to create encrypted debug information package
  • Loading branch information
guggero authored Jan 9, 2024
2 parents 2b54774 + ad34f80 commit 9afe1b7
Show file tree
Hide file tree
Showing 16 changed files with 4,288 additions and 3,289 deletions.
450 changes: 450 additions & 0 deletions cmd/lncli/cmd_debug.go

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions cmd/lncli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,9 @@ func main() {
walletBalanceCommand,
channelBalanceCommand,
getInfoCommand,
getDebugInfoCommand,
encryptDebugPackageCommand,
decryptDebugPackageCommand,
getRecoveryInfoCommand,
pendingChannelsCommand,
sendPaymentCommand,
Expand Down
93 changes: 93 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2111,3 +2111,96 @@ func checkEstimateMode(estimateMode string) error {
return fmt.Errorf("estimatemode must be one of the following: %v",
bitcoindEstimateModes[:])
}

// configToFlatMap converts the given config struct into a flat map of key/value
// pairs using the dot notation we are used to from the config file or command
// line flags.
func configToFlatMap(cfg Config) (map[string]string, error) {
result := make(map[string]string)

// redact is the helper function that redacts sensitive values like
// passwords.
redact := func(key, value string) string {
sensitiveKeySuffixes := []string{
"pass",
"password",
"dsn",
}
for _, suffix := range sensitiveKeySuffixes {
if strings.HasSuffix(key, suffix) {
return "[redacted]"
}
}

return value
}

// printConfig is the helper function that goes into nested structs
// recursively. Because we call it recursively, we need to declare it
// before we define it.
var printConfig func(reflect.Value, string)
printConfig = func(obj reflect.Value, prefix string) {
// Turn struct pointers into the actual struct, so we can
// iterate over the fields as we would with a struct value.
if obj.Kind() == reflect.Ptr {
obj = obj.Elem()
}

// Abort on nil values.
if !obj.IsValid() {
return
}

// Loop over all fields of the struct and inspect the type.
for i := 0; i < obj.NumField(); i++ {
field := obj.Field(i)
fieldType := obj.Type().Field(i)

longName := fieldType.Tag.Get("long")
namespace := fieldType.Tag.Get("namespace")
group := fieldType.Tag.Get("group")
switch {
// We have a long name defined, this is a config value.
case longName != "":
key := longName
if prefix != "" {
key = prefix + "." + key
}

// Add the value directly to the flattened map.
result[key] = redact(key, fmt.Sprintf(
"%v", field.Interface(),
))

// We have no long name but a namespace, this is a
// nested struct.
case longName == "" && namespace != "":
key := namespace
if prefix != "" {
key = prefix + "." + key
}

printConfig(field, key)

// Just a group means this is a dummy struct to house
// multiple config values, the group name doesn't go
// into the final field name.
case longName == "" && group != "":
printConfig(field, prefix)

// Anonymous means embedded struct. We need to recurse
// into it but without adding anything to the prefix.
case fieldType.Anonymous:
printConfig(field, prefix)

default:
continue
}
}
}

// Turn the whole config struct into a flat map.
printConfig(reflect.ValueOf(cfg), "")

return result, nil
}
48 changes: 48 additions & 0 deletions config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package lnd

import (
"fmt"
"testing"

"github.com/lightningnetwork/lnd/chainreg"
"github.com/lightningnetwork/lnd/routing"
"github.com/stretchr/testify/require"
)

var (
testPassword = "testpassword"
redactedPassword = "[redacted]"
)

// TestConfigToFlatMap tests that the configToFlatMap function works as
// expected on the default configuration.
func TestConfigToFlatMap(t *testing.T) {
cfg := DefaultConfig()
cfg.BitcoindMode.RPCPass = testPassword
cfg.BtcdMode.RPCPass = testPassword
cfg.Tor.Password = testPassword
cfg.DB.Etcd.Pass = testPassword
cfg.DB.Postgres.Dsn = testPassword

result, err := configToFlatMap(cfg)
require.NoError(t, err)

// Pick a couple of random values to check.
require.Equal(t, DefaultLndDir, result["lnddir"])
require.Equal(
t, fmt.Sprintf("%v", chainreg.DefaultBitcoinTimeLockDelta),
result["bitcoin.timelockdelta"],
)
require.Equal(
t, fmt.Sprintf("%v", routing.DefaultAprioriWeight),
result["routerrpc.apriori.weight"],
)
require.Contains(t, result, "routerrpc.routermacaroonpath")

// Check that sensitive values are not included.
require.Equal(t, redactedPassword, result["bitcoind.rpcpass"])
require.Equal(t, redactedPassword, result["btcd.rpcpass"])
require.Equal(t, redactedPassword, result["tor.password"])
require.Equal(t, redactedPassword, result["db.etcd.pass"])
require.Equal(t, redactedPassword, result["db.postgres.dsn"])
}
14 changes: 14 additions & 0 deletions docs/release-notes/release-notes-0.18.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,15 @@
the new payment status `Payment_INITIATED` should be used for payment-related
RPCs. It's recommended to use it to provide granular controls over payments.

* A [helper command (`lncli encryptdebugpackage`) for collecting and encrypting
useful debug information](https://github.com/lightningnetwork/lnd/pull/8188)
was added. This allows a user to collect the most relevant information about
their node with a single command and securely encrypt it to the public key of
a developer or support person. That way the person supporting the user with
their issue has an eas way to get all the information they usually require
without the user needing to publicly give away a lot of privacy-sensitive
data.

## RPC Additions

* [Deprecated](https://github.com/lightningnetwork/lnd/pull/7175)
Expand All @@ -100,6 +109,11 @@
* Adds a new rpc endpoint gettx to the walletrpc sub-server to [fetch
transaction details](https://github.com/lightningnetwork/lnd/pull/7654).

* [The new `GetDebugInfo` RPC method was added that returns the full runtime
configuration of the node as well as the complete log
file](https://github.com/lightningnetwork/lnd/pull/8188). The corresponding
`lncli getdebuginfo` command was also added.

## lncli Additions

# Improvements
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
23 changes: 21 additions & 2 deletions lnencrypt/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
"crypto/sha256"
"fmt"
"io"
"io/ioutil"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/keychain"
"golang.org/x/crypto/chacha20poly1305"
)
Expand Down Expand Up @@ -69,6 +69,25 @@ func KeyRingEncrypter(keyRing keychain.KeyRing) (*Encrypter, error) {
}, nil
}

// ECDHEncrypter derives an encryption key by performing an ECDH operation on
// the passed keys. The resulting key is used to encrypt or decrypt files with
// sensitive content.
func ECDHEncrypter(localKey *btcec.PrivateKey,
remoteKey *btcec.PublicKey) (*Encrypter, error) {

ecdh := keychain.PrivKeyECDH{
PrivKey: localKey,
}
encryptionKey, err := ecdh.ECDH(remoteKey)
if err != nil {
return nil, fmt.Errorf("error deriving encryption key: %w", err)
}

return &Encrypter{
encryptionKey: encryptionKey[:],
}, nil
}

// EncryptPayloadToWriter attempts to write the set of provided bytes into the
// passed io.Writer in an encrypted form. We use a 24-byte chachapoly AEAD
// instance with a randomized nonce that's pre-pended to the final payload and
Expand Down Expand Up @@ -112,7 +131,7 @@ func (e Encrypter) DecryptPayloadFromReader(payload io.Reader) ([]byte,

// Next, we'll read out the entire blob as we need to isolate the nonce
// from the rest of the ciphertext.
packedPayload, err := ioutil.ReadAll(payload)
packedPayload, err := io.ReadAll(payload)
if err != nil {
return nil, err
}
Expand Down
105 changes: 53 additions & 52 deletions lnencrypt/crypto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"testing"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -35,7 +36,8 @@ func TestEncryptDecryptPayload(t *testing.T) {
{
plaintext: []byte("payload test plain text"),
mutator: func(p *[]byte) {
// Flip a byte in the payload to render it invalid.
// Flip a byte in the payload to render it
// invalid.
(*p)[0] ^= 1
},
valid: false,
Expand All @@ -53,54 +55,55 @@ func TestEncryptDecryptPayload(t *testing.T) {
}

keyRing := &MockKeyRing{}

for i, payloadCase := range payloadCases {
var cipherBuffer bytes.Buffer
encrypter, err := KeyRingEncrypter(keyRing)
require.NoError(t, err)

// First, we'll encrypt the passed payload with our scheme.
err = encrypter.EncryptPayloadToWriter(
payloadCase.plaintext, &cipherBuffer,
)
if err != nil {
t.Fatalf("unable encrypt paylaod: %v", err)
}

// If we have a mutator, then we'll wrong the mutator over the
// cipher text, then reset the main buffer and re-write the new
// cipher text.
if payloadCase.mutator != nil {
cipherText := cipherBuffer.Bytes()

payloadCase.mutator(&cipherText)

cipherBuffer.Reset()
cipherBuffer.Write(cipherText)
}

plaintext, err := encrypter.DecryptPayloadFromReader(
&cipherBuffer,
)

switch {
// If this was meant to be a valid decryption, but we failed,
// then we'll return an error.
case err != nil && payloadCase.valid:
t.Fatalf("unable to decrypt valid payload case %v", i)

// If this was meant to be an invalid decryption, and we didn't
// fail, then we'll return an error.
case err == nil && !payloadCase.valid:
t.Fatalf("payload was invalid yet was able to decrypt")
}

// Only if this case was mean to be valid will we ensure the
// resulting decrypted plaintext matches the original input.
if payloadCase.valid &&
!bytes.Equal(plaintext, payloadCase.plaintext) {
t.Fatalf("#%v: expected %v, got %v: ", i,
payloadCase.plaintext, plaintext)
keyRingEnc, err := KeyRingEncrypter(keyRing)
require.NoError(t, err)

_, pubKey := btcec.PrivKeyFromBytes([]byte{0x01, 0x02, 0x03, 0x04})

privKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
privKeyEnc, err := ECDHEncrypter(privKey, pubKey)
require.NoError(t, err)

for _, payloadCase := range payloadCases {
payloadCase := payloadCase
for _, enc := range []*Encrypter{keyRingEnc, privKeyEnc} {
enc := enc

// First, we'll encrypt the passed payload with our
// scheme.
var cipherBuffer bytes.Buffer
err = enc.EncryptPayloadToWriter(
payloadCase.plaintext, &cipherBuffer,
)
require.NoError(t, err)

// If we have a mutator, then we'll wrong the mutator
// over the cipher text, then reset the main buffer and
// re-write the new cipher text.
if payloadCase.mutator != nil {
cipherText := cipherBuffer.Bytes()

payloadCase.mutator(&cipherText)

cipherBuffer.Reset()
cipherBuffer.Write(cipherText)
}

plaintext, err := enc.DecryptPayloadFromReader(
&cipherBuffer,
)

if !payloadCase.valid {
require.Error(t, err)

continue
}

require.NoError(t, err)
require.Equal(
t, plaintext, payloadCase.plaintext,
)
}
}
}
Expand All @@ -111,7 +114,5 @@ func TestInvalidKeyGeneration(t *testing.T) {
t.Parallel()

_, err := KeyRingEncrypter(&MockKeyRing{true})
if err == nil {
t.Fatal("expected error due to fail key gen")
}
require.Error(t, err)
}
Loading

0 comments on commit 9afe1b7

Please sign in to comment.