mirror of
https://github.com/lightninglabs/aperture.git
synced 2026-01-31 15:14:26 +01:00
aperturedb: implement onion store
This commit is contained in:
170
aperturedb/onion.go
Normal file
170
aperturedb/onion.go
Normal 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
56
aperturedb/onion_test.go
Normal 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)
|
||||
}
|
||||
@@ -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{}
|
||||
|
||||
Reference in New Issue
Block a user