diff --git a/pkg/crypto/crypto.go b/pkg/crypto/crypto.go index f352dfb..b37eafa 100644 --- a/pkg/crypto/crypto.go +++ b/pkg/crypto/crypto.go @@ -40,5 +40,8 @@ var ErrKeyNotFound = fmt.Errorf("key not found") // ErrKeyExistsForSubjectID encryption key has already been set for subjectID. var ErrKeyExistsForSubjectID = fmt.Errorf("key already exists for subjectID") +// ErrKeyAlreadyUsed encryption key has already been set for subjectID. +var ErrKeyAlreadyUsed = fmt.Errorf("encryption key already used") + // ErrInvalidKey encryption key is not valid. var ErrInvalidKey = fmt.Errorf("invalid encryption key") diff --git a/pkg/crypto/cryptotest/verify_key_store.go b/pkg/crypto/cryptotest/verify_key_store.go index 2986b17..c14f84d 100644 --- a/pkg/crypto/cryptotest/verify_key_store.go +++ b/pkg/crypto/cryptotest/verify_key_store.go @@ -95,5 +95,22 @@ func VerifyKeyStore(t *testing.T, newStore func(t *testing.T) crypto.KeyStore) { // Then require.Equal(t, crypto.ErrInvalidKey, err) }) + + t.Run("errors due to duplicate encryption key for two subjectIDs", func(t *testing.T) { + t.Skip("TODO: add support for unique secrets") + // Given + const key = "062cb6d874ac49f4ac48e3ff7b0124d3" + subjectID1 := shortuuid.New().String() + subjectID2 := shortuuid.New().String() + store := newStore(t) + err := store.Set(subjectID1, key) + require.NoError(t, err) + + // When + err = store.Set(subjectID2, key) + + // Then + require.Equal(t, crypto.ErrKeyAlreadyUsed, err) + }) }) } diff --git a/pkg/crypto/provider/inmemorykeystore/inmemory_keystore.go b/pkg/crypto/provider/inmemorykeystore/inmemory_keystore.go index e65b404..d2d9820 100644 --- a/pkg/crypto/provider/inmemorykeystore/inmemory_keystore.go +++ b/pkg/crypto/provider/inmemorykeystore/inmemory_keystore.go @@ -7,13 +7,15 @@ import ( ) type inMemoryKeyStore struct { - mux sync.RWMutex - EncryptionKeys map[string]string + mux sync.RWMutex + EncryptionKeysBySubjectID map[string]string + EncryptionKeys map[string]struct{} } func New() *inMemoryKeyStore { return &inMemoryKeyStore{ - EncryptionKeys: make(map[string]string), + EncryptionKeysBySubjectID: make(map[string]string), + EncryptionKeys: make(map[string]struct{}), } } @@ -21,7 +23,7 @@ func (i *inMemoryKeyStore) Get(subjectID string) (string, error) { i.mux.RLock() defer i.mux.RUnlock() - if key, ok := i.EncryptionKeys[subjectID]; ok { + if key, ok := i.EncryptionKeysBySubjectID[subjectID]; ok { if key == "" { return "", crypto.ErrKeyWasDeleted } @@ -40,11 +42,16 @@ func (i *inMemoryKeyStore) Set(subjectID, key string) error { i.mux.Lock() defer i.mux.Unlock() - if _, ok := i.EncryptionKeys[subjectID]; ok { + if _, ok := i.EncryptionKeys[key]; ok { + return crypto.ErrKeyAlreadyUsed + } + + if _, ok := i.EncryptionKeysBySubjectID[subjectID]; ok { return crypto.ErrKeyExistsForSubjectID } - i.EncryptionKeys[subjectID] = key + i.EncryptionKeysBySubjectID[subjectID] = key + i.EncryptionKeys[key] = struct{}{} return nil } @@ -53,7 +60,7 @@ func (i *inMemoryKeyStore) Delete(subjectID string) error { i.mux.Lock() defer i.mux.Unlock() - i.EncryptionKeys[subjectID] = "" + i.EncryptionKeysBySubjectID[subjectID] = "" return nil } diff --git a/pkg/crypto/provider/inmemorykeystore/inmemory_keystore_test.go b/pkg/crypto/provider/inmemorykeystore/inmemory_keystore_test.go index d31a976..bbf781f 100644 --- a/pkg/crypto/provider/inmemorykeystore/inmemory_keystore_test.go +++ b/pkg/crypto/provider/inmemorykeystore/inmemory_keystore_test.go @@ -8,7 +8,7 @@ import ( "github.com/inklabs/rangedb/pkg/crypto/provider/inmemorykeystore" ) -func TestInMemoryCrypto_VerifyEngineInterface(t *testing.T) { +func TestInMemoryCrypto_VerifyKeyStoreInterface(t *testing.T) { cryptotest.VerifyKeyStore(t, func(t *testing.T) crypto.KeyStore { return inmemorykeystore.New() }) diff --git a/pkg/crypto/provider/postgreskeystore/postgres_keystore.go b/pkg/crypto/provider/postgreskeystore/postgres_keystore.go new file mode 100644 index 0000000..2ba54e9 --- /dev/null +++ b/pkg/crypto/provider/postgreskeystore/postgres_keystore.go @@ -0,0 +1,133 @@ +package postgreskeystore + +import ( + "database/sql" + "fmt" + "time" + + "github.com/lib/pq" + + "github.com/inklabs/rangedb/pkg/crypto" + "github.com/inklabs/rangedb/provider/postgresstore" +) + +const ( + PgUniqueViolationCode = pq.ErrorCode("23505") + PgDuplicateSubjectIDViolation = "vault_pkey" + PgDuplicateEncryptionKeyViolation = "vault_encryptionkey_key" +) + +type postgresKeyStore struct { + config *postgresstore.Config + db *sql.DB +} + +func New(config *postgresstore.Config) (*postgresKeyStore, error) { + p := &postgresKeyStore{ + config: config, + } + + err := p.connectToDB() + if err != nil { + return nil, err + } + err = p.initDB() + if err != nil { + return nil, err + } + + return p, nil +} + +func (p *postgresKeyStore) Get(subjectID string) (string, error) { + row := p.db.QueryRow("SELECT EncryptionKey, DeletedAtTimestamp FROM vault WHERE SubjectID = $1", + subjectID) + + var encryptionKey string + var deletedAtTimestamp *uint64 + err := row.Scan(&encryptionKey, &deletedAtTimestamp) + if err != nil { + if err == sql.ErrNoRows { + return "", crypto.ErrKeyNotFound + } + + return "", err + } + + if deletedAtTimestamp != nil { + return "", crypto.ErrKeyWasDeleted + } + + return encryptionKey, nil +} + +func (p *postgresKeyStore) Set(subjectID, encryptionKey string) error { + if encryptionKey == "" { + return crypto.ErrInvalidKey + } + + _, err := p.db.Exec("INSERT INTO vault (SubjectID, EncryptionKey) VALUES ($1, $2)", + subjectID, encryptionKey) + if err != nil { + if err, ok := err.(*pq.Error); ok { + if err.Code == PgUniqueViolationCode { + switch err.Constraint { + case PgDuplicateSubjectIDViolation: + return crypto.ErrKeyExistsForSubjectID + + case PgDuplicateEncryptionKeyViolation: + return crypto.ErrKeyAlreadyUsed + } + } + } + return err + } + + return nil +} + +func (p *postgresKeyStore) Delete(subjectID string) error { + _, err := p.db.Exec("UPDATE vault SET DeletedAtTimestamp = $1 WHERE SubjectID = $2", + time.Now().Unix(), + subjectID) + if err != nil { + return err + } + + return nil +} + +func (p *postgresKeyStore) connectToDB() error { + db, err := sql.Open("postgres", p.config.DataSourceName()) + if err != nil { + return fmt.Errorf("unable to open DB connection: %v", err) + } + + err = db.Ping() + if err != nil { + return fmt.Errorf("unable to connect to DB: %v", err) + } + + p.db = db + + return nil +} + +func (p *postgresKeyStore) initDB() error { + sqlStatements := []string{ + `CREATE TABLE IF NOT EXISTS vault ( + SubjectID TEXT PRIMARY KEY, + EncryptionKey TEXT UNIQUE, + DeletedAtTimestamp BIGINT + );`, + } + + for _, statement := range sqlStatements { + _, err := p.db.Exec(statement) + if err != nil { + return err + } + } + + return nil +} diff --git a/pkg/crypto/provider/postgreskeystore/postgres_keystore_test.go b/pkg/crypto/provider/postgreskeystore/postgres_keystore_test.go new file mode 100644 index 0000000..a8fdaa8 --- /dev/null +++ b/pkg/crypto/provider/postgreskeystore/postgres_keystore_test.go @@ -0,0 +1,37 @@ +package postgreskeystore_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/inklabs/rangedb/pkg/crypto" + "github.com/inklabs/rangedb/pkg/crypto/cryptotest" + "github.com/inklabs/rangedb/pkg/crypto/provider/postgreskeystore" + "github.com/inklabs/rangedb/provider/postgresstore" +) + +func TestPostgresKeyStore_VerifyKeyStoreInterface(t *testing.T) { + config := configFromEnvironment(t) + + cryptotest.VerifyKeyStore(t, func(t *testing.T) crypto.KeyStore { + keyStore, err := postgreskeystore.New(config) + require.NoError(t, err) + + return keyStore + }) +} + +type testSkipper interface { + Skip(args ...interface{}) +} + +// TODO: Move postgresstore.Config to separate package +func configFromEnvironment(t testSkipper) *postgresstore.Config { + config, err := postgresstore.NewConfigFromEnvironment() + if err != nil { + t.Skip("Postgres DB has not been configured via environment variables to run integration tests") + } + + return config +}