mirror of
https://github.com/lightninglabs/lightning-terminal.git
synced 2026-08-13 12:33:36 +02:00
rpcserver+firewall: obfuscate configuration
We obfuscate pubkeys, channel points and ids entered in configurations. The channel id lengths for different block heights can be checked with: ```python len(str(1 << 40 | 2923 << 16 | 30)) len(str(10_000_000 << 40 | 2923 << 16 | 30)) ```
This commit is contained in:
parent
5580d6861d
commit
eb58941e76
3 changed files with 378 additions and 1 deletions
|
|
@ -3,9 +3,13 @@ package firewall
|
|||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/lightninglabs/lightning-terminal/firewalldb"
|
||||
|
|
@ -30,6 +34,15 @@ const (
|
|||
// between which timeVariation can be set.
|
||||
minTimeVariation = time.Minute
|
||||
maxTimeVariation = time.Duration(24) * time.Hour
|
||||
|
||||
// min and maxChanIDLen are the lengths to consider an int to be a
|
||||
// channel id. 13 corresponds to block height 1 and 20 to block height
|
||||
// 10_000_000.
|
||||
minChanIDLen = 13
|
||||
maxChanIDLen = 20
|
||||
|
||||
// pubKeyLen is the length of a node pubkey.
|
||||
pubKeyLen = 66
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -845,3 +858,145 @@ func CryptoRandIntn(n int) (int, error) {
|
|||
|
||||
return int(nBig.Int64()), nil
|
||||
}
|
||||
|
||||
// ObfuscateConfig alters the config string by replacing sensitive data with
|
||||
// random values and returns new replacement pairs. We only substitute items in
|
||||
// strings, numbers are left unchanged.
|
||||
func ObfuscateConfig(db firewalldb.PrivacyMapReader, configB []byte) ([]byte,
|
||||
map[string]string, error) {
|
||||
|
||||
if len(configB) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// We assume that the config is a json dict.
|
||||
var configMap map[string]any
|
||||
err := json.Unmarshal(configB, &configMap)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
privMapPairs := make(map[string]string)
|
||||
newConfigMap := make(map[string]any)
|
||||
for k, v := range configMap {
|
||||
// We only substitute items in lists.
|
||||
list, ok := v.([]any)
|
||||
if !ok {
|
||||
newConfigMap[k] = v
|
||||
continue
|
||||
}
|
||||
|
||||
// We only substitute items in lists of strings.
|
||||
stringList := make([]string, len(list))
|
||||
anyString := false
|
||||
allStrings := true
|
||||
for i, item := range list {
|
||||
item, ok := item.(string)
|
||||
allStrings = allStrings && ok
|
||||
anyString = anyString || ok
|
||||
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
stringList[i] = item
|
||||
}
|
||||
if anyString && !allStrings {
|
||||
return nil, nil, fmt.Errorf("invalid config, "+
|
||||
"expected list of only strings for key %s", k)
|
||||
} else if !anyString {
|
||||
newConfigMap[k] = v
|
||||
continue
|
||||
}
|
||||
|
||||
obfuscatedValues := make([]string, len(stringList))
|
||||
for i, value := range stringList {
|
||||
value := strings.TrimSpace(value)
|
||||
|
||||
// We first check if we have a mapping for this value
|
||||
// already.
|
||||
obfVal, haveValue := db.GetPseudo(value)
|
||||
if haveValue {
|
||||
obfuscatedValues[i] = obfVal
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// We check if we have obfuscated this value already in
|
||||
// this run.
|
||||
obfVal, haveValue = privMapPairs[value]
|
||||
if haveValue {
|
||||
obfuscatedValues[i] = obfVal
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// From here on we create new obfuscated values.
|
||||
// Try to replace with a chan point.
|
||||
_, _, err := firewalldb.DecodeChannelPoint(value)
|
||||
if err == nil {
|
||||
obfVal, err = firewalldb.NewPseudoChanPoint()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
obfuscatedValues[i] = obfVal
|
||||
privMapPairs[value] = obfVal
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// If the value is a pubkey, replace it with a random
|
||||
// value.
|
||||
_, err = hex.DecodeString(value)
|
||||
if err == nil && len(value) == pubKeyLen {
|
||||
obfVal, err := firewalldb.NewPseudoStr(
|
||||
len(value),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
obfuscatedValues[i] = obfVal
|
||||
privMapPairs[value] = obfVal
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// If the value is a channel id, replace it with
|
||||
// a random value.
|
||||
_, err = strconv.ParseInt(value, 10, 64)
|
||||
length := len(value)
|
||||
|
||||
// Channel ids can have different lenghts depending on
|
||||
// the blockheight, 20 is equivalent to 10E9 blocks.
|
||||
if err == nil && minChanIDLen <= length &&
|
||||
length <= maxChanIDLen {
|
||||
|
||||
obfVal, err := firewalldb.NewPseudoStr(length)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
obfuscatedValues[i] = obfVal
|
||||
privMapPairs[value] = obfVal
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// If we don't have a replacement for this value, we
|
||||
// just leave it as is.
|
||||
obfuscatedValues[i] = value
|
||||
}
|
||||
|
||||
newConfigMap[k] = obfuscatedValues
|
||||
}
|
||||
|
||||
// Marshal the map back into a JSON blob.
|
||||
newConfigB, err := json.Marshal(newConfigMap)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return newConfigB, privMapPairs, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package firewall
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
|
@ -718,6 +719,196 @@ func TestHideBool(t *testing.T) {
|
|||
require.False(t, val)
|
||||
}
|
||||
|
||||
// TestObfuscateConfig tests that we substitute substrings in the config
|
||||
// correctly.
|
||||
func TestObfuscateConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config []byte
|
||||
knownMap map[string]string
|
||||
expectedNewPairs int
|
||||
expectErr bool
|
||||
notExpectSameLen bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
},
|
||||
{
|
||||
// We substitue pubkeys of different forms.
|
||||
name: "several pubkeys",
|
||||
config: []byte(`{"version":1,"list":` +
|
||||
`["d23da57575cdcb878ac191e1e0c8a5c4f061b11cfdc7a8ec5c9d495270de66fdbf",` +
|
||||
`"0e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca85",` +
|
||||
`"DEAD2708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca85",` +
|
||||
`"586b59212da4623c40dcc68c4573da1719e5893630790c9f2db8940fff3efd8cd4"]}`),
|
||||
expectedNewPairs: 4,
|
||||
},
|
||||
{
|
||||
// We don't generate new pairs for pubkeys that we
|
||||
// already have a mapping.
|
||||
name: "several pubkeys with known replacement or duplicates",
|
||||
config: []byte(`{"version":1,"list":` +
|
||||
`["d23da57575cdcb878ac191e1e0c8a5c4f061b11cfdc7a8ec5c9d495270de66fdbf",` +
|
||||
`"0e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca85",` +
|
||||
`"DEAD2708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca85",` +
|
||||
`"0e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca85",` +
|
||||
`"586b59212da4623c40dcc68c4573da1719e5893630790c9f2db8940fff3efd8cd4"]}`),
|
||||
knownMap: map[string]string{
|
||||
"586b59212da4623c40dcc68c4573da1719e5893630790c9f2db8940fff3efd8cd4": "123456789012345678901234567890123456789012345678901234567890123456",
|
||||
},
|
||||
expectedNewPairs: 3,
|
||||
},
|
||||
{
|
||||
// We don't substitute unknown items.
|
||||
name: "all invalid pubkeys",
|
||||
config: []byte(`{"version":1,"list":` +
|
||||
`["d23da57575cdcb878ac191e1e0c8a5c4f061b11",` +
|
||||
`"586b59212da4623c40dcc68c4573da1719e5893630790c9f2db8940fff3efd8cd4dead",` +
|
||||
`"x86b59212da4623c40dcc68c4573da1719e5893630790c9f2db8940fff3efd8cd4"]}`),
|
||||
expectedNewPairs: 0,
|
||||
},
|
||||
{
|
||||
// We only substitute channel ids that have a sane
|
||||
// format.
|
||||
name: "channel ids",
|
||||
config: []byte(`{"version":1,"list":` +
|
||||
`["1",` +
|
||||
`"12345",` +
|
||||
`"1234567890123",` +
|
||||
`"1234567890123456789",` +
|
||||
`"123456789012345678901"]}`),
|
||||
expectedNewPairs: 2,
|
||||
},
|
||||
{
|
||||
// We obfuscate channel points, the character length may
|
||||
// vary due to the output index.
|
||||
name: "channel points",
|
||||
config: []byte(`{"version":1,"list":` +
|
||||
`["0e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca:1",` +
|
||||
`"e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca:1",` +
|
||||
`"0e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca3:1",` +
|
||||
`"0e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca:3000"]}`),
|
||||
expectedNewPairs: 2,
|
||||
notExpectSameLen: true,
|
||||
},
|
||||
{
|
||||
// We only act on items that are in lists of strings.
|
||||
name: "single pubkey with another field",
|
||||
config: []byte(`{"version":1,"list":` +
|
||||
`["586b59212da4623c40dcc68c4573da1719e5893630790c9f2db8940fff3efd8cd4"],` +
|
||||
`"another":"0e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca85"}`),
|
||||
expectedNewPairs: 1,
|
||||
},
|
||||
{
|
||||
// We don't obfuscate any numbers even though they may
|
||||
// be channel ids. This is to be able to set numerical
|
||||
// values in the range of channel ids.
|
||||
name: "number",
|
||||
config: []byte(`{"version":1,"number":12345678901234567890}`),
|
||||
expectedNewPairs: 0,
|
||||
},
|
||||
{
|
||||
// We don't allow to mix strings with other types, which
|
||||
// may be a configuration mistake.
|
||||
name: "list of invalid types",
|
||||
config: []byte(`{"version":1,"channels":` +
|
||||
`[12345,` +
|
||||
`"e092708c9e737115ff14a85ab65466561280d77c1b8cd666bc655536ad81ccca:1"]}`),
|
||||
expectErr: true,
|
||||
expectedNewPairs: 0,
|
||||
},
|
||||
{
|
||||
// A list of numbers is not obfuscated. Those can be
|
||||
// useful to submit histograms for example.
|
||||
name: "channel ids",
|
||||
config: []byte(`{"version":1,"list":` +
|
||||
`[1,` +
|
||||
`12345,` +
|
||||
`1234567890123,` +
|
||||
`1234567890123456789,` +
|
||||
`123456789012345678901]}`),
|
||||
expectedNewPairs: 0,
|
||||
},
|
||||
}
|
||||
|
||||
// assertConfigStructure checks that the structure of the config is
|
||||
// preserved.
|
||||
assertConfigStructure := func(wantConfig, gotConfig []byte) {
|
||||
t.Helper()
|
||||
|
||||
if len(wantConfig) == 0 {
|
||||
require.Equal(t, wantConfig, gotConfig)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var wantConfigMap map[string]any
|
||||
err := json.Unmarshal(wantConfig, &wantConfigMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
var gotConfigMap map[string]any
|
||||
err = json.Unmarshal(gotConfig, &gotConfigMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
// We test that the number of top level items is the same.
|
||||
require.Equal(t, len(wantConfigMap), len(gotConfigMap))
|
||||
|
||||
listLen := func(config map[string]any) int {
|
||||
for k, v := range config {
|
||||
if k == "list" {
|
||||
list, ok := v.([]interface{})
|
||||
require.True(t, ok)
|
||||
return len(list)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// We test that we have the same number of items in the list.
|
||||
require.Equal(t, listLen(wantConfigMap), listLen(gotConfigMap))
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := firewalldb.NewPrivacyMapPairs(tt.knownMap)
|
||||
|
||||
config, privMapPairs, err := ObfuscateConfig(
|
||||
db, tt.config,
|
||||
)
|
||||
if tt.expectErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
// We expect the config to be obfuscated in any parts
|
||||
// only if there is sensitive data.
|
||||
if tt.expectedNewPairs > 0 {
|
||||
require.NotEqual(t, config, tt.config)
|
||||
}
|
||||
|
||||
// We check that we recognized the correct number of new
|
||||
// substitutions.
|
||||
require.Equal(t, tt.expectedNewPairs,
|
||||
len(privMapPairs))
|
||||
|
||||
// We expect the same number of items in the config
|
||||
// after obfuscation.
|
||||
assertConfigStructure(tt.config, config)
|
||||
|
||||
// We don't perform exact length checks for cases where
|
||||
// we know the length can change.
|
||||
if !tt.notExpectSameLen {
|
||||
require.Equal(t, len(tt.config), len(config))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// mean computes the mean of the given slice of numbers.
|
||||
func mean(numbers []uint64) uint64 {
|
||||
sum := uint64(0)
|
||||
|
|
|
|||
|
|
@ -1125,6 +1125,37 @@ func (s *sessionRpcServer) AddAutopilotSession(ctx context.Context,
|
|||
prevSessionPub = linkedGroupSession.LocalPublicKey
|
||||
}
|
||||
|
||||
// The feature configurations may contain sensitive data like pubkeys,
|
||||
// which we replace here and add to the privacy map.
|
||||
obfuscatedConfig := make(session.FeaturesConfig, len(clientConfig))
|
||||
if privacy {
|
||||
for name, configB := range clientConfig {
|
||||
configB, privMapPairs, err := firewall.ObfuscateConfig(
|
||||
knownPrivMapPairs, configB,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Store the new privacy map pairs in the newPrivMap
|
||||
// pairs map so that they are later persisted to the
|
||||
// real priv map db.
|
||||
for k, v := range privMapPairs {
|
||||
newPrivMapPairs[k] = v
|
||||
}
|
||||
|
||||
// Also add the new pairs to the known set of pairs.
|
||||
err = knownPrivMapPairs.Add(privMapPairs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
obfuscatedConfig[name] = configB
|
||||
}
|
||||
} else {
|
||||
obfuscatedConfig = clientConfig
|
||||
}
|
||||
|
||||
// Register all the privacy map pairs for this session ID.
|
||||
privDB := s.cfg.privMap(sess.GroupID)
|
||||
err = privDB.Update(func(tx firewalldb.PrivacyMapTx) error {
|
||||
|
|
@ -1143,7 +1174,7 @@ func (s *sessionRpcServer) AddAutopilotSession(ctx context.Context,
|
|||
// Attempt to register the session with the Autopilot server.
|
||||
remoteKey, err := s.cfg.autopilot.RegisterSession(
|
||||
ctx, sess.LocalPublicKey, sess.ServerAddr, sess.DevServer,
|
||||
clientConfig, prevSessionPub, linkSig,
|
||||
obfuscatedConfig, prevSessionPub, linkSig,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error registering session with "+
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue