Skip to content

Commit 5a453cd

Browse files
bitromortacellemouton
authored andcommitted
firewall: add amount fuzzing
Adds helper functions to randomize amounts, timestamps, and booleans. Amounts are randomized based on a percentage and timestamps based on an absolute scale.
1 parent b348c9d commit 5a453cd

File tree

2 files changed

+283
-2
lines changed

2 files changed

+283
-2
lines changed

firewall/privacy_mapper.go

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ package firewall
22

33
import (
44
"context"
5+
"crypto/rand"
56
"errors"
67
"fmt"
8+
"math/big"
9+
"time"
710

811
"github.com/lightninglabs/lightning-terminal/firewalldb"
912
mid "github.com/lightninglabs/lightning-terminal/rpcmiddleware"
@@ -12,8 +15,22 @@ import (
1215
"google.golang.org/protobuf/proto"
1316
)
1417

15-
// privacyMapperName is the name of the RequestLogger interceptor.
16-
const privacyMapperName = "lit-privacy-mapper"
18+
const (
19+
// privacyMapperName is the name of the RequestLogger interceptor.
20+
privacyMapperName = "lit-privacy-mapper"
21+
22+
// amountVariation and timeVariation are used to set the randomization
23+
// of amounts and timestamps that are sent to the autopilot. Changing
24+
// these values may lead to unintended consequences in the behavior of
25+
// the autpilot.
26+
amountVariation = 0.05
27+
timeVariation = time.Duration(10) * time.Minute
28+
29+
// minTimeVariation and maxTimeVariation are the acceptable bounds
30+
// between which timeVariation can be set.
31+
minTimeVariation = time.Minute
32+
maxTimeVariation = time.Duration(24) * time.Hour
33+
)
1734

1835
var (
1936
// ErrNotSupportedByPrivacyMapper indicates that the invoked RPC method
@@ -492,3 +509,115 @@ func handleUpdatePolicyResponse(db firewalldb.PrivacyMapDB) func(
492509
return r, nil
493510
}
494511
}
512+
513+
// hideAmount symmetrically randomizes an amount around a given relative
514+
// variation interval. relativeVariation should be between 0 and 1.
515+
func hideAmount(randIntn func(n int) (int, error), relativeVariation float64,
516+
amount uint64) (uint64, error) {
517+
518+
if relativeVariation < 0 || relativeVariation > 1 {
519+
return 0, fmt.Errorf("hide amount: relative variation is not "+
520+
"between allowed bounds of [0, 1], is %v",
521+
relativeVariation)
522+
}
523+
524+
if amount == 0 {
525+
return 0, nil
526+
}
527+
528+
// fuzzInterval is smaller than the amount provided fuzzVariation is
529+
// between 0 and 1.
530+
fuzzInterval := uint64(float64(amount) * relativeVariation)
531+
532+
amountMin := int(amount - fuzzInterval/2)
533+
amountMax := int(amount + fuzzInterval/2)
534+
535+
randAmount, err := randBetween(randIntn, amountMin, amountMax)
536+
if err != nil {
537+
return 0, err
538+
}
539+
540+
return uint64(randAmount), nil
541+
}
542+
543+
// hideTimestamp symmetrically randomizes a unix timestamp given an absolute
544+
// variation interval. The random input is expected to be rand.Intn.
545+
func hideTimestamp(randIntn func(n int) (int, error),
546+
absoluteVariation time.Duration,
547+
timestamp time.Time) (time.Time, error) {
548+
549+
if absoluteVariation < minTimeVariation ||
550+
absoluteVariation > maxTimeVariation {
551+
552+
return time.Time{}, fmt.Errorf("hide timestamp: absolute time "+
553+
"variation is out of bounds, have %v",
554+
absoluteVariation)
555+
}
556+
557+
// Don't fuzz meaningless timestamps.
558+
if timestamp.Add(-absoluteVariation).Unix() < 0 ||
559+
timestamp.IsZero() {
560+
561+
return timestamp, nil
562+
}
563+
564+
// We vary symmetrically around the provided timestamp.
565+
timeMin := timestamp.Add(-absoluteVariation / 2)
566+
timeMax := timestamp.Add(absoluteVariation / 2)
567+
568+
timeNs, err := randBetween(
569+
randIntn, int(timeMin.UnixNano()), int(timeMax.UnixNano()),
570+
)
571+
if err != nil {
572+
return time.Time{}, err
573+
}
574+
575+
return time.Unix(0, int64(timeNs)), nil
576+
}
577+
578+
// randBetween generates a random number between [min, max) given a source of
579+
// randomness.
580+
func randBetween(randIntn func(int) (int, error), min, max int) (int, error) {
581+
if max < min {
582+
return 0, fmt.Errorf("min is not allowed to be greater than "+
583+
"max, (min: %v, max: %v)", min, max)
584+
}
585+
586+
// We don't want to pass zero to randIntn to avoid panics.
587+
if max == min {
588+
return min, nil
589+
}
590+
591+
add, err := randIntn(max - min)
592+
if err != nil {
593+
return 0, err
594+
}
595+
596+
return min + add, nil
597+
}
598+
599+
// hideBool generates a random bool given a random input.
600+
func hideBool(randIntn func(n int) (int, error)) (bool, error) {
601+
random, err := randIntn(2)
602+
if err != nil {
603+
return false, err
604+
}
605+
606+
// For testing we may expect larger random numbers, which we map to
607+
// true.
608+
return random >= 1, nil
609+
}
610+
611+
// CryptoRandIntn generates a random number between [0, n).
612+
func CryptoRandIntn(n int) (int, error) {
613+
if n == 0 {
614+
return 0, nil
615+
}
616+
617+
nBig, err := rand.Int(rand.Reader, big.NewInt(int64(n)))
618+
if err != nil {
619+
return 0, err
620+
}
621+
622+
return int(nBig.Int64()), nil
623+
}

firewall/privacy_mapper_test.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package firewall
33
import (
44
"context"
55
"testing"
6+
"time"
67

78
"github.com/lightninglabs/lightning-terminal/firewalldb"
89
"github.com/lightninglabs/lightning-terminal/session"
@@ -366,3 +367,154 @@ func (m *mockPrivacyMapDB) RealToPseudo(real string) (string, error) {
366367
}
367368

368369
var _ firewalldb.PrivacyMapDB = (*mockPrivacyMapDB)(nil)
370+
371+
// TestRandBetween tests random number generation for numbers in an interval.
372+
func TestRandBetween(t *testing.T) {
373+
min := 0
374+
max := 10
375+
376+
for i := 0; i < 100; i++ {
377+
val, err := randBetween(CryptoRandIntn, min, max)
378+
require.NoError(t, err)
379+
require.Less(t, val, max)
380+
require.GreaterOrEqual(t, val, min)
381+
}
382+
}
383+
384+
// TestHideAmount tests that we hide amounts correctly.
385+
func TestHideAmount(t *testing.T) {
386+
testAmount := uint64(10_000)
387+
relativeVariation := 0.05
388+
fuzzInterval := int(float64(testAmount) * relativeVariation)
389+
390+
tests := []struct {
391+
name string
392+
amount uint64
393+
randIntFn func(int) (int, error)
394+
expected uint64
395+
}{
396+
{
397+
name: "zero test amount",
398+
randIntFn: func(int) (int, error) { return 0, nil },
399+
},
400+
{
401+
name: "test small amount",
402+
randIntFn: func(int) (int, error) { return 0, nil },
403+
amount: 1,
404+
expected: 1,
405+
},
406+
{
407+
name: "min value",
408+
randIntFn: func(int) (int, error) { return 0, nil },
409+
amount: testAmount,
410+
expected: 9750,
411+
},
412+
{
413+
name: "max value",
414+
randIntFn: func(int) (int, error) {
415+
return fuzzInterval, nil
416+
},
417+
amount: testAmount,
418+
expected: 10250,
419+
},
420+
{
421+
name: "some fuzz",
422+
randIntFn: func(int) (int, error) { return 123, nil },
423+
amount: testAmount,
424+
expected: 9750 + 123,
425+
},
426+
}
427+
428+
for _, test := range tests {
429+
test := test
430+
431+
t.Run(test.name, func(t *testing.T) {
432+
val, err := hideAmount(
433+
test.randIntFn,
434+
relativeVariation,
435+
test.amount,
436+
)
437+
require.NoError(t, err)
438+
require.Equal(t, test.expected, val)
439+
})
440+
}
441+
442+
// Subtest with real randomness.
443+
t.Run("real randomness for small numbers", func(t *testing.T) {
444+
for i := 0; i < 1000; i++ {
445+
_, err := hideAmount(
446+
CryptoRandIntn,
447+
relativeVariation,
448+
uint64(i),
449+
)
450+
require.NoError(t, err)
451+
}
452+
})
453+
}
454+
455+
// TestHideTimestamp test correct timestamp hiding.
456+
func TestHideTimestamp(t *testing.T) {
457+
timestamp := time.Unix(1_000_000, 0)
458+
absoluteVariation := time.Duration(10) * time.Minute
459+
460+
tests := []struct {
461+
name string
462+
randIntFn func(int) (int, error)
463+
timestamp time.Time
464+
expected time.Time
465+
}{
466+
{
467+
name: "zero timestamp",
468+
randIntFn: func(int) (int, error) { return 0, nil },
469+
},
470+
{
471+
name: "min value",
472+
randIntFn: func(int) (int, error) { return 0, nil },
473+
timestamp: timestamp,
474+
expected: time.Unix(999_700, 0),
475+
},
476+
{
477+
name: "max value",
478+
randIntFn: func(int) (int, error) {
479+
return int(absoluteVariation), nil
480+
},
481+
timestamp: timestamp,
482+
expected: time.Unix(1_000_300, 0),
483+
},
484+
{
485+
name: "some fuzz",
486+
randIntFn: func(int) (int, error) { return 123, nil },
487+
timestamp: timestamp,
488+
expected: time.Unix(999_700, 123),
489+
},
490+
}
491+
492+
for _, test := range tests {
493+
test := test
494+
495+
t.Run(test.name, func(t *testing.T) {
496+
val, err := hideTimestamp(
497+
test.randIntFn,
498+
absoluteVariation,
499+
test.timestamp,
500+
)
501+
require.NoError(t, err)
502+
require.Equal(t, test.expected, val)
503+
})
504+
}
505+
}
506+
507+
// TestHideBool test correct boolean hiding.
508+
func TestHideBool(t *testing.T) {
509+
val, err := hideBool(func(int) (int, error) { return 100, nil })
510+
require.NoError(t, err)
511+
require.True(t, val)
512+
513+
val, err = hideBool(func(int) (int, error) { return 1, nil })
514+
require.NoError(t, err)
515+
require.True(t, val)
516+
517+
val, err = hideBool(func(int) (int, error) { return 0, nil })
518+
require.NoError(t, err)
519+
require.False(t, val)
520+
}

0 commit comments

Comments
 (0)