Skip to content

Commit eb58941

Browse files
committed
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)) ```
1 parent 5580d68 commit eb58941

File tree

3 files changed

+378
-1
lines changed

3 files changed

+378
-1
lines changed

firewall/privacy_mapper.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@ package firewall
33
import (
44
"context"
55
"crypto/rand"
6+
"encoding/hex"
7+
"encoding/json"
68
"errors"
79
"fmt"
810
"math/big"
11+
"strconv"
12+
"strings"
913
"time"
1014

1115
"github.com/lightninglabs/lightning-terminal/firewalldb"
@@ -30,6 +34,15 @@ const (
3034
// between which timeVariation can be set.
3135
minTimeVariation = time.Minute
3236
maxTimeVariation = time.Duration(24) * time.Hour
37+
38+
// min and maxChanIDLen are the lengths to consider an int to be a
39+
// channel id. 13 corresponds to block height 1 and 20 to block height
40+
// 10_000_000.
41+
minChanIDLen = 13
42+
maxChanIDLen = 20
43+
44+
// pubKeyLen is the length of a node pubkey.
45+
pubKeyLen = 66
3346
)
3447

3548
var (
@@ -845,3 +858,145 @@ func CryptoRandIntn(n int) (int, error) {
845858

846859
return int(nBig.Int64()), nil
847860
}
861+
862+
// ObfuscateConfig alters the config string by replacing sensitive data with
863+
// random values and returns new replacement pairs. We only substitute items in
864+
// strings, numbers are left unchanged.
865+
func ObfuscateConfig(db firewalldb.PrivacyMapReader, configB []byte) ([]byte,
866+
map[string]string, error) {
867+
868+
if len(configB) == 0 {
869+
return nil, nil, nil
870+
}
871+
872+
// We assume that the config is a json dict.
873+
var configMap map[string]any
874+
err := json.Unmarshal(configB, &configMap)
875+
if err != nil {
876+
return nil, nil, err
877+
}
878+
879+
privMapPairs := make(map[string]string)
880+
newConfigMap := make(map[string]any)
881+
for k, v := range configMap {
882+
// We only substitute items in lists.
883+
list, ok := v.([]any)
884+
if !ok {
885+
newConfigMap[k] = v
886+
continue
887+
}
888+
889+
// We only substitute items in lists of strings.
890+
stringList := make([]string, len(list))
891+
anyString := false
892+
allStrings := true
893+
for i, item := range list {
894+
item, ok := item.(string)
895+
allStrings = allStrings && ok
896+
anyString = anyString || ok
897+
898+
if !ok {
899+
continue
900+
}
901+
902+
stringList[i] = item
903+
}
904+
if anyString && !allStrings {
905+
return nil, nil, fmt.Errorf("invalid config, "+
906+
"expected list of only strings for key %s", k)
907+
} else if !anyString {
908+
newConfigMap[k] = v
909+
continue
910+
}
911+
912+
obfuscatedValues := make([]string, len(stringList))
913+
for i, value := range stringList {
914+
value := strings.TrimSpace(value)
915+
916+
// We first check if we have a mapping for this value
917+
// already.
918+
obfVal, haveValue := db.GetPseudo(value)
919+
if haveValue {
920+
obfuscatedValues[i] = obfVal
921+
922+
continue
923+
}
924+
925+
// We check if we have obfuscated this value already in
926+
// this run.
927+
obfVal, haveValue = privMapPairs[value]
928+
if haveValue {
929+
obfuscatedValues[i] = obfVal
930+
931+
continue
932+
}
933+
934+
// From here on we create new obfuscated values.
935+
// Try to replace with a chan point.
936+
_, _, err := firewalldb.DecodeChannelPoint(value)
937+
if err == nil {
938+
obfVal, err = firewalldb.NewPseudoChanPoint()
939+
if err != nil {
940+
return nil, nil, err
941+
}
942+
943+
obfuscatedValues[i] = obfVal
944+
privMapPairs[value] = obfVal
945+
946+
continue
947+
}
948+
949+
// If the value is a pubkey, replace it with a random
950+
// value.
951+
_, err = hex.DecodeString(value)
952+
if err == nil && len(value) == pubKeyLen {
953+
obfVal, err := firewalldb.NewPseudoStr(
954+
len(value),
955+
)
956+
if err != nil {
957+
return nil, nil, err
958+
}
959+
960+
obfuscatedValues[i] = obfVal
961+
privMapPairs[value] = obfVal
962+
963+
continue
964+
}
965+
966+
// If the value is a channel id, replace it with
967+
// a random value.
968+
_, err = strconv.ParseInt(value, 10, 64)
969+
length := len(value)
970+
971+
// Channel ids can have different lenghts depending on
972+
// the blockheight, 20 is equivalent to 10E9 blocks.
973+
if err == nil && minChanIDLen <= length &&
974+
length <= maxChanIDLen {
975+
976+
obfVal, err := firewalldb.NewPseudoStr(length)
977+
if err != nil {
978+
return nil, nil, err
979+
}
980+
981+
obfuscatedValues[i] = obfVal
982+
privMapPairs[value] = obfVal
983+
984+
continue
985+
}
986+
987+
// If we don't have a replacement for this value, we
988+
// just leave it as is.
989+
obfuscatedValues[i] = value
990+
}
991+
992+
newConfigMap[k] = obfuscatedValues
993+
}
994+
995+
// Marshal the map back into a JSON blob.
996+
newConfigB, err := json.Marshal(newConfigMap)
997+
if err != nil {
998+
return nil, nil, err
999+
}
1000+
1001+
return newConfigB, privMapPairs, nil
1002+
}

firewall/privacy_mapper_test.go

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package firewall
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
67
"testing"
78
"time"
@@ -718,6 +719,196 @@ func TestHideBool(t *testing.T) {
718719
require.False(t, val)
719720
}
720721

722+
// TestObfuscateConfig tests that we substitute substrings in the config
723+
// correctly.
724+
func TestObfuscateConfig(t *testing.T) {
725+
tests := []struct {
726+
name string
727+
config []byte
728+
knownMap map[string]string
729+
expectedNewPairs int
730+
expectErr bool
731+
notExpectSameLen bool
732+
}{
733+
{
734+
name: "empty",
735+
},
736+
{
737+
// We substitue pubkeys of different forms.
738+
name: "several pubkeys",
739+
config: []byte(`{"version":1,"list":` +
740+
`["d23da57575cdcb878ac191e1e0c8a5c4f061b11cfdc7a8ec5c9d495270de66fdbf",` +
741+
`"0e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca85",` +
742+
`"DEAD2708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca85",` +
743+
`"586b59212da4623c40dcc68c4573da1719e5893630790c9f2db8940fff3efd8cd4"]}`),
744+
expectedNewPairs: 4,
745+
},
746+
{
747+
// We don't generate new pairs for pubkeys that we
748+
// already have a mapping.
749+
name: "several pubkeys with known replacement or duplicates",
750+
config: []byte(`{"version":1,"list":` +
751+
`["d23da57575cdcb878ac191e1e0c8a5c4f061b11cfdc7a8ec5c9d495270de66fdbf",` +
752+
`"0e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca85",` +
753+
`"DEAD2708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca85",` +
754+
`"0e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca85",` +
755+
`"586b59212da4623c40dcc68c4573da1719e5893630790c9f2db8940fff3efd8cd4"]}`),
756+
knownMap: map[string]string{
757+
"586b59212da4623c40dcc68c4573da1719e5893630790c9f2db8940fff3efd8cd4": "123456789012345678901234567890123456789012345678901234567890123456",
758+
},
759+
expectedNewPairs: 3,
760+
},
761+
{
762+
// We don't substitute unknown items.
763+
name: "all invalid pubkeys",
764+
config: []byte(`{"version":1,"list":` +
765+
`["d23da57575cdcb878ac191e1e0c8a5c4f061b11",` +
766+
`"586b59212da4623c40dcc68c4573da1719e5893630790c9f2db8940fff3efd8cd4dead",` +
767+
`"x86b59212da4623c40dcc68c4573da1719e5893630790c9f2db8940fff3efd8cd4"]}`),
768+
expectedNewPairs: 0,
769+
},
770+
{
771+
// We only substitute channel ids that have a sane
772+
// format.
773+
name: "channel ids",
774+
config: []byte(`{"version":1,"list":` +
775+
`["1",` +
776+
`"12345",` +
777+
`"1234567890123",` +
778+
`"1234567890123456789",` +
779+
`"123456789012345678901"]}`),
780+
expectedNewPairs: 2,
781+
},
782+
{
783+
// We obfuscate channel points, the character length may
784+
// vary due to the output index.
785+
name: "channel points",
786+
config: []byte(`{"version":1,"list":` +
787+
`["0e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca:1",` +
788+
`"e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca:1",` +
789+
`"0e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca3:1",` +
790+
`"0e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca:3000"]}`),
791+
expectedNewPairs: 2,
792+
notExpectSameLen: true,
793+
},
794+
{
795+
// We only act on items that are in lists of strings.
796+
name: "single pubkey with another field",
797+
config: []byte(`{"version":1,"list":` +
798+
`["586b59212da4623c40dcc68c4573da1719e5893630790c9f2db8940fff3efd8cd4"],` +
799+
`"another":"0e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca85"}`),
800+
expectedNewPairs: 1,
801+
},
802+
{
803+
// We don't obfuscate any numbers even though they may
804+
// be channel ids. This is to be able to set numerical
805+
// values in the range of channel ids.
806+
name: "number",
807+
config: []byte(`{"version":1,"number":12345678901234567890}`),
808+
expectedNewPairs: 0,
809+
},
810+
{
811+
// We don't allow to mix strings with other types, which
812+
// may be a configuration mistake.
813+
name: "list of invalid types",
814+
config: []byte(`{"version":1,"channels":` +
815+
`[12345,` +
816+
`"e092708c9e737115ff14a85ab65466561280d77c1b8cd666bc655536ad81ccca:1"]}`),
817+
expectErr: true,
818+
expectedNewPairs: 0,
819+
},
820+
{
821+
// A list of numbers is not obfuscated. Those can be
822+
// useful to submit histograms for example.
823+
name: "channel ids",
824+
config: []byte(`{"version":1,"list":` +
825+
`[1,` +
826+
`12345,` +
827+
`1234567890123,` +
828+
`1234567890123456789,` +
829+
`123456789012345678901]}`),
830+
expectedNewPairs: 0,
831+
},
832+
}
833+
834+
// assertConfigStructure checks that the structure of the config is
835+
// preserved.
836+
assertConfigStructure := func(wantConfig, gotConfig []byte) {
837+
t.Helper()
838+
839+
if len(wantConfig) == 0 {
840+
require.Equal(t, wantConfig, gotConfig)
841+
842+
return
843+
}
844+
845+
var wantConfigMap map[string]any
846+
err := json.Unmarshal(wantConfig, &wantConfigMap)
847+
require.NoError(t, err)
848+
849+
var gotConfigMap map[string]any
850+
err = json.Unmarshal(gotConfig, &gotConfigMap)
851+
require.NoError(t, err)
852+
853+
// We test that the number of top level items is the same.
854+
require.Equal(t, len(wantConfigMap), len(gotConfigMap))
855+
856+
listLen := func(config map[string]any) int {
857+
for k, v := range config {
858+
if k == "list" {
859+
list, ok := v.([]interface{})
860+
require.True(t, ok)
861+
return len(list)
862+
}
863+
}
864+
return 0
865+
}
866+
867+
// We test that we have the same number of items in the list.
868+
require.Equal(t, listLen(wantConfigMap), listLen(gotConfigMap))
869+
}
870+
871+
for _, tt := range tests {
872+
tt := tt
873+
874+
t.Run(tt.name, func(t *testing.T) {
875+
t.Parallel()
876+
877+
db := firewalldb.NewPrivacyMapPairs(tt.knownMap)
878+
879+
config, privMapPairs, err := ObfuscateConfig(
880+
db, tt.config,
881+
)
882+
if tt.expectErr {
883+
require.Error(t, err)
884+
return
885+
}
886+
require.NoError(t, err)
887+
888+
// We expect the config to be obfuscated in any parts
889+
// only if there is sensitive data.
890+
if tt.expectedNewPairs > 0 {
891+
require.NotEqual(t, config, tt.config)
892+
}
893+
894+
// We check that we recognized the correct number of new
895+
// substitutions.
896+
require.Equal(t, tt.expectedNewPairs,
897+
len(privMapPairs))
898+
899+
// We expect the same number of items in the config
900+
// after obfuscation.
901+
assertConfigStructure(tt.config, config)
902+
903+
// We don't perform exact length checks for cases where
904+
// we know the length can change.
905+
if !tt.notExpectSameLen {
906+
require.Equal(t, len(tt.config), len(config))
907+
}
908+
})
909+
}
910+
}
911+
721912
// mean computes the mean of the given slice of numbers.
722913
func mean(numbers []uint64) uint64 {
723914
sum := uint64(0)

0 commit comments

Comments
 (0)