Skip to content

Commit

Permalink
loopdb: add macaroon encrpytion
Browse files Browse the repository at this point in the history
This commit adds macaroon encryption to the sql db.
  • Loading branch information
sputn1ck committed Jun 15, 2023
1 parent 7d644f8 commit 12104e4
Show file tree
Hide file tree
Showing 17 changed files with 439 additions and 61 deletions.
37 changes: 32 additions & 5 deletions loopd/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -376,20 +376,35 @@ func (d *Daemon) initialize(withMacaroonService bool) error {

log.Infof("Swap server address: %v", d.cfg.Server.Host)

dbPassword, err := getDbPassword(d.mainCtx, &d.lnd.LndServices)
if err != nil {
return err
}

// Check if we need to migrate the database.
if needSqliteMigration(d.cfg) {
log.Infof("Boltdb found, running migration")

err := migrateBoltdb(d.mainCtx, d.cfg)
// First we migrate the macaroon database.
err := migrateMacaroons(d.mainCtx, d.cfg, dbPassword)
if err != nil {
return fmt.Errorf("unable to migrate macaroons: %v", err)
}

// Then we migrate the swap database.
err = migrateBoltdb(d.mainCtx, d.cfg)
if err != nil {
return fmt.Errorf("unable to migrate boltdb: %v", err)
}

log.Infof("Successfully migrated boltdb")

}

// Create an instance of the loop client library.
swapclient, clientCleanup, err := getClient(d.cfg, &d.lnd.LndServices)
swapclient, clientCleanup, err := getClient(
d.mainCtx, d.cfg, &d.lnd.LndServices, dbPassword,
)
if err != nil {
return err
}
Expand All @@ -413,10 +428,8 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
macaroons.IPLockChecker,
},
RequiredPerms: perms.RequiredPermissions,
DBPassword: macDbDefaultPw,
DBPassword: dbPassword,
LndClient: &d.lnd.LndServices,
EphemeralKey: lndclient.SharedKeyNUMS,
KeyLocator: lndclient.SharedKeyLocator,
},
)
if err != nil {
Expand Down Expand Up @@ -605,3 +618,17 @@ func allowCORS(handler http.Handler, origin string) http.Handler {
handler.ServeHTTP(w, r)
})
}

// getDbPassword derives a password from the LND seed that is used to encrypt
// the database.
func getDbPassword(ctx context.Context, lnd *lndclient.LndServices) ([]byte, error) {
sharedKey, err := lnd.Signer.DeriveSharedKey(
ctx, lndclient.SharedKeyNUMS, lndclient.SharedKeyLocator,
)
if err != nil {
return nil, fmt.Errorf("unable to derive a shared "+
"secret with LND: %v", err)
}

return sharedKey[:], nil
}
114 changes: 114 additions & 0 deletions loopd/migration.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
package loopd

import (
"bytes"
"context"
"fmt"
"os"
"path/filepath"

"github.com/btcsuite/btcwallet/snacl"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/loopdb/sqlc"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/macaroons"
)

var (
encryptionKeyID = []byte("enckey")
rootKeyBucketName = []byte("macrootkeys")
)

// migrateBoltdb migrates the boltdb to sqlite.
Expand Down Expand Up @@ -75,6 +84,111 @@ func migrateBoltdb(ctx context.Context, cfg *Config) error {
return nil
}

