From 867cb9dae9a8147022a02d68e4ace09ce2a1d842 Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab <31316147+abdulmth@users.noreply.github.com> Date: Thu, 2 Nov 2023 09:37:51 +0100 Subject: [PATCH] Fix wasm panic caused by a race condition in `IotaDocument` and `CoreDocument` (#1258) --- bindings/wasm/docs/api-reference.md | 242 ++++++++++--- .../wasm/src/common/imported_document_lock.rs | 11 +- .../credential/domain_linkage_validator.rs | 4 +- .../jwt_credential_validator.rs | 17 +- .../jwt_presentation_validator.rs | 2 +- bindings/wasm/src/did/wasm_core_document.rs | 329 ++++++++++-------- bindings/wasm/src/error.rs | 10 + bindings/wasm/src/iota/identity_client_ext.rs | 4 +- bindings/wasm/src/iota/iota_document.rs | 193 +++++----- bindings/wasm/tests/storage.ts | 63 ++++ 10 files changed, 562 insertions(+), 313 deletions(-) diff --git a/bindings/wasm/docs/api-reference.md b/bindings/wasm/docs/api-reference.md index 373fa12d05..a817a73136 100644 --- a/bindings/wasm/docs/api-reference.md +++ b/bindings/wasm/docs/api-reference.md @@ -6,6 +6,8 @@
CoreDocument

A method-agnostic DID Document.

+

Note: All methods that involve reading from this class may potentially raise an error +if the object is being concurrently modified.

Credential
@@ -38,11 +40,18 @@ See: Duration

A span of time.

+
EdDSAJwsVerifier
+

An implementor of IJwsVerifier that can handle the +EdDSA algorithm.

+
IotaDID

A DID conforming to the IOTA DID method specification.

IotaDocument
-
+

A DID Document adhering to the IOTA DID method specification.

+

Note: All methods that involve reading from this class may potentially raise an error +if the object is being concurrently modified.

+
IotaDocumentMetadata

Additional attributes related to an IOTA DID Document.

@@ -140,10 +149,6 @@ working with storage backed DID documents.

## Members
-
StateMetadataEncoding
-
-
MethodRelationship
-
StatusCheck

Controls validation behaviour when checking whether or not a credential has been revoked by its credentialStatus.

@@ -184,29 +189,32 @@ This variant is the default.

FirstError

Return after the first error occurs.

+
StateMetadataEncoding
+
+
MethodRelationship
+
## Functions
-
start()
-

Initializes the console error panic hook for better error messages

-
encodeB64(data)string

Encode the given bytes in url-safe base64.

decodeB64(data)Uint8Array

Decode the given url-safe base64-encoded slice into its raw bytes.

-
verifyEdDSA(alg, signingInput, decodedSignature, publicKey)
-

Verify a JWS signature secured with the JwsAlgorithm::EdDSA algorithm. -Only the EdCurve::Ed25519 variant is supported for now.

-

This function is useful when one is building an IJwsVerifier that extends the default provided by -the IOTA Identity Framework.

+
verifyEd25519(alg, signingInput, decodedSignature, publicKey)
+

Verify a JWS signature secured with the EdDSA algorithm and curve Ed25519.

+

This function is useful when one is composing a IJwsVerifier that delegates +EdDSA verification with curve Ed25519 to this function.

Warning

This function does not check whether alg = EdDSA in the protected header. Callers are expected to assert this prior to calling the function.

+
start()
+

Initializes the console error panic hook for better error messages

+
@@ -397,6 +405,9 @@ Deserializes an instance from a JSON object. ## CoreDocument A method-agnostic DID Document. +Note: All methods that involve reading from this class may potentially raise an error +if the object is being concurrently modified. + **Kind**: global class * [CoreDocument](#CoreDocument) @@ -437,7 +448,7 @@ A method-agnostic DID Document. * [.generateMethod(storage, keyType, alg, fragment, scope)](#CoreDocument+generateMethod) ⇒ Promise.<string> * [.purgeMethod(storage, id)](#CoreDocument+purgeMethod) ⇒ Promise.<void> * [.createJws(storage, fragment, payload, options)](#CoreDocument+createJws) ⇒ [Promise.<Jws>](#Jws) - * [.createCredentialJwt(storage, fragment, credential, options)](#CoreDocument+createCredentialJwt) ⇒ [Promise.<Jwt>](#Jwt) + * [.createCredentialJwt(storage, fragment, credential, options, custom_claims)](#CoreDocument+createCredentialJwt) ⇒ [Promise.<Jwt>](#Jwt) * [.createPresentationJwt(storage, fragment, presentation, signature_options, presentation_options)](#CoreDocument+createPresentationJwt) ⇒ [Promise.<Jwt>](#Jwt) * _static_ * [.fromJSON(json)](#CoreDocument.fromJSON) ⇒ [CoreDocument](#CoreDocument) @@ -709,7 +720,8 @@ verifying EdDSA signatures. Regardless of which options are passed the following conditions must be met in order for a verification attempt to take place. - The JWS must be encoded according to the JWS compact serialization. -- The `kid` value in the protected header must be an identifier of a verification method in this DID document. +- The `kid` value in the protected header must be an identifier of a verification method in this DID document, +or set explicitly in the `options`. **Kind**: instance method of [CoreDocument](#CoreDocument) @@ -717,7 +729,7 @@ take place. | --- | --- | | jws | [Jws](#Jws) | | options | [JwsVerificationOptions](#JwsVerificationOptions) | -| signatureVerifier | IJwsVerifier \| undefined | +| signatureVerifier | IJwsVerifier | | detachedPayload | string \| undefined | @@ -827,12 +839,15 @@ See [RFC7515 section 3.1](https://www.rfc-editor.org/rfc/rfc7515#section-3.1). -### coreDocument.createCredentialJwt(storage, fragment, credential, options) ⇒ [Promise.<Jwt>](#Jwt) +### coreDocument.createCredentialJwt(storage, fragment, credential, options, custom_claims) ⇒ [Promise.<Jwt>](#Jwt) Produces a JWT where the payload is produced from the given `credential` -in accordance with [VC-JWT version 1.1](https://w3c.github.io/vc-jwt/#version-1.1). +in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token). + +Unless the `kid` is explicitly set in the options, the `kid` in the protected header is the `id` +of the method identified by `fragment` and the JWS signature will be produced by the corresponding +private key backed by the `storage` in accordance with the passed `options`. -The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be -produced by the corresponding private key backed by the `storage` in accordance with the passed `options`. +The `custom_claims` can be used to set additional claims on the resulting JWT. **Kind**: instance method of [CoreDocument](#CoreDocument) @@ -842,15 +857,17 @@ produced by the corresponding private key backed by the `storage` in accordance | fragment | string | | credential | [Credential](#Credential) | | options | [JwsSignatureOptions](#JwsSignatureOptions) | +| custom_claims | Record.<string, any> \| undefined | ### coreDocument.createPresentationJwt(storage, fragment, presentation, signature_options, presentation_options) ⇒ [Promise.<Jwt>](#Jwt) Produces a JWT where the payload is produced from the given presentation. -in accordance with [VC-JWT version 1.1](https://w3c.github.io/vc-jwt/#version-1.1). +in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token). -The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be -produced by the corresponding private key backed by the `storage` in accordance with the passed `options`. +Unless the `kid` is explicitly set in the options, the `kid` in the protected header is the `id` +of the method identified by `fragment` and the JWS signature will be produced by the corresponding +private key backed by the `storage` in accordance with the passed `options`. **Kind**: instance method of [CoreDocument](#CoreDocument) @@ -1274,6 +1291,7 @@ It does not imply anything about a potentially present proof property on the cre * [DecodedJwtCredential](#DecodedJwtCredential) * [.credential()](#DecodedJwtCredential+credential) ⇒ [Credential](#Credential) * [.protectedHeader()](#DecodedJwtCredential+protectedHeader) ⇒ [JwsHeader](#JwsHeader) + * [.customClaims()](#DecodedJwtCredential+customClaims) ⇒ Record.<string, any> \| undefined * [.intoCredential()](#DecodedJwtCredential+intoCredential) ⇒ [Credential](#Credential) @@ -1287,6 +1305,12 @@ Returns a copy of the credential parsed to the [Verifiable Credentials Data mode ### decodedJwtCredential.protectedHeader() ⇒ [JwsHeader](#JwsHeader) Returns a copy of the protected header parsed from the decoded JWS. +**Kind**: instance method of [DecodedJwtCredential](#DecodedJwtCredential) + + +### decodedJwtCredential.customClaims() ⇒ Record.<string, any> \| undefined +The custom claims parsed from the JWT. + **Kind**: instance method of [DecodedJwtCredential](#DecodedJwtCredential) @@ -1315,6 +1339,7 @@ It does not imply anything about a potentially present proof property on the pre * [.expirationDate()](#DecodedJwtPresentation+expirationDate) ⇒ [Timestamp](#Timestamp) \| undefined * [.issuanceDate()](#DecodedJwtPresentation+issuanceDate) ⇒ [Timestamp](#Timestamp) \| undefined * [.audience()](#DecodedJwtPresentation+audience) ⇒ string \| undefined + * [.customClaims()](#DecodedJwtPresentation+customClaims) ⇒ Record.<string, any> \| undefined @@ -1352,6 +1377,12 @@ The issuance date parsed from the JWT claims. ### decodedJwtPresentation.audience() ⇒ string \| undefined The `aud` property parsed from JWT claims. +**Kind**: instance method of [DecodedJwtPresentation](#DecodedJwtPresentation) + + +### decodedJwtPresentation.customClaims() ⇒ Record.<string, any> \| undefined +The custom claims parsed from the JWT. + **Kind**: instance method of [DecodedJwtPresentation](#DecodedJwtPresentation) @@ -1510,6 +1541,46 @@ Deserializes an instance from a JSON object. | --- | --- | | json | any | + + +## EdDSAJwsVerifier +An implementor of `IJwsVerifier` that can handle the +`EdDSA` algorithm. + +**Kind**: global class + +* [EdDSAJwsVerifier](#EdDSAJwsVerifier) + * [new EdDSAJwsVerifier()](#new_EdDSAJwsVerifier_new) + * [.verify(alg, signingInput, decodedSignature, publicKey)](#EdDSAJwsVerifier+verify) + + + +### new EdDSAJwsVerifier() +Constructs an EdDSAJwsVerifier. + + + +### edDSAJwsVerifier.verify(alg, signingInput, decodedSignature, publicKey) +Verify a JWS signature secured with the `EdDSA` algorithm. +Only the `Ed25519` curve is supported for now. + +This function is useful when one is building an `IJwsVerifier` that extends the default provided by +the IOTA Identity Framework. + +# Warning + +This function does not check whether `alg = EdDSA` in the protected header. Callers are expected to assert this +prior to calling the function. + +**Kind**: instance method of [EdDSAJwsVerifier](#EdDSAJwsVerifier) + +| Param | Type | +| --- | --- | +| alg | JwsAlgorithm | +| signingInput | Uint8Array | +| decodedSignature | Uint8Array | +| publicKey | [Jwk](#Jwk) | + ## IotaDID @@ -1724,6 +1795,11 @@ Deserializes an instance from a JSON object. ## IotaDocument +A DID Document adhering to the IOTA DID method specification. + +Note: All methods that involve reading from this class may potentially raise an error +if the object is being concurrently modified. + **Kind**: global class * [IotaDocument](#IotaDocument) @@ -1768,7 +1844,7 @@ Deserializes an instance from a JSON object. * [.generateMethod(storage, keyType, alg, fragment, scope)](#IotaDocument+generateMethod) ⇒ Promise.<string> * [.purgeMethod(storage, id)](#IotaDocument+purgeMethod) ⇒ Promise.<void> * [.createJwt(storage, fragment, payload, options)](#IotaDocument+createJwt) ⇒ [Promise.<Jws>](#Jws) - * [.createCredentialJwt(storage, fragment, credential, options)](#IotaDocument+createCredentialJwt) ⇒ [Promise.<Jwt>](#Jwt) + * [.createCredentialJwt(storage, fragment, credential, options, custom_claims)](#IotaDocument+createCredentialJwt) ⇒ [Promise.<Jwt>](#Jwt) * [.createPresentationJwt(storage, fragment, presentation, signature_options, presentation_options)](#IotaDocument+createPresentationJwt) ⇒ [Promise.<Jwt>](#Jwt) * _static_ * [.newWithId(id)](#IotaDocument.newWithId) ⇒ [IotaDocument](#IotaDocument) @@ -1982,7 +2058,7 @@ take place. | --- | --- | | jws | [Jws](#Jws) | | options | [JwsVerificationOptions](#JwsVerificationOptions) | -| signatureVerifier | IJwsVerifier \| undefined | +| signatureVerifier | IJwsVerifier | | detachedPayload | string \| undefined | @@ -2201,12 +2277,15 @@ See [RFC7515 section 3.1](https://www.rfc-editor.org/rfc/rfc7515#section-3.1). -### iotaDocument.createCredentialJwt(storage, fragment, credential, options) ⇒ [Promise.<Jwt>](#Jwt) +### iotaDocument.createCredentialJwt(storage, fragment, credential, options, custom_claims) ⇒ [Promise.<Jwt>](#Jwt) Produces a JWS where the payload is produced from the given `credential` -in accordance with [VC-JWT version 1.1](https://w3c.github.io/vc-jwt/#version-1.1). +in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token). + +Unless the `kid` is explicitly set in the options, the `kid` in the protected header is the `id` +of the method identified by `fragment` and the JWS signature will be produced by the corresponding +private key backed by the `storage` in accordance with the passed `options`. -The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be -produced by the corresponding private key backed by the `storage` in accordance with the passed `options`. +The `custom_claims` can be used to set additional claims on the resulting JWT. **Kind**: instance method of [IotaDocument](#IotaDocument) @@ -2216,15 +2295,17 @@ produced by the corresponding private key backed by the `storage` in accordance | fragment | string | | credential | [Credential](#Credential) | | options | [JwsSignatureOptions](#JwsSignatureOptions) | +| custom_claims | Record.<string, any> \| undefined | ### iotaDocument.createPresentationJwt(storage, fragment, presentation, signature_options, presentation_options) ⇒ [Promise.<Jwt>](#Jwt) Produces a JWT where the payload is produced from the given presentation. -in accordance with [VC-JWT version 1.1](https://w3c.github.io/vc-jwt/#version-1.1). +in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token). -The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be -produced by the corresponding private key backed by the `storage` in accordance with the passed `options`. +Unless the `kid` is explicitly set in the options, the `kid` in the protected header is the `id` +of the method identified by `fragment` and the JWS signature will be produced by the corresponding +private key backed by the `storage` in accordance with the passed `options`. **Kind**: instance method of [IotaDocument](#IotaDocument) @@ -2725,6 +2806,7 @@ Returns a clone of the JWS string. * [.setAlg(value)](#JwsHeader+setAlg) * [.b64()](#JwsHeader+b64) ⇒ boolean \| undefined * [.setB64(value)](#JwsHeader+setB64) + * [.custom()](#JwsHeader+custom) ⇒ Record.<string, any> \| undefined * [.has(claim)](#JwsHeader+has) ⇒ boolean * [.isDisjoint(other)](#JwsHeader+isDisjoint) ⇒ boolean * [.jku()](#JwsHeader+jku) ⇒ string \| undefined @@ -2795,6 +2877,12 @@ Sets a value for the base64url-encode payload claim (b64). | --- | --- | | value | boolean | + + +### jwsHeader.custom() ⇒ Record.<string, any> \| undefined +Additional header parameters. + +**Kind**: instance method of [JwsHeader](#JwsHeader) ### jwsHeader.has(claim) ⇒ boolean @@ -3058,7 +3146,9 @@ Deserializes an instance from a JSON object. * [.setCty(value)](#JwsSignatureOptions+setCty) * [.serUrl(value)](#JwsSignatureOptions+serUrl) * [.setNonce(value)](#JwsSignatureOptions+setNonce) + * [.setKid(value)](#JwsSignatureOptions+setKid) * [.setDetachedPayload(value)](#JwsSignatureOptions+setDetachedPayload) + * [.setCustomHeaderParameters(value)](#JwsSignatureOptions+setCustomHeaderParameters) * [.toJSON()](#JwsSignatureOptions+toJSON) ⇒ any * [.clone()](#JwsSignatureOptions+clone) ⇒ [JwsSignatureOptions](#JwsSignatureOptions) * _static_ @@ -3138,6 +3228,17 @@ Replace the value of the `nonce` field. | --- | --- | | value | string | + + +### jwsSignatureOptions.setKid(value) +Replace the value of the `kid` field. + +**Kind**: instance method of [JwsSignatureOptions](#JwsSignatureOptions) + +| Param | Type | +| --- | --- | +| value | string | + ### jwsSignatureOptions.setDetachedPayload(value) @@ -3149,6 +3250,17 @@ Replace the value of the `detached_payload` field. | --- | --- | | value | boolean | + + +### jwsSignatureOptions.setCustomHeaderParameters(value) +Add additional header parameters. + +**Kind**: instance method of [JwsSignatureOptions](#JwsSignatureOptions) + +| Param | Type | +| --- | --- | +| value | Record.<string, any> | + ### jwsSignatureOptions.toJSON() ⇒ any @@ -3181,7 +3293,8 @@ Deserializes an instance from a JSON object. * [new JwsVerificationOptions(options)](#new_JwsVerificationOptions_new) * _instance_ * [.setNonce(value)](#JwsVerificationOptions+setNonce) - * [.setScope(value)](#JwsVerificationOptions+setScope) + * [.setMethodScope(value)](#JwsVerificationOptions+setMethodScope) + * [.setMethodId(value)](#JwsVerificationOptions+setMethodId) * [.toJSON()](#JwsVerificationOptions+toJSON) ⇒ any * [.clone()](#JwsVerificationOptions+clone) ⇒ [JwsVerificationOptions](#JwsVerificationOptions) * _static_ @@ -3208,9 +3321,9 @@ Set the expected value for the `nonce` parameter of the protected header. | --- | --- | | value | string | - + -### jwsVerificationOptions.setScope(value) +### jwsVerificationOptions.setMethodScope(value) Set the scope of the verification methods that may be used to verify the given JWS. **Kind**: instance method of [JwsVerificationOptions](#JwsVerificationOptions) @@ -3219,6 +3332,17 @@ Set the scope of the verification methods that may be used to verify the given J | --- | --- | | value | [MethodScope](#MethodScope) | + + +### jwsVerificationOptions.setMethodId(value) +Set the DID URl of the method, whose JWK should be used to verify the JWS. + +**Kind**: instance method of [JwsVerificationOptions](#JwsVerificationOptions) + +| Param | Type | +| --- | --- | +| value | [DIDUrl](#DIDUrl) | + ### jwsVerificationOptions.toJSON() ⇒ any @@ -3373,7 +3497,7 @@ algorithm will be used. | Param | Type | | --- | --- | -| signatureVerifier | IJwsVerifier \| undefined | +| signatureVerifier | IJwsVerifier | @@ -3545,7 +3669,7 @@ algorithm will be used. | Param | Type | | --- | --- | -| signatureVerifier | IJwsVerifier \| undefined | +| signatureVerifier | IJwsVerifier | @@ -3712,7 +3836,7 @@ algorithm will be used. | Param | Type | | --- | --- | -| signatureVerifier | IJwsVerifier \| undefined | +| signatureVerifier | IJwsVerifier | @@ -4619,6 +4743,7 @@ Obtain the wrapped `JwkStorage`. **Kind**: global class * [Timestamp](#Timestamp) + * [new Timestamp()](#new_Timestamp_new) * _instance_ * [.toRFC3339()](#Timestamp+toRFC3339) ⇒ string * [.checkedAdd(duration)](#Timestamp+checkedAdd) ⇒ [Timestamp](#Timestamp) \| undefined @@ -4629,6 +4754,11 @@ Obtain the wrapped `JwkStorage`. * [.nowUTC()](#Timestamp.nowUTC) ⇒ [Timestamp](#Timestamp) * [.fromJSON(json)](#Timestamp.fromJSON) ⇒ [Timestamp](#Timestamp) + + +### new Timestamp() +Creates a new [Timestamp](#Timestamp) with the current date and time. + ### timestamp.toRFC3339() ⇒ string @@ -4911,14 +5041,6 @@ Deserializes an instance from a JSON object. | --- | --- | | json | any | - - -## StateMetadataEncoding -**Kind**: global variable - - -## MethodRelationship -**Kind**: global variable ## StatusCheck @@ -4995,12 +5117,14 @@ Return all errors that occur during validation. Return after the first error occurs. **Kind**: global variable - + -## start() -Initializes the console error panic hook for better error messages +## StateMetadataEncoding +**Kind**: global variable + -**Kind**: global function +## MethodRelationship +**Kind**: global variable ## encodeB64(data) ⇒ string @@ -5023,16 +5147,16 @@ Decode the given url-safe base64-encoded slice into its raw bytes. | --- | --- | | data | Uint8Array | - + -## verifyEdDSA(alg, signingInput, decodedSignature, publicKey) -Verify a JWS signature secured with the `JwsAlgorithm::EdDSA` algorithm. -Only the `EdCurve::Ed25519` variant is supported for now. +## verifyEd25519(alg, signingInput, decodedSignature, publicKey) +Verify a JWS signature secured with the `EdDSA` algorithm and curve `Ed25519`. -This function is useful when one is building an `IJwsVerifier` that extends the default provided by -the IOTA Identity Framework. +This function is useful when one is composing a `IJwsVerifier` that delegates +`EdDSA` verification with curve `Ed25519` to this function. # Warning + This function does not check whether `alg = EdDSA` in the protected header. Callers are expected to assert this prior to calling the function. @@ -5045,3 +5169,9 @@ prior to calling the function. | decodedSignature | Uint8Array | | publicKey | [Jwk](#Jwk) | + + +## start() +Initializes the console error panic hook for better error messages + +**Kind**: global function diff --git a/bindings/wasm/src/common/imported_document_lock.rs b/bindings/wasm/src/common/imported_document_lock.rs index 5590424067..4852ab216e 100644 --- a/bindings/wasm/src/common/imported_document_lock.rs +++ b/bindings/wasm/src/common/imported_document_lock.rs @@ -11,6 +11,7 @@ use crate::did::ArrayIToCoreDocument; use crate::did::CoreDocumentLock; use crate::did::IToCoreDocument; use crate::did::WasmCoreDocument; +use crate::error::Result; use crate::iota::IotaDocumentLock; use crate::iota::WasmIotaDocument; @@ -27,13 +28,13 @@ pub(crate) enum ImportedDocumentLock { impl ImportedDocumentLock { /// Obtain a read guard which implements `AsRef`. - pub(crate) fn blocking_read(&self) -> ImportedDocumentReadGuard<'_> { + pub(crate) fn try_read(&self) -> Result> { match self { - Self::Iota(lock) => ImportedDocumentReadGuard(tokio::sync::RwLockReadGuard::map( - lock.blocking_read(), + Self::Iota(lock) => Ok(ImportedDocumentReadGuard(tokio::sync::RwLockReadGuard::map( + lock.try_read()?, IotaDocument::core_document, - )), - Self::Core(lock) => ImportedDocumentReadGuard(lock.blocking_read()), + ))), + Self::Core(lock) => Ok(ImportedDocumentReadGuard(lock.try_read()?)), } } /// Must only be called on values implementing `IToCoreDocument`. diff --git a/bindings/wasm/src/credential/domain_linkage_validator.rs b/bindings/wasm/src/credential/domain_linkage_validator.rs index 4dc8840b4b..a38639d853 100644 --- a/bindings/wasm/src/credential/domain_linkage_validator.rs +++ b/bindings/wasm/src/credential/domain_linkage_validator.rs @@ -60,7 +60,7 @@ impl WasmJwtDomainLinkageValidator { ) -> Result<()> { let domain = Url::parse(domain).wasm_result()?; let doc = ImportedDocumentLock::from(issuer); - let doc_guard = doc.blocking_read(); + let doc_guard = doc.try_read()?; self .validator .validate_linkage(&doc_guard, &configuration.0, &domain, &options.0) @@ -81,7 +81,7 @@ impl WasmJwtDomainLinkageValidator { ) -> Result<()> { let domain = Url::parse(domain).wasm_result()?; let doc = ImportedDocumentLock::from(issuer); - let doc_guard = doc.blocking_read(); + let doc_guard = doc.try_read()?; self .validator .validate_credential(&doc_guard, &credentialJwt.0, &domain, &options.0) diff --git a/bindings/wasm/src/credential/jwt_credential_validation/jwt_credential_validator.rs b/bindings/wasm/src/credential/jwt_credential_validation/jwt_credential_validator.rs index cf1a48254b..74682d3e0d 100644 --- a/bindings/wasm/src/credential/jwt_credential_validation/jwt_credential_validator.rs +++ b/bindings/wasm/src/credential/jwt_credential_validation/jwt_credential_validator.rs @@ -78,7 +78,7 @@ impl WasmJwtCredentialValidator { fail_fast: WasmFailFast, ) -> Result { let issuer_lock = ImportedDocumentLock::from(issuer); - let issuer_guard = issuer_lock.blocking_read(); + let issuer_guard = issuer_lock.try_read()?; self .0 @@ -112,8 +112,12 @@ impl WasmJwtCredentialValidator { options: &WasmJwsVerificationOptions, ) -> Result { let issuer_locks: Vec = trustedIssuers.into(); - let trusted_issuers: Vec> = - issuer_locks.iter().map(ImportedDocumentLock::blocking_read).collect(); + let trusted_issuers: Vec> = issuer_locks + .iter() + .map(ImportedDocumentLock::try_read) + .collect::>>>( + )?; + self .0 .verify_signature(&credential.0, &trusted_issuers, &options.0) @@ -157,8 +161,11 @@ impl WasmJwtCredentialValidator { statusCheck: WasmStatusCheck, ) -> Result<()> { let issuer_locks: Vec = trustedIssuers.into(); - let trusted_issuers: Vec> = - issuer_locks.iter().map(ImportedDocumentLock::blocking_read).collect(); + let trusted_issuers: Vec> = issuer_locks + .iter() + .map(ImportedDocumentLock::try_read) + .collect::>>>( + )?; let status_check: StatusCheck = statusCheck.into(); JwtCredentialValidatorUtils::check_status(&credential.0, &trusted_issuers, status_check).wasm_result() } diff --git a/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs b/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs index a502c20075..40a44f916b 100644 --- a/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs +++ b/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs @@ -65,7 +65,7 @@ impl WasmJwtPresentationValidator { validation_options: &WasmJwtPresentationValidationOptions, ) -> Result { let holder_lock = ImportedDocumentLock::from(holder); - let holder_guard = holder_lock.blocking_read(); + let holder_guard = holder_lock.try_read()?; self .0 diff --git a/bindings/wasm/src/did/wasm_core_document.rs b/bindings/wasm/src/did/wasm_core_document.rs index bd349b7723..0bae16c048 100644 --- a/bindings/wasm/src/did/wasm_core_document.rs +++ b/bindings/wasm/src/did/wasm_core_document.rs @@ -72,12 +72,12 @@ impl CoreDocumentLock { Self(tokio::sync::RwLock::new(input)) } - pub(crate) fn blocking_read(&self) -> tokio::sync::RwLockReadGuard<'_, CoreDocument> { - self.0.blocking_read() + pub(crate) fn try_read(&self) -> Result> { + self.0.try_read().wasm_result() } - pub(crate) fn blocking_write(&self) -> tokio::sync::RwLockWriteGuard<'_, CoreDocument> { - self.0.blocking_write() + pub(crate) fn try_write(&self) -> Result> { + self.0.try_write().wasm_result() } pub(crate) async fn read(&self) -> tokio::sync::RwLockReadGuard<'_, CoreDocument> { @@ -90,6 +90,9 @@ impl CoreDocumentLock { } /// A method-agnostic DID Document. +/// +/// Note: All methods that involve reading from this class may potentially raise an error +/// if the object is being concurrently modified. #[wasm_bindgen(js_name = CoreDocument, inspectable)] pub struct WasmCoreDocument(pub(crate) Rc); @@ -104,8 +107,8 @@ impl WasmCoreDocument { /// Returns a copy of the DID Document `id`. #[wasm_bindgen] - pub fn id(&self) -> WasmCoreDID { - WasmCoreDID::from(self.0.blocking_read().id().clone()) + pub fn id(&self) -> Result { + Ok(WasmCoreDID::from(self.0.try_read()?.id().clone())) } /// Sets the DID of the document. @@ -116,14 +119,15 @@ impl WasmCoreDocument { /// `resolve_method`, `resolve_service` and the related /// [DID URL dereferencing](https://w3c-ccg.github.io/did-resolution/#dereferencing) algorithm. #[wasm_bindgen(js_name = setId)] - pub fn set_id(&mut self, id: &WasmCoreDID) { - *self.0.blocking_write().id_mut_unchecked() = id.0.clone(); + pub fn set_id(&mut self, id: &WasmCoreDID) -> Result<()> { + *self.0.try_write()?.id_mut_unchecked() = id.0.clone(); + Ok(()) } /// Returns a copy of the document controllers. #[wasm_bindgen] - pub fn controller(&self) -> ArrayCoreDID { - match self.0.blocking_read().controller() { + pub fn controller(&self) -> Result { + let controller = match self.0.try_read()?.controller() { Some(controllers) => controllers .iter() .cloned() @@ -132,7 +136,8 @@ impl WasmCoreDocument { .collect::() .unchecked_into::(), None => js_sys::Array::new().unchecked_into::(), - } + }; + Ok(controller) } /// Sets the controllers of the DID Document. @@ -151,22 +156,24 @@ impl WasmCoreDocument { } else { None }; - *self.0.blocking_write().controller_mut() = controller_set; + *self.0.try_write()?.controller_mut() = controller_set; Ok(()) } /// Returns a copy of the document's `alsoKnownAs` set. #[wasm_bindgen(js_name = alsoKnownAs)] - pub fn also_known_as(&self) -> ArrayString { - self - .0 - .blocking_read() - .also_known_as() - .iter() - .map(|url| url.to_string()) - .map(JsValue::from) - .collect::() - .unchecked_into::() + pub fn also_known_as(&self) -> Result { + Ok( + self + .0 + .try_read()? + .also_known_as() + .iter() + .map(|url| url.to_string()) + .map(JsValue::from) + .collect::() + .unchecked_into::(), + ) } /// Sets the `alsoKnownAs` property in the DID document. @@ -179,114 +186,126 @@ impl WasmCoreDocument { urls_set.append(Url::parse(url).wasm_result()?); } } - *self.0.blocking_write().also_known_as_mut() = urls_set; + *self.0.try_write()?.also_known_as_mut() = urls_set; Ok(()) } /// Returns a copy of the document's `verificationMethod` set. #[wasm_bindgen(js_name = verificationMethod)] - pub fn verification_method(&self) -> ArrayVerificationMethod { - self - .0 - .blocking_read() - .verification_method() - .iter() - .cloned() - .map(WasmVerificationMethod::from) - .map(JsValue::from) - .collect::() - .unchecked_into::() + pub fn verification_method(&self) -> Result { + Ok( + self + .0 + .try_read()? + .verification_method() + .iter() + .cloned() + .map(WasmVerificationMethod::from) + .map(JsValue::from) + .collect::() + .unchecked_into::(), + ) } /// Returns a copy of the document's `authentication` set. #[wasm_bindgen] - pub fn authentication(&self) -> ArrayCoreMethodRef { - self - .0 - .blocking_read() - .authentication() - .iter() - .cloned() - .map(|method_ref| match method_ref { - MethodRef::Embed(verification_method) => JsValue::from(WasmVerificationMethod(verification_method)), - MethodRef::Refer(did_url) => JsValue::from(WasmDIDUrl(did_url)), - }) - .collect::() - .unchecked_into::() + pub fn authentication(&self) -> Result { + Ok( + self + .0 + .try_read()? + .authentication() + .iter() + .cloned() + .map(|method_ref| match method_ref { + MethodRef::Embed(verification_method) => JsValue::from(WasmVerificationMethod(verification_method)), + MethodRef::Refer(did_url) => JsValue::from(WasmDIDUrl(did_url)), + }) + .collect::() + .unchecked_into::(), + ) } /// Returns a copy of the document's `assertionMethod` set. #[wasm_bindgen(js_name = assertionMethod)] - pub fn assertion_method(&self) -> ArrayCoreMethodRef { - self - .0 - .blocking_read() - .assertion_method() - .iter() - .cloned() - .map(|method_ref| match method_ref { - MethodRef::Embed(verification_method) => JsValue::from(WasmVerificationMethod(verification_method)), - MethodRef::Refer(did_url) => JsValue::from(WasmDIDUrl(did_url)), - }) - .collect::() - .unchecked_into::() + pub fn assertion_method(&self) -> Result { + Ok( + self + .0 + .try_read()? + .assertion_method() + .iter() + .cloned() + .map(|method_ref| match method_ref { + MethodRef::Embed(verification_method) => JsValue::from(WasmVerificationMethod(verification_method)), + MethodRef::Refer(did_url) => JsValue::from(WasmDIDUrl(did_url)), + }) + .collect::() + .unchecked_into::(), + ) } /// Returns a copy of the document's `keyAgreement` set. #[wasm_bindgen(js_name = keyAgreement)] - pub fn key_agreement(&self) -> ArrayCoreMethodRef { - self - .0 - .blocking_read() - .key_agreement() - .iter() - .cloned() - .map(|method_ref| match method_ref { - MethodRef::Embed(verification_method) => JsValue::from(WasmVerificationMethod(verification_method)), - MethodRef::Refer(did_url) => JsValue::from(WasmDIDUrl(did_url)), - }) - .collect::() - .unchecked_into::() + pub fn key_agreement(&self) -> Result { + Ok( + self + .0 + .try_read()? + .key_agreement() + .iter() + .cloned() + .map(|method_ref| match method_ref { + MethodRef::Embed(verification_method) => JsValue::from(WasmVerificationMethod(verification_method)), + MethodRef::Refer(did_url) => JsValue::from(WasmDIDUrl(did_url)), + }) + .collect::() + .unchecked_into::(), + ) } /// Returns a copy of the document's `capabilityDelegation` set. #[wasm_bindgen(js_name = capabilityDelegation)] - pub fn capability_delegation(&self) -> ArrayCoreMethodRef { - self - .0 - .blocking_read() - .capability_delegation() - .iter() - .cloned() - .map(|method_ref| match method_ref { - MethodRef::Embed(verification_method) => JsValue::from(WasmVerificationMethod(verification_method)), - MethodRef::Refer(did_url) => JsValue::from(WasmDIDUrl(did_url)), - }) - .collect::() - .unchecked_into::() + pub fn capability_delegation(&self) -> Result { + Ok( + self + .0 + .try_read()? + .capability_delegation() + .iter() + .cloned() + .map(|method_ref| match method_ref { + MethodRef::Embed(verification_method) => JsValue::from(WasmVerificationMethod(verification_method)), + MethodRef::Refer(did_url) => JsValue::from(WasmDIDUrl(did_url)), + }) + .collect::() + .unchecked_into::(), + ) } /// Returns a copy of the document's `capabilityInvocation` set. #[wasm_bindgen(js_name = capabilityInvocation)] - pub fn capability_invocation(&self) -> ArrayCoreMethodRef { - self - .0 - .blocking_read() - .capability_invocation() - .iter() - .cloned() - .map(|method_ref| match method_ref { - MethodRef::Embed(verification_method) => JsValue::from(WasmVerificationMethod(verification_method)), - MethodRef::Refer(did_url) => JsValue::from(WasmDIDUrl(did_url)), - }) - .collect::() - .unchecked_into::() + pub fn capability_invocation(&self) -> Result { + Ok( + self + .0 + .try_read()? + .capability_invocation() + .iter() + .cloned() + .map(|method_ref| match method_ref { + MethodRef::Embed(verification_method) => JsValue::from(WasmVerificationMethod(verification_method)), + MethodRef::Refer(did_url) => JsValue::from(WasmDIDUrl(did_url)), + }) + .collect::() + .unchecked_into::(), + ) } /// Returns a copy of the custom DID Document properties. #[wasm_bindgen] pub fn properties(&self) -> Result { - MapStringAny::try_from(self.0.blocking_read().properties()) + MapStringAny::try_from(self.0.try_read()?.properties()) } /// Sets a custom property in the DID Document. @@ -300,10 +319,10 @@ impl WasmCoreDocument { let value: Option = value.into_serde().wasm_result()?; match value { Some(value) => { - self.0.blocking_write().properties_mut_unchecked().insert(key, value); + self.0.try_write()?.properties_mut_unchecked().insert(key, value); } None => { - self.0.blocking_write().properties_mut_unchecked().remove(&key); + self.0.try_write()?.properties_mut_unchecked().remove(&key); } } Ok(()) @@ -315,17 +334,19 @@ impl WasmCoreDocument { /// Returns a set of all {@link Service} in the document. #[wasm_bindgen] - pub fn service(&self) -> ArrayService { - self - .0 - .blocking_read() - .service() - .iter() - .cloned() - .map(WasmService) - .map(JsValue::from) - .collect::() - .unchecked_into::() + pub fn service(&self) -> Result { + Ok( + self + .0 + .try_read()? + .service() + .iter() + .cloned() + .map(WasmService) + .map(JsValue::from) + .collect::() + .unchecked_into::(), + ) } /// Add a new {@link Service} to the document. @@ -333,7 +354,7 @@ impl WasmCoreDocument { /// Errors if there already exists a service or verification method with the same id. #[wasm_bindgen(js_name = insertService)] pub fn insert_service(&mut self, service: &WasmService) -> Result<()> { - self.0.blocking_write().insert_service(service.0.clone()).wasm_result() + self.0.try_write()?.insert_service(service.0.clone()).wasm_result() } /// Remove a {@link Service} identified by the given {@link DIDUrl} from the document. @@ -341,25 +362,23 @@ impl WasmCoreDocument { /// Returns `true` if the service was removed. #[wasm_bindgen(js_name = removeService)] #[allow(non_snake_case)] - pub fn remove_service(&mut self, didUrl: &WasmDIDUrl) -> Option { - self - .0 - .blocking_write() - .remove_service(&didUrl.0.clone()) - .map(Into::into) + pub fn remove_service(&mut self, didUrl: &WasmDIDUrl) -> Result> { + Ok(self.0.try_write()?.remove_service(&didUrl.0.clone()).map(Into::into)) } /// Returns the first {@link Service} with an `id` property matching the provided `query`, /// if present. #[wasm_bindgen(js_name = resolveService)] - pub fn resolve_service(&self, query: &UDIDUrlQuery) -> Option { - let service_query: String = query.into_serde().ok()?; - self - .0 - .blocking_read() - .resolve_service(&service_query) - .cloned() - .map(WasmService::from) + pub fn resolve_service(&self, query: &UDIDUrlQuery) -> Result> { + let service_query: String = query.into_serde().wasm_result()?; + Ok( + self + .0 + .try_read()? + .resolve_service(&service_query) + .cloned() + .map(WasmService::from), + ) } // =========================================================================== @@ -375,7 +394,7 @@ impl WasmCoreDocument { let scope: Option = scope.map(|js| js.into_serde().wasm_result()).transpose()?; let methods = self .0 - .blocking_read() + .try_read()? .methods(scope) .into_iter() .cloned() @@ -388,18 +407,20 @@ impl WasmCoreDocument { /// Returns an array of all verification relationships. #[wasm_bindgen(js_name = verificationRelationships)] - pub fn verification_relationships(&self) -> ArrayCoreMethodRef { - self - .0 - .blocking_read() - .verification_relationships() - .cloned() - .map(|method_ref| match method_ref { - MethodRef::Embed(verification_method) => JsValue::from(WasmVerificationMethod(verification_method)), - MethodRef::Refer(did_url) => JsValue::from(WasmDIDUrl(did_url)), - }) - .collect::() - .unchecked_into::() + pub fn verification_relationships(&self) -> Result { + Ok( + self + .0 + .try_read()? + .verification_relationships() + .cloned() + .map(|method_ref| match method_ref { + MethodRef::Embed(verification_method) => JsValue::from(WasmVerificationMethod(verification_method)), + MethodRef::Refer(did_url) => JsValue::from(WasmDIDUrl(did_url)), + }) + .collect::() + .unchecked_into::(), + ) } /// Adds a new `method` to the document in the given `scope`. @@ -407,15 +428,15 @@ impl WasmCoreDocument { pub fn insert_method(&mut self, method: &WasmVerificationMethod, scope: &WasmMethodScope) -> Result<()> { self .0 - .blocking_write() + .try_write()? .insert_method(method.0.clone(), scope.0) .wasm_result() } /// Removes all references to the specified Verification Method. #[wasm_bindgen(js_name = removeMethod)] - pub fn remove_method(&mut self, did: &WasmDIDUrl) -> Option { - self.0.blocking_write().remove_method(&did.0).map(Into::into) + pub fn remove_method(&mut self, did: &WasmDIDUrl) -> Result> { + Ok(self.0.try_write()?.remove_method(&did.0).map(Into::into)) } /// Returns a copy of the first verification method with an `id` property @@ -430,7 +451,7 @@ impl WasmCoreDocument { let method_query: String = query.into_serde().wasm_result()?; let method_scope: Option = scope.map(|js| js.into_serde().wasm_result()).transpose()?; - let guard = self.0.blocking_read(); + let guard = self.0.try_read()?; let method: Option<&VerificationMethod> = guard.resolve_method(&method_query, method_scope); Ok(method.cloned().map(WasmVerificationMethod)) } @@ -448,7 +469,7 @@ impl WasmCoreDocument { ) -> Result { self .0 - .blocking_write() + .try_write()? .attach_method_relationship(&didUrl.0, relationship.into()) .wasm_result() } @@ -463,7 +484,7 @@ impl WasmCoreDocument { ) -> Result { self .0 - .blocking_write() + .try_write()? .detach_method_relationship(&didUrl.0, relationship.into()) .wasm_result() } @@ -493,7 +514,7 @@ impl WasmCoreDocument { let jws_verifier = WasmJwsVerifier::new(signatureVerifier); self .0 - .blocking_read() + .try_read()? .verify_jws( jws.0.as_str(), detachedPayload.as_deref().map(|detached| detached.as_bytes()), @@ -518,7 +539,7 @@ impl WasmCoreDocument { self .0 - .blocking_write() + .try_write()? .revoke_credentials(&query, indices.as_slice()) .wasm_result() } @@ -533,7 +554,7 @@ impl WasmCoreDocument { self .0 - .blocking_write() + .try_write()? .unrevoke_credentials(&query, indices.as_slice()) .wasm_result() } @@ -544,8 +565,10 @@ impl WasmCoreDocument { /// Deep clones the {@link CoreDocument}. #[wasm_bindgen(js_name = clone)] - pub fn deep_clone(&self) -> WasmCoreDocument { - WasmCoreDocument(Rc::new(CoreDocumentLock::new(self.0.blocking_read().clone()))) + pub fn deep_clone(&self) -> Result { + Ok(WasmCoreDocument(Rc::new(CoreDocumentLock::new( + self.0.try_read()?.clone(), + )))) } /// ### Warning @@ -569,7 +592,7 @@ impl WasmCoreDocument { /// Serializes to a plain JS representation. #[wasm_bindgen(js_name = toJSON)] pub fn to_json(&self) -> Result { - JsValue::from_serde(&self.0.blocking_read().as_ref()).wasm_result() + JsValue::from_serde(&self.0.try_read()?.as_ref()).wasm_result() } /// Deserializes an instance from a plain JS representation. diff --git a/bindings/wasm/src/error.rs b/bindings/wasm/src/error.rs index 57d7094e44..aa13d289c0 100644 --- a/bindings/wasm/src/error.rs +++ b/bindings/wasm/src/error.rs @@ -13,6 +13,7 @@ use std::borrow::Cow; use std::fmt::Debug; use std::fmt::Display; use std::result::Result as StdResult; +use tokio::sync::TryLockError; use wasm_bindgen::JsValue; /// Convenience wrapper for `Result`. @@ -249,6 +250,15 @@ impl From for WasmError<'_> { } } +impl From for WasmError<'_> { + fn from(error: TryLockError) -> Self { + Self { + name: Cow::Borrowed("TryLockError"), + message: Cow::Owned(ErrorMessage(&error).to_string()), + } + } +} + /// Convenience struct to convert Result to errors in the Rust library. pub struct JsValueResult(pub(crate) Result); diff --git a/bindings/wasm/src/iota/identity_client_ext.rs b/bindings/wasm/src/iota/identity_client_ext.rs index 7804b4f258..cdb0ae7ec4 100644 --- a/bindings/wasm/src/iota/identity_client_ext.rs +++ b/bindings/wasm/src/iota/identity_client_ext.rs @@ -72,7 +72,7 @@ impl WasmIotaIdentityClientExt { identity_iota::iota::Error::JsError(format!("newDidOutput failed to decode Address: {err}: {address_dto:?}")) }) .wasm_result()?; - let doc: IotaDocument = document.0.blocking_read().clone(); + let doc: IotaDocument = document.0.try_read()?.clone(); let promise: Promise = future_to_promise(async move { let rent_structure: Option = rentStructure @@ -102,7 +102,7 @@ impl WasmIotaIdentityClientExt { client: WasmIotaIdentityClient, document: &WasmIotaDocument, ) -> Result { - let document: IotaDocument = document.0.blocking_read().clone(); + let document: IotaDocument = document.0.try_read()?.clone(); let promise: Promise = future_to_promise(async move { let output: AliasOutput = IotaIdentityClientExt::update_did_output(&client, document) .await diff --git a/bindings/wasm/src/iota/iota_document.rs b/bindings/wasm/src/iota/iota_document.rs index 883986981e..e01c96a90b 100644 --- a/bindings/wasm/src/iota/iota_document.rs +++ b/bindings/wasm/src/iota/iota_document.rs @@ -80,12 +80,12 @@ impl IotaDocumentLock { Self(tokio::sync::RwLock::new(value)) } - pub(crate) fn blocking_read(&self) -> tokio::sync::RwLockReadGuard<'_, IotaDocument> { - self.0.blocking_read() + pub(crate) fn try_read(&self) -> Result> { + self.0.try_read().wasm_result() } - pub(crate) fn blocking_write(&self) -> tokio::sync::RwLockWriteGuard<'_, IotaDocument> { - self.0.blocking_write() + pub(crate) fn try_write(&self) -> Result> { + self.0.try_write().wasm_result() } pub(crate) async fn read(&self) -> tokio::sync::RwLockReadGuard<'_, IotaDocument> { @@ -99,6 +99,10 @@ impl IotaDocumentLock { // ============================================================================= // ============================================================================= +/// A DID Document adhering to the IOTA DID method specification. +/// +/// Note: All methods that involve reading from this class may potentially raise an error +/// if the object is being concurrently modified. #[wasm_bindgen(js_name = IotaDocument, inspectable)] pub struct WasmIotaDocument(pub(crate) Rc); @@ -129,8 +133,8 @@ impl WasmIotaDocument { /// Returns a copy of the DID Document `id`. #[wasm_bindgen] - pub fn id(&self) -> WasmIotaDID { - WasmIotaDID::from(self.0.blocking_read().id().clone()) + pub fn id(&self) -> Result { + Ok(WasmIotaDID::from(self.0.try_read()?.id().clone())) } /// Returns a copy of the list of document controllers. @@ -138,30 +142,34 @@ impl WasmIotaDocument { /// NOTE: controllers are determined by the `state_controller` unlock condition of the output /// during resolution and are omitted when publishing. #[wasm_bindgen] - pub fn controller(&self) -> ArrayIotaDID { - self - .0 - .blocking_read() - .controller() - .cloned() - .map(WasmIotaDID::from) - .map(JsValue::from) - .collect::() - .unchecked_into::() + pub fn controller(&self) -> Result { + Ok( + self + .0 + .try_read()? + .controller() + .cloned() + .map(WasmIotaDID::from) + .map(JsValue::from) + .collect::() + .unchecked_into::(), + ) } /// Returns a copy of the document's `alsoKnownAs` set. #[wasm_bindgen(js_name = alsoKnownAs)] - pub fn also_known_as(&self) -> ArrayString { - self - .0 - .blocking_read() - .also_known_as() - .iter() - .map(|url| url.to_string()) - .map(JsValue::from) - .collect::() - .unchecked_into::() + pub fn also_known_as(&self) -> Result { + Ok( + self + .0 + .try_read()? + .also_known_as() + .iter() + .map(|url| url.to_string()) + .map(JsValue::from) + .collect::() + .unchecked_into::(), + ) } /// Sets the `alsoKnownAs` property in the DID document. @@ -174,14 +182,14 @@ impl WasmIotaDocument { urls_set.append(Url::parse(url).wasm_result()?); } } - *self.0.blocking_write().also_known_as_mut() = urls_set; + *self.0.try_write()?.also_known_as_mut() = urls_set; Ok(()) } /// Returns a copy of the custom DID Document properties. #[wasm_bindgen] pub fn properties(&self) -> Result { - MapStringAny::try_from(self.0.blocking_read().properties()) + MapStringAny::try_from(self.0.try_read()?.properties()) } /// Sets a custom property in the DID Document. @@ -195,10 +203,10 @@ impl WasmIotaDocument { let value: Option = value.into_serde().wasm_result()?; match value { Some(value) => { - self.0.blocking_write().properties_mut_unchecked().insert(key, value); + self.0.try_write()?.properties_mut_unchecked().insert(key, value); } None => { - self.0.blocking_write().properties_mut_unchecked().remove(&key); + self.0.try_write()?.properties_mut_unchecked().remove(&key); } } Ok(()) @@ -210,17 +218,19 @@ impl WasmIotaDocument { /// Return a set of all {@link Service} in the document. #[wasm_bindgen] - pub fn service(&self) -> ArrayService { - self - .0 - .blocking_read() - .service() - .iter() - .cloned() - .map(WasmService) - .map(JsValue::from) - .collect::() - .unchecked_into::() + pub fn service(&self) -> Result { + Ok( + self + .0 + .try_read()? + .service() + .iter() + .cloned() + .map(WasmService) + .map(JsValue::from) + .collect::() + .unchecked_into::(), + ) } /// Add a new {@link Service} to the document. @@ -228,28 +238,30 @@ impl WasmIotaDocument { /// Returns `true` if the service was added. #[wasm_bindgen(js_name = insertService)] pub fn insert_service(&mut self, service: &WasmService) -> Result<()> { - self.0.blocking_write().insert_service(service.0.clone()).wasm_result() + self.0.try_write()?.insert_service(service.0.clone()).wasm_result() } /// Remove a {@link Service} identified by the given {@link DIDUrl} from the document. /// /// Returns `true` if a service was removed. #[wasm_bindgen(js_name = removeService)] - pub fn remove_service(&mut self, did: &WasmDIDUrl) -> Option { - self.0.blocking_write().remove_service(&did.0).map(Into::into) + pub fn remove_service(&mut self, did: &WasmDIDUrl) -> Result> { + Ok(self.0.try_write()?.remove_service(&did.0).map(Into::into)) } /// Returns the first {@link Service} with an `id` property matching the provided `query`, /// if present. #[wasm_bindgen(js_name = resolveService)] - pub fn resolve_service(&self, query: &UDIDUrlQuery) -> Option { - let service_query: String = query.into_serde().ok()?; - self - .0 - .blocking_read() - .resolve_service(&service_query) - .cloned() - .map(WasmService::from) + pub fn resolve_service(&self, query: &UDIDUrlQuery) -> Result> { + let service_query: String = query.into_serde().wasm_result()?; + Ok( + self + .0 + .try_read()? + .resolve_service(&service_query) + .cloned() + .map(WasmService::from), + ) } // =========================================================================== @@ -265,7 +277,7 @@ impl WasmIotaDocument { let scope: Option = scope.map(|js| js.into_serde().wasm_result()).transpose()?; let methods = self .0 - .blocking_read() + .try_read()? .methods(scope) .into_iter() .cloned() @@ -281,7 +293,7 @@ impl WasmIotaDocument { pub fn insert_method(&mut self, method: &WasmVerificationMethod, scope: &WasmMethodScope) -> Result<()> { self .0 - .blocking_write() + .try_write()? .insert_method(method.0.clone(), scope.0) .wasm_result()?; Ok(()) @@ -289,8 +301,8 @@ impl WasmIotaDocument { /// Removes all references to the specified Verification Method. #[wasm_bindgen(js_name = removeMethod)] - pub fn remove_method(&mut self, did: &WasmDIDUrl) -> Option { - self.0.blocking_write().remove_method(&did.0).map(Into::into) + pub fn remove_method(&mut self, did: &WasmDIDUrl) -> Result> { + Ok(self.0.try_write()?.remove_method(&did.0).map(Into::into)) } /// Returns a copy of the first verification method with an `id` property @@ -305,7 +317,7 @@ impl WasmIotaDocument { let method_query: String = query.into_serde().wasm_result()?; let method_scope: Option = scope.map(|js| js.into_serde().wasm_result()).transpose()?; - let guard = self.0.blocking_read(); + let guard = self.0.try_read()?; let method: Option<&VerificationMethod> = guard.resolve_method(&method_query, method_scope); Ok(method.cloned().map(WasmVerificationMethod)) } @@ -323,7 +335,7 @@ impl WasmIotaDocument { ) -> Result { self .0 - .blocking_write() + .try_write()? .attach_method_relationship(&didUrl.0, relationship.into()) .wasm_result() } @@ -338,7 +350,7 @@ impl WasmIotaDocument { ) -> Result { self .0 - .blocking_write() + .try_write()? .detach_method_relationship(&didUrl.0, relationship.into()) .wasm_result() } @@ -367,7 +379,7 @@ impl WasmIotaDocument { let jws_verifier = WasmJwsVerifier::new(signatureVerifier); self .0 - .blocking_read() + .try_read()? .verify_jws( &jws.0, detachedPayload.as_deref().map(|detached| detached.as_bytes()), @@ -386,7 +398,7 @@ impl WasmIotaDocument { /// with the default {@link StateMetadataEncoding}. #[wasm_bindgen] pub fn pack(&self) -> Result> { - self.0.blocking_read().clone().pack().wasm_result() + self.0.try_read()?.clone().pack().wasm_result() } /// Serializes the document for inclusion in an Alias Output's state metadata. @@ -394,7 +406,7 @@ impl WasmIotaDocument { pub fn pack_with_encoding(&self, encoding: WasmStateMetadataEncoding) -> Result> { self .0 - .blocking_read() + .try_read()? .clone() .pack_with_encoding(StateMetadataEncoding::from(encoding)) .wasm_result() @@ -467,60 +479,61 @@ impl WasmIotaDocument { /// NOTE: Copies all the metadata. See also `metadataCreated`, `metadataUpdated`, /// `metadataPreviousMessageId`, `metadataProof` if only a subset of the metadata required. #[wasm_bindgen] - pub fn metadata(&self) -> WasmIotaDocumentMetadata { - WasmIotaDocumentMetadata::from(self.0.blocking_read().metadata.clone()) + pub fn metadata(&self) -> Result { + Ok(WasmIotaDocumentMetadata::from(self.0.try_read()?.metadata.clone())) } /// Returns a copy of the timestamp of when the DID document was created. #[wasm_bindgen(js_name = metadataCreated)] - pub fn metadata_created(&self) -> Option { - self.0.blocking_read().metadata.created.map(WasmTimestamp::from) + pub fn metadata_created(&self) -> Result> { + Ok(self.0.try_read()?.metadata.created.map(WasmTimestamp::from)) } /// Sets the timestamp of when the DID document was created. #[wasm_bindgen(js_name = setMetadataCreated)] pub fn set_metadata_created(&mut self, timestamp: OptionTimestamp) -> Result<()> { let timestamp: Option = timestamp.into_serde().wasm_result()?; - self.0.blocking_write().metadata.created = timestamp; + self.0.try_write()?.metadata.created = timestamp; Ok(()) } /// Returns a copy of the timestamp of the last DID document update. #[wasm_bindgen(js_name = metadataUpdated)] - pub fn metadata_updated(&self) -> Option { - self.0.blocking_read().metadata.updated.map(WasmTimestamp::from) + pub fn metadata_updated(&self) -> Result> { + Ok(self.0.try_read()?.metadata.updated.map(WasmTimestamp::from)) } /// Sets the timestamp of the last DID document update. #[wasm_bindgen(js_name = setMetadataUpdated)] pub fn set_metadata_updated(&mut self, timestamp: OptionTimestamp) -> Result<()> { let timestamp: Option = timestamp.into_serde().wasm_result()?; - self.0.blocking_write().metadata.updated = timestamp; + self.0.try_write()?.metadata.updated = timestamp; Ok(()) } /// Returns a copy of the deactivated status of the DID document. #[wasm_bindgen(js_name = metadataDeactivated)] - pub fn metadata_deactivated(&self) -> Option { - self.0.blocking_read().metadata.deactivated + pub fn metadata_deactivated(&self) -> Result> { + Ok(self.0.try_read()?.metadata.deactivated) } /// Sets the deactivated status of the DID document. #[wasm_bindgen(js_name = setMetadataDeactivated)] - pub fn set_metadata_deactivated(&mut self, deactivated: Option) { - self.0.blocking_write().metadata.deactivated = deactivated; + pub fn set_metadata_deactivated(&mut self, deactivated: Option) -> Result<()> { + self.0.try_write()?.metadata.deactivated = deactivated; + Ok(()) } /// Returns a copy of the Bech32-encoded state controller address, if present. #[wasm_bindgen(js_name = metadataStateControllerAddress)] - pub fn metadata_state_controller_address(&self) -> Option { - self.0.blocking_read().metadata.state_controller_address.clone() + pub fn metadata_state_controller_address(&self) -> Result> { + Ok(self.0.try_read()?.metadata.state_controller_address.clone()) } /// Returns a copy of the Bech32-encoded governor address, if present. #[wasm_bindgen(js_name = metadataGovernorAddress)] - pub fn metadata_governor_address(&self) -> Option { - self.0.blocking_read().metadata.governor_address.clone() + pub fn metadata_governor_address(&self) -> Result> { + Ok(self.0.try_read()?.metadata.governor_address.clone()) } /// Sets a custom property in the document metadata. @@ -530,10 +543,10 @@ impl WasmIotaDocument { let value: Option = value.into_serde().wasm_result()?; match value { Some(value) => { - self.0.blocking_write().metadata.properties_mut().insert(key, value); + self.0.try_write()?.metadata.properties_mut().insert(key, value); } None => { - self.0.blocking_write().metadata.properties_mut().remove(&key); + self.0.try_write()?.metadata.properties_mut().remove(&key); } } Ok(()) @@ -553,7 +566,7 @@ impl WasmIotaDocument { self .0 - .blocking_write() + .try_write()? .revoke_credentials(&query, indices.as_slice()) .wasm_result() } @@ -568,7 +581,7 @@ impl WasmIotaDocument { self .0 - .blocking_write() + .try_write()? .unrevoke_credentials(&query, indices.as_slice()) .wasm_result() } @@ -579,8 +592,10 @@ impl WasmIotaDocument { #[wasm_bindgen(js_name = clone)] /// Returns a deep clone of the {@link IotaDocument}. - pub fn deep_clone(&self) -> WasmIotaDocument { - WasmIotaDocument(Rc::new(IotaDocumentLock::new(self.0.blocking_read().clone()))) + pub fn deep_clone(&self) -> Result { + Ok(WasmIotaDocument(Rc::new(IotaDocumentLock::new( + self.0.try_read()?.clone(), + )))) } /// ### Warning @@ -604,7 +619,7 @@ impl WasmIotaDocument { /// Serializes to a plain JS representation. #[wasm_bindgen(js_name = toJSON)] pub fn to_json(&self) -> Result { - JsValue::from_serde(&self.0.blocking_read().as_ref()).wasm_result() + JsValue::from_serde(&self.0.try_read()?.as_ref()).wasm_result() } /// Deserializes an instance from a plain JS representation. @@ -621,10 +636,10 @@ impl WasmIotaDocument { // =========================================================================== /// Transforms the {@link IotaDocument} to its {@link CoreDocument} representation. #[wasm_bindgen(js_name = toCoreDocument)] - pub fn as_core_document(&self) -> WasmCoreDocument { - WasmCoreDocument(Rc::new(CoreDocumentLock::new( - self.0.blocking_read().core_document().clone(), - ))) + pub fn as_core_document(&self) -> Result { + Ok(WasmCoreDocument(Rc::new(CoreDocumentLock::new( + self.0.try_read()?.core_document().clone(), + )))) } // =========================================================================== diff --git a/bindings/wasm/tests/storage.ts b/bindings/wasm/tests/storage.ts index a526a80e7d..cad4e85fe9 100644 --- a/bindings/wasm/tests/storage.ts +++ b/bindings/wasm/tests/storage.ts @@ -472,3 +472,66 @@ describe("#OptionParsing", function() { }); }); }); + +describe("#Documents throw error on concurrent synchronous access", async function() { + const wait: any = (ms: any) => new Promise(r => setTimeout(r, ms)); + + class MyJwkStore extends JwkMemStore { + constructor() { + super(); + } + + async generate(keyType: string, algorithm: JwsAlgorithm) { + await wait(10000); + return await super.generate(keyType, algorithm); + } + } + it("CoreDocument", async () => { + const document = new CoreDocument({ id: "did:example:123" }); + const storage = new Storage(new MyJwkStore(), new KeyIdMemStore()); + const insertPromise = document.generateMethod( + storage, + JwkMemStore.ed25519KeyType(), + JwsAlgorithm.EdDSA, + "#key-1", + MethodScope.VerificationMethod(), + ); + + const idPromise = wait(10).then((_value: any) => { + return document.id(); + }); + + let resolvedToError = false; + try { + await Promise.all([insertPromise, idPromise]); + } catch (e: any) { + resolvedToError = true; + assert.equal(e.name, "TryLockError"); + } + assert.ok(resolvedToError, "Promise.all did not throw an error"); + }); + + it("IotaDocument", async () => { + const document = new IotaDocument("rms"); + const storage = new Storage(new MyJwkStore(), new KeyIdMemStore()); + const insertPromise = document.generateMethod( + storage, + JwkMemStore.ed25519KeyType(), + JwsAlgorithm.EdDSA, + "#key-1", + MethodScope.VerificationMethod(), + ); + + const idPromise = wait(10).then((_value: any) => { + return document.id(); + }); + let resolvedToError = false; + try { + await Promise.all([insertPromise, idPromise]); + } catch (e: any) { + resolvedToError = true; + assert.equal(e.name, "TryLockError"); + } + assert.ok(resolvedToError, "Promise.all did not throw an error"); + }); +});