aperturedb: implement onion store

This commit is contained in:
positiveblue
2023-05-27 13:49:51 -07:00
parent 9c5453b410
commit 680da97b62
3 changed files with 228 additions and 2 deletions

170
aperturedb/onion.go Normal file
View File

@@ -0,0 +1,170 @@
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
}

56
aperturedb/onion_test.go Normal file
View File

@@ -0,0 +1,56 @@
package aperturedb
import (
"database/sql"
"testing"
"github.com/lightningnetwork/lnd/tor"
"github.com/stretchr/testify/require"
)
func newOnionStoreWithDB(db *BaseDB) *OnionStore {
dbTxer := NewTransactionExecutor(db,
func(tx *sql.Tx) OnionDB {
return db.WithTx(tx)
},
)
return NewOnionStore(dbTxer)
}
func TestOnionDB(t *testing.T) {
// First, create a new test database.
db := NewTestDB(t)
store := newOnionStoreWithDB(db.BaseDB)
// Attempting to retrieve a private key when none is stored returns the
// expected error.
_, err := store.PrivateKey()
require.ErrorIs(t, err, tor.ErrNoPrivateKey)
// Store a private key.
privateKey := []byte("private key")
err = store.StorePrivateKey(privateKey)
require.NoError(t, err)
// Retrieving the private key should return the stored value.
privateKeyDB, err := store.PrivateKey()
require.NoError(t, err)
require.Equal(t, privateKey, privateKeyDB)
// Storing the same private key should not return an error.
err = store.StorePrivateKey(privateKey)
require.NoError(t, err)
// We can only store one private key.
newPrivateKey := []byte("second private key")
err = store.StorePrivateKey(newPrivateKey)
require.Error(t, err)
// Remove the stored private key.
err = store.DeletePrivateKey()
require.NoError(t, err)
err = store.StorePrivateKey(newPrivateKey)
require.NoError(t, err)
}

View File

@@ -16,7 +16,7 @@ var (
defaultTestTimeout = 5 * time.Second
)
func newSecretsStoreWithDB(t *testing.T, db *BaseDB) *SecretsStore {
func newSecretsStoreWithDB(db *BaseDB) *SecretsStore {
dbTxer := NewTransactionExecutor(db,
func(tx *sql.Tx) SecretsDB {
return db.WithTx(tx)
@@ -34,7 +34,7 @@ func TestSecretDB(t *testing.T) {
// First, create a new test database.
db := NewTestDB(t)
store := newSecretsStoreWithDB(t, db.BaseDB)
store := newSecretsStoreWithDB(db.BaseDB)
// Create a random hash.
hash := [sha256.Size]byte{}