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 }