Skip to content

Commit a74ae88

Browse files
bitromortacellemouton
authored andcommitted
firewall: randomize responses with PrivacyMapper
Adds amount, timestamp, and channel initiator obfuscation to the two response handlers `handleFwdHistoryResponse` and `handleListChannelsResponse`. In order to preserve privacy and still ensure functioning of algorithms that rely on the randomized data, a trade-off between randomization and accuracy needs to be found. We choose ten minutes for forwarding timestamps as this breaks time correlation of payments. The amount obfuscation is chosen to be 5% and applies to the forwarding amount and channel details to hide balances. We also remove details of pending HTLCs in channels. Random obfuscation for amounts is chosen here instead of rounding to have non-deterministic alteration of amounts, which is especially important for forwardings to also break amount correlation. Randomly varying around a certain value will statistically skew averages less than rounding for algorithms that rely on aggregation of individual data. The privacy mapper is chosen to accept a randomness input in order to ensure deterministic testing even when other handlers are changed in the future.
1 parent 5a453cd commit a74ae88

File tree

3 files changed

+348
-38
lines changed

3 files changed

+348
-38
lines changed

firewall/privacy_mapper.go

Lines changed: 146 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,16 @@ var _ mid.RequestInterceptor = (*PrivacyMapper)(nil)
4646
// PrivacyMapper is a RequestInterceptor that maps any pseudo names in certain
4747
// requests to their real values and vice versa for responses.
4848
type PrivacyMapper struct {
49-
newDB firewalldb.NewPrivacyMapDB
49+
newDB firewalldb.NewPrivacyMapDB
50+
randIntn func(int) (int, error)
5051
}
5152