// migrateMacaroons migrates the macaroon root keys to the new store. This
// is done so that we don't need to delete and recreate macaroons on migration.
func migrateMacaroons(ctx context.Context, cfg *Config,
dbPassword []byte) error {
// First get the chain params.
chainParams, err := lndclient.Network(cfg.Network).ChainParams()
if err != nil {
return err
}

// Get the sql db.
var (
db loopdb.BaseDB
)
switch cfg.DatabaseBackend {
case DatabaseBackendSqlite:
log.Infof("Opening sqlite3 database at: %v",
cfg.Sqlite.DatabaseFileName)
sqlite, err := loopdb.NewSqliteStore(
cfg.Sqlite, chainParams,
loopdb.WithEncryptionMiddleware(dbPassword),
)
if err != nil {
return fmt.Errorf("unable to open database: %v", err)
}
db = *sqlite.BaseDB

case DatabaseBackendPostgres:
log.Infof("Opening postgres database at: %v",
cfg.Postgres.DSN(true))
postgres, err := loopdb.NewPostgresStore(
cfg.Postgres, chainParams,
loopdb.WithEncryptionMiddleware(dbPassword),
)
if err != nil {
return fmt.Errorf("unable to open database: %v", err)
}
db = *postgres.BaseDB

default:
return fmt.Errorf("unknown database backend: %s",
cfg.DatabaseBackend)
}

defer db.Close()

// Get the bolt db.
_, boltDb, err := lndclient.NewBoltMacaroonStore(
cfg.DataDir, "macaroons.db", loopdb.DefaultLoopDBTimeout,
)
if err != nil {
return err
}

tx, err := boltDb.BeginReadTx()
if err != nil {
return err
}

bucket := tx.ReadBucket(rootKeyBucketName)
if bucket == nil {
return macaroons.ErrRootKeyBucketNotFound
}

encKeyDB := bucket.Get(encryptionKeyID)
if encKeyDB == nil {
return macaroons.ErrEncKeyNotFound
}

// Unmarshal parameters for the encryption key and derive the
// key with them.
encKeyOld := &snacl.SecretKey{}
err = encKeyOld.Unmarshal(encKeyDB)
if err != nil {
return err
}

// Verify that the password is correct.
err = encKeyOld.DeriveKey(&dbPassword)
if err != nil {
return err
}

// We'll now store the encryption key in the database.
err = db.InsertSecretKeyParams(ctx, encKeyDB)
if err != nil {
return err
}

// Now we'll iterate over the macaroon buckets and store the macaroons
// in the database.
return bucket.ForEach(func(k, v []byte) error {
// Skip the key if it is the encryption key ID since
// we do not want to re-encrypt this.
if bytes.Equal(k, encryptionKeyID) {
return nil
}

return db.InsertRootKey(ctx, sqlc.InsertRootKeyParams{
ID: k,
RootKey: v,
})
})
}

