2021-08-10 19:06:49 +02:00
|
|
|
package session
|
|
|
|
|
|
|
|
|
|
import (
|
2025-02-25 13:14:28 +02:00
|
|
|
"context"
|
2021-08-10 19:06:49 +02:00
|
|
|
"fmt"
|
|
|
|
|
"time"
|
|
|
|
|
|
2022-03-28 22:03:17 +02:00
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
2021-08-10 19:06:49 +02:00
|
|
|
"github.com/lightninglabs/lightning-node-connect/mailbox"
|
2025-02-27 11:15:54 +02:00
|
|
|
"github.com/lightninglabs/lightning-terminal/accounts"
|
2025-02-09 10:37:40 +02:00
|
|
|
"github.com/lightninglabs/lightning-terminal/macaroons"
|
2025-02-27 11:15:54 +02:00
|
|
|
"github.com/lightningnetwork/lnd/fn"
|
2022-02-02 13:13:24 +02:00
|
|
|
"gopkg.in/macaroon-bakery.v2/bakery"
|
2021-08-10 19:06:49 +02:00
|
|
|
"gopkg.in/macaroon.v2"
|
|
|
|
|
)
|
|
|
|
|
|
2025-04-19 14:21:49 +02:00
|
|
|
// EmptyID is an empty session ID.
|
|
|
|
|
var EmptyID ID
|
|
|
|
|
|
2021-08-10 19:06:49 +02:00
|
|
|
// Type represents the type of session.
|
|
|
|
|
type Type uint8
|
|
|
|
|
|
|
|
|
|
const (
|
2022-07-08 14:33:00 +02:00
|
|
|
TypeMacaroonReadonly Type = 0
|
|
|
|
|
TypeMacaroonAdmin Type = 1
|
|
|
|
|
TypeMacaroonCustom Type = 2
|
|
|
|
|
TypeUIPassword Type = 3
|
|
|
|
|
TypeAutopilot Type = 4
|
|
|
|
|
TypeMacaroonAccount Type = 5
|
2021-08-10 19:06:49 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// State represents the state of a session.
|
|
|
|
|
type State uint8
|
|
|
|
|
|
2025-02-09 12:36:27 +02:00
|
|
|
/*
|
2025-02-09 13:41:18 +02:00
|
|
|
/---> StateExpired (terminal)
|
|
|
|
|
StateReserved ---> StateCreated ---
|
|
|
|
|
\---> StateRevoked (terminal)
|
2025-02-09 12:36:27 +02:00
|
|
|
*/
|
|
|
|
|
|
2021-08-10 19:06:49 +02:00
|
|
|
const (
|
2025-02-09 12:36:27 +02:00
|
|
|
// StateCreated is the state of a session once it has been fully
|
2025-02-09 13:41:18 +02:00
|
|
|
// committed to the BoltStore and is ready to be used. This is the
|
|
|
|
|
// first state after StateReserved.
|
2021-08-10 19:06:49 +02:00
|
|
|
StateCreated State = 0
|
2025-02-09 12:36:27 +02:00
|
|
|
|
|
|
|
|
// StateInUse is the state of a session that is currently being used.
|
|
|
|
|
//
|
|
|
|
|
// NOTE: this state is not currently used, but we keep it around for now
|
|
|
|
|
// since old sessions might still have this state persisted.
|
|
|
|
|
StateInUse State = 1
|
|
|
|
|
|
|
|
|
|
// StateRevoked is the state of a session that has been revoked before
|
|
|
|
|
// its expiry date.
|
2021-08-10 19:06:49 +02:00
|
|
|
StateRevoked State = 2
|
2025-02-09 12:36:27 +02:00
|
|
|
|
|
|
|
|
// StateExpired is the state of a session that has passed its expiry
|
|
|
|
|
// date.
|
2021-08-10 19:06:49 +02:00
|
|
|
StateExpired State = 3
|
2025-02-09 12:49:01 +02:00
|
|
|
|
2025-02-09 13:41:18 +02:00
|
|
|
// StateReserved is a temporary initial state of a session. This is used
|
|
|
|
|
// to reserve a unique ID and private key pair for a session before it
|
|
|
|
|
// is fully created. On start-up, any sessions in this state should be
|
|
|
|
|
// cleaned up.
|
2025-02-09 12:49:01 +02:00
|
|
|
StateReserved State = 4
|
2021-08-10 19:06:49 +02:00
|
|
|
)
|
|
|
|
|
|
2025-02-25 08:13:56 +02:00
|
|
|
// Terminal returns true if the state is a terminal state.
|
|
|
|
|
func (s State) Terminal() bool {
|
|
|
|
|
return s == StateExpired || s == StateRevoked
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// legalStateShifts is a map that defines the legal State transitions that a
|
|
|
|
|
// Session can be put through.
|
|
|
|
|
var legalStateShifts = map[State]map[State]bool{
|
2025-02-09 13:41:18 +02:00
|
|
|
StateReserved: {
|
|
|
|
|
StateCreated: true,
|
|
|
|
|
},
|
2025-02-25 08:13:56 +02:00
|
|
|
StateCreated: {
|
|
|
|
|
StateExpired: true,
|
|
|
|
|
StateRevoked: true,
|
|
|
|
|
},
|
|
|
|
|
StateInUse: {
|
|
|
|
|
StateRevoked: true,
|
|
|
|
|
StateExpired: true,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-02 13:13:24 +02:00
|
|
|
// MacaroonRecipe defines the permissions and caveats that should be used
|
|
|
|
|
// to bake a macaroon.
|
|
|
|
|
type MacaroonRecipe struct {
|
|
|
|
|
Permissions []bakery.Op
|
|
|
|
|
Caveats []macaroon.Caveat
|
|
|
|
|
}
|
|
|
|
|
|
2022-08-24 08:37:28 +02:00
|
|
|
// FeaturesConfig is a map from feature name to a raw byte array which stores
|
|
|
|
|
// any config feature config options.
|
|
|
|
|
type FeaturesConfig map[string][]byte
|
|
|
|
|
|
2021-08-10 19:06:49 +02:00
|
|
|
// Session is a struct representing a long-term Terminal Connect session.
|
|
|
|
|
type Session struct {
|
2022-09-08 12:27:59 +02:00
|
|
|
ID ID
|
|
|
|
|
Label string
|
|
|
|
|
State State
|
|
|
|
|
Type Type
|
|
|
|
|
Expiry time.Time
|
|
|
|
|
CreatedAt time.Time
|
|
|
|
|
RevokedAt time.Time
|
|
|
|
|
ServerAddr string
|
|
|
|
|
DevServer bool
|
|
|
|
|
MacaroonRootKey uint64
|
|
|
|
|
MacaroonRecipe *MacaroonRecipe
|
|
|
|
|
PairingSecret [mailbox.NumPassphraseEntropyBytes]byte
|
|
|
|
|
LocalPrivateKey *btcec.PrivateKey
|
|
|
|
|
LocalPublicKey *btcec.PublicKey
|
|
|
|
|
RemotePublicKey *btcec.PublicKey
|
|
|
|
|
FeatureConfig *FeaturesConfig
|
|
|
|
|
WithPrivacyMapper bool
|
2023-11-10 15:55:31 +01:00
|
|
|
PrivacyFlags PrivacyFlags
|
2023-06-18 13:04:56 +02:00
|
|
|
|
|
|
|
|
// GroupID is the Session ID of the very first Session in the linked
|
|
|
|
|
// group of sessions. If this is the very first session in the group
|
|
|
|
|
// then this will be the same as ID.
|
|
|
|
|
GroupID ID
|
2025-02-27 11:15:54 +02:00
|
|
|
|
|
|
|
|
// AccountID is an optional account that the session has been linked to.
|
|
|
|
|
AccountID fn.Option[accounts.AccountID]
|
2021-08-10 19:06:49 +02:00
|
|
|
}
|
|
|
|
|
|
2025-02-09 11:21:53 +02:00
|
|
|
// buildSession creates a new session with the given user-defined parameters.
|
|
|
|
|
func buildSession(id ID, localPrivKey *btcec.PrivateKey, label string, typ Type,
|
2025-02-27 07:24:27 +02:00
|
|
|
created, expiry time.Time, serverAddr string,
|
|
|
|
|
options ...Option) (*Session, error) {
|
|
|
|
|
|
|
|
|
|
opts := defaultSessionOptions()
|
|
|
|
|
for _, o := range options {
|
|
|
|
|
o(opts)
|
|
|
|
|
}
|
2021-08-10 19:06:49 +02:00
|
|
|
|
2022-04-29 14:15:35 +02:00
|
|
|
_, pairingSecret, err := mailbox.NewPassphraseEntropy()
|
2021-08-10 19:06:49 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("error deriving pairing secret: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-09 10:37:40 +02:00
|
|
|
macRootKey := macaroons.NewSuperMacaroonRootKeyID(id)
|
2021-08-10 19:06:49 +02:00
|
|
|
|
2023-06-18 13:04:56 +02:00
|
|
|
// The group ID will by default be the same as the Session ID
|
|
|
|
|
// unless this session links to a previous session.
|
|
|
|
|
groupID := id
|
2025-02-27 07:24:27 +02:00
|
|
|
if opts.linkedGroupID != nil {
|
2023-06-18 13:04:56 +02:00
|
|
|
// If this session is linked to a previous session, then the
|
|
|
|
|
// group ID is the same as the linked session's group ID.
|
2025-02-27 07:24:27 +02:00
|
|
|
groupID = *opts.linkedGroupID
|
2023-06-18 13:04:56 +02:00
|
|
|
}
|
|
|
|
|
|
2022-02-02 13:13:24 +02:00
|
|
|
sess := &Session{
|
2023-08-29 14:23:42 +02:00
|
|
|
ID: id,
|
2022-09-08 12:27:59 +02:00
|
|
|
Label: label,
|
2025-02-09 13:41:18 +02:00
|
|
|
State: StateReserved,
|
2022-09-08 12:27:59 +02:00
|
|
|
Type: typ,
|
2025-02-09 11:28:16 +02:00
|
|
|
Expiry: expiry.UTC(),
|
|
|
|
|
CreatedAt: created.UTC(),
|
2022-09-08 12:27:59 +02:00
|
|
|
ServerAddr: serverAddr,
|
2025-02-27 07:24:27 +02:00
|
|
|
DevServer: opts.devServer,
|
2022-09-08 12:27:59 +02:00
|
|
|
MacaroonRootKey: macRootKey,
|
|
|
|
|
PairingSecret: pairingSecret,
|
2023-08-29 14:23:42 +02:00
|
|
|
LocalPrivateKey: localPrivKey,
|
|
|
|
|
LocalPublicKey: localPrivKey.PubKey(),
|
2022-09-08 12:27:59 +02:00
|
|
|
RemotePublicKey: nil,
|
2025-02-27 07:24:27 +02:00
|
|
|
WithPrivacyMapper: opts.privacy,
|
|
|
|
|
PrivacyFlags: opts.privacyFlags,
|
2023-06-18 13:04:56 +02:00
|
|
|
GroupID: groupID,
|
2025-02-27 07:24:27 +02:00
|
|
|
MacaroonRecipe: opts.macaroonRecipe,
|
2025-02-27 11:15:54 +02:00
|
|
|
AccountID: opts.accountID,
|
2022-02-02 13:13:24 +02:00
|
|
|
}
|
|
|
|
|
|
2025-02-27 07:24:27 +02:00
|
|
|
if len(opts.featureConfig) != 0 {
|
|
|
|
|
sess.FeatureConfig = &opts.featureConfig
|
2022-02-02 13:13:24 +02:00
|
|
|
}
|
|
|
|
|
|
2025-02-27 07:24:27 +02:00
|
|
|
return sess, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// sessionOptions defines various options that can be tweaked via functional
|
|
|
|
|
// parameters for session creation.
|
|
|
|
|
type sessionOptions struct {
|
|
|
|
|
// privacy indicates if a privacy map should be used with this session.
|
|
|
|
|
privacy bool
|
|
|
|
|
|
|
|
|
|
// privacyFlags to use in combination with the session's privacy mapper.
|
|
|
|
|
privacyFlags PrivacyFlags
|
|
|
|
|
|
|
|
|
|
// featureConfig holds any feature configuration bytes to use for this
|
|
|
|
|
// session.
|
|
|
|
|
featureConfig FeaturesConfig
|
|
|
|
|
|
|
|
|
|
// linkedGroupID is the ID of the group that this session is linked
|
|
|
|
|
// to. By default, a session is not linked to another group.
|
|
|
|
|
linkedGroupID *ID
|
|
|
|
|
|
|
|
|
|
// devServer is true if TLS should be skipped when connecting to the
|
|
|
|
|
// mailbox server.
|
|
|
|
|
devServer bool
|
|
|
|
|
|
|
|
|
|
// macaroonRecipe holds the permissions and caveats that should be used
|
|
|
|
|
// to bake the macaroon to be used with this session.
|
|
|
|
|
macaroonRecipe *MacaroonRecipe
|
2025-02-27 11:15:54 +02:00
|
|
|
|
|
|
|
|
// accountID is an optional account that the session has been linked to.
|
|
|
|
|
accountID fn.Option[accounts.AccountID]
|
2025-02-27 07:24:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// defaultSessionOptions returns a new sessionOptions struct with default
|
|
|
|
|
// values set.
|
|
|
|
|
func defaultSessionOptions() *sessionOptions {
|
|
|
|
|
return &sessionOptions{
|
|
|
|
|
privacy: false,
|
|
|
|
|
privacyFlags: PrivacyFlags{},
|
|
|
|
|
featureConfig: FeaturesConfig{},
|
|
|
|
|
linkedGroupID: nil,
|
|
|
|
|
devServer: false,
|
2022-08-24 08:37:28 +02:00
|
|
|
}
|
2025-02-27 07:24:27 +02:00
|
|
|
}
|
2022-08-24 08:37:28 +02:00
|
|
|
|
2025-02-27 07:24:27 +02:00
|
|
|
// Option defines the signature of a functional option that can be used to
|
|
|
|
|
// tweak various session creation options.
|
|
|
|
|
type Option func(*sessionOptions)
|
|
|
|
|
|
|
|
|
|
// WithPrivacy can be used to enable the privacy mapper for this session.
|
|
|
|
|
// An optional set of privacy flags can be provided to further customize the
|
|
|
|
|
// privacy mapper.
|
|
|
|
|
func WithPrivacy(flags PrivacyFlags) Option {
|
|
|
|
|
return func(o *sessionOptions) {
|
|
|
|
|
o.privacy = true
|
|
|
|
|
o.privacyFlags = flags
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// WithFeatureConfig can be used to set the feature configuration bytes for
|
|
|
|
|
// this session.
|
|
|
|
|
func WithFeatureConfig(config FeaturesConfig) Option {
|
|
|
|
|
return func(o *sessionOptions) {
|
|
|
|
|
o.featureConfig = config
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// WithLinkedGroupID can be used to link this session to a previous session.
|
|
|
|
|
func WithLinkedGroupID(groupID *ID) Option {
|
|
|
|
|
return func(o *sessionOptions) {
|
|
|
|
|
o.linkedGroupID = groupID
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// WithDevServer can be used to set if TLS verification should be skipped when
|
|
|
|
|
// connecting to the mailbox server.
|
|
|
|
|
func WithDevServer() Option {
|
|
|
|
|
return func(o *sessionOptions) {
|
|
|
|
|
o.devServer = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// WithMacaroonRecipe can be used to set the permissions and caveats that
|
|
|
|
|
// should be used to bake the macaroon for a session.
|
|
|
|
|
func WithMacaroonRecipe(caveats []macaroon.Caveat, perms []bakery.Op) Option {
|
|
|
|
|
return func(o *sessionOptions) {
|
|
|
|
|
o.macaroonRecipe = &MacaroonRecipe{
|
|
|
|
|
Permissions: perms,
|
|
|
|
|
Caveats: caveats,
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-08-10 19:06:49 +02:00
|
|
|
}
|
|
|
|
|
|
2025-02-27 11:15:54 +02:00
|
|
|
// WithAccount can be used to link the session to an account.
|
|
|
|
|
func WithAccount(id accounts.AccountID) Option {
|
|
|
|
|
return func(o *sessionOptions) {
|
|
|
|
|
o.accountID = fn.Some(id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-24 11:25:12 +02:00
|
|
|
// IDToGroupIndex defines an interface for the session ID to group ID index.
|
|
|
|
|
type IDToGroupIndex interface {
|
|
|
|
|
// GetGroupID will return the group ID for the given session ID.
|
2025-02-25 13:39:00 +02:00
|
|
|
GetGroupID(ctx context.Context, sessionID ID) (ID, error)
|
2023-08-24 11:25:12 +02:00
|
|
|
|
|
|
|
|
// GetSessionIDs will return the set of session IDs that are in the
|
|
|
|
|
// group with the given ID.
|
2025-02-25 13:39:00 +02:00
|
|
|
GetSessionIDs(ctx context.Context, groupID ID) ([]ID, error)
|
2023-08-24 11:25:12 +02:00
|
|
|
}
|
|
|
|
|
|
2021-08-10 19:06:49 +02:00
|
|
|
// Store is the interface a persistent storage must implement for storing and
|
|
|
|
|
// retrieving Terminal Connect sessions.
|
|
|
|
|
type Store interface {
|
2025-02-09 11:21:53 +02:00
|
|
|
// NewSession creates a new session with the given user-defined
|
2025-02-09 13:41:18 +02:00
|
|
|
// parameters. The session will remain in the StateReserved state until
|
|
|
|
|
// ShiftState is called to update the state.
|
2025-02-25 13:14:28 +02:00
|
|
|
NewSession(ctx context.Context, label string, typ Type,
|
|
|
|
|
expiry time.Time, serverAddr string, opts ...Option) (*Session,
|
|
|
|
|
error)
|
2025-02-09 11:21:53 +02:00
|
|
|
|
2025-03-04 17:30:43 +02:00
|
|
|
// GetSessionByLocalPub fetches the session with the given local pub
|
|
|
|
|
// key.
|
|
|
|
|
GetSessionByLocalPub(ctx context.Context,
|
|
|
|
|
key *btcec.PublicKey) (*Session, error)
|
2023-08-25 14:20:12 +02:00
|
|
|
|
2025-02-09 12:29:40 +02:00
|
|
|
// ListAllSessions returns all sessions currently known to the store.
|
2025-02-25 13:19:34 +02:00
|
|
|
ListAllSessions(ctx context.Context) ([]*Session, error)
|
2021-08-10 19:06:49 +02:00
|
|
|
|
2025-02-09 12:22:53 +02:00
|
|
|
// ListSessionsByType returns all sessions of the given type.
|
2025-02-25 13:19:34 +02:00
|
|
|
ListSessionsByType(ctx context.Context, t Type) ([]*Session, error)
|
2025-02-09 12:22:53 +02:00
|
|
|
|
2025-02-13 15:28:23 +02:00
|
|
|
// ListSessionsByState returns all sessions currently known to the store
|
2025-03-02 08:22:59 +02:00
|
|
|
// that are in the given state.
|
|
|
|
|
ListSessionsByState(ctx context.Context, state State) ([]*Session,
|
2025-02-25 13:19:34 +02:00
|
|
|
error)
|
2025-02-13 15:28:23 +02:00
|
|
|
|
2023-08-25 14:36:59 +02:00
|
|
|
// UpdateSessionRemotePubKey can be used to add the given remote pub key
|
2025-03-04 17:39:03 +02:00
|
|
|
// to the session with the given ID.
|
|
|
|
|
UpdateSessionRemotePubKey(ctx context.Context, id ID,
|
2023-08-25 14:36:59 +02:00
|
|
|
remotePubKey *btcec.PublicKey) error
|
2023-08-29 14:23:42 +02:00
|
|
|
|
2025-03-04 17:34:31 +02:00
|
|
|
// GetSession fetches the session with the given ID.
|
|
|
|
|
GetSession(ctx context.Context, id ID) (*Session, error)
|
2023-08-24 11:25:12 +02:00
|
|
|
|
2025-02-09 12:49:01 +02:00
|
|
|
// DeleteReservedSessions deletes all sessions that are in the
|
|
|
|
|
// StateReserved state.
|
2025-02-25 13:25:26 +02:00
|
|
|
DeleteReservedSessions(ctx context.Context) error
|
2025-02-09 12:49:01 +02:00
|
|
|
|
2025-11-07 11:32:49 +01:00
|
|
|
// DeleteReservedSession deletes the session with the given ID if it is
|
|
|
|
|
// in the StateReserved state.
|
|
|
|
|
DeleteReservedSession(ctx context.Context, id ID) error
|
|
|
|
|
|
2025-02-25 08:13:56 +02:00
|
|
|
// ShiftState updates the state of the session with the given ID to the
|
|
|
|
|
// "dest" state.
|
2025-02-25 13:42:18 +02:00
|
|
|
ShiftState(ctx context.Context, id ID, dest State) error
|
2025-02-25 08:13:56 +02:00
|
|
|
|
2023-08-24 11:25:12 +02:00
|
|
|
IDToGroupIndex
|
2021-08-10 19:06:49 +02:00
|
|
|
}
|