diff --git a/firewall/privacy_mapper.go b/firewall/privacy_mapper.go index f9c92478..2a57e976 100644 --- a/firewall/privacy_mapper.go +++ b/firewall/privacy_mapper.go @@ -110,12 +110,6 @@ func (p *PrivacyMapper) Intercept(ctx context.Context, return nil, fmt.Errorf("could not extract ID from macaroon") } - // Get group ID for session ID. - groupID, err := p.sessionDB.GetGroupID(sessionID) - if err != nil { - return nil, err - } - log.Tracef("PrivacyMapper: Intercepting %v", ri) switch r := req.InterceptType.(type) { @@ -133,7 +127,7 @@ func (p *PrivacyMapper) Intercept(ctx context.Context, } replacement, err := p.checkAndReplaceIncomingRequest( - ctx, r.Request.MethodFullUri, msg, groupID, + ctx, r.Request.MethodFullUri, msg, sessionID, ) if err != nil { return mid.RPCErr(req, err) @@ -167,7 +161,7 @@ func (p *PrivacyMapper) Intercept(ctx context.Context, } replacement, err := p.replaceOutgoingResponse( - ctx, r.Response.MethodFullUri, msg, groupID, + ctx, r.Response.MethodFullUri, msg, sessionID, ) if err != nil { return mid.RPCErr(req, err) @@ -192,14 +186,19 @@ func (p *PrivacyMapper) Intercept(ctx context.Context, // checkAndReplaceIncomingRequest inspects an incoming request and optionally // modifies some of the request parameters. func (p *PrivacyMapper) checkAndReplaceIncomingRequest(ctx context.Context, - uri string, req proto.Message, groupID session.ID) (proto.Message, + uri string, req proto.Message, sessionID session.ID) (proto.Message, error) { - db := p.newDB(groupID) + session, err := p.sessionDB.GetSessionByID(sessionID) + if err != nil { + return nil, err + } + + db := p.newDB(session.GroupID) // If we don't have a handler for the URI, we don't allow the request // to go through. - checker, ok := p.checkers(db)[uri] + checker, ok := p.checkers(db, session.PrivacyFlags)[uri] if !ok { return nil, ErrNotSupportedByPrivacyMapper } @@ -218,13 +217,18 @@ func (p *PrivacyMapper) checkAndReplaceIncomingRequest(ctx context.Context, // replaceOutgoingResponse inspects the responses before sending them out to the // client and replaces them if needed. func (p *PrivacyMapper) replaceOutgoingResponse(ctx context.Context, uri string, - resp proto.Message, groupID session.ID) (proto.Message, error) { + resp proto.Message, sessionID session.ID) (proto.Message, error) { - db := p.newDB(groupID) + session, err := p.sessionDB.GetSessionByID(sessionID) + if err != nil { + return nil, err + } + + db := p.newDB(session.GroupID) // If we don't have a handler for the URI, we don't allow the response // to go to avoid accidental leaks. - checker, ok := p.checkers(db)[uri] + checker, ok := p.checkers(db, session.PrivacyFlags)[uri] if !ok { return nil, ErrNotSupportedByPrivacyMapper } @@ -240,64 +244,66 @@ func (p *PrivacyMapper) replaceOutgoingResponse(ctx context.Context, uri string, return checker.HandleResponse(ctx, resp) } -func (p *PrivacyMapper) checkers( - db firewalldb.PrivacyMapDB) map[string]mid.RoundTripChecker { +func (p *PrivacyMapper) checkers(db firewalldb.PrivacyMapDB, + flags session.PrivacyFlags) map[string]mid.RoundTripChecker { return map[string]mid.RoundTripChecker{ "/lnrpc.Lightning/GetInfo": mid.NewResponseRewriter( &lnrpc.GetInfoRequest{}, &lnrpc.GetInfoResponse{}, - handleGetInfoResponse(db), mid.PassThroughErrorHandler, + handleGetInfoResponse(db, flags), + mid.PassThroughErrorHandler, ), "/lnrpc.Lightning/ForwardingHistory": mid.NewResponseRewriter( &lnrpc.ForwardingHistoryRequest{}, &lnrpc.ForwardingHistoryResponse{}, - handleFwdHistoryResponse(db, p.randIntn), + handleFwdHistoryResponse(db, flags, p.randIntn), mid.PassThroughErrorHandler, ), "/lnrpc.Lightning/FeeReport": mid.NewResponseRewriter( &lnrpc.FeeReportRequest{}, &lnrpc.FeeReportResponse{}, - handleFeeReportResponse(db), + handleFeeReportResponse(db, flags), mid.PassThroughErrorHandler, ), "/lnrpc.Lightning/ListChannels": mid.NewFullRewriter( &lnrpc.ListChannelsRequest{}, &lnrpc.ListChannelsResponse{}, - handleListChannelsRequest(db), - handleListChannelsResponse(db, p.randIntn), + handleListChannelsRequest(db, flags), + handleListChannelsResponse(db, flags, p.randIntn), mid.PassThroughErrorHandler, ), "/lnrpc.Lightning/UpdateChannelPolicy": mid.NewFullRewriter( &lnrpc.PolicyUpdateRequest{}, &lnrpc.PolicyUpdateResponse{}, - handleUpdatePolicyRequest(db), - handleUpdatePolicyResponse(db), + handleUpdatePolicyRequest(db, flags), + handleUpdatePolicyResponse(db, flags), mid.PassThroughErrorHandler, ), } } -func handleGetInfoResponse(db firewalldb.PrivacyMapDB) func(ctx context.Context, +func handleGetInfoResponse(db firewalldb.PrivacyMapDB, + flags session.PrivacyFlags) func(ctx context.Context, r *lnrpc.GetInfoResponse) (proto.Message, error) { - return func(ctx context.Context, r *lnrpc.GetInfoResponse) ( + return func(_ context.Context, r *lnrpc.GetInfoResponse) ( proto.Message, error) { - var pseudoPubKey string - err := db.Update( - func(tx firewalldb.PrivacyMapTx) error { - var err error - pseudoPubKey, err = firewalldb.HideString( - tx, r.IdentityPubkey, - ) - if err != nil { - return err - } + // We hide the pubkey unless it is disabled. + pseudoPubKey := r.IdentityPubkey + if !flags.Contains(session.ClearPubkeys) { + err := db.Update( + func(tx firewalldb.PrivacyMapTx) error { + var err error + pseudoPubKey, err = firewalldb.HideString( + tx, r.IdentityPubkey, + ) - return nil - }, - ) - if err != nil { - return nil, err + return err + }, + ) + if err != nil { + return nil, err + } } return &lnrpc.GetInfoResponse{ @@ -327,6 +333,7 @@ func handleGetInfoResponse(db firewalldb.PrivacyMapDB) func(ctx context.Context, } func handleFwdHistoryResponse(db firewalldb.PrivacyMapDB, + flags session.PrivacyFlags, randIntn func(int) (int, error)) func(ctx context.Context, r *lnrpc.ForwardingHistoryResponse) (proto.Message, error) { @@ -339,36 +346,48 @@ func handleFwdHistoryResponse(db firewalldb.PrivacyMapDB, err := db.Update(func(tx firewalldb.PrivacyMapTx) error { for i, fe := range r.ForwardingEvents { - // Deterministically hide channel ids. - chanIn, err := firewalldb.HideUint64( - tx, fe.ChanIdIn, - ) - if err != nil { - return err + var err error + + chanIn := fe.ChanIdIn + chanOut := fe.ChanIdOut + if !flags.Contains(session.ClearChanIDs) { + // Deterministically hide channel ids. + chanIn, err = firewalldb.HideUint64( + tx, chanIn, + ) + if err != nil { + return err + } + + chanOut, err = firewalldb.HideUint64( + tx, chanOut, + ) + if err != nil { + return err + } } - chanOut, err := firewalldb.HideUint64( - tx, fe.ChanIdOut, - ) - if err != nil { - return err - } + amtOutMsat := fe.AmtOutMsat + feeMsat := fe.FeeMsat + if !flags.Contains(session.ClearAmounts) { + // We randomize the outgoing amount for + // privacy. + amtOutMsat, err = hideAmount( + randIntn, amountVariation, + amtOutMsat, + ) + if err != nil { + return err + } - // We randomize the outgoing amount for privacy. - amtOutMsat, err := hideAmount( - randIntn, amountVariation, - fe.AmtOutMsat, - ) - if err != nil { - return err - } - - // We randomize fees for privacy. - feeMsat, err := hideAmount( - randIntn, amountVariation, fe.FeeMsat, - ) - if err != nil { - return err + // We randomize fees for privacy. + feeMsat, err = hideAmount( + randIntn, amountVariation, + feeMsat, + ) + if err != nil { + return err + } } // Populate other fields in a consistent manner. @@ -377,13 +396,16 @@ func handleFwdHistoryResponse(db firewalldb.PrivacyMapDB, amtIn := amtInMsat / 1000 fee := feeMsat / 1000 - // We randomize the forwarding timestamp. - timestamp, err := hideTimestamp( - randIntn, timeVariation, - time.Unix(0, int64(fe.TimestampNs)), - ) - if err != nil { - return err + timestamp := time.Unix(0, int64(fe.TimestampNs)) + if !flags.Contains(session.ClearTimeStamps) { + // We randomize the forwarding timestamp. + timestamp, err = hideTimestamp( + randIntn, timeVariation, + timestamp, + ) + if err != nil { + return err + } } fwdEvents[i] = &lnrpc.ForwardingEvent{ @@ -416,9 +438,9 @@ func handleFwdHistoryResponse(db firewalldb.PrivacyMapDB, } } -func handleFeeReportResponse(db firewalldb.PrivacyMapDB) func( - ctx context.Context, r *lnrpc.FeeReportResponse) (proto.Message, - error) { +func handleFeeReportResponse(db firewalldb.PrivacyMapDB, + flags session.PrivacyFlags) func(ctx context.Context, + r *lnrpc.FeeReportResponse) (proto.Message, error) { return func(ctx context.Context, r *lnrpc.FeeReportResponse) ( proto.Message, error) { @@ -426,19 +448,27 @@ func handleFeeReportResponse(db firewalldb.PrivacyMapDB) func( chanFees := make([]*lnrpc.ChannelFeeReport, len(r.ChannelFees)) err := db.Update(func(tx firewalldb.PrivacyMapTx) error { + var err error + for i, c := range r.ChannelFees { - chanID, err := firewalldb.HideUint64( - tx, c.ChanId, - ) - if err != nil { - return err + chanID := c.ChanId + if !flags.Contains(session.ClearChanIDs) { + chanID, err = firewalldb.HideUint64( + tx, chanID, + ) + if err != nil { + return err + } } - chanPoint, err := firewalldb.HideChanPointStr( - tx, c.ChannelPoint, - ) - if err != nil { - return err + chanPoint := c.ChannelPoint + if !flags.Contains(session.ClearChanIDs) { + chanPoint, err = firewalldb.HideChanPointStr( + tx, chanPoint, + ) + if err != nil { + return err + } } chanFees[i] = &lnrpc.ChannelFeeReport{ @@ -465,9 +495,9 @@ func handleFeeReportResponse(db firewalldb.PrivacyMapDB) func( } } -func handleListChannelsRequest(db firewalldb.PrivacyMapDB) func( - ctx context.Context, r *lnrpc.ListChannelsRequest) (proto.Message, - error) { +func handleListChannelsRequest(db firewalldb.PrivacyMapDB, + flags session.PrivacyFlags) func(ctx context.Context, + r *lnrpc.ListChannelsRequest) (proto.Message, error) { return func(ctx context.Context, r *lnrpc.ListChannelsRequest) ( proto.Message, error) { @@ -476,6 +506,10 @@ func handleListChannelsRequest(db firewalldb.PrivacyMapDB) func( return nil, nil } + if flags.Contains(session.ClearPubkeys) { + return r, nil + } + err := db.View(func(tx firewalldb.PrivacyMapTx) error { peer, err := firewalldb.RevealBytes(tx, r.Peer) if err != nil { @@ -494,6 +528,7 @@ func handleListChannelsRequest(db firewalldb.PrivacyMapDB) func( } func handleListChannelsResponse(db firewalldb.PrivacyMapDB, + flags session.PrivacyFlags, randIntn func(int) (int, error)) func(ctx context.Context, r *lnrpc.ListChannelsResponse) (proto.Message, error) { @@ -501,47 +536,66 @@ func handleListChannelsResponse(db firewalldb.PrivacyMapDB, proto.Message, error) { hideAmount := func(a int64) (int64, error) { - hiddenAmount, err := hideAmount( - randIntn, amountVariation, uint64(a), - ) - if err != nil { - return 0, err + if !flags.Contains(session.ClearAmounts) { + hiddenAmount, err := hideAmount( + randIntn, amountVariation, uint64(a), + ) + if err != nil { + return 0, err + } + + return int64(hiddenAmount), nil } - return int64(hiddenAmount), nil + return a, nil } + hidePubkeys := !flags.Contains(session.ClearPubkeys) + hideChanIds := !flags.Contains(session.ClearChanIDs) + channels := make([]*lnrpc.Channel, len(r.Channels)) err := db.Update(func(tx firewalldb.PrivacyMapTx) error { for i, c := range r.Channels { - // Deterministically hide the peer pubkey, - // the channel point, and the channel id. - remotePub, err := firewalldb.HideString( - tx, c.RemotePubkey, - ) - if err != nil { - return err + var err error + + // We hide the remote pubkey unless it is + // disabled. + remotePub := c.RemotePubkey + if hidePubkeys { + remotePub, err = firewalldb.HideString( + tx, c.RemotePubkey, + ) + if err != nil { + return err + } } - chanPoint, err := firewalldb.HideChanPointStr( - tx, c.ChannelPoint, - ) - if err != nil { - return err - } + chanPoint := c.ChannelPoint + chanID := c.ChanId + if hideChanIds { + chanPoint, err = firewalldb.HideChanPointStr( + tx, c.ChannelPoint, + ) + if err != nil { + return err + } - chanID, err := firewalldb.HideUint64( - tx, c.ChanId, - ) - if err != nil { - return err + chanID, err = firewalldb.HideUint64( + tx, c.ChanId, + ) + if err != nil { + return err + } } // We hide the initiator. - initiator, err := hideBool(randIntn) - if err != nil { - return err + initiator := c.Initiator + if !flags.Contains(session.ClearChanInitiator) { + initiator, err = hideBool(randIntn) + if err != nil { + return err + } } // Consider the capacity to be public @@ -563,8 +617,12 @@ func handleListChannelsResponse(db firewalldb.PrivacyMapDB, localBalance = c.Capacity } - // We adapt the remote balance accordingly. - remoteBalance := c.Capacity - localBalance + remoteBalance := c.RemoteBalance + if !flags.Contains(session.ClearAmounts) { + // We adapt the remote balance + // accordingly. + remoteBalance = c.Capacity - localBalance + } // We hide the total sats sent and received. satsReceived, err := hideAmount( @@ -587,6 +645,11 @@ func handleListChannelsResponse(db firewalldb.PrivacyMapDB, []*lnrpc.HTLC, len(c.PendingHtlcs), ) + // Only show the HTLCs if the flag is set. + if flags.Contains(session.ClearHTLCs) { + copy(pendingHtlcs, c.PendingHtlcs) + } + // We hide the unsettled balance. unsettled, err := hideAmount(c.UnsettledBalance) if err != nil { @@ -648,11 +711,11 @@ func handleListChannelsResponse(db firewalldb.PrivacyMapDB, } } -func handleUpdatePolicyRequest(db firewalldb.PrivacyMapDB) func( - ctx context.Context, r *lnrpc.PolicyUpdateRequest) (proto.Message, - error) { +func handleUpdatePolicyRequest(db firewalldb.PrivacyMapDB, + flags session.PrivacyFlags) func(ctx context.Context, + r *lnrpc.PolicyUpdateRequest) (proto.Message, error) { - return func(ctx context.Context, r *lnrpc.PolicyUpdateRequest) ( + return func(_ context.Context, r *lnrpc.PolicyUpdateRequest) ( proto.Message, error) { chanPoint := r.GetChanPoint() @@ -668,21 +731,19 @@ func handleUpdatePolicyRequest(db firewalldb.PrivacyMapDB) func( return nil, err } - index := chanPoint.GetOutputIndex() - - var ( - newTxid string - newIndex uint32 - ) - err = db.View(func(tx firewalldb.PrivacyMapTx) error { - var err error - newTxid, newIndex, err = firewalldb.RevealChanPoint( - tx, txid.String(), index, - ) - return err - }) - if err != nil { - return nil, err + newTxid := txid.String() + newIndex := chanPoint.GetOutputIndex() + if !flags.Contains(session.ClearChanIDs) { + err = db.View(func(tx firewalldb.PrivacyMapTx) error { + var err error + newTxid, newIndex, err = firewalldb.RevealChanPoint( + tx, newTxid, newIndex, + ) + return err + }) + if err != nil { + return nil, err + } } r.Scope = &lnrpc.PolicyUpdateRequest_ChanPoint{ @@ -698,13 +759,17 @@ func handleUpdatePolicyRequest(db firewalldb.PrivacyMapDB) func( } } -func handleUpdatePolicyResponse(db firewalldb.PrivacyMapDB) func( - ctx context.Context, r *lnrpc.PolicyUpdateResponse) (proto.Message, - error) { +func handleUpdatePolicyResponse(db firewalldb.PrivacyMapDB, + flags session.PrivacyFlags) func(ctx context.Context, + r *lnrpc.PolicyUpdateResponse) (proto.Message, error) { - return func(ctx context.Context, r *lnrpc.PolicyUpdateResponse) ( + return func(_ context.Context, r *lnrpc.PolicyUpdateResponse) ( proto.Message, error) { + if flags.Contains(session.ClearChanIDs) { + return r, nil + } + failedUpdates := make( []*lnrpc.FailedUpdate, len(r.FailedUpdates), ) diff --git a/firewall/privacy_mapper_test.go b/firewall/privacy_mapper_test.go index a58a6b1c..4c302e8c 100644 --- a/firewall/privacy_mapper_test.go +++ b/firewall/privacy_mapper_test.go @@ -19,8 +19,58 @@ import ( // TestPrivacyMapper tests that the PrivacyMapper correctly intercepts specific // RPC calls. func TestPrivacyMapper(t *testing.T) { + var ( + clearForwarding = &lnrpc.ForwardingHistoryResponse{ + ForwardingEvents: []*lnrpc.ForwardingEvent{ + { + AmtIn: 2_000, + AmtInMsat: 2_000_000, + AmtOut: 1_000, + AmtOutMsat: 1_000_000, + Fee: 1_000, + FeeMsat: 1_000_000, + Timestamp: 1_000, + TimestampNs: 1_000_000_000_000, + ChanIdIn: 123, + ChanIdOut: 321, + }, + { + AmtIn: 3_000, + AmtInMsat: 3_000_000, + AmtOut: 2_000, + AmtOutMsat: 2_000_000, + Fee: 1_000, + FeeMsat: 1_000_000, + Timestamp: 1_000, + TimestampNs: 1_000_000_000_000, + ChanIdIn: 678, + ChanIdOut: 876, + }, + }, + } + + clearListChannel = &lnrpc.ListChannelsResponse{ + Channels: []*lnrpc.Channel{ + { + Capacity: 1_000_000, + RemoteBalance: 600_000, + LocalBalance: 499_000, + CommitFee: 1_000, + TotalSatoshisSent: 500_000, + TotalSatoshisReceived: 450_000, + RemotePubkey: "01020304", + Initiator: false, + ChanId: 123, + ChannelPoint: "abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd:0", + PendingHtlcs: []*lnrpc.HTLC{{HashLock: []byte("aaaa")}, {HashLock: []byte("bbbb")}}, + }, + }, + } + ) + tests := []struct { name string + privacyFlags session.PrivacyFlags uri string msgType rpcperms.InterceptType msg proto.Message @@ -42,38 +92,42 @@ func TestPrivacyMapper(t *testing.T) { IdentityPubkey: "a44ef01c3bff970ef495c", }, }, + { + name: "GetInfo Response clear pubkey", + uri: "/lnrpc.Lightning/GetInfo", + msgType: rpcperms.TypeResponse, + privacyFlags: session.PrivacyFlags{ + session.ClearPubkeys, + }, + msg: &lnrpc.GetInfoResponse{ + Alias: "Tinker Bell", + IdentityPubkey: "Tinker Bell's pub key", + Uris: []string{ + "Neverland 1", + "Neverland 2", + }, + }, + expectedReplacement: &lnrpc.GetInfoResponse{ + IdentityPubkey: "Tinker Bell's pub key", + }, + }, + { + name: "ForwardingHistory Response clear", + uri: "/lnrpc.Lightning/ForwardingHistory", + privacyFlags: []session.PrivacyFlag{ + session.ClearChanIDs, + session.ClearAmounts, + session.ClearTimeStamps, + }, + msgType: rpcperms.TypeResponse, + msg: clearForwarding, + expectedReplacement: clearForwarding, + }, { name: "ForwardingHistory Response", uri: "/lnrpc.Lightning/ForwardingHistory", msgType: rpcperms.TypeResponse, - msg: &lnrpc.ForwardingHistoryResponse{ - ForwardingEvents: []*lnrpc.ForwardingEvent{ - { - AmtIn: 2_000, - AmtInMsat: 2_000_000, - AmtOut: 1_000, - AmtOutMsat: 1_000_000, - Fee: 1_000, - FeeMsat: 1_000_000, - Timestamp: 1_000, - TimestampNs: 1_000_000_000_000, - ChanIdIn: 123, - ChanIdOut: 321, - }, - { - AmtIn: 3_000, - AmtInMsat: 3_000_000, - AmtOut: 2_000, - AmtOutMsat: 2_000_000, - Fee: 1_000, - FeeMsat: 1_000_000, - Timestamp: 1_000, - TimestampNs: 1_000_000_000_000, - ChanIdIn: 678, - ChanIdOut: 876, - }, - }, - }, + msg: clearForwarding, expectedReplacement: &lnrpc.ForwardingHistoryResponse{ ForwardingEvents: []*lnrpc.ForwardingEvent{ { @@ -147,23 +201,7 @@ func TestPrivacyMapper(t *testing.T) { name: "ListChannels Response", uri: "/lnrpc.Lightning/ListChannels", msgType: rpcperms.TypeResponse, - msg: &lnrpc.ListChannelsResponse{ - Channels: []*lnrpc.Channel{ - { - Capacity: 1_000_000, - RemoteBalance: 600_000, - LocalBalance: 499_000, - CommitFee: 1_000, - TotalSatoshisSent: 500_000, - TotalSatoshisReceived: 450_000, - RemotePubkey: "01020304", - Initiator: false, - ChanId: 123, - ChannelPoint: "abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd:0", - PendingHtlcs: []*lnrpc.HTLC{{HashLock: []byte("aaaa")}, {HashLock: []byte("bbbb")}}, - }, - }, - }, + msg: clearListChannel, expectedReplacement: &lnrpc.ListChannelsResponse{ Channels: []*lnrpc.Channel{ { @@ -182,6 +220,20 @@ func TestPrivacyMapper(t *testing.T) { }, }, }, + { + name: "ListChannels Response clear", + privacyFlags: []session.PrivacyFlag{ + session.ClearPubkeys, + session.ClearChanIDs, + session.ClearAmounts, + session.ClearHTLCs, + session.ClearChanInitiator, + }, + uri: "/lnrpc.Lightning/ListChannels", + msgType: rpcperms.TypeResponse, + msg: clearListChannel, + expectedReplacement: clearListChannel, + }, { name: "UpdateChannelPolicy Request txid string", uri: "/lnrpc.Lightning/UpdateChannelPolicy", @@ -298,6 +350,8 @@ func TestPrivacyMapper(t *testing.T) { pd := firewalldb.NewMockSessionDB() pd.AddPair(sessionID, sessionID) + err = pd.AddPrivacyFlags(sessionID, test.privacyFlags) + require.NoError(t, err) // randIntn is used for deterministic testing. randIntn := func(n int) (int, error) { return 100, nil } @@ -346,6 +400,8 @@ func TestPrivacyMapper(t *testing.T) { pd := firewalldb.NewMockSessionDB() pd.AddPair(sessionID, sessionID) + err := pd.AddPrivacyFlags(sessionID, session.PrivacyFlags{}) + require.NoError(t, err) msg := &lnrpc.ForwardingHistoryResponse{ ForwardingEvents: []*lnrpc.ForwardingEvent{