diff --git a/pkg/mock/storage/mock_store.go b/pkg/mock/storage/mock_store.go index eeaf724c0f..fd17c5d80e 100644 --- a/pkg/mock/storage/mock_store.go +++ b/pkg/mock/storage/mock_store.go @@ -19,6 +19,8 @@ type MockStoreProvider struct { Store *MockStore Custom storage.Store ErrOpenStoreHandle error + ErrClose error + ErrCloseStore error FailNamespace string } @@ -50,12 +52,12 @@ func (s *MockStoreProvider) OpenStore(name string) (storage.Store, error) { // Close closes all stores created under this store provider. func (s *MockStoreProvider) Close() error { - return nil + return s.ErrClose } // CloseStore closes store for given name space. func (s *MockStoreProvider) CloseStore(name string) error { - return nil + return s.ErrCloseStore } // MockStore mock store. diff --git a/pkg/storage/sds/edv/documentprocessor/documentprocessor.go b/pkg/storage/sds/edv/documentprocessor/documentprocessor.go new file mode 100644 index 0000000000..b9696597ae --- /dev/null +++ b/pkg/storage/sds/edv/documentprocessor/documentprocessor.go @@ -0,0 +1,18 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package documentprocessor + +import ( + "github.com/hyperledger/aries-framework-go/pkg/storage/sds" +) + +// DocumentProcessor represents a type that can encrypt and decrypt between +// Structured Documents and Encrypted Documents. +type DocumentProcessor interface { + Encrypt(*sds.StructuredDocument) (*sds.EncryptedDocument, error) + Decrypt(*sds.EncryptedDocument) (*sds.StructuredDocument, error) +} diff --git a/pkg/storage/sds/edv/formatprovider/formatprovider.go b/pkg/storage/sds/edv/formatprovider/formatprovider.go new file mode 100644 index 0000000000..4a9c66e3ef --- /dev/null +++ b/pkg/storage/sds/edv/formatprovider/formatprovider.go @@ -0,0 +1,183 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package formatprovider + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/hyperledger/aries-framework-go/pkg/storage" + "github.com/hyperledger/aries-framework-go/pkg/storage/sds" + "github.com/hyperledger/aries-framework-go/pkg/storage/sds/edv/documentprocessor" +) + +const ( + failOpenUnderlyingStore = "failed to open underlying store in FormatProvider: %w" + failCloseUnderlyingStore = "failed to close underlying store in FormatProvider: %w" + failCloseAllUnderlyingStores = "failed to close all underlying stores in FormatProvider: %w" + failEncryptStructuredDocument = "failed to encrypt structured document into a encrypted document: %w" + failMarshalEncryptedDocument = "failed to marshal encrypted document into bytes: %w" + failPutInUnderlyingStore = "failed to put encrypted document in underlying store in formatStore: %w" + failGetFromUnderlyingStore = "failed to get encrypted document bytes from underlying store in formatStore: %w" + failUnmarshalEncryptedDocument = "failed to unmarshal encrypted document bytes into encrypted document struct: %w" + failDecryptEncryptedDocument = "failed to decrypt encrypted document into a structured document: %w" + failDeleteInUnderlyingStore = "failed to delete key-value pair in underlying store in formatStore: %w" + failQueryUnderlyingStore = "failed to query underlying store in formatStore: %w" + payloadKey = "payload" +) + +var ( + errPayloadKeyMissing = errors.New(`the structured document content did not contain the ` + + `expected "payload" key`) + errPayloadNotAssertableAsByteArray = errors.New("unable to assert the payload value as a []byte") +) + +type marshalFunc func(interface{}) ([]byte, error) + +// FormatProvider is an encrypted storage provider that uses EDV document models +// as defined in https://identity.foundation/secure-data-store/#data-model. +type FormatProvider struct { + storeProvider storage.Provider + documentProcessor documentprocessor.DocumentProcessor +} + +// New instantiates a new FormatProvider with the given underlying provider and EDV document processor. +// The underlying store provider determines where/how the data (in EDV Encrypted Document format) is actually stored. It +// only deals with data in encrypted form and cannot read the data flowing in or out of it. +// The EDV document processor handles encryption/decryption between structured documents and encrypted documents. +// It contains the necessary crypto functionality. +func New(underlyingProvider storage.Provider, + encryptedDocumentProcessor documentprocessor.DocumentProcessor) (FormatProvider, error) { + return FormatProvider{ + storeProvider: underlyingProvider, + documentProcessor: encryptedDocumentProcessor, + }, nil +} + +// OpenStore opens a store in the underlying provider with the given name and returns a handle to it. +func (p FormatProvider) OpenStore(name string) (storage.Store, error) { + store, err := p.storeProvider.OpenStore(name) + if err != nil { + return nil, fmt.Errorf(failOpenUnderlyingStore, err) + } + + edvStore := formatStore{ + underlyingStore: store, + documentProcessor: p.documentProcessor, + marshal: json.Marshal, + } + + return &edvStore, nil +} + +// CloseStore closes the store with the given name in the underlying provider. +func (p FormatProvider) CloseStore(name string) error { + err := p.storeProvider.CloseStore(name) + if err != nil { + return fmt.Errorf(failCloseUnderlyingStore, err) + } + + return p.storeProvider.CloseStore(name) +} + +// Close closes all stores created in the underlying store provider. +func (p FormatProvider) Close() error { + err := p.storeProvider.Close() + if err != nil { + return fmt.Errorf(failCloseAllUnderlyingStores, err) + } + + return p.storeProvider.Close() +} + +type formatStore struct { + underlyingStore storage.Store + documentProcessor documentprocessor.DocumentProcessor + + marshal marshalFunc +} + +func (s formatStore) Put(k string, v []byte) error { + content := make(map[string]interface{}) + content["payload"] = v + + structuredDoc := sds.StructuredDocument{ + ID: k, + Content: content, + } + + encryptedDoc, err := s.documentProcessor.Encrypt(&structuredDoc) + if err != nil { + return fmt.Errorf(failEncryptStructuredDocument, err) + } + + encryptedDocBytes, err := s.marshal(encryptedDoc) + if err != nil { + return fmt.Errorf(failMarshalEncryptedDocument, err) + } + + err = s.underlyingStore.Put(k, encryptedDocBytes) + if err != nil { + return fmt.Errorf(failPutInUnderlyingStore, err) + } + + return nil +} + +func (s formatStore) Get(k string) ([]byte, error) { + encryptedDocBytes, err := s.underlyingStore.Get(k) + if err != nil { + return nil, fmt.Errorf(failGetFromUnderlyingStore, err) + } + + var encryptedDocument sds.EncryptedDocument + + err = json.Unmarshal(encryptedDocBytes, &encryptedDocument) + if err != nil { + return nil, fmt.Errorf(failUnmarshalEncryptedDocument, err) + } + + structuredDocument, err := s.documentProcessor.Decrypt(&encryptedDocument) + if err != nil { + return nil, fmt.Errorf(failDecryptEncryptedDocument, err) + } + + payloadValue, ok := structuredDocument.Content[payloadKey] + if !ok { + return nil, errPayloadKeyMissing + } + + payloadValueBytes, ok := payloadValue.([]byte) + if !ok { + return nil, errPayloadNotAssertableAsByteArray + } + + return payloadValueBytes, nil +} + +func (s formatStore) Iterator(startKey, endKey string) storage.StoreIterator { + return s.underlyingStore.Iterator(startKey, endKey) +} + +func (s formatStore) Delete(k string) error { + err := s.underlyingStore.Delete(k) + if err != nil { + return fmt.Errorf(failDeleteInUnderlyingStore, err) + } + + return nil +} + +func (s formatStore) Query(query string) (storage.StoreIterator, error) { + iterator, err := s.underlyingStore.Query(query) + if err != nil { + return nil, fmt.Errorf(failQueryUnderlyingStore, err) + } + + return iterator, err +} diff --git a/pkg/storage/sds/edv/formatprovider/formatprovider_test.go b/pkg/storage/sds/edv/formatprovider/formatprovider_test.go new file mode 100644 index 0000000000..fff8be523a --- /dev/null +++ b/pkg/storage/sds/edv/formatprovider/formatprovider_test.go @@ -0,0 +1,399 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package formatprovider + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + mockstorage "github.com/hyperledger/aries-framework-go/pkg/mock/storage" + "github.com/hyperledger/aries-framework-go/pkg/storage/mem" + "github.com/hyperledger/aries-framework-go/pkg/storage/sds" +) + +func TestNew(t *testing.T) { + provider, err := New(mem.NewProvider(), &mockDocumentProcessor{}) + require.NoError(t, err) + require.NotNil(t, provider) +} + +func TestFormatProvider_OpenStore(t *testing.T) { + t.Run("Success", func(t *testing.T) { + provider, err := New(mem.NewProvider(), &mockDocumentProcessor{}) + require.NoError(t, err) + require.NotNil(t, provider) + + store, err := provider.OpenStore("testName") + require.NoError(t, err) + require.NotNil(t, store) + }) + t.Run("Fail to open store in underlying provider", func(t *testing.T) { + errTest := errors.New("test error") + + mockStoreProvider := mockstorage.MockStoreProvider{ErrOpenStoreHandle: errTest} + + provider, err := New(&mockStoreProvider, &mockDocumentProcessor{}) + require.NoError(t, err) + require.NotNil(t, provider) + + store, err := provider.OpenStore("testName") + require.EqualError(t, err, fmt.Errorf(failOpenUnderlyingStore, errTest).Error()) + require.Nil(t, store) + }) +} + +func TestFormatProvider_CloseStore(t *testing.T) { + t.Run("Success", func(t *testing.T) { + provider, err := New(mem.NewProvider(), &mockDocumentProcessor{}) + require.NoError(t, err) + require.NotNil(t, provider) + + err = provider.CloseStore("testName") + require.NoError(t, err) + }) + t.Run("Fail to close store in underlying provider", func(t *testing.T) { + errTest := errors.New("test error") + + mockStoreProvider := mockstorage.NewMockStoreProvider() + mockStoreProvider.ErrCloseStore = errTest + + provider, err := New(mockStoreProvider, &mockDocumentProcessor{}) + require.NoError(t, err) + require.NotNil(t, provider) + + err = provider.CloseStore("testName") + require.EqualError(t, err, fmt.Errorf(failCloseUnderlyingStore, errTest).Error()) + }) +} + +func TestFormatProvider_Close(t *testing.T) { + t.Run("Success", func(t *testing.T) { + provider, err := New(mem.NewProvider(), &mockDocumentProcessor{}) + require.NoError(t, err) + require.NotNil(t, provider) + + err = provider.Close() + require.NoError(t, err) + }) + t.Run("Fail to close all stores in underlying provider", func(t *testing.T) { + errTest := errors.New("test error") + + mockStoreProvider := mockstorage.NewMockStoreProvider() + mockStoreProvider.ErrClose = errTest + + provider, err := New(mockStoreProvider, &mockDocumentProcessor{}) + require.NoError(t, err) + require.NotNil(t, provider) + + err = provider.Close() + require.EqualError(t, err, fmt.Errorf(failCloseAllUnderlyingStores, errTest).Error()) + }) +} + +func Test_formatStore_Put(t *testing.T) { + t.Run("Success", func(t *testing.T) { + provider, err := New(mem.NewProvider(), &mockDocumentProcessor{}) + require.NoError(t, err) + require.NotNil(t, provider) + + store, err := provider.OpenStore("testName") + require.NoError(t, err) + require.NotNil(t, store) + + err = store.Put("key", []byte("data")) + require.NoError(t, err) + }) + t.Run("Fail to encrypt structured document", func(t *testing.T) { + errTest := errors.New("test error") + + provider, err := New(mem.NewProvider(), &mockDocumentProcessor{errEncrypt: errTest}) + require.NoError(t, err) + require.NotNil(t, provider) + + store, err := provider.OpenStore("testName") + require.NoError(t, err) + require.NotNil(t, store) + + err = store.Put("key", []byte("data")) + require.EqualError(t, err, fmt.Errorf(failEncryptStructuredDocument, errTest).Error()) + }) + t.Run("Fail to marshal encrypted document", func(t *testing.T) { + provider, err := New(mem.NewProvider(), &mockDocumentProcessor{}) + require.NoError(t, err) + require.NotNil(t, provider) + + store, err := provider.OpenStore("testName") + require.NoError(t, err) + require.NotNil(t, store) + + fmtStore, ok := store.(*formatStore) + require.True(t, ok, "Failed to assert store as an *formatStore") + fmtStore.marshal = failingMarshal + + err = store.Put("key", []byte("data")) + require.EqualError(t, err, fmt.Errorf(failMarshalEncryptedDocument, errFailingMarshal).Error()) + }) + t.Run("Fail to put encrypted document bytes into underlying store", func(t *testing.T) { + errTest := errors.New("test error") + + mockStoreProvider := mockstorage.NewMockStoreProvider() + mockStoreProvider.Store.ErrPut = errTest + + provider, err := New(mockStoreProvider, &mockDocumentProcessor{}) + require.NoError(t, err) + require.NotNil(t, provider) + + store, err := provider.OpenStore("testName") + require.NoError(t, err) + require.NotNil(t, store) + + err = store.Put("key", []byte("data")) + require.EqualError(t, err, fmt.Errorf(failPutInUnderlyingStore, errTest).Error()) + }) +} + +func Test_formatStore_Get(t *testing.T) { + t.Run("Success", func(t *testing.T) { + provider, err := New(mem.NewProvider(), &mockDocumentProcessor{}) + require.NoError(t, err) + require.NotNil(t, provider) + + store, err := provider.OpenStore("testName") + require.NoError(t, err) + require.NotNil(t, store) + + err = store.Put("key", []byte("data")) + require.NoError(t, err) + + value, err := store.Get("key") + require.NoError(t, err) + require.Equal(t, "data", string(value)) + }) + t.Run("Fail to get encrypted document bytes from underlying store", func(t *testing.T) { + errTest := errors.New("test error") + + mockStoreProvider := mockstorage.NewMockStoreProvider() + mockStoreProvider.Store.ErrGet = errTest + + provider, err := New(mockStoreProvider, &mockDocumentProcessor{}) + require.NoError(t, err) + require.NotNil(t, provider) + + store, err := provider.OpenStore("testName") + require.NoError(t, err) + require.NotNil(t, store) + + err = store.Put("key", []byte("data")) + require.NoError(t, err) + + value, err := store.Get("key") + require.EqualError(t, err, fmt.Errorf(failGetFromUnderlyingStore, errTest).Error()) + require.Nil(t, value) + }) + t.Run("Fail to unmarshal encrypted document", func(t *testing.T) { + provider, err := New(mem.NewProvider(), &mockDocumentProcessor{}) + require.NoError(t, err) + require.NotNil(t, provider) + + store, err := provider.OpenStore("testName") + require.NoError(t, err) + require.NotNil(t, store) + + fmtStore, ok := store.(*formatStore) + require.True(t, ok, "Failed to assert store as an *formatStore") + fmtStore.marshal = failingMarshal + + err = fmtStore.underlyingStore.Put("key", []byte("Not a valid Encrypted Document!")) + require.NoError(t, err) + + value, err := store.Get("key") + require.EqualError(t, err, + fmt.Errorf(failUnmarshalEncryptedDocument, + errors.New("invalid character 'N' looking for beginning of value")).Error()) + require.Nil(t, value) + }) + t.Run("Fail to decrypt encrypted document", func(t *testing.T) { + errTest := errors.New("test error") + + provider, err := New(mem.NewProvider(), &mockDocumentProcessor{errDecrypt: errTest}) + require.NoError(t, err) + require.NotNil(t, provider) + + store, err := provider.OpenStore("testName") + require.NoError(t, err) + require.NotNil(t, store) + + err = store.Put("key", []byte("data")) + require.NoError(t, err) + + value, err := store.Get("key") + require.EqualError(t, err, fmt.Errorf(failDecryptEncryptedDocument, errTest).Error()) + require.Nil(t, value) + }) + t.Run("Structured document is missing the payload key", func(t *testing.T) { + provider, err := New(mem.NewProvider(), + &mockDocumentProcessor{structuredDocToReturnOnDecrypt: &sds.StructuredDocument{}}) + require.NoError(t, err) + require.NotNil(t, provider) + + store, err := provider.OpenStore("testName") + require.NoError(t, err) + require.NotNil(t, store) + + err = store.Put("key", []byte("data")) + require.NoError(t, err) + + value, err := store.Get("key") + require.EqualError(t, err, errPayloadKeyMissing.Error()) + require.Nil(t, value) + }) + t.Run("Structured document payload cannot be asserted as []byte", func(t *testing.T) { + content := make(map[string]interface{}) + content["payload"] = "not a []byte" + + unexpectedStructuredDocument := &sds.StructuredDocument{ + Content: content, + } + provider, err := New(mem.NewProvider(), + &mockDocumentProcessor{structuredDocToReturnOnDecrypt: unexpectedStructuredDocument}) + require.NoError(t, err) + require.NotNil(t, provider) + + store, err := provider.OpenStore("testName") + require.NoError(t, err) + require.NotNil(t, store) + + err = store.Put("key", []byte("data")) + require.NoError(t, err) + + value, err := store.Get("key") + require.EqualError(t, err, errPayloadNotAssertableAsByteArray.Error()) + require.Nil(t, value) + }) +} + +func Test_formatStore_Iterator(t *testing.T) { + t.Run("Success", func(t *testing.T) { + provider, err := New(mem.NewProvider(), &mockDocumentProcessor{}) + require.NoError(t, err) + require.NotNil(t, provider) + + store, err := provider.OpenStore("testName") + require.NoError(t, err) + require.NotNil(t, store) + + iterator := store.Iterator("", "") + require.NotNil(t, iterator) + }) +} + +func Test_formatStore_Delete(t *testing.T) { + t.Run("Success", func(t *testing.T) { + provider, err := New(mem.NewProvider(), &mockDocumentProcessor{}) + require.NoError(t, err) + require.NotNil(t, provider) + + store, err := provider.OpenStore("testName") + require.NoError(t, err) + require.NotNil(t, store) + + err = store.Delete("key") + require.NoError(t, err) + }) + t.Run("Fail to delete underlying store", func(t *testing.T) { + errTest := errors.New("test error") + + mockStoreProvider := mockstorage.NewMockStoreProvider() + mockStoreProvider.Store.ErrDelete = errTest + + provider, err := New(mockStoreProvider, &mockDocumentProcessor{}) + require.NoError(t, err) + require.NotNil(t, provider) + + store, err := provider.OpenStore("testName") + require.NoError(t, err) + require.NotNil(t, store) + + err = store.Delete("key") + require.EqualError(t, err, fmt.Errorf(failDeleteInUnderlyingStore, errTest).Error()) + }) +} + +func Test_formatStore_Query(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mockStoreProvider := mockstorage.NewMockStoreProvider() + mockStoreProvider.Store.QueryReturnValue = mockstorage.NewMockIterator(nil) + + provider, err := New(mockStoreProvider, &mockDocumentProcessor{}) + require.NoError(t, err) + require.NotNil(t, provider) + + store, err := provider.OpenStore("testName") + require.NoError(t, err) + require.NotNil(t, store) + + iterator, err := store.Query("query") + require.NoError(t, err) + require.NotNil(t, iterator) + }) + t.Run("Fail to query underlying store", func(t *testing.T) { + errTest := errors.New("test error") + + mockStoreProvider := mockstorage.NewMockStoreProvider() + mockStoreProvider.Store.ErrQuery = errTest + + provider, err := New(mockStoreProvider, &mockDocumentProcessor{}) + require.NoError(t, err) + require.NotNil(t, provider) + + store, err := provider.OpenStore("testName") + require.NoError(t, err) + require.NotNil(t, store) + + iterator, err := store.Query("query") + require.EqualError(t, err, fmt.Errorf(failQueryUnderlyingStore, errTest).Error()) + require.Nil(t, iterator) + }) +} + +type mockDocumentProcessor struct { + errEncrypt error + errDecrypt error + structuredDocToReturnOnDecrypt *sds.StructuredDocument +} + +func (m *mockDocumentProcessor) Encrypt(*sds.StructuredDocument) (*sds.EncryptedDocument, error) { + return &sds.EncryptedDocument{}, m.errEncrypt +} + +func (m *mockDocumentProcessor) Decrypt(*sds.EncryptedDocument) (*sds.StructuredDocument, error) { + var structuredDocToReturn *sds.StructuredDocument + + if m.structuredDocToReturnOnDecrypt != nil { + structuredDocToReturn = m.structuredDocToReturnOnDecrypt + } else { + content := make(map[string]interface{}) + content["payload"] = []byte("data") + + structuredDoc := sds.StructuredDocument{ + Content: content, + } + + structuredDocToReturn = &structuredDoc + } + + return structuredDocToReturn, m.errDecrypt +} + +var errFailingMarshal = errors.New("failingMarshal always fails") + +func failingMarshal(interface{}) ([]byte, error) { + return nil, errFailingMarshal +} diff --git a/pkg/storage/sds/models.go b/pkg/storage/sds/models.go new file mode 100644 index 0000000000..17dce66323 --- /dev/null +++ b/pkg/storage/sds/models.go @@ -0,0 +1,50 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package sds + +import ( + "github.com/hyperledger/aries-framework-go/pkg/doc/jose" +) + +// StructuredDocument represents a Structured Document +// as defined in https://identity.foundation/secure-data-store/#structureddocument. +type StructuredDocument struct { + ID string `json:"id"` + Meta map[string]interface{} `json:"meta"` + Content map[string]interface{} `json:"content"` +} + +// EncryptedDocument represents an Encrypted Document as defined in +// https://identity.foundation/secure-data-store/#encrypteddocument. +type EncryptedDocument struct { + ID string `json:"id"` + Sequence int `json:"sequence"` + IndexedAttributeCollections []IndexedAttributeCollection `json:"indexed,omitempty"` + JWE jose.JSONWebEncryption `json:"jwe"` +} + +// IndexedAttributeCollection represents a collection of indexed attributes, +// all of which share a common MAC algorithm and key. +// This format is based on https://identity.foundation/secure-data-store/#creating-encrypted-indexes. +type IndexedAttributeCollection struct { + Sequence int `json:"sequence"` + HMAC IDTypePair `json:"hmac"` + IndexedAttributes []IndexedAttribute `json:"attributes"` +} + +// IndexedAttribute represents a single indexed attribute. +type IndexedAttribute struct { + Name string `json:"name"` + Value string `json:"value"` + Unique bool `json:"unique"` +} + +// IDTypePair represents an ID+Type pair. +type IDTypePair struct { + ID string `json:"id"` + Type string `json:"type"` +}