mirror of
https://github.com/lightninglabs/aperture.git
synced 2026-01-31 15:14:26 +01:00
aperturedb: implement secrets store
This commit is contained in:
171
aperturedb/secrets.go
Normal file
171
aperturedb/secrets.go
Normal 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
|
||||
}
|
||||
64
aperturedb/secrets_test.go
Normal file
64
aperturedb/secrets_test.go
Normal 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
2
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
|
||||
|
||||
Reference in New Issue
Block a user