github.com/hyperledger/aries-framework-go@v0.3.2/pkg/wallet/contents.go (about)

     1  /*
     2  Copyright SecureKey Technologies Inc. All Rights Reserved.
     3  
     4  SPDX-License-Identifier: Apache-2.0
     5  */
     6  
     7  package wallet
     8  
     9  import (
    10  	"crypto/sha256"
    11  	"encoding/base64"
    12  	"encoding/hex"
    13  	"encoding/json"
    14  	"errors"
    15  	"fmt"
    16  	"strings"
    17  	"sync"
    18  	"time"
    19  
    20  	"github.com/bluele/gcache"
    21  	"github.com/piprate/json-gold/ld"
    22  
    23  	"github.com/hyperledger/aries-framework-go/pkg/doc/did"
    24  	"github.com/hyperledger/aries-framework-go/pkg/doc/jsonld"
    25  	"github.com/hyperledger/aries-framework-go/pkg/framework/aries/api/vdr"
    26  	"github.com/hyperledger/aries-framework-go/pkg/kms"
    27  	"github.com/hyperledger/aries-framework-go/spi/storage"
    28  )
    29  
    30  // ContentType is wallet content type.
    31  type ContentType string
    32  
    33  const (
    34  	// Collection content type which can be used to group wallet contents together.
    35  	// https://w3c-ccg.github.io/universal-wallet-interop-spec/#Collection
    36  	Collection ContentType = "collection"
    37  
    38  	// Credential content type for handling credential data models.
    39  	// https://w3c-ccg.github.io/universal-wallet-interop-spec/#Credential
    40  	Credential ContentType = "credential"
    41  
    42  	// DIDResolutionResponse content type for handling DID document data models.
    43  	// https://w3c-ccg.github.io/universal-wallet-interop-spec/#DIDResolutionResponse
    44  	DIDResolutionResponse ContentType = "didResolutionResponse"
    45  
    46  	// Metadata content type for handling wallet metadata data models.
    47  	// https://w3c-ccg.github.io/universal-wallet-interop-spec/#meta-data
    48  	Metadata ContentType = "metadata"
    49  
    50  	// Connection content type for handling wallet connection data models.
    51  	// https://w3c-ccg.github.io/universal-wallet-interop-spec/#connection
    52  	Connection ContentType = "connection"
    53  
    54  	// Key content type for handling key data models.
    55  	// https://w3c-ccg.github.io/universal-wallet-interop-spec/#Key
    56  	Key ContentType = "key"
    57  )
    58  
    59  // IsValid checks if underlying content type is supported.
    60  func (ct ContentType) IsValid() error {
    61  	switch ct {
    62  	case Collection, Credential, DIDResolutionResponse, Metadata, Connection, Key:
    63  		return nil
    64  	}
    65  
    66  	return fmt.Errorf("invalid content type '%s', supported types are %s", ct,
    67  		[]ContentType{Collection, Credential, DIDResolutionResponse, Metadata, Connection, Key})
    68  }
    69  
    70  // Name of the content type.
    71  func (ct ContentType) Name() string {
    72  	return string(ct)
    73  }
    74  
    75  const (
    76  	// collectionMappingKeyPrefix is db name space for saving collection ID to wallet content mappings.
    77  	collectionMappingKeyPrefix = "collectionmapping"
    78  )
    79  
    80  // keyContent is wallet content for key type
    81  // https://w3c-ccg.github.io/universal-wallet-interop-spec/#Key
    82  type keyContent struct {
    83  	ID               string          `json:"id"`
    84  	KeyType          string          `json:"type"`
    85  	PrivateKeyJwk    json.RawMessage `json:"privateKeyJwk"`
    86  	PrivateKeyBase58 string          `json:"privateKeyBase58"`
    87  }
    88  
    89  type contentID struct {
    90  	ID string `json:"id"`
    91  }
    92  
    93  type storeOpenHandle func(string) (storage.Store, error)
    94  
    95  type storeCloseHandle func() error
    96  
    97  // default store handles for content store.
    98  //nolint:gochecknoglobals
    99  var (
   100  	storeLocked storeOpenHandle = func(string) (storage.Store, error) {
   101  		return nil, ErrWalletLocked
   102  	}
   103  
   104  	noOp = func() error { return nil }
   105  )
   106  
   107  // contentStore is store for wallet contents for given user profile.
   108  type contentStore struct {
   109  	storeID              string
   110  	provider             *storageProvider
   111  	open                 storeOpenHandle
   112  	close                storeCloseHandle
   113  	lock                 sync.RWMutex
   114  	jsonldDocumentLoader ld.DocumentLoader
   115  }
   116  
   117  // newContentStore returns new wallet content store instance.
   118  // will use underlying storage provider as content storage if profile doesn't have edv settings.
   119  func newContentStore(p storage.Provider, jsonldDocumentLoader ld.DocumentLoader, pr *profile) *contentStore {
   120  	contents := &contentStore{
   121  		open:                 storeLocked,
   122  		close:                noOp,
   123  		provider:             newWalletStorageProvider(pr, p),
   124  		storeID:              pr.ID,
   125  		jsonldDocumentLoader: jsonldDocumentLoader,
   126  	}
   127  
   128  	if store, err := storeManager().get(pr.ID); err == nil {
   129  		contents.updateStoreHandles(store)
   130  	}
   131  
   132  	return contents
   133  }
   134  
   135  func (cs *contentStore) Open(keyMgr kms.KeyManager, opts *unlockOpts) error {
   136  	store, err := cs.provider.OpenStore(keyMgr, opts, storage.StoreConfiguration{TagNames: []string{
   137  		Collection.Name(), Credential.Name(), Connection.Name(), DIDResolutionResponse.Name(), Connection.Name(), Key.Name(),
   138  	}})
   139  	if err != nil {
   140  		return err
   141  	}
   142  
   143  	// store instances needs to be cached to share unlock session between multiple instances of wallet.
   144  	if err := storeManager().persist(cs.storeID, store, opts.tokenExpiry); err != nil {
   145  		return err
   146  	}
   147  
   148  	cs.lock.Lock()
   149  	defer cs.lock.Unlock()
   150  
   151  	cs.updateStoreHandles(store)
   152  
   153  	return nil
   154  }
   155  
   156  func (cs *contentStore) updateStoreHandles(store storage.Store) {
   157  	// give access to store only when auth is valid & not expired.
   158  	cs.open = func(auth string) (storage.Store, error) {
   159  		_, err := sessionManager().getSession(auth)
   160  		if err != nil {
   161  			return nil, err
   162  		}
   163  
   164  		return store, nil
   165  	}
   166  
   167  	cs.close = func() error {
   168  		return store.Close()
   169  	}
   170  }
   171  
   172  func (cs *contentStore) Close() bool {
   173  	cs.lock.Lock()
   174  	defer cs.lock.Unlock()
   175  
   176  	if err := cs.close(); err != nil {
   177  		logger.Debugf("failed to close wallet content store: %s", err)
   178  	}
   179  
   180  	cs.open = storeLocked
   181  	cs.close = noOp
   182  
   183  	return storeManager().delete(cs.storeID)
   184  }
   185  
   186  // Save for storing given wallet content to store by content ID (content document id) & content type.
   187  // if content document id is missing from content, then system generated id will be used as key for storage.
   188  // returns error if content with same ID already exists in store.
   189  // For replacing already existing content, use 'Remove() + Add()'.
   190  func (cs *contentStore) Save(auth string, ct ContentType, content []byte, options ...AddContentOptions) error { //nolint:lll,gocyclo
   191  	opts := &addContentOpts{}
   192  
   193  	for _, option := range options {
   194  		option(opts)
   195  	}
   196  
   197  	switch ct {
   198  	case Collection, Metadata, Connection, Credential:
   199  		if err := cs.checkDataModel(content, opts); err != nil {
   200  			return err
   201  		}
   202  
   203  		key, err := getContentID(content)
   204  		if err != nil {
   205  			return err
   206  		}
   207  
   208  		err = cs.mapCollection(auth, key, opts.collectionID, ct)
   209  		if err != nil {
   210  			return err
   211  		}
   212  
   213  		return cs.safeSave(auth, getContentKeyPrefix(ct, key), content, storage.Tag{Name: ct.Name()})
   214  	case DIDResolutionResponse:
   215  		// verify did resolution result before storing and also use DID ID as content key
   216  		docRes, err := did.ParseDocumentResolution(content)
   217  		if err != nil {
   218  			return fmt.Errorf("invalid DID resolution response model: %w", err)
   219  		}
   220  
   221  		err = cs.mapCollection(auth, docRes.DIDDocument.ID, opts.collectionID, ct)
   222  		if err != nil {
   223  			return err
   224  		}
   225  
   226  		return cs.safeSave(auth, getContentKeyPrefix(ct, docRes.DIDDocument.ID), content, storage.Tag{Name: ct.Name()})
   227  	case Key:
   228  		if err := cs.checkDataModel(content, opts); err != nil {
   229  			return err
   230  		}
   231  
   232  		// never save keys in store, just import them into kms
   233  		var key keyContent
   234  
   235  		err := json.Unmarshal(content, &key)
   236  		if err != nil {
   237  			return fmt.Errorf("failed to read key contents: %w", err)
   238  		}
   239  
   240  		return saveKey(auth, &key)
   241  	default:
   242  		return fmt.Errorf("invalid content type '%s', supported types are %s", ct,
   243  			[]ContentType{Collection, Credential, DIDResolutionResponse, Metadata, Connection, Key})
   244  	}
   245  }
   246  
   247  // safeSave saves given content to store by given key but returns error if content with given key already exists.
   248  func (cs *contentStore) safeSave(auth, key string, content []byte, tags ...storage.Tag) error {
   249  	cs.lock.RLock()
   250  	defer cs.lock.RUnlock()
   251  
   252  	store, err := cs.open(auth)
   253  	if err != nil {
   254  		return err
   255  	}
   256  
   257  	_, err = store.Get(key)
   258  	if errors.Is(err, storage.ErrDataNotFound) {
   259  		return store.Put(key, content, tags...)
   260  	} else if err != nil {
   261  		return err
   262  	}
   263  
   264  	return errors.New("content with same type and id already exists in this wallet")
   265  }
   266  
   267  // mapCollection maps given collection to given content.
   268  func (cs *contentStore) mapCollection(auth, key, collectionID string, ct ContentType) error {
   269  	if collectionID == "" {
   270  		return nil
   271  	}
   272  
   273  	cs.lock.RLock()
   274  	defer cs.lock.RUnlock()
   275  
   276  	store, err := cs.open(auth)
   277  	if err != nil {
   278  		return err
   279  	}
   280  
   281  	_, err = store.Get(getContentKeyPrefix(Collection, collectionID))
   282  	if err != nil {
   283  		return fmt.Errorf("failed to find existing collection with ID '%s' : %w", collectionID, err)
   284  	}
   285  
   286  	// collection IDs can contain ':' characters which can not be supported by tags.
   287  	return store.Put(getCollectionMappingKeyPrefix(ct, key), []byte(ct.Name()),
   288  		storage.Tag{Name: base64.StdEncoding.EncodeToString([]byte(collectionID))})
   289  }
   290  
   291  func saveKey(auth string, key *keyContent) error {
   292  	if len(key.PrivateKeyJwk) > 0 {
   293  		err := importKeyJWK(auth, key)
   294  		if err != nil {
   295  			return fmt.Errorf("failed to import private key jwk: %w", err)
   296  		}
   297  	}
   298  
   299  	if key.PrivateKeyBase58 != "" {
   300  		err := importKeyBase58(auth, key)
   301  		if err != nil {
   302  			return fmt.Errorf("failed to import private key base58: %w", err)
   303  		}
   304  	}
   305  
   306  	return nil
   307  }
   308  
   309  // Remove to remove wallet content from wallet contents store.
   310  func (cs *contentStore) Remove(auth, key string, ct ContentType) error {
   311  	cs.lock.RLock()
   312  	defer cs.lock.RUnlock()
   313  
   314  	store, err := cs.open(auth)
   315  	if err != nil {
   316  		return err
   317  	}
   318  
   319  	// delete mapping
   320  	err = store.Delete(getCollectionMappingKeyPrefix(ct, key))
   321  	if err != nil {
   322  		return err
   323  	}
   324  
   325  	// delete from store
   326  	return store.Delete(getContentKeyPrefix(ct, key))
   327  }
   328  
   329  // Get to get wallet content from wallet contents store.
   330  func (cs *contentStore) Get(auth, key string, ct ContentType) ([]byte, error) {
   331  	cs.lock.RLock()
   332  	defer cs.lock.RUnlock()
   333  
   334  	store, err := cs.open(auth)
   335  	if err != nil {
   336  		return nil, err
   337  	}
   338  
   339  	return store.Get(getContentKeyPrefix(ct, key))
   340  }
   341  
   342  // GetAll returns all wallet contents of give type.
   343  // returns empty result when no data found.
   344  func (cs *contentStore) GetAll(auth string, ct ContentType) (map[string]json.RawMessage, error) {
   345  	cs.lock.RLock()
   346  	defer cs.lock.RUnlock()
   347  
   348  	store, err := cs.open(auth)
   349  	if err != nil {
   350  		return nil, err
   351  	}
   352  
   353  	iter, err := store.Query(ct.Name())
   354  	if err != nil {
   355  		return nil, err
   356  	}
   357  
   358  	result := make(map[string]json.RawMessage)
   359  
   360  	for {
   361  		ok, err := iter.Next()
   362  		if err != nil {
   363  			return nil, err
   364  		}
   365  
   366  		if !ok {
   367  			break
   368  		}
   369  
   370  		key, err := iter.Key()
   371  		if err != nil {
   372  			return nil, err
   373  		}
   374  
   375  		val, err := iter.Value()
   376  		if err != nil {
   377  			return nil, err
   378  		}
   379  
   380  		result[removeKeyPrefix(ct.Name(), key)] = val
   381  	}
   382  
   383  	return result, nil
   384  }
   385  
   386  // FilterByCollection returns all wallet contents of give type and collection.
   387  // returns empty result when no data found.
   388  func (cs *contentStore) GetAllByCollection(auth,
   389  	collectionID string, ct ContentType) (map[string]json.RawMessage, error) {
   390  	cs.lock.RLock()
   391  	defer cs.lock.RUnlock()
   392  
   393  	store, err := cs.open(auth)
   394  	if err != nil {
   395  		return nil, err
   396  	}
   397  
   398  	iter, err := store.Query(base64.StdEncoding.EncodeToString([]byte(collectionID)))
   399  	if err != nil {
   400  		return nil, err
   401  	}
   402  
   403  	result := make(map[string]json.RawMessage)
   404  
   405  	for {
   406  		ok, err := iter.Next()
   407  		if err != nil {
   408  			return nil, err
   409  		}
   410  
   411  		if !ok {
   412  			break
   413  		}
   414  
   415  		key, err := iter.Key()
   416  		if err != nil {
   417  			return nil, err
   418  		}
   419  
   420  		val, err := iter.Value()
   421  		if err != nil {
   422  			return nil, err
   423  		}
   424  
   425  		// filter by content type
   426  		if string(val) != ct.Name() {
   427  			continue
   428  		}
   429  
   430  		contentKey := removeCollectionMappingKeyPrefix(ct, key)
   431  
   432  		contentVal, err := store.Get(getContentKeyPrefix(ct, contentKey))
   433  		if err != nil {
   434  			return nil, err
   435  		}
   436  
   437  		result[contentKey] = contentVal
   438  	}
   439  
   440  	return result, nil
   441  }
   442  
   443  func (cs *contentStore) checkDataModel(content []byte, opts *addContentOpts) error {
   444  	if opts.validateDataModel {
   445  		err := jsonld.ValidateJSONLD(string(content), jsonld.WithDocumentLoader(cs.jsonldDocumentLoader))
   446  		if err != nil {
   447  			return fmt.Errorf("incorrect document structure: %w", err)
   448  		}
   449  	}
   450  
   451  	return nil
   452  }
   453  
   454  func getContentID(content []byte) (string, error) {
   455  	jti, err := getJWTContentID(string(content))
   456  	if err != nil {
   457  		return "", err
   458  	}
   459  
   460  	if jti != "" {
   461  		return jti, nil
   462  	}
   463  
   464  	var cid contentID
   465  	if err = json.Unmarshal(content, &cid); err != nil {
   466  		return "", fmt.Errorf("failed to read content to be saved : %w", err)
   467  	}
   468  
   469  	key := cid.ID
   470  	if strings.TrimSpace(key) == "" {
   471  		// use document hash as key to avoid duplicates if id is missing
   472  		digest := sha256.Sum256(content)
   473  		return hex.EncodeToString(digest[0:]), nil
   474  	}
   475  
   476  	return key, nil
   477  }
   478  
   479  type hasJTI struct {
   480  	JTI string `json:"jti"`
   481  }
   482  
   483  func getJWTContentID(jwtStr string) (string, error) {
   484  	parts := strings.Split(unQuote(jwtStr), ".")
   485  	if len(parts) != 3 { // nolint: gomnd
   486  		return "", nil // assume not a jwt
   487  	}
   488  
   489  	credBytes, err := base64.RawURLEncoding.DecodeString(parts[1])
   490  	if err != nil {
   491  		return "", fmt.Errorf("decode base64 JWT data: %w", err)
   492  	}
   493  
   494  	cred := &hasJTI{}
   495  
   496  	err = json.Unmarshal(credBytes, cred)
   497  	if err != nil {
   498  		return "", fmt.Errorf("failed to unmarshal JWT data: %w", err)
   499  	}
   500  
   501  	if cred.JTI == "" {
   502  		return "", fmt.Errorf("JWT data has no ID")
   503  	}
   504  
   505  	return cred.JTI, nil
   506  }
   507  
   508  func unQuote(s string) string {
   509  	if len(s) <= 1 {
   510  		return s
   511  	}
   512  
   513  	if s[0] == '"' && s[len(s)-1] == '"' {
   514  		return s[1 : len(s)-1]
   515  	}
   516  
   517  	return s
   518  }
   519  
   520  // getContentKeyPrefix returns key prefix by wallet content type and storage key.
   521  func getContentKeyPrefix(ct ContentType, key string) string {
   522  	return fmt.Sprintf("%s_%s", ct, key)
   523  }
   524  
   525  // getCollectionMappingKeyPrefix returns key prefix by wallet collection ID and storage key.
   526  func getCollectionMappingKeyPrefix(ct ContentType, key string) string {
   527  	return fmt.Sprintf("%s_%s_%s", collectionMappingKeyPrefix, ct, key)
   528  }
   529  
   530  // removeCollectionMappingKeyPrefix removes collection mapping key prefix.
   531  func removeCollectionMappingKeyPrefix(ct ContentType, key string) string {
   532  	return strings.Replace(key, fmt.Sprintf("%s_%s_", collectionMappingKeyPrefix, ct), "", 1)
   533  }
   534  
   535  // removeContentKeyPrefix removes content key prefix.
   536  func removeKeyPrefix(prefix, key string) string {
   537  	return strings.Replace(key, fmt.Sprintf("%s_", prefix), "", 1)
   538  }
   539  
   540  // newContentBasedVDR returns new wallet content store based VDR.
   541  func newContentBasedVDR(auth string, v vdr.Registry, c *contentStore) *walletVDR {
   542  	return &walletVDR{auth: auth, Registry: v, contents: c}
   543  }
   544  
   545  // walletVDR is wallet content based on VDR which tries to resolve DIDs from wallet content store,
   546  // if DID document not found then it falls back to vdr registry.
   547  // Note: For using this has to be unlocked by auth token.
   548  type walletVDR struct {
   549  	vdr.Registry
   550  	contents *contentStore
   551  	auth     string
   552  }
   553  
   554  func (v *walletVDR) Resolve(didID string, opts ...vdr.DIDMethodOption) (*did.DocResolution, error) {
   555  	docBytes, err := v.contents.Get(v.auth, didID, DIDResolutionResponse)
   556  	if err == nil {
   557  		resolvedDOC, e := did.ParseDocumentResolution(docBytes)
   558  		if e != nil {
   559  			return nil, fmt.Errorf("failed to parse stored DID: %w", e)
   560  		}
   561  
   562  		return resolvedDOC, nil
   563  	} else if errors.Is(err, ErrWalletLocked) {
   564  		return nil, err
   565  	}
   566  
   567  	return v.Registry.Resolve(didID, opts...)
   568  }
   569  
   570  //nolint:gochecknoglobals
   571  var (
   572  	walletStoreInstance *walletStoreManager
   573  	walletStoreOnce     sync.Once
   574  )
   575  
   576  func storeManager() *walletStoreManager {
   577  	walletStoreOnce.Do(func() {
   578  		walletStoreInstance = &walletStoreManager{
   579  			gstore: gcache.New(0).Build(),
   580  		}
   581  	})
   582  
   583  	return walletStoreInstance
   584  }
   585  
   586  // walletStoreManager manages store instances in cache.
   587  // this is store manager singleton - access only via storeManager()
   588  // underlying gcache is threasafe, no need of locks.
   589  type walletStoreManager struct {
   590  	gstore gcache.Cache
   591  }
   592  
   593  func (ws *walletStoreManager) persist(id string, store storage.Store, expiration time.Duration) error {
   594  	if expiration == 0 {
   595  		expiration = defaultCacheExpiry
   596  	}
   597  
   598  	return ws.gstore.SetWithExpire(id, store, expiration)
   599  }
   600  
   601  func (ws *walletStoreManager) get(id string) (storage.Store, error) {
   602  	val, err := ws.gstore.Get(id)
   603  	if err != nil {
   604  		return nil, err
   605  	}
   606  
   607  	return val.(storage.Store), nil
   608  }
   609  
   610  func (ws *walletStoreManager) delete(id string) bool {
   611  	return ws.gstore.Remove(id)
   612  }