aperturedb: implement secrets store

This commit is contained in:
positiveblue
2023-05-27 09:36:34 -07:00
parent 6700d01b39
commit 9c5453b410
3 changed files with 236 additions and 1 deletions

171
aperturedb/secrets.go Normal file
View File

@@ -0,0 +1,171 @@
package aperturedb
import (
"context"
"crypto/rand"
"crypto/sha256"
"database/sql"
"fmt"
"github.com/lightninglabs/aperture/aperturedb/sqlc"
"github.com/lightninglabs/aperture/lsat"
"github.com/lightninglabs/aperture/mint"
"github.com/lightningnetwork/lnd/clock"
)
type (
// NewSecret is a struct that contains the parameters required to insert
// a new secret into the database.
NewSecret = sqlc.InsertSecretParams
)
// SecretsDB is an interface that defines the set of operations that can be
// executed against the secrets database.
type SecretsDB interface {
// InsertSecret inserts a new secret into the database.
InsertSecret(ctx context.Context, arg NewSecret) (int32, error)
// GetSecretByHash returns the secret that corresponds to the given
// hash.
GetSecretByHash(ctx context.Context, hash []byte) ([]byte, error)
// DeleteSecretByHash removes the secret that corresponds to the given
// hash.
DeleteSecretByHash(ctx context.Context, hash []byte) (int64, error)
}
// SecretsTxOptions defines the set of db txn options the SecretsStore
// understands.
type SecretsDBTxOptions 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 *SecretsDBTxOptions) ReadOnly() bool {
return a.readOnly
}
// NewSecretsDBReadTx creates a new read transaction option set.
func NewSecretsDBReadTx() SecretsDBTxOptions {
return SecretsDBTxOptions{
readOnly: true,
}
}
// BatchedSecretsDB is a version of the SecretsDB that's capable of batched
// database operations.
type BatchedSecretsDB interface {
SecretsDB
BatchedTx[SecretsDB]
}
// SecretsStore represents a storage backend.
type SecretsStore struct {
db BatchedSecretsDB
clock clock.Clock
}
// NewSecretsStore creates a new SecretsStore instance given a open
// BatchedSecretsDB storage backend.
func NewSecretsStore(db BatchedSecretsDB) *SecretsStore {
return &SecretsStore{
db: db,
clock: clock.NewDefaultClock(),
}
}
// NewSecret creates a new cryptographically random secret which is
// keyed by the given hash.
func (s *SecretsStore) NewSecret(ctx context.Context,
hash [sha256.Size]byte) ([lsat.SecretSize]byte, error) {
var secret [lsat.SecretSize]byte
if _, err := rand.Read(secret[:]); err != nil {
return [lsat.SecretSize]byte{}, err
}
var writeTxOpts SecretsDBTxOptions
err := s.db.ExecTx(ctx, &writeTxOpts, func(tx SecretsDB) error {
_, err := tx.InsertSecret(ctx, NewSecret{
Hash: hash[:],
Secret: secret[:],
CreatedAt: s.clock.Now().UTC(),
})
if err != nil {
return err
}
return nil
})
if err != nil {
return [lsat.SecretSize]byte{}, fmt.Errorf("unable to insert "+
"new secret for hash(%x): %w", hash, err)
}
return secret, nil
}
// GetSecret returns the cryptographically random secret that
// corresponds to the given hash. If there is no secret, then
// ErrSecretNotFound is returned.
func (s *SecretsStore) GetSecret(ctx context.Context,
hash [sha256.Size]byte) ([lsat.SecretSize]byte, error) {
var secret [lsat.SecretSize]byte
readOpts := NewSecretsDBReadTx()
err := s.db.ExecTx(ctx, &readOpts, func(db SecretsDB) error {
secretRow, err := db.GetSecretByHash(ctx, hash[:])
switch {
case err == sql.ErrNoRows:
return mint.ErrSecretNotFound
case err != nil:
return err
}
copy(secret[:], secretRow)
return nil
})
if err != nil {
return [lsat.SecretSize]byte{}, fmt.Errorf("unable to get "+
"secret for hash(%x): %w", hash, err)
}
return secret, nil
}
// RevokeSecret removes the cryptographically random secret that
// corresponds to the given hash. This acts as a NOP if the secret does
// not exist.
func (s *SecretsStore) RevokeSecret(ctx context.Context,
hash [sha256.Size]byte) error {
var writeTxOpts SecretsDBTxOptions
err := s.db.ExecTx(ctx, &writeTxOpts, func(tx SecretsDB) error {
nRows, err := tx.DeleteSecretByHash(ctx, hash[:])
if err != nil {
return err
}
if nRows != 1 {
log.Info("deleting secret(%x) did not affect %w rows",
hash, nRows)
}
return nil
})
if err != nil {
return fmt.Errorf("unable to revoke secret for hash(%x): %w",
hash, err)
}
return nil
}