52-
// NewPrivacyMapper returns a new instance of PrivacyMapper.
53-
func NewPrivacyMapper(newDB firewalldb.NewPrivacyMapDB) *PrivacyMapper {
54-
return &PrivacyMapper{
55-
newDB: newDB,
56-
}
53+
// NewPrivacyMapper returns a new instance of PrivacyMapper. The randIntn
54+
// function is used to draw randomness for request field obfuscation.
55+
func NewPrivacyMapper(newDB firewalldb.NewPrivacyMapDB,
56+
randIntn func(int) (int, error)) *PrivacyMapper {
57+
58+
return &PrivacyMapper{newDB: newDB, randIntn: randIntn}
5759
}
5860

5961
// Name returns the name of the interceptor.
@@ -224,7 +226,7 @@ func (p *PrivacyMapper) checkers(
224226
"/lnrpc.Lightning/ForwardingHistory": mid.NewResponseRewriter(
225227
&lnrpc.ForwardingHistoryRequest{},
226228
&lnrpc.ForwardingHistoryResponse{},
227-
handleFwdHistoryResponse(db),
229+
handleFwdHistoryResponse(db, p.randIntn),
228230
mid.PassThroughErrorHandler,
229231
),
230232
"/lnrpc.Lightning/FeeReport": mid.NewResponseRewriter(
@@ -236,7 +238,8 @@ func (p *PrivacyMapper) checkers(
236238
&lnrpc.ListChannelsRequest{},
237239
&lnrpc.ListChannelsResponse{},
238240
handleListChannelsRequest(db),
239-
handleListChannelsResponse(db),
241+
handleListChannelsResponse(db, p.randIntn),
242+
240243
mid.PassThroughErrorHandler,
241244
),
242245
"/lnrpc.Lightning/UpdateChannelPolicy": mid.NewFullRewriter(
@@ -282,15 +285,16 @@ func handleGetInfoRequest(db firewalldb.PrivacyMapDB) func(ctx context.Context,
282285
}
283286
}
284287

285-
func handleFwdHistoryResponse(db firewalldb.PrivacyMapDB) func(
286-
ctx context.Context, r *lnrpc.ForwardingHistoryResponse) (proto.Message,
287-
error) {
288+
func handleFwdHistoryResponse(db firewalldb.PrivacyMapDB,
289+
randIntn func(int) (int, error)) func(ctx context.Context,
290+
r *lnrpc.ForwardingHistoryResponse) (proto.Message, error) {
288291

289-
return func(ctx context.Context, r *lnrpc.ForwardingHistoryResponse) (
292+
return func(_ context.Context, r *lnrpc.ForwardingHistoryResponse) (
290293
proto.Message, error) {
291294

292295
err := db.Update(func(tx firewalldb.PrivacyMapTx) error {
293296
for _, fe := range r.ForwardingEvents {
297+
// Deterministically hide channel ids.
294298
chanIn, err := firewalldb.HideUint64(
295299
tx, fe.ChanIdIn,
296300
)
@@ -306,6 +310,44 @@ func handleFwdHistoryResponse(db firewalldb.PrivacyMapDB) func(
306310
return err
307311
}
308312
fe.ChanIdOut = chanOut
313+
314+
// We randomize the outgoing amount for privacy.
315+
hiddenAmtOutMsat, err := hideAmount(
316+
randIntn, amountVariation,
317+
fe.AmtOutMsat,
318+
)
319+
if err != nil {
320+
return err
321+
}
322+
fe.AmtOutMsat = hiddenAmtOutMsat
323+
324+
// We randomize fees for privacy.
325+
hiddenFeeMsat, err := hideAmount(
326+
randIntn, amountVariation, fe.FeeMsat,
327+
)
328+
if err != nil {
329+
return err
330+
}
331+
fe.FeeMsat = hiddenFeeMsat
332+
333+
// Populate other fields in a consistent manner.
334+
fe.AmtInMsat = fe.AmtOutMsat + fe.FeeMsat
335+
fe.AmtOut = fe.AmtOutMsat / 1000
336+
fe.AmtIn = fe.AmtInMsat / 1000
337+
fe.Fee = fe.FeeMsat / 1000
338+
339+
// We randomize the forwarding timestamp.
340+
timestamp := time.Unix(0, int64(fe.TimestampNs))
341+
hiddenTimestamp, err := hideTimestamp(
342+
randIntn, timeVariation, timestamp,
343+
)
344+
if err != nil {
345+
return err
346+
}
347+
fe.TimestampNs = uint64(
348+
hiddenTimestamp.UnixNano(),
349+
)
350+
fe.Timestamp = uint64(hiddenTimestamp.Unix())
309351
}
310352
return nil
311353
})
@@ -382,36 +424,121 @@ func handleListChannelsRequest(db firewalldb.PrivacyMapDB) func(
382424
}
383425
}
384426

385-
func handleListChannelsResponse(db firewalldb.PrivacyMapDB) func(
386-
ctx context.Context, r *lnrpc.ListChannelsResponse) (proto.Message,
387-
error) {
427+
func handleListChannelsResponse(db firewalldb.PrivacyMapDB,
428+
randIntn func(int) (int, error)) func(ctx context.Context,
429+
r *lnrpc.ListChannelsResponse) (proto.Message, error) {
388430

389-
return func(ctx context.Context, r *lnrpc.ListChannelsResponse) (
431+
return func(_ context.Context, r *lnrpc.ListChannelsResponse) (
390432
proto.Message, error) {
391433

434+
hideAmount := func(a int64) (int64, error) {
435+
hiddenAmount, err := hideAmount(
436+
randIntn, amountVariation, uint64(a),
437+
)
438+
if err != nil {
439+
return 0, err
440+
}
441+
442+
return int64(hiddenAmount), nil
443+
}
444+
392445
err := db.Update(func(tx firewalldb.PrivacyMapTx) error {
393446
for i, c := range r.Channels {
447+
ch := r.Channels[i]
448+
449+
// Deterministically hide the peer pubkey,
450+
// the channel point, and the channel id.
394451
pk, err := firewalldb.HideString(
395452
tx, c.RemotePubkey,
396453
)
397454
if err != nil {
398455
return err
399456
}
400-
r.Channels[i].RemotePubkey = pk
457+
ch.RemotePubkey = pk
401458

402459
cp, err := firewalldb.HideChanPointStr(
403460
tx, c.ChannelPoint,
404461
)
405462
if err != nil {
406463
return err
407464
}
408-
r.Channels[i].ChannelPoint = cp
465+
ch.ChannelPoint = cp
409466

410467
cid, err := firewalldb.HideUint64(tx, c.ChanId)
411468
if err != nil {
412469
return err
413470
}
414-
r.Channels[i].ChanId = cid
471+
ch.ChanId = cid
472+
473+
// We hide the initiator.
474+
initiator, err := hideBool(randIntn)
475+
if err != nil {
476+
return err
477+
}
478+
ch.Initiator = initiator
479+
480+
// Consider the capacity to be public
481+
// information. We don't care about reserves, as
482+
// having some funds as a balance is the normal
483+
// state over the lifetime of a channel. The
484+
// balance would be zero only for the initial
485+
// state as a non-funder.
486+
487+
// We randomize local/remote balances.
488+
localBalance, err := hideAmount(c.LocalBalance)
489+
if err != nil {
490+
return err
491+
}
492+
493+
// We may have a too large value for the local
494+
// balance, restrict it to the capacity.
495+
if localBalance > c.Capacity {
496+
localBalance = c.Capacity
497+
}
498+
if ch.Initiator {
499+
localBalance -= ch.CommitFee
500+
}
501+
ch.LocalBalance = localBalance
502+
503+
// We adapt the remote balance accordingly.
504+
remoteBalance := c.Capacity - localBalance -
505+
c.CommitFee
506+
if !ch.Initiator {
507+
remoteBalance -= ch.CommitFee
508+
}
509+
ch.RemoteBalance = remoteBalance
510+
511+
// We hide the total sats sent and received.
512+
hiddenSatsReceived, err := hideAmount(
513+
c.TotalSatoshisReceived,
514+
)
515+
if err != nil {
516+
return err
517+
}
518+
ch.TotalSatoshisReceived = hiddenSatsReceived
519+
520+
hiddenSatsSent, err := hideAmount(
521+
c.TotalSatoshisSent,
522+
)
523+
if err != nil {
524+
return err
525+
}
526+
ch.TotalSatoshisSent = hiddenSatsSent
527+
528+
// We only keep track of the number of unsettled
529+
// HTLCs.
530+
ch.PendingHtlcs = make(
531+
[]*lnrpc.HTLC, len(ch.PendingHtlcs),
532+
)
533+
534+
// We hide the unsettled balance.
535+
unsettled, err := hideAmount(
536+
c.UnsettledBalance,
537+
)
538+
if err != nil {
539+
return err
540+
}
541+
ch.UnsettledBalance = unsettled
415542
}
416543

417544
return nil

0 commit comments

Comments
 (0)