diff --git a/accounts/rpcserver.go b/accounts/rpcserver.go index 79e3e674b..5795696cc 100644 --- a/accounts/rpcserver.go +++ b/accounts/rpcserver.go @@ -8,7 +8,7 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/lightninglabs/lightning-terminal/litrpc" - "github.com/lightninglabs/lightning-terminal/session" + litmac "github.com/lightninglabs/lightning-terminal/macaroons" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/macaroons" @@ -22,12 +22,12 @@ type RPCServer struct { service *InterceptorService - superMacBaker session.MacaroonBaker + superMacBaker litmac.Baker } // NewRPCServer returns a new RPC server for the given service. func NewRPCServer(service *InterceptorService, - superMacBaker session.MacaroonBaker) *RPCServer { + superMacBaker litmac.Baker) *RPCServer { return &RPCServer{ service: service, @@ -79,19 +79,17 @@ func (s *RPCServer) CreateAccount(ctx context.Context, var rootKeyIdSuffix [4]byte copy(rootKeyIdSuffix[:], account.ID[0:4]) - macRootKey := session.NewSuperMacaroonRootKeyID(rootKeyIdSuffix) + macRootKey := litmac.NewSuperMacaroonRootKeyID(rootKeyIdSuffix) accountCaveat := checkers.Condition( macaroons.CondLndCustom, fmt.Sprintf("%s %x", CondAccount, account.ID[:]), ) - macHex, err := s.superMacBaker(ctx, macRootKey, &session.MacaroonRecipe{ - Permissions: MacaroonPermissions, - Caveats: []macaroon.Caveat{{ - Id: []byte(accountCaveat), - }}, - }) + macHex, err := s.superMacBaker( + ctx, macRootKey, MacaroonPermissions, + []macaroon.Caveat{{Id: []byte(accountCaveat)}}, + ) if err != nil { return nil, fmt.Errorf("error baking account macaroon: %w", err) } diff --git a/cmd/litcli/helpers.go b/cmd/litcli/helpers.go index 1d3170ead..a06818e59 100644 --- a/cmd/litcli/helpers.go +++ b/cmd/litcli/helpers.go @@ -7,7 +7,7 @@ import ( "encoding/json" "os" - "github.com/lightninglabs/lightning-terminal/session" + "github.com/lightninglabs/lightning-terminal/macaroons" "github.com/urfave/cli" ) @@ -66,7 +66,7 @@ func superMacRootKey(ctx *cli.Context) error { } } - id := session.NewSuperMacaroonRootKeyID(suffix) + id := macaroons.NewSuperMacaroonRootKeyID(suffix) printJSON(struct { RootKeyID uint64 `json:"root_key_id"` @@ -97,7 +97,7 @@ var isSuperMacaroonCmd = cli.Command{ // isSuperMacaroon checks if the users given macaroon is considered a super // macaroon. func isSuperMacaroon(ctx *cli.Context) error { - isSuperMac := session.IsSuperMacaroon(ctx.String("mac")) + isSuperMac := macaroons.IsSuperMacaroon(ctx.String("mac")) printJSON(struct { IsSuperMacaroon bool `json:"is_super_macaroon"` diff --git a/itest/litd_firewall_test.go b/itest/litd_firewall_test.go index 8037bdeb2..ae35107ce 100644 --- a/itest/litd_firewall_test.go +++ b/itest/litd_firewall_test.go @@ -22,6 +22,7 @@ import ( "github.com/lightninglabs/lightning-terminal/firewall" "github.com/lightninglabs/lightning-terminal/firewalldb" "github.com/lightninglabs/lightning-terminal/litrpc" + "github.com/lightninglabs/lightning-terminal/macaroons" "github.com/lightninglabs/lightning-terminal/rules" "github.com/lightninglabs/lightning-terminal/session" "github.com/lightningnetwork/lnd" @@ -2566,7 +2567,7 @@ func (c *caveatCredentials) GetRequestMetadata(ctx context.Context, return metadata, nil } - mac, err := session.ParseMacaroon(macHex) + mac, err := macaroons.ParseMacaroon(macHex) if err != nil { return nil, err } diff --git a/macaroons/macaroons.go b/macaroons/macaroons.go new file mode 100644 index 000000000..f125c9dd6 --- /dev/null +++ b/macaroons/macaroons.go @@ -0,0 +1,35 @@ +package macaroons + +import ( + "context" + "fmt" + "strconv" + + "github.com/lightningnetwork/lnd/lnrpc" + "google.golang.org/protobuf/proto" + "gopkg.in/macaroon-bakery.v2/bakery" + "gopkg.in/macaroon.v2" +) + +// Baker is a function type for baking a super macaroon. +type Baker func(ctx context.Context, rootKeyID uint64, + perms []bakery.Op, caveats []macaroon.Caveat) (string, error) + +// RootKeyIDFromMacaroon extracts the root key ID of the passed macaroon. +func RootKeyIDFromMacaroon(mac *macaroon.Macaroon) (uint64, error) { + rawID := mac.Id() + if rawID[0] != byte(bakery.LatestVersion) { + return 0, fmt.Errorf("mac id is not on the latest version") + } + + decodedID := &lnrpc.MacaroonId{} + idProto := rawID[1:] + err := proto.Unmarshal(idProto, decodedID) + if err != nil { + return 0, err + } + + // The storage ID is a string representation of a 64-bit unsigned + // number. + return strconv.ParseUint(string(decodedID.StorageId), 10, 64) +} diff --git a/macaroons/super_mac.go b/macaroons/super_mac.go new file mode 100644 index 000000000..32cad9ff4 --- /dev/null +++ b/macaroons/super_mac.go @@ -0,0 +1,120 @@ +package macaroons + +import ( + "bytes" + "context" + "encoding/binary" + "encoding/hex" + "errors" + + "github.com/lightningnetwork/lnd/lnrpc" + "gopkg.in/macaroon-bakery.v2/bakery" + "gopkg.in/macaroon.v2" +) + +// SuperMacaroonRootKeyPrefix is the prefix we set on a super macaroon's root +// key to clearly mark it as such. +var SuperMacaroonRootKeyPrefix = [4]byte{0xFF, 0xEE, 0xDD, 0xCC} + +// SuperMacaroonValidator is a function type for validating a super macaroon. +type SuperMacaroonValidator func(ctx context.Context, + superMacaroon []byte, requiredPermissions []bakery.Op, + fullMethod string) error + +// NewSuperMacaroonRootKeyID returns a new macaroon root key ID that has the +// prefix to mark it as a super macaroon root key. +func NewSuperMacaroonRootKeyID(id [4]byte) uint64 { + rootKeyBytes := make([]byte, 8) + copy(rootKeyBytes[:], SuperMacaroonRootKeyPrefix[:]) + copy(rootKeyBytes[4:], id[:]) + + return binary.BigEndian.Uint64(rootKeyBytes) +} + +// ParseMacaroon parses a hex encoded macaroon into its native struct. +func ParseMacaroon(macHex string) (*macaroon.Macaroon, error) { + macBytes, err := hex.DecodeString(macHex) + if err != nil { + return nil, err + } + + mac := &macaroon.Macaroon{} + if err := mac.UnmarshalBinary(macBytes); err != nil { + return nil, err + } + + return mac, nil +} + +// IsSuperMacaroon returns true if the given hex encoded macaroon is a super +// macaroon baked by LiT which can be identified by its root key ID. +func IsSuperMacaroon(macHex string) bool { + mac, err := ParseMacaroon(macHex) + if err != nil { + return false + } + + rootKeyID, err := RootKeyIDFromMacaroon(mac) + if err != nil { + return false + } + + return isSuperMacaroonRootKeyID(rootKeyID) +} + +// isSuperMacaroonRootKeyID returns true if the given macaroon root key ID (also +// known as storage ID) is a super macaroon, which can be identified by its +// first 4 bytes. +func isSuperMacaroonRootKeyID(rootKeyID uint64) bool { + rootKeyBytes := make([]byte, 8) + binary.BigEndian.PutUint64(rootKeyBytes, rootKeyID) + return bytes.HasPrefix(rootKeyBytes, SuperMacaroonRootKeyPrefix[:]) +} + +// BakeSuperMacaroon uses the lnd client to bake a macaroon that can include +// permissions for multiple daemons. +func BakeSuperMacaroon(ctx context.Context, lnd lnrpc.LightningClient, + rootKeyID uint64, perms []bakery.Op, caveats []macaroon.Caveat) (string, + error) { + + if lnd == nil { + return "", errors.New("lnd not yet connected") + } + + req := &lnrpc.BakeMacaroonRequest{ + Permissions: make( + []*lnrpc.MacaroonPermission, len(perms), + ), + AllowExternalPermissions: true, + RootKeyId: rootKeyID, + } + for idx, perm := range perms { + req.Permissions[idx] = &lnrpc.MacaroonPermission{ + Entity: perm.Entity, + Action: perm.Action, + } + } + + res, err := lnd.BakeMacaroon(ctx, req) + if err != nil { + return "", err + } + + mac, err := ParseMacaroon(res.Macaroon) + if err != nil { + return "", err + } + + for _, caveat := range caveats { + if err := mac.AddFirstPartyCaveat(caveat.Id); err != nil { + return "", err + } + } + + macBytes, err := mac.MarshalBinary() + if err != nil { + return "", err + } + + return hex.EncodeToString(macBytes), err +} diff --git a/session/macaroon_test.go b/macaroons/super_mac_test.go similarity index 83% rename from session/macaroon_test.go rename to macaroons/super_mac_test.go index 7b48a57cf..d4b91aea9 100644 --- a/session/macaroon_test.go +++ b/macaroons/super_mac_test.go @@ -1,4 +1,4 @@ -package session +package macaroons import ( "testing" @@ -25,13 +25,21 @@ var ( "60a6caf" ) +// TestSuperMacaroonRootKeyID tests that adding the super macaroon prefix to +// a root key ID results in a valid super macaroon root key ID. func TestSuperMacaroonRootKeyID(t *testing.T) { + t.Parallel() + someBytes := [4]byte{02, 03, 44, 88} rootKeyID := NewSuperMacaroonRootKeyID(someBytes) require.True(t, isSuperMacaroonRootKeyID(rootKeyID)) require.False(t, isSuperMacaroonRootKeyID(123)) } +// TestIsSuperMacaroon tests that we can correctly identify an example super +// macaroon. func TestIsSuperMacaroon(t *testing.T) { + t.Parallel() + require.True(t, IsSuperMacaroon(testMacHex)) } diff --git a/rpc_proxy.go b/rpc_proxy.go index be03683bb..f9011eb70 100644 --- a/rpc_proxy.go +++ b/rpc_proxy.go @@ -13,8 +13,8 @@ import ( "github.com/improbable-eng/grpc-web/go/grpcweb" "github.com/lightninglabs/lightning-terminal/litrpc" + litmac "github.com/lightninglabs/lightning-terminal/macaroons" "github.com/lightninglabs/lightning-terminal/perms" - "github.com/lightninglabs/lightning-terminal/session" litstatus "github.com/lightninglabs/lightning-terminal/status" "github.com/lightninglabs/lightning-terminal/subservers" "github.com/lightningnetwork/lnd/lncfg" @@ -71,7 +71,7 @@ func (e *proxyErr) Unwrap() error { // or REST request and delegate (and convert if necessary) it to the correct // component. func newRpcProxy(cfg *Config, validator macaroons.MacaroonValidator, - superMacValidator session.SuperMacaroonValidator, + superMacValidator litmac.SuperMacaroonValidator, permsMgr *perms.Manager, subServerMgr *subservers.Manager, statusMgr *litstatus.Manager, getLNDClient lndBasicClientFn) *rpcProxy { @@ -176,7 +176,7 @@ type rpcProxy struct { bakeSuperMac bakeSuperMac macValidator macaroons.MacaroonValidator - superMacValidator session.SuperMacaroonValidator + superMacValidator litmac.SuperMacaroonValidator superMacaroon string @@ -331,7 +331,9 @@ func (p *rpcProxy) makeDirector(allowLitRPC bool) func(ctx context.Context, )) } - case len(macHeader) == 1 && session.IsSuperMacaroon(macHeader[0]): + case len(macHeader) == 1 && + litmac.IsSuperMacaroon(macHeader[0]): + // If we have a macaroon, and it's a super macaroon, // then we need to convert it into the actual daemon // macaroon if they're running in remote mode. diff --git a/session/interface.go b/session/interface.go index 00f109d5d..d29ee567b 100644 --- a/session/interface.go +++ b/session/interface.go @@ -1,12 +1,12 @@ package session import ( - "context" "fmt" "time" "github.com/btcsuite/btcd/btcec/v2" "github.com/lightninglabs/lightning-node-connect/mailbox" + "github.com/lightninglabs/lightning-terminal/macaroons" "gopkg.in/macaroon-bakery.v2/bakery" "gopkg.in/macaroon.v2" ) @@ -71,10 +71,6 @@ type Session struct { GroupID ID } -// MacaroonBaker is a function type for baking a super macaroon. -type MacaroonBaker func(ctx context.Context, rootKeyID uint64, - recipe *MacaroonRecipe) (string, error) - // NewSession creates a new session with the given user-defined parameters. func NewSession(id ID, localPrivKey *btcec.PrivateKey, label string, typ Type, expiry time.Time, serverAddr string, devServer bool, perms []bakery.Op, @@ -86,7 +82,7 @@ func NewSession(id ID, localPrivKey *btcec.PrivateKey, label string, typ Type, return nil, fmt.Errorf("error deriving pairing secret: %v", err) } - macRootKey := NewSuperMacaroonRootKeyID(id) + macRootKey := macaroons.NewSuperMacaroonRootKeyID(id) // The group ID will by default be the same as the Session ID // unless this session links to a previous session. diff --git a/session/macaroon.go b/session/macaroon.go index 72dad8816..90bb57a09 100644 --- a/session/macaroon.go +++ b/session/macaroon.go @@ -1,87 +1,21 @@ package session import ( - "bytes" - "context" "encoding/binary" - "encoding/hex" "fmt" - "strconv" "github.com/btcsuite/btcd/btcec/v2" - "github.com/lightningnetwork/lnd/lnrpc" - "google.golang.org/protobuf/proto" - "gopkg.in/macaroon-bakery.v2/bakery" + "github.com/lightninglabs/lightning-terminal/macaroons" "gopkg.in/macaroon.v2" ) -var ( - // SuperMacaroonRootKeyPrefix is the prefix we set on a super macaroon's - // root key to clearly mark it as such. - SuperMacaroonRootKeyPrefix = [4]byte{0xFF, 0xEE, 0xDD, 0xCC} -) - // ID represents the id of a session. type ID [4]byte -// SuperMacaroonValidator is a function type for validating a super macaroon. -type SuperMacaroonValidator func(ctx context.Context, - superMacaroon []byte, requiredPermissions []bakery.Op, - fullMethod string) error - -// NewSuperMacaroonRootKeyID returns a new macaroon root key ID that has the -// prefix to mark it as a super macaroon root key. -func NewSuperMacaroonRootKeyID(id [4]byte) uint64 { - rootKeyBytes := make([]byte, 8) - copy(rootKeyBytes[:], SuperMacaroonRootKeyPrefix[:]) - copy(rootKeyBytes[4:], id[:]) - return binary.BigEndian.Uint64(rootKeyBytes) -} - -// ParseMacaroon parses a hex encoded macaroon into its native struct. -func ParseMacaroon(macHex string) (*macaroon.Macaroon, error) { - macBytes, err := hex.DecodeString(macHex) - if err != nil { - return nil, err - } - - mac := &macaroon.Macaroon{} - if err := mac.UnmarshalBinary(macBytes); err != nil { - return nil, err - } - - return mac, nil -} - -// IsSuperMacaroon returns true if the given hex encoded macaroon is a super -// macaroon baked by LiT which can be identified by its root key ID. -func IsSuperMacaroon(macHex string) bool { - mac, err := ParseMacaroon(macHex) - if err != nil { - return false - } - - rootKeyID, err := RootKeyIDFromMacaroon(mac) - if err != nil { - return false - } - - return isSuperMacaroonRootKeyID(rootKeyID) -} - -// isSuperMacaroonRootKeyID returns true if the given macaroon root key ID (also -// known as storage ID) is a super macaroon, which can be identified by its -// first 4 bytes. -func isSuperMacaroonRootKeyID(rootKeyID uint64) bool { - rootKeyBytes := make([]byte, 8) - binary.BigEndian.PutUint64(rootKeyBytes, rootKeyID) - return bytes.HasPrefix(rootKeyBytes, SuperMacaroonRootKeyPrefix[:]) -} - // IDFromMacaroon is a helper function that creates a session ID from // a macaroon ID. func IDFromMacaroon(mac *macaroon.Macaroon) (ID, error) { - rootKeyID, err := RootKeyIDFromMacaroon(mac) + rootKeyID, err := macaroons.RootKeyIDFromMacaroon(mac) if err != nil { return ID{}, err } @@ -110,25 +44,6 @@ func IDFromBytes(b []byte) (ID, error) { return id, nil } -// RootKeyIDFromMacaroon extracts the root key ID of the passed macaroon. -func RootKeyIDFromMacaroon(mac *macaroon.Macaroon) (uint64, error) { - rawID := mac.Id() - if rawID[0] != byte(bakery.LatestVersion) { - return 0, fmt.Errorf("mac id is not on the latest version") - } - - decodedID := &lnrpc.MacaroonId{} - idProto := rawID[1:] - err := proto.Unmarshal(idProto, decodedID) - if err != nil { - return 0, err - } - - // The storage ID is a string representation of a 64-bit unsigned - // number. - return strconv.ParseUint(string(decodedID.StorageId), 10, 64) -} - // NewSessionPrivKeyAndID randomly derives a new private key and session ID // pair. func NewSessionPrivKeyAndID() (*btcec.PrivateKey, ID, error) { diff --git a/session_rpcserver.go b/session_rpcserver.go index 3ce968157..32387fd8c 100644 --- a/session_rpcserver.go +++ b/session_rpcserver.go @@ -18,6 +18,7 @@ import ( "github.com/lightninglabs/lightning-terminal/firewall" "github.com/lightninglabs/lightning-terminal/firewalldb" "github.com/lightninglabs/lightning-terminal/litrpc" + litmac "github.com/lightninglabs/lightning-terminal/macaroons" "github.com/lightninglabs/lightning-terminal/perms" "github.com/lightninglabs/lightning-terminal/rules" "github.com/lightninglabs/lightning-terminal/session" @@ -62,7 +63,7 @@ type sessionRpcServerConfig struct { basicAuth string grpcOptions []grpc.ServerOption registerGrpcServers func(server *grpc.Server) - superMacBaker session.MacaroonBaker + superMacBaker litmac.Baker firstConnectionDeadline time.Duration permMgr *perms.Manager actionsDB *firewalldb.DB @@ -424,11 +425,7 @@ func (s *sessionRpcServer) resumeSession(ctx context.Context, }) mac, err := s.cfg.superMacBaker( - ctx, sess.MacaroonRootKey, - &session.MacaroonRecipe{ - Permissions: permissions, - Caveats: caveats, - }, + ctx, sess.MacaroonRootKey, permissions, caveats, ) if err != nil { log.Debugf("Not resuming session %x. Could not bake "+ diff --git a/terminal.go b/terminal.go index c38b414d0..bf67e2a28 100644 --- a/terminal.go +++ b/terminal.go @@ -26,6 +26,7 @@ import ( "github.com/lightninglabs/lightning-terminal/firewall" "github.com/lightninglabs/lightning-terminal/firewalldb" "github.com/lightninglabs/lightning-terminal/litrpc" + litmac "github.com/lightninglabs/lightning-terminal/macaroons" "github.com/lightninglabs/lightning-terminal/perms" "github.com/lightninglabs/lightning-terminal/queue" mid "github.com/lightninglabs/lightning-terminal/rpcmiddleware" @@ -430,11 +431,10 @@ func (g *LightningTerminal) start(ctx context.Context) error { } superMacBaker := func(ctx context.Context, rootKeyID uint64, - recipe *session.MacaroonRecipe) (string, error) { + perms []bakery.Op, caveats []macaroon.Caveat) (string, error) { - return BakeSuperMacaroon( - ctx, g.basicClient, rootKeyID, - recipe.Permissions, recipe.Caveats, + return litmac.BakeSuperMacaroon( + ctx, g.basicClient, rootKeyID, perms, caveats, ) } @@ -662,9 +662,9 @@ func (g *LightningTerminal) start(ctx context.Context) error { var suffixBytes [4]byte binary.BigEndian.PutUint32(suffixBytes[:], rootKeyIDSuffix) - rootKeyID := session.NewSuperMacaroonRootKeyID(suffixBytes) + rootKeyID := litmac.NewSuperMacaroonRootKeyID(suffixBytes) - return BakeSuperMacaroon( + return litmac.BakeSuperMacaroon( ctx, g.basicClient, rootKeyID, g.permsMgr.ActivePermissions(readOnly), nil, ) @@ -952,8 +952,8 @@ func (g *LightningTerminal) setUpLNDClients(ctx context.Context, // Create a super macaroon that can be used to control lnd, // faraday, loop, and pool, all at the same time. log.Infof("Baking internal super macaroon") - superMacaroon, err := BakeSuperMacaroon( - ctx, g.basicClient, session.NewSuperMacaroonRootKeyID( + superMacaroon, err := litmac.BakeSuperMacaroon( + ctx, g.basicClient, litmac.NewSuperMacaroonRootKeyID( [4]byte{}, ), g.permsMgr.ActivePermissions(false), nil, @@ -1265,7 +1265,7 @@ func (g *LightningTerminal) ValidateMacaroon(ctx context.Context, // the proxy and its director and any super macaroon will be converted // to a daemon specific macaroon before directing the call to the remote // daemon. Those calls don't land here. - if session.IsSuperMacaroon(macHex) { + if litmac.IsSuperMacaroon(macHex) { macBytes, err := hex.DecodeString(macHex) if err != nil { return err @@ -1844,54 +1844,6 @@ func (g *LightningTerminal) initSubServers() error { return nil } -// BakeSuperMacaroon uses the lnd client to bake a macaroon that can include -// permissions for multiple daemons. -func BakeSuperMacaroon(ctx context.Context, lnd lnrpc.LightningClient, - rootKeyID uint64, perms []bakery.Op, caveats []macaroon.Caveat) (string, - error) { - - if lnd == nil { - return "", errors.New("lnd not yet connected") - } - - req := &lnrpc.BakeMacaroonRequest{ - Permissions: make( - []*lnrpc.MacaroonPermission, len(perms), - ), - AllowExternalPermissions: true, - RootKeyId: rootKeyID, - } - for idx, perm := range perms { - req.Permissions[idx] = &lnrpc.MacaroonPermission{ - Entity: perm.Entity, - Action: perm.Action, - } - } - - res, err := lnd.BakeMacaroon(ctx, req) - if err != nil { - return "", err - } - - mac, err := session.ParseMacaroon(res.Macaroon) - if err != nil { - return "", err - } - - for _, caveat := range caveats { - if err := mac.AddFirstPartyCaveat(caveat.Id); err != nil { - return "", err - } - } - - macBytes, err := mac.MarshalBinary() - if err != nil { - return "", err - } - - return hex.EncodeToString(macBytes), err -} - // allowCORS wraps the given http.Handler with a function that adds the // Access-Control-Allow-Origin header to the response. func allowCORS(handler http.Handler, origins []string) http.Handler {