diff --git a/aperturedb/secrets.go b/aperturedb/secrets.go new file mode 100644 index 0000000..e698cca --- /dev/null +++ b/aperturedb/secrets.go @@ -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 +} diff --git a/aperturedb/secrets_test.go b/aperturedb/secrets_test.go new file mode 100644 index 0000000..a3d94d5 --- /dev/null +++ b/aperturedb/secrets_test.go @@ -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) +} diff --git a/go.mod b/go.mod index de41004..0acccac 100644 --- a/go.mod +++ b/go.mod @@ -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