Skip to content

Commit c8b78bd

Browse files
committed
session: add new ID-to-key index
This commit does a few things: 1. Instead of deriving IDs using the first 4 bytes of the session's serialised local pub key, we instead use bytes [1:5] in order to skip the first byte which is either 0x02 or 0x03. This results in a greater entropy set. 2. We also add a new index from ID to key and we write to this index each time a new session is added. 3. We add a `ReserveNewSessionID` method to the session store which will grind through private keys until it finds one that does not clash with the current ID set. 4. A migration is added to back-fill the ID-to-key index. If any old sessions are found that _do_ have a colliding ID, they are sorted by created time and all but the newest session is revoked. Only an entry for the newest session will be added to the ID-to-key index.
1 parent d3a2626 commit c8b78bd

16 files changed

+1707
-32
lines changed

session/db.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,13 @@ func initDB(filepath string, firstInit bool) (*bbolt.DB, error) {
105105
}
106106
}
107107

108-
_, err = tx.CreateBucketIfNotExists(sessionBucketKey)
108+
sessionBkt, err := tx.CreateBucketIfNotExists(sessionBucketKey)
109+
if err != nil {
110+
return err
111+
}
112+
113+
_, err = sessionBkt.CreateBucketIfNotExists(idIndexKey)
114+
109115
return err
110116
})
111117
if err != nil {

session/interface.go

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -70,27 +70,20 @@ type MacaroonBaker func(ctx context.Context, rootKeyID uint64,
7070
recipe *MacaroonRecipe) (string, error)
7171

7272
// NewSession creates a new session with the given user-defined parameters.
73-
func NewSession(label string, typ Type, expiry time.Time, serverAddr string,
74-
devServer bool, perms []bakery.Op, caveats []macaroon.Caveat,
75-
featureConfig FeaturesConfig, privacy bool) (*Session, error) {
73+
func NewSession(id ID, localPrivKey *btcec.PrivateKey, label string, typ Type,
74+
expiry time.Time, serverAddr string, devServer bool, perms []bakery.Op,
75+
caveats []macaroon.Caveat, featureConfig FeaturesConfig,
76+
privacy bool) (*Session, error) {
7677

7778
_, pairingSecret, err := mailbox.NewPassphraseEntropy()
7879
if err != nil {
7980
return nil, fmt.Errorf("error deriving pairing secret: %v", err)
8081
}
8182

82-
privateKey, err := btcec.NewPrivateKey()
83-
if err != nil {
84-
return nil, fmt.Errorf("error deriving private key: %v", err)
85-
}
86-
pubKey := privateKey.PubKey()
87-
88-
var macRootKeyBase [4]byte
89-
copy(macRootKeyBase[:], pubKey.SerializeCompressed())
90-
macRootKey := NewSuperMacaroonRootKeyID(macRootKeyBase)
83+
macRootKey := NewSuperMacaroonRootKeyID(id)
9184

9285
sess := &Session{
93-
ID: macRootKeyBase,
86+
ID: id,
9487
Label: label,
9588
State: StateCreated,
9689
Type: typ,
@@ -100,8 +93,8 @@ func NewSession(label string, typ Type, expiry time.Time, serverAddr string,
10093
DevServer: devServer,
10194
MacaroonRootKey: macRootKey,
10295
PairingSecret: pairingSecret,
103-
LocalPrivateKey: privateKey,
104-
LocalPublicKey: pubKey,
96+
LocalPrivateKey: localPrivKey,
97+
LocalPublicKey: localPrivKey.PubKey(),
10598
RemotePublicKey: nil,
10699
WithPrivacyMapper: privacy,
107100
}
@@ -123,8 +116,10 @@ func NewSession(label string, typ Type, expiry time.Time, serverAddr string,
123116
// Store is the interface a persistent storage must implement for storing and
124117
// retrieving Terminal Connect sessions.
125118
type Store interface {
126-
// CreateSession adds a new session to the store. If a session with the same
127-
// local public key already exists an error is returned.
119+
// CreateSession adds a new session to the store. If a session with the
120+
// same local public key already exists an error is returned. This
121+
// can only be called with a Session with an ID that the Store has
122+
// reserved.
128123
CreateSession(*Session) error
129124

130125
// GetSession fetches the session with the given key.
@@ -141,4 +136,13 @@ type Store interface {
141136
// to the session with the given local pub key.
142137
UpdateSessionRemotePubKey(localPubKey,
143138
remotePubKey *btcec.PublicKey) error
139+
140+
// GetUnusedIDAndKeyPair can be used to generate a new, unused, local
141+
// private key and session ID pair. Care must be taken to ensure that no
142+
// other thread calls this before the returned ID and key pair from this
143+
// method are either used or discarded.
144+
GetUnusedIDAndKeyPair() (ID, *btcec.PrivateKey, error)
145+
146+
// GetSessionByID fetches the session with the given ID.
147+
GetSessionByID(id ID) (*Session, error)
144148
}

session/macaroon.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"strconv"
1010

11+
"github.com/btcsuite/btcd/btcec/v2"
1112
"github.com/lightningnetwork/lnd/lnrpc"
1213
"google.golang.org/protobuf/proto"
1314
"gopkg.in/macaroon-bakery.v2/bakery"
@@ -127,3 +128,28 @@ func RootKeyIDFromMacaroon(mac *macaroon.Macaroon) (uint64, error) {
127128
// number.
128129
return strconv.ParseUint(string(decodedID.StorageId), 10, 64)
129130
}
131+
132+
// NewSessionPrivKeyAndID randomly derives a new private key and session ID
133+
// pair.
134+
func NewSessionPrivKeyAndID() (*btcec.PrivateKey, ID, error) {
135+
var id ID
136+
137+
privateKey, err := btcec.NewPrivateKey()
138+
if err != nil {
139+
return nil, id, fmt.Errorf("error deriving private key: %v",
140+
err)
141+
}
142+
143+
pubKey := privateKey.PubKey()
144+
145+
// NOTE: we use 4 bytes [1:5] of the serialised public key to create the
146+
// macaroon root key base along with the Session ID. This will provide
147+
// 4 bytes of entropy. Previously, bytes [0:4] where used but this
148+
// resulted in lower entropy due to the first byte always being either
149+
// 0x02 or 0x03.
150+
copy(id[:], pubKey.SerializeCompressed()[1:5])
151+
152+
log.Debugf("Generated new Session ID: %x", id)
153+
154+
return privateKey, id, nil
155+
}

session/metadata.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package session
33
import (
44
"errors"
55
"fmt"
6+
"time"
67

8+
"github.com/lightninglabs/lightning-terminal/session/migration1"
79
"go.etcd.io/bbolt"
810
)
911

@@ -29,7 +31,13 @@ var (
2931
// version of the database doesn't match the latest version this list
3032
// will be used for retrieving all migration function that are need to
3133
// apply to the current db.
32-
dbVersions []migration
34+
dbVersions = []migration{
35+
func(tx *bbolt.Tx) error {
36+
return migration1.MigrateSessionIDToKeyIndex(
37+
tx, time.Now,
38+
)
39+
},
40+
}
3341

3442
latestDBVersion = uint32(len(dbVersions))
3543
)

session/migration1/id_to_key_index.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package migration1
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"fmt"
7+
"sort"
8+
"time"
9+
10+
"go.etcd.io/bbolt"
11+
)
12+
13+
var (
14+
// sessionBucketKey is the top level bucket where we can find all
15+
// information about sessions. These sessions are indexed by their
16+
// public key.
17+
//
18+
// The session bucket has the following structure:
19+
// session -> <key> -> <serialised session>
20+
// -> id-index -> <session-id> -> key -> <session key>
21+
sessionBucketKey = []byte("session")
22+
23+
// idIndexKey is the key used to define the id-index sub-bucket within
24+
// the main session bucket. This bucket will be used to store the
25+
// mapping from session ID to various other fields.
26+
idIndexKey = []byte("id-index")
27+
28+
// sessionKeyKey is the key used within the id-index bucket to store the
29+
// session key (serialised local public key) associated with the given
30+
// session ID.
31+
sessionKeyKey = []byte("key")
32+
33+
// ErrDBInitErr is returned when a bucket that we expect to have been
34+
// set up during DB initialisation is not found.
35+
ErrDBInitErr = errors.New("db did not initialise properly")
36+
)
37+
38+
type sessionInfo struct {
39+
key []byte
40+
state State
41+
createdAt time.Time
42+
}
43+
44+
// MigrateSessionIDToKeyIndex back-fills the session ID to key index so that it
45+
// has an entry for all sessions that the session store is currently aware of.
46+
func MigrateSessionIDToKeyIndex(tx *bbolt.Tx, timeNow func() time.Time) error {
47+
sessionBucket := tx.Bucket(sessionBucketKey)
48+
if sessionBucket == nil {
49+
return fmt.Errorf("session bucket not found")
50+
}
51+
52+
idIndexBkt := sessionBucket.Bucket(idIndexKey)
53+
if idIndexBkt == nil {
54+
return ErrDBInitErr
55+
}
56+
57+
// Collect all the index entries.
58+
idToSessionPairs := make(map[ID][]*sessionInfo)
59+
err := sessionBucket.ForEach(func(key, sessionBytes []byte) error {
60+
// The session bucket contains both keys and sub-buckets. So
61+
// here we ensure that we skip any sub-buckets.
62+
if len(sessionBytes) == 0 {
63+
return nil
64+
}
65+
66+
session, err := DeserializeSession(
67+
bytes.NewReader(sessionBytes),
68+
)
69+
if err != nil {
70+
return err
71+
}
72+
73+
var id ID
74+
copy(id[:], key[0:4])
75+
76+
idToSessionPairs[id] = append(
77+
idToSessionPairs[id], &sessionInfo{
78+
key: key,
79+
createdAt: session.CreatedAt,
80+
state: session.State,
81+
},
82+
)
83+
84+
return nil
85+
})
86+
if err != nil {
87+
return err
88+
}
89+
90+
addIndexEntry := func(id ID, key []byte) error {
91+
idBkt, err := idIndexBkt.CreateBucket(id[:])
92+
if err != nil {
93+
return err
94+
}
95+
96+
return idBkt.Put(sessionKeyKey[:], key)
97+
}
98+
99+
for id, sessions := range idToSessionPairs {
100+
if len(sessions) == 1 {
101+
err = addIndexEntry(id, sessions[0].key)
102+
if err != nil {
103+
return err
104+
}
105+
106+
continue
107+
}
108+
109+
// Sort the sessions from oldest to newest.
110+
sort.Slice(sessions, func(i, j int) bool {
111+
return sessions[i].createdAt.Before(
112+
sessions[j].createdAt,
113+
)
114+
})
115+
116+
// For each session other than the newest one, we ensure that
117+
// the session is revoked. We do this in case there was a
118+
// collision in the ID used for the session since now we want to
119+
// populate the ID-to-key index which should be a one-to-one
120+
// mapping. So there is a small chance that the DB contains a
121+
// session with no entry in this ID-to-key index but at least
122+
// this will not be an active session.
123+
for _, session := range sessions[:len(sessions)-1] {
124+
serialisedSession := sessionBucket.Get(session.key)
125+
126+
sess, err := DeserializeSession(
127+
bytes.NewReader(serialisedSession),
128+
)
129+
if err != nil {
130+
return err
131+
}
132+
133+
sess.State = StateRevoked
134+
sess.RevokedAt = timeNow()
135+
136+
var buf bytes.Buffer
137+
if err := SerializeSession(&buf, sess); err != nil {
138+
return err
139+
}
140+
141+
err = sessionBucket.Put(session.key, buf.Bytes())
142+
if err != nil {
143+
return err
144+
}
145+
}
146+
147+
// Add an entry for the last session in the set.
148+
err = addIndexEntry(id, sessions[len(sessions)-1].key)
149+
if err != nil {
150+
return err
151+
}
152+
}
153+
154+
return nil
155+
}

0 commit comments

Comments
 (0)