View File

@@ -0,0 +1,64 @@
package aperturedb
import (
"context"
"crypto/rand"
"crypto/sha256"
"database/sql"
"testing"
"time"
"github.com/lightninglabs/aperture/mint"
"github.com/stretchr/testify/require"
)
var (
defaultTestTimeout = 5 * time.Second
)
func newSecretsStoreWithDB(t *testing.T, db *BaseDB) *SecretsStore {
dbTxer := NewTransactionExecutor(db,
func(tx *sql.Tx) SecretsDB {
return db.WithTx(tx)
},
)
return NewSecretsStore(dbTxer)
}
func TestSecretDB(t *testing.T) {
ctxt, cancel := context.WithTimeout(
context.Background(), defaultTestTimeout,
)
defer cancel()
// First, create a new test database.
db := NewTestDB(t)
store := newSecretsStoreWithDB(t, db.BaseDB)
// Create a random hash.
hash := [sha256.Size]byte{}
_, err := rand.Read(hash[:])
require.NoError(t, err)
// Trying to get a secret that doesn't exist should fail.
_, err = store.GetSecret(ctxt, hash)
require.ErrorIs(t, err, mint.ErrSecretNotFound)
// Create a new secret.
secret, err := store.NewSecret(ctxt, hash)
require.NoError(t, err)
// Get the secret from the db.
dbSecret, err := store.GetSecret(ctxt, hash)
require.NoError(t, err)
require.Equal(t, secret, dbSecret)
// Revoke the secret.
err = store.RevokeSecret(ctxt, hash)
require.NoError(t, err)
// The secret should no longer exist.
_, err = store.GetSecret(ctxt, hash)
require.ErrorIs(t, err, mint.ErrSecretNotFound)
}

2
go.mod
View File

@@ -22,6 +22,7 @@ require (
github.com/lightninglabs/lndclient v0.16.0-10
github.com/lightningnetwork/lnd v0.16.0-beta
github.com/lightningnetwork/lnd/cert v1.2.1
github.com/lightningnetwork/lnd/clock v1.1.0
github.com/lightningnetwork/lnd/tlv v1.1.0
github.com/lightningnetwork/lnd/tor v1.1.0
github.com/ory/dockertest/v3 v3.10.0
@@ -113,7 +114,6 @@ require (
github.com/lightninglabs/neutrino v0.15.0 // indirect
github.com/lightninglabs/neutrino/cache v1.1.1 // indirect
github.com/lightningnetwork/lightning-onion v1.2.1-0.20221202012345-ca23184850a1 // indirect
github.com/lightningnetwork/lnd/clock v1.1.0 // indirect
github.com/lightningnetwork/lnd/healthcheck v1.2.2 // indirect
github.com/lightningnetwork/lnd/kvdb v1.4.1 // indirect
github.com/lightningnetwork/lnd/queue v1.1.0 // indirect