Files
aperture/aperturedb/onion.go
2023-06-14 21:44:19 -07:00

171 lines
4.0 KiB
Go

package aperturedb
import (
"bytes"
"context"
"database/sql"
"fmt"
"github.com/lightninglabs/aperture/aperturedb/sqlc"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/tor"
)
type (
NewOnionPrivateKey = sqlc.UpsertOnionParams
)
// OnionDB is an interface that defines the set of operations that can be
// executed against the onion database.
type OnionDB interface {
// UpsertOnion inserts a new onion private key into the database. If
// the onion private key already exists in the db this is a NOOP
// operation.
UpsertOnion(ctx context.Context, arg NewOnionPrivateKey) error
// SelectOnionPrivateKey selects the onion private key from the
// database.
SelectOnionPrivateKey(ctx context.Context) ([]byte, error)
// DeleteOnionPrivateKey deletes the onion private key from the
// database.
DeleteOnionPrivateKey(ctx context.Context) error
}
// OnionTxOptions defines the set of db txn options the OnionStore
// understands.
type OnionDBTxOptions struct {
// readOnly governs if a read only transaction is needed or not.
readOnly bool
}
// ReadOnly returns true if the transaction should be read only.
//
// NOTE: This implements the TxOptions
func (a *OnionDBTxOptions) ReadOnly() bool {
return a.readOnly
}
// NewOnionDBReadTx creates a new read transaction option set.
func NewOnionDBReadTx() OnionDBTxOptions {
return OnionDBTxOptions{
readOnly: true,
}
}
// BatchedOnionDB is a version of the OnionDB that's capable of batched
// database operations.
type BatchedOnionDB interface {
OnionDB
BatchedTx[OnionDB]
}
// OnionStore represents a storage backend.
type OnionStore struct {
db BatchedOnionDB
clock clock.Clock
}
// NewOnionStore creates a new OnionStore instance given a open BatchedOnionDB
// storage backend.
func NewOnionStore(db BatchedOnionDB) *OnionStore {
return &OnionStore{
db: db,
clock: clock.NewDefaultClock(),
}
}
// StorePrivateKey stores the private key according to the implementation of
// the OnionStore interface.
func (o *OnionStore) StorePrivateKey(privateKey []byte) error {
ctxt, cancel := context.WithTimeout(
context.Background(), DefaultStoreTimeout,
)
defer cancel()
var writeTxOpts OnionDBTxOptions
err := o.db.ExecTx(ctxt, &writeTxOpts, func(tx OnionDB) error {
// Only store the private key if it doesn't already exist.
dbPK, err := tx.SelectOnionPrivateKey(ctxt)
switch {
// If there is already a different private key stored in the
/// database, return an error.
case dbPK != nil && !bytes.Equal(dbPK, privateKey):
return fmt.Errorf("private key already exists")
case err != nil && err != sql.ErrNoRows:
return err
}
params := NewOnionPrivateKey{
PrivateKey: privateKey,
CreatedAt: o.clock.Now().UTC(),
}
return tx.UpsertOnion(ctxt, params)
})
if err != nil {
return fmt.Errorf("failed to store private key: %v", err)
}
return nil
}
// PrivateKey retrieves a stored private key. If it is not found, then
// ErrNoPrivateKey should be returned.
func (o *OnionStore) PrivateKey() ([]byte, error) {
ctxt, cancel := context.WithTimeout(
context.Background(), DefaultStoreTimeout,
)
defer cancel()
var (
privateKey []byte
)
var readTxOpts OnionDBTxOptions
err := o.db.ExecTx(ctxt, &readTxOpts, func(tx OnionDB) error {
row, err := o.db.SelectOnionPrivateKey(ctxt)
switch {
case err == sql.ErrNoRows:
return tor.ErrNoPrivateKey
case err != nil:
return err
}
privateKey = make([]byte, len(row))
copy(privateKey, row)
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to retrieve private key: %w",
err)
}
return privateKey, nil
}
// DeletePrivateKey securely removes the private key from the store.
func (o *OnionStore) DeletePrivateKey() error {
ctxt, cancel := context.WithTimeout(
context.Background(), DefaultStoreTimeout,
)
defer cancel()
var writeTxOpts OnionDBTxOptions
err := o.db.ExecTx(ctxt, &writeTxOpts, func(tx OnionDB) error {
return tx.DeleteOnionPrivateKey(ctxt)
})
if err != nil {
return fmt.Errorf("failed to delete private key: %v", err)
}
return nil
}