// needSqliteMigration checks if the boltdb exists at it's default location
// and returns true if it does.
func needSqliteMigration(cfg *Config) bool {
Expand Down
11 changes: 10 additions & 1 deletion loopd/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import (
)

// getClient returns an instance of the swap client.
func getClient(cfg *Config, lnd *lndclient.LndServices) (*loop.Client,
func getClient(ctx context.Context, cfg *Config,
lnd *lndclient.LndServices, dbPassword []byte) (*loop.Client,
func(), error) {

clientConfig := &loop.ClientConfig{
Expand All @@ -43,6 +44,7 @@ func getClient(cfg *Config, lnd *lndclient.LndServices) (*loop.Client,
cfg.Sqlite.DatabaseFileName)
db, err = loopdb.NewSqliteStore(
cfg.Sqlite, clientConfig.Lnd.ChainParams,
loopdb.WithEncryptionMiddleware(dbPassword),
)
rksDB = loopdb.NewRootKeyStore(db.(*loopdb.SqliteSwapStore).BaseDB)

Expand All @@ -51,6 +53,7 @@ func getClient(cfg *Config, lnd *lndclient.LndServices) (*loop.Client,
cfg.Postgres.DSN(true))
db, err = loopdb.NewPostgresStore(
cfg.Postgres, clientConfig.Lnd.ChainParams,
loopdb.WithEncryptionMiddleware(dbPassword),
)
rksDB = loopdb.NewRootKeyStore(db.(*loopdb.PostgresStore).BaseDB)

Expand All @@ -62,6 +65,12 @@ func getClient(cfg *Config, lnd *lndclient.LndServices) (*loop.Client,
return nil, nil, fmt.Errorf("unable to open database: %v", err)
}

// Initialize the secret key
err = db.Init(ctx)
if err != nil {
return nil, nil, err
}

swapClient, cleanUp, err := loop.NewClient(
cfg.DataDir, db, rksDB, clientConfig,
)
Expand Down
13 changes: 12 additions & 1 deletion loopd/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package loopd
import (
"context"
"fmt"
"time"

"github.com/btcsuite/btcd/chaincfg"
"github.com/lightninglabs/lndclient"
Expand All @@ -14,13 +15,23 @@ import (
func view(config *Config, lisCfg *ListenerCfg) error {
network := lndclient.Network(config.Network)

ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()

lnd, err := lisCfg.getLnd(network, config.Lnd)
if err != nil {
return err
}
defer lnd.Close()

swapClient, cleanup, err := getClient(config, &lnd.LndServices)
dbPassword, err := getDbPassword(ctx, &lnd.LndServices)
if err != nil {
return err
}

swapClient, cleanup, err := getClient(
ctx, config, &lnd.LndServices, dbPassword,
)
if err != nil {
return err
}
Expand Down
3 changes: 3 additions & 0 deletions loopdb/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ type SwapStore interface {
// it's decoding using the proto package's `Unmarshal` method.
FetchLiquidityParams(ctx context.Context) ([]byte, error)

// Init initializes the db and sets the macaroon store secret key.
Init(ctx context.Context) error

// Close closes the underlying database.
Close() error
}
Expand Down
74 changes: 42 additions & 32 deletions loopdb/macaroons.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"database/sql"
"io"

"github.com/jackc/pgx/v4"
"github.com/lightninglabs/loop/loopdb/sqlc"
"github.com/lightningnetwork/lnd/macaroons"
"gopkg.in/macaroon-bakery.v2/bakery"
Expand Down Expand Up @@ -45,7 +46,12 @@ func (r *RootKeyStore) Get(ctx context.Context,
return nil, err
}

return mac.RootKey, nil
decryptedMac, err := r.db.decryptOrNil(mac.RootKey)
if err != nil {
return nil, err
}

return decryptedMac, nil
}

// RootKey returns the root key to be used for making a new macaroon, and an id
Expand All @@ -58,45 +64,49 @@ func (r *RootKeyStore) RootKey(ctx context.Context) ([]byte, []byte, error) {
err error
)

// Create pass in the set of options to create a read/write
// transaction, which is the default.
var writeTxOpts SqliteTxOptions
dbErr := r.db.ExecTx(ctx, &writeTxOpts, func(q *sqlc.Queries) error {
// Read the root key ID from the context. If no key is
// specified in the context, an error will be returned.
id, err = macaroons.RootKeyIDFromContext(ctx)
if err != nil {
return err
}

// Check to see if there's a root key already stored for this
// ID.
mac, err := r.db.GetRootKey(ctx, id)
switch err {
case nil:
rootKey = mac.RootKey
return nil

case sql.ErrNoRows:
// Read the root key ID from the context. If no key is
// specified in the context, an error will be returned.
id, err = macaroons.RootKeyIDFromContext(ctx)
if err != nil {
return nil, nil, err
}

default:
return err
// Check to see if there's a root key already stored for this
// ID.
mac, err := r.db.GetRootKey(ctx, id)
switch err {
case nil:
// If there is, decrypt it and return it.
decryptedMac, err := r.db.decryptOrNil(mac.RootKey)
if err != nil {
return nil, nil, err
}

return decryptedMac, id, nil
case sql.ErrNoRows:
fallthrough
case pgx.ErrNoRows:
// Otherwise, we'll create a new root key for this ID.
rootKey = make([]byte, macaroons.RootKeyLen)
if _, err := io.ReadFull(rand.Reader, rootKey); err != nil {
return err
return nil, nil, err
}
default:
return nil, nil, err
}

// Insert this new root key into the database.
return r.db.InsertRootKey(ctx, sqlc.InsertRootKeyParams{
ID: id,
RootKey: rootKey,
})
// Encrypt the root key before storing it in the database.
encryptedRootKey, err := r.db.encrypt(rootKey)
if err != nil {
return nil, nil, err
}

// Insert this new root key into the database.
err = r.db.InsertRootKey(ctx, sqlc.InsertRootKeyParams{
ID: id,
RootKey: encryptedRootKey,
})
if dbErr != nil {
return nil, nil, dbErr
if err != nil {
return nil, nil, err
}

return rootKey, id, nil
Expand Down
Loading

0 comments on commit 12104e4

Please sign in to comment.