lightning-terminal/session/tlv.go
ffranr a0e63124c0
multi: reformat long lines for readability
- Replace occurrences of `// nolint:lll` with `// nolint:ll` across
  files for consistency.
- Reformat multiline strings, comments, and function parameters to
  improve clarity and adhere to style guidelines.
- Add `// nolint:ll` comments where necessary to prevent linter
  warnings.
2025-12-09 16:12:03 +00:00

738 lines
18 KiB
Go

package session
import (
"bytes"
"fmt"
"io"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightninglabs/lightning-terminal/accounts"
"github.com/lightningnetwork/lnd/tlv"
"gopkg.in/macaroon-bakery.v2/bakery"
"gopkg.in/macaroon.v2"
)
const (
typeLabel tlv.Type = 1
typeState tlv.Type = 2
typeType tlv.Type = 3
typeExpiry tlv.Type = 4
typeServerAddr tlv.Type = 5
typeDevServer tlv.Type = 6
typeMacaroonRootKey tlv.Type = 7
typePairingSecret tlv.Type = 9
typeLocalPrivateKey tlv.Type = 10
typeRemotePublicKey tlv.Type = 11
typeMacaroonRecipe tlv.Type = 12
typeCreatedAt tlv.Type = 13
typeFeaturesConfig tlv.Type = 14
typeWithPrivacy tlv.Type = 15
typeRevokedAt tlv.Type = 16
typeGroupID tlv.Type = 17
typePrivacyFlags tlv.Type = 18
// typeMacaroon is no longer used, but we leave it defined for backwards
// compatibility.
typeMacaroon tlv.Type = 8 // nolint
typeMacPerms tlv.Type = 1
typeMacCaveats tlv.Type = 2
typeCaveatID tlv.Type = 1
typeCaveatVerificationID tlv.Type = 2
typeCaveatLocation tlv.Type = 3
typePermEntity tlv.Type = 1
typePermAction tlv.Type = 2
typeFeatureName tlv.Type = 1
typeFeatureConfig tlv.Type = 2
)
// SerializeSession binary serializes the given session to the writer using the
// tlv format.
func SerializeSession(w io.Writer, session *Session) error {
if session == nil {
return fmt.Errorf("session cannot be nil")
}
tlvRecords, err := constructSessionTLVRecords(session, true)
if err != nil {
return err
}
tlvStream, err := tlv.NewStream(tlvRecords...)
if err != nil {
return err
}
return tlvStream.Encode(w)
}
// constructSessionTlvRecords takes a Session and gathers all the TLV records
// that need to be serialised for the session. The withGroupID boolean can be
// used to exclude the tlv record for the GroupID of the session. This should
// only ever be set to true for testing purposes to ensure that older sessions
// which did do not have the GroupID serialised still correctly deserialize and
// set the correct GroupID in the Session struct.
func constructSessionTLVRecords(session *Session, withGroupID bool) (
[]tlv.Record, error) {
var (
label = []byte(session.Label)
state = uint8(session.State)
typ = uint8(session.Type)
expiry = uint64(session.Expiry.Unix())
serverAddr = []byte(session.ServerAddr)
devServer = uint8(0)
pairingSecret = session.PairingSecret[:]
privateKey = session.LocalPrivateKey.Serialize()
createdAt = uint64(session.CreatedAt.Unix())
revokedAt uint64
withPrivacy = uint8(0)
privacyFlags = uint64(0)
)
if !session.RevokedAt.IsZero() {
revokedAt = uint64(session.RevokedAt.Unix())
}
if session.WithPrivacyMapper {
withPrivacy = 1
}
privacyFlags = session.PrivacyFlags.Serialize()
if session.DevServer {
devServer = 1
}
tlvRecords := []tlv.Record{
tlv.MakePrimitiveRecord(typeLabel, &label),
tlv.MakePrimitiveRecord(typeState, &state),
tlv.MakePrimitiveRecord(typeType, &typ),
tlv.MakePrimitiveRecord(typeExpiry, &expiry),
tlv.MakePrimitiveRecord(typeServerAddr, &serverAddr),
tlv.MakePrimitiveRecord(typeDevServer, &devServer),
tlv.MakePrimitiveRecord(
typeMacaroonRootKey, &session.MacaroonRootKey,
),
}
tlvRecords = append(
tlvRecords,
tlv.MakePrimitiveRecord(typePairingSecret, &pairingSecret),
tlv.MakePrimitiveRecord(typeLocalPrivateKey, &privateKey),
)
if session.RemotePublicKey != nil {
tlvRecords = append(tlvRecords, tlv.MakePrimitiveRecord(
typeRemotePublicKey, &session.RemotePublicKey,
))
}
if session.MacaroonRecipe != nil {
tlvRecords = append(tlvRecords, tlv.MakeDynamicRecord(
typeMacaroonRecipe, session.MacaroonRecipe,
func() uint64 {
return recordSize(
macaroonRecipeEncoder,
session.MacaroonRecipe,
)
},
macaroonRecipeEncoder, macaroonRecipeDecoder,
))
}
tlvRecords = append(
tlvRecords, tlv.MakePrimitiveRecord(typeCreatedAt, &createdAt),
)
if session.FeatureConfig != nil && len(*session.FeatureConfig) != 0 {
tlvRecords = append(tlvRecords, tlv.MakeDynamicRecord(
typeFeaturesConfig, session.FeatureConfig,
func() uint64 {
return recordSize(
featureConfigEncoder,
session.FeatureConfig,
)
},
featureConfigEncoder, featureConfigDecoder,
))
}
tlvRecords = append(
tlvRecords,
tlv.MakePrimitiveRecord(typeWithPrivacy, &withPrivacy),
tlv.MakePrimitiveRecord(typeRevokedAt, &revokedAt),
)
if withGroupID {
groupID := session.GroupID[:]
tlvRecords = append(tlvRecords, tlv.MakePrimitiveRecord(
typeGroupID, &groupID,
))
}
tlvRecords = append(
tlvRecords,
tlv.MakePrimitiveRecord(typePrivacyFlags, &privacyFlags),
)
return tlvRecords, nil
}
// DeserializeSession deserializes a session from the given reader, expecting
// the data to be encoded in the tlv format.
func DeserializeSession(r io.Reader) (*Session, error) {
var (
session = &Session{}
label, serverAddr []byte
pairingSecret, privateKey []byte
state, typ, devServer, privacy uint8
expiry, createdAt, revokedAt, privacyFlags uint64
macRecipe MacaroonRecipe
featureConfig FeaturesConfig
groupID []byte
)
tlvStream, err := tlv.NewStream(
tlv.MakePrimitiveRecord(typeLabel, &label),
tlv.MakePrimitiveRecord(typeState, &state),
tlv.MakePrimitiveRecord(typeType, &typ),
tlv.MakePrimitiveRecord(typeExpiry, &expiry),
tlv.MakePrimitiveRecord(typeServerAddr, &serverAddr),
tlv.MakePrimitiveRecord(typeDevServer, &devServer),
tlv.MakePrimitiveRecord(
typeMacaroonRootKey, &session.MacaroonRootKey,
),
tlv.MakePrimitiveRecord(typePairingSecret, &pairingSecret),
tlv.MakePrimitiveRecord(typeLocalPrivateKey, &privateKey),
tlv.MakePrimitiveRecord(
typeRemotePublicKey, &session.RemotePublicKey,
),
tlv.MakeDynamicRecord(
typeMacaroonRecipe, &macRecipe, nil,
macaroonRecipeEncoder, macaroonRecipeDecoder,
),
tlv.MakePrimitiveRecord(typeCreatedAt, &createdAt),
tlv.MakeDynamicRecord(
typeFeaturesConfig, &featureConfig, nil,
featureConfigEncoder, featureConfigDecoder,
),
tlv.MakePrimitiveRecord(typeWithPrivacy, &privacy),
tlv.MakePrimitiveRecord(typeRevokedAt, &revokedAt),
tlv.MakePrimitiveRecord(typeGroupID, &groupID),
tlv.MakePrimitiveRecord(typePrivacyFlags, &privacyFlags),
)
if err != nil {
return nil, err
}
parsedTypes, err := tlvStream.DecodeWithParsedTypes(r)
if err != nil {
return nil, err
}
session.ID = IDFromMacRootKeyID(session.MacaroonRootKey)
session.Label = string(label)
session.State = State(state)
session.Type = Type(typ)
session.Expiry = time.Unix(int64(expiry), 0).UTC()
session.CreatedAt = time.Unix(int64(createdAt), 0).UTC()
session.ServerAddr = string(serverAddr)
session.DevServer = devServer == 1
session.WithPrivacyMapper = privacy == 1
session.PrivacyFlags, err = Deserialize(privacyFlags)
if err != nil {
return nil, err
}
if revokedAt != 0 {
session.RevokedAt = time.Unix(int64(revokedAt), 0).UTC()
}
if t, ok := parsedTypes[typeMacaroonRecipe]; ok && t == nil {
session.MacaroonRecipe = &macRecipe
}
if t, ok := parsedTypes[typePairingSecret]; ok && t == nil {
copy(session.PairingSecret[:], pairingSecret)
}
if t, ok := parsedTypes[typeLocalPrivateKey]; ok && t == nil {
session.LocalPrivateKey, session.LocalPublicKey =
btcec.PrivKeyFromBytes(privateKey)
}
if t, ok := parsedTypes[typeFeaturesConfig]; ok && t == nil {
session.FeatureConfig = &featureConfig
}
// The GroupID field might not be set for older sessions. So we attempt
// to read it from the DB, otherwise we set the group ID to the session
// ID.
if t, ok := parsedTypes[typeGroupID]; ok && t == nil {
copy(session.GroupID[:], groupID)
} else {
session.GroupID = session.ID
}
// For any sessions stored in the BBolt store, a coupled account (if
// any) is linked implicitly via the macaroon recipe caveat. So we
// need to extract it from there.
if session.MacaroonRecipe != nil {
session.AccountID, err = accounts.IDFromCaveats(
session.MacaroonRecipe.Caveats,
)
if err != nil {
return nil, err
}
}
return session, nil
}
func featureConfigEncoder(w io.Writer, val interface{}, buf *[8]byte) error {
if v, ok := val.(*FeaturesConfig); ok {
for n, config := range *v {
name := []byte(n)
config := config
var permsTLVBytes bytes.Buffer
tlvStream, err := tlv.NewStream(
tlv.MakePrimitiveRecord(typeFeatureName, &name),
tlv.MakePrimitiveRecord(
typeFeatureConfig, &config,
),
)
if err != nil {
return err
}
err = tlvStream.Encode(&permsTLVBytes)
if err != nil {
return err
}
// We encode the record with a varint length followed by
// the _raw_ TLV bytes.
tlvLen := uint64(len(permsTLVBytes.Bytes()))
if err := tlv.WriteVarInt(w, tlvLen, buf); err != nil {
return err
}
_, err = w.Write(permsTLVBytes.Bytes())
if err != nil {
return err
}
}
return nil
}
return tlv.NewTypeForEncodingErr(val, "FeaturesConfig")
}
func featureConfigDecoder(r io.Reader, val interface{}, buf *[8]byte,
l uint64) error {
if v, ok := val.(*FeaturesConfig); ok {
featureConfig := make(map[string][]byte)
// Using this information, we'll create a new limited
// reader that'll return an EOF once the end has been
// reached so the stream stops consuming bytes.
innerTlvReader := io.LimitedReader{
R: r,
N: int64(l),
}
for {
// Read out the varint that encodes the size of this
// inner TLV record.
blobSize, err := tlv.ReadVarInt(&innerTlvReader, buf)
if err == io.EOF {
break
} else if err != nil {
return err
}
innerInnerTlvReader := io.LimitedReader{
R: &innerTlvReader,
N: int64(blobSize),
}
var (
name []byte
config []byte
)
tlvStream, err := tlv.NewStream(
tlv.MakePrimitiveRecord(
typeFeatureName, &name,
),
tlv.MakePrimitiveRecord(
typeFeatureConfig, &config,
),
)
if err != nil {
return err
}
err = tlvStream.Decode(&innerInnerTlvReader)
if err != nil {
return err
}
featureConfig[string(name)] = config
}
*v = featureConfig
return nil
}
return tlv.NewTypeForEncodingErr(val, "FeaturesConfig")
}
// macaroonRecipeEncoder is a custom TLV encoder for a MacaroonRecipe record.
func macaroonRecipeEncoder(w io.Writer, val interface{}, buf *[8]byte) error {
if v, ok := val.(*MacaroonRecipe); ok {
var recipeTLVBytes bytes.Buffer
tlvStream, err := tlv.NewStream(
tlv.MakeDynamicRecord(
typeMacPerms, &v.Permissions, func() uint64 {
return recordSize(
permsEncoder, &v.Permissions,
)
}, permsEncoder, permsDecoder,
),
tlv.MakeDynamicRecord(
typeMacCaveats, &v.Caveats, func() uint64 {
return recordSize(
caveatsEncoder, &v.Caveats,
)
}, caveatsEncoder, caveatsDecoder,
),
)
if err != nil {
return err
}
err = tlvStream.Encode(&recipeTLVBytes)
if err != nil {
return err
}
_, err = w.Write(recipeTLVBytes.Bytes())
if err != nil {
return err
}
return nil
}
return tlv.NewTypeForEncodingErr(val, "MacaroonRecipe")
}
// macaroonRecipeDecoder is a custom TLV decoder for a MacaroonRecipe record.
func macaroonRecipeDecoder(r io.Reader, val interface{}, buf *[8]byte,
l uint64) error {
if v, ok := val.(*MacaroonRecipe); ok {
// Using this information, we'll create a new limited
// reader that'll return an EOF once the end has been
// reached so the stream stops consuming bytes.
innerTlvReader := io.LimitedReader{
R: r,
N: int64(l),
}
var (
perms []bakery.Op
caveats []macaroon.Caveat
)
tlvStream, err := tlv.NewStream(
tlv.MakeDynamicRecord(
typeMacPerms, &perms, nil, permsEncoder,
permsDecoder,
),
tlv.MakeDynamicRecord(
typeMacCaveats, &caveats, nil, caveatsEncoder,
caveatsDecoder,
),
)
if err != nil {
return err
}
err = tlvStream.Decode(&innerTlvReader)
if err != nil {
return err
}
*v = MacaroonRecipe{
Permissions: perms,
Caveats: caveats,
}
return nil
}
return tlv.NewTypeForDecodingErr(val, "MacaroonRecipe", l, l)
}
// permsEncoder is a custom TLV encoder for macaroon Permission records.
func permsEncoder(w io.Writer, val interface{}, buf *[8]byte) error {
if v, ok := val.(*[]bakery.Op); ok {
for _, c := range *v {
entity := []byte(c.Entity)
action := []byte(c.Action)
var permsTLVBytes bytes.Buffer
tlvStream, err := tlv.NewStream(
tlv.MakePrimitiveRecord(
typePermEntity, &entity,
),
tlv.MakePrimitiveRecord(
typePermAction, &action,
),
)
if err != nil {
return err
}
err = tlvStream.Encode(&permsTLVBytes)
if err != nil {
return err
}
// We encode the record with a varint length followed by
// the _raw_ TLV bytes.
tlvLen := uint64(len(permsTLVBytes.Bytes()))
if err := tlv.WriteVarInt(w, tlvLen, buf); err != nil {
return err
}
_, err = w.Write(permsTLVBytes.Bytes())
if err != nil {
return err
}
}
return nil
}
return tlv.NewTypeForEncodingErr(val, "MacaroonPermission")
}
// permsDecoder is a custom TLV decoder for macaroon Permission records.
func permsDecoder(r io.Reader, val interface{}, buf *[8]byte, l uint64) error {
if v, ok := val.(*[]bakery.Op); ok {
var perms []bakery.Op
// Using this information, we'll create a new limited
// reader that'll return an EOF once the end has been
// reached so the stream stops consuming bytes.
innerTlvReader := io.LimitedReader{
R: r,
N: int64(l),
}
for {
// Read out the varint that encodes the size of this
// inner TLV record.
blobSize, err := tlv.ReadVarInt(&innerTlvReader, buf)
if err == io.EOF {
break
} else if err != nil {
return err
}
innerInnerTlvReader := io.LimitedReader{
R: &innerTlvReader,
N: int64(blobSize),
}
var (
entity []byte
action []byte
)
tlvStream, err := tlv.NewStream(
tlv.MakePrimitiveRecord(
typePermEntity, &entity,
),
tlv.MakePrimitiveRecord(
typePermAction, &action,
),
)
if err != nil {
return err
}
err = tlvStream.Decode(&innerInnerTlvReader)
if err != nil {
return err
}
perms = append(perms, bakery.Op{
Entity: string(entity),
Action: string(action),
})
}
*v = perms
return nil
}
return tlv.NewTypeForEncodingErr(val, "MacaroonPermission")
}
// caveatsEncoder is a custom TLV decoder for macaroon Caveat records.
func caveatsEncoder(w io.Writer, val interface{}, buf *[8]byte) error {
if v, ok := val.(*[]macaroon.Caveat); ok {
for _, c := range *v {
id := c.Id
tlvRecords := []tlv.Record{
tlv.MakePrimitiveRecord(typeCaveatID, &id),
}
verificationId := c.VerificationId
if verificationId != nil {
tlvRecords = append(tlvRecords,
tlv.MakePrimitiveRecord(
typeCaveatVerificationID,
&verificationId,
),
)
}
location := []byte(c.Location)
if location != nil {
tlvRecords = append(tlvRecords,
tlv.MakePrimitiveRecord(
typeCaveatLocation, &location,
),
)
}
tlvStream, err := tlv.NewStream(tlvRecords...)
if err != nil {
return err
}
var caveatTLVBytes bytes.Buffer
err = tlvStream.Encode(&caveatTLVBytes)
if err != nil {
return err
}
// We encode the record with a varint length followed by
// the _raw_ TLV bytes.
tlvLen := uint64(len(caveatTLVBytes.Bytes()))
if err := tlv.WriteVarInt(w, tlvLen, buf); err != nil {
return err
}
_, err = w.Write(caveatTLVBytes.Bytes())
if err != nil {
return err
}
}
return nil
}
return tlv.NewTypeForEncodingErr(val, "MacaroonCaveat")
}
// caveatsDecoder is a custom TLV decoder for the macaroon Caveat record.
func caveatsDecoder(r io.Reader, val interface{}, buf *[8]byte,
l uint64) error {
if v, ok := val.(*[]macaroon.Caveat); ok {
var caveats []macaroon.Caveat
// Using this information, we'll create a new limited
// reader that'll return an EOF once the end has been
// reached so the stream stops consuming bytes.
innerTlvReader := io.LimitedReader{
R: r,
N: int64(l),
}
for {
// Read out the varint that encodes the size of this
// inner TLV record.
blobSize, err := tlv.ReadVarInt(r, buf)
if err == io.EOF {
break
} else if err != nil {
return err
}
innerInnerTlvReader := io.LimitedReader{
R: &innerTlvReader,
N: int64(blobSize),
}
var (
id []byte
verificationID []byte
location []byte
)
tlvStream, err := tlv.NewStream(
tlv.MakePrimitiveRecord(
typeCaveatID, &id,
),
tlv.MakePrimitiveRecord(
typeCaveatVerificationID,
&verificationID,
),
tlv.MakePrimitiveRecord(
typeCaveatLocation, &location,
),
)
if err != nil {
return err
}
err = tlvStream.Decode(&innerInnerTlvReader)
if err != nil {
return err
}
caveats = append(caveats, macaroon.Caveat{
Id: id,
VerificationId: verificationID,
Location: string(location),
})
}
*v = caveats
return nil
}
return tlv.NewTypeForDecodingErr(val, "MacaroonCaveat", l, l)
}
// recordSize returns the amount of bytes this TLV record will occupy when
// encoded.
func recordSize(encoder tlv.Encoder, v interface{}) uint64 {
var (
b bytes.Buffer
buf [8]byte
)
// We know that encoding works since the tests pass in the build this
// file is checked into, so we'll simplify things and simply encode it
// ourselves then report the total amount of bytes used.
if err := encoder(&b, v, &buf); err != nil {
// This should never error out, but we log it just in case it
// does.
log.Errorf("encoding the amp invoice state failed: %v", err)
}
return uint64(len(b.Bytes()))
}