├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── identity │ ├── did-document-base.ts │ ├── did-document-json-properties.ts │ ├── did-method-operation.ts │ ├── did-parser.ts │ ├── did-syntax.ts │ ├── hcs │ │ ├── address-book.ts │ │ ├── did │ │ │ ├── hcs-did-message.ts │ │ │ ├── hcs-did-resolver.ts │ │ │ ├── hcs-did-root-key.ts │ │ │ ├── hcs-did-topic-listener.ts │ │ │ ├── hcs-did-transaction.ts │ │ │ └── hcs-did.ts │ │ ├── hcs-identity-network-builder.ts │ │ ├── hcs-identity-network.ts │ │ ├── json-class.ts │ │ ├── message-envelope.ts │ │ ├── message-listener.ts │ │ ├── message-mode.ts │ │ ├── message-resolver.ts │ │ ├── message-transaction.ts │ │ ├── message.interface.ts │ │ ├── message.ts │ │ ├── serializable-mirror-consensus-response.ts │ │ └── vc │ │ │ ├── credential-subject.ts │ │ │ ├── hcs-vc-document-base.ts │ │ │ ├── hcs-vc-document-hash-base.ts │ │ │ ├── hcs-vc-document-json-properties.ts │ │ │ ├── hcs-vc-message.ts │ │ │ ├── hcs-vc-operation.ts │ │ │ ├── hcs-vc-status-resolver.ts │ │ │ ├── hcs-vc-topic-listener.ts │ │ │ ├── hcs-vc-transaction.ts │ │ │ └── issuer.ts │ └── hedera-did.ts ├── index.ts ├── typings.d.ts └── utils │ ├── arrays-utils.ts │ ├── hashing.ts │ ├── sleep.ts │ ├── timestamp-utils.ts │ └── validator.ts ├── test ├── aes-encryption-util.js ├── did-document-base.js ├── did │ ├── hcs-did-message.js │ ├── hcs-did-method-operations.js │ ├── hcs-did-root-key.js │ └── hcs-did.js ├── hcs-identity-network.js ├── network-ready-test-base.js ├── variables.js └── vc │ ├── demo-access-credential.js │ ├── demo-verifiable-credential-document.js │ ├── hcs-vc-document-base-test.js │ ├── hcs-vc-document-operations-test.js │ └── hcs-vc-encryption-test.js └── tsconfig.json /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Default code owners for entire repository 2 | * @hashgraph/developer-advocates 3 | ######################### 4 | ##### Core Files ###### 5 | ######################### 6 | 7 | # NOTE: Must be placed last to ensure enforcement over all other rules 8 | 9 | # Protection Rules for Github Configuration Files and Actions Workflows 10 | /.github/ @hashgraph/platform-ci @hashgraph/platform-ci-committers @hashgraph/release-engineering-managers @hashgraph/developer-advocates 11 | /.github/workflows/ @hashgraph/platform-ci @hashgraph/platform-ci-committers @hashgraph/release-engineering-managers 12 | 13 | # Self-protection for root CODEOWNERS files (this file should not exist and should definitely require approval) 14 | /CODEOWNERS @hashgraph/release-engineering-managers 15 | 16 | # Protect the repository root files 17 | /README.md @hashgraph/platform-ci @hashgraph/release-engineering-managers @hashgraph/developer-advocates 18 | **/LICENSE @hashgraph/platform-ci @hashgraph/release-engineering-managers 19 | 20 | # Git Ignore definitions 21 | **/.gitignore @hashgraph/platform-ci @hashgraph/platform-ci-committers @hashgraph/release-engineering-managers @hashgraph/developer-advocates 22 | **/.gitignore.* @hashgraph/platform-ci @hashgraph/platform-ci-committers @hashgraph/release-engineering-managers @hashgraph/developer-advocates 23 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: github-actions 10 | directory: "/" 11 | schedule: 12 | interval: daily 13 | open-pull-requests-limit: 10 14 | 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a tag is created. 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Release 5 | 6 | on: 7 | push: 8 | tags: [ 'v*' ] 9 | 10 | workflow_dispatch: 11 | inputs: 12 | version: 13 | type: string 14 | description: Test Version String (No release to Maven Central) 15 | required: true 16 | 17 | defaults: 18 | run: 19 | shell: bash 20 | 21 | permissions: 22 | contents: read 23 | packages: write 24 | 25 | jobs: 26 | build: 27 | runs-on: decentralized-identity-linux-medium 28 | strategy: 29 | matrix: 30 | node: [ '14', '16' ] 31 | steps: 32 | - name: Harden Runner 33 | uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1 34 | with: 35 | egress-policy: audit 36 | 37 | - name: Checkout Code 38 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 39 | 40 | - name: Setup Node 41 | uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 42 | with: 43 | cache: 'npm' 44 | node-version: ${{ matrix.node }} 45 | 46 | - name: Run CI 47 | run: npm ci 48 | 49 | - name: Run Tests 50 | run: npm test 51 | env: 52 | OPERATOR_ID: ${{ secrets.OPERATOR_ID }} 53 | OPERATOR_KEY: ${{ secrets.OPERATOR_KEY }} 54 | 55 | publish: 56 | needs: build 57 | runs-on: decentralized-identity-linux-medium 58 | steps: 59 | - name: Harden Runner 60 | uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 61 | with: 62 | egress-policy: audit 63 | 64 | - name: Checkout Code 65 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 66 | 67 | - name: Setup Node 68 | uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 69 | with: 70 | cache: 'npm' 71 | node-version: 14 72 | registry-url: https://registry.npmjs.org/ 73 | 74 | - name: Run CI 75 | run: npm ci 76 | 77 | - name: Publish build 78 | run: npm publish 79 | env: 80 | NODE_AUTH_TOKEN: ${{secrets.NODE_AUTH_TOKEN }} 81 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | push: 7 | branches: [ main ] 8 | 9 | defaults: 10 | run: 11 | shell: bash 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | build: 18 | runs-on: decentralized-identity-linux-medium 19 | strategy: 20 | matrix: 21 | node: [ '14', '16' ] 22 | steps: 23 | - name: Harden Runner 24 | uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 25 | with: 26 | egress-policy: audit 27 | 28 | - name: Checkout Code 29 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 30 | 31 | - name: Setup Node 32 | uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 33 | with: 34 | cache: 'npm' 35 | node-version: ${{ matrix.node }} 36 | 37 | - name: Run CI 38 | run: npm ci 39 | 40 | - name: Test code 41 | run: npm test 42 | env: 43 | OPERATOR_ID: ${{ secrets.OPERATOR_ID }} 44 | OPERATOR_KEY: ${{ secrets.OPERATOR_KEY }} 45 | 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # did-sdk-js 2 | Support for the Hedera Hashgraph DID Method and Verifiable Credentials on the Hedera JavaScript/TypeScript SDK. 3 | 4 | This repository contains the Javascript SDK for managing [DID Documents][did-core] & [Verifiable Credentials][vc-data-model] registry using the Hedera Consensus Service. 5 | 6 | did-sdk-js based on [did-sdk-java](https://github.com/hashgraph/did-sdk-java), so both of them contain similar methods and classes. 7 | 8 | ## Overview 9 | 10 | Identity networks are set of artifacts on Hedera Consensus Service that allow applications to share common channels to publish and resolve DID documents, issue verifiable credentials and control their validity status. These artifacts include: 11 | 12 | - address book - a file on Hedera File Service that provides information about HCS topics and appnet servers, 13 | - DID topic - an HCS topic intended for publishing DID documents, 14 | - and VC topic - an HCS topic playing a role of verifiable credentials registry. 15 | 16 | This SDK is designed to simplify : 17 | 18 | - creation of identity networks within appnets, that is: creation and initialization of the artifacts mentioned above, 19 | - generation of decentralized identifiers for [Hedera DID Method][did-method-spec] and creation of their basic DID documents, 20 | - creation (publishing), update, deletion and resolution of DID documents in appnet identity networks, 21 | - issuance, revocation and status verification of [Verifiable Credentials][vc-data-model]. 22 | 23 | The SDK does not impose any particular way of how the DID or verifiable credential documents are constructed. Each appnet creators can choose their best way of creating those documents and as long as these are valid JSON-LD files adhering to W3C standards, they will be handled by the SDK. 24 | 25 | ## Usage 26 | ``` 27 | npm install --save @hashgraph/did-sdk-js 28 | ``` 29 | 30 | ## Example: 31 | 32 | ### Identity Network 33 | ``` 34 | const client = ... // Client 35 | 36 | const identityNetwork = new HcsIdentityNetworkBuilder() 37 | .setNetwork("testnet") 38 | .setAppnetName("MyIdentityAppnet") 39 | .addAppnetDidServer("https://appnet-did-server-url:port/path-to-did-api") 40 | .setPublicKey(publicKey) 41 | .setMaxTransactionFee(new Hbar(2)) 42 | .setDidTopicMemo("MyIdentityAppnet DID topic") 43 | .setVCTopicMemo("MyIdentityAppnet VC topic") 44 | .execute(client); 45 | ``` 46 | 47 | ### DID Generation 48 | From already instantiated network: 49 | ``` 50 | const identityNetwork = ...; //HcsIdentityNetwork 51 | // From a given DID root key: 52 | const didRootKey = ...; //PrivateKey 53 | const hcsDid = identityNetwork.generateDid(didRootKey.publicKey, false); 54 | ``` 55 | or: 56 | ``` 57 | // Without having a DID root key - it will be generated automatically: 58 | // Here we decided to add DID topic ID parameter `tid` to the DID. 59 | const hcsDidWithDidRootKey = identityNetwork.generateDid(true); 60 | const didRootKeyPrivateKey = hcsDidWithDidRootKey.getPrivateDidRootKey().get(); 61 | ``` 62 | or by directly constructing HcsDid object: 63 | ``` 64 | const didRootKey = HcsDid.generateDidRootKey(); 65 | const addressBookFileId = FileId.fromString(""); 66 | 67 | const hcsDid = new HcsDid(HederaNetwork.TESTNET, didRootKey.publicKey, addressBookFileId); 68 | ``` 69 | Existing Hedera DID strings can be parsed into HcsDid object by calling fromString method: 70 | ``` 71 | const didString = "did:hedera:testnet:7c38oC4ytrYDGCqsaZ1AXt7ZPQ8etzfwaxoKjfJNzfoc;hedera:testnet:fid=0.0.1"; 72 | const did = HcsDid.fromString(didString); 73 | ``` 74 | 75 | ### Transaction 76 | ``` 77 | const client = ...; //Client 78 | const identityNetwork = ...; //HcsIdentityNetwork 79 | 80 | const didRootKey = ...; //PrivateKey 81 | const hcsDid = ...; //HcsDid 82 | 83 | const didDocument = hcsDid.generateDidDocument().toJson(); 84 | 85 | // Build and execute transaction 86 | await identityNetwork.createDidTransaction(DidMethodOperation.CREATE) 87 | // Provide DID document as JSON string 88 | .setDidDocument(didDocument) 89 | // Sign it with DID root key 90 | .signMessage(doc => didRootKey.sign(doc)) 91 | // Configure ConsensusMessageSubmitTransaction, build it and sign if required by DID topic 92 | .buildAndSignTransaction(tx => tx.setMaxTransactionFee(new Hbar(2))) 93 | // Define callback function when consensus was reached and DID document came back from mirror node 94 | .onMessageConfirmed(msg => { 95 | //DID document published! 96 | }) 97 | // Execute transaction 98 | .execute(client); 99 | ``` 100 | 101 | [did-method-spec]: https://github.com/hashgraph/did-method 102 | [did-core]: https://www.w3.org/TR/did-core/ 103 | [vc-data-model]: https://www.w3.org/TR/vc-data-model/ 104 | 105 | ## Development 106 | ``` 107 | git clone git@github.com:hashgraph/did-sdk-js.git 108 | ``` 109 | 110 | First you need install dependencies and build project 111 | ``` 112 | npm install 113 | ``` 114 | Run build in dev mode (with sourcemap generation and following changes) 115 | ``` 116 | npm run build:dev 117 | ``` 118 | 119 | ## Tests 120 | For run tests you need to create and fill ```test/variables.js``` file before. There is ```test/variables.js.sample``` file as example. 121 | 122 | Update the following environment variables with your `testnet` account details 123 | 124 | * OPERATOR_ID=0.0.xxxx 125 | * OPERATOR_KEY=302... 126 | 127 | You may also edit the following to use a different network (ensure your OPERATOR_ID and OPERATOR_KEY are valid) 128 | 129 | * NETWORK=testnet (can be `testnet`, `previewnet` or `mainnet`) 130 | * MIRROR_PROVIDER=hedera (can be `hedera` or `kabuto` (note `kabuto` not available on `previewnet`)) 131 | 132 | Run tests 133 | ``` 134 | npm run test 135 | ``` 136 | 137 | ## References 138 | - 139 | - 140 | - 141 | - 142 | - 143 | - 144 | 145 | ## License Information 146 | 147 | Licensed under _license placeholder_. 148 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hashgraph/did-sdk-js", 3 | "version": "0.1.1", 4 | "description": "Support for the Hedera Hashgraph DID Method and Verifiable Credentials on the Hedera JavaScript/TypeScript SDK", 5 | "main": "dist/index.js", 6 | "module": "dist/index.js", 7 | "files": [ 8 | "dist/**/*" 9 | ], 10 | "types": "dist/index.d.ts", 11 | "scripts": { 12 | "prepare": "npm run build", 13 | "prepublish": "npm run build", 14 | "build": "tsc", 15 | "build:dev": "tsc --sourceMap -w", 16 | "start": "node dist/index.js", 17 | "start:dev": "nodemon --inspect dist/index.js", 18 | "test": "mocha test/**/*.js" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/hashgraph/did-sdk-js.git" 23 | }, 24 | "author": "Hedera Hashgraph, LLC", 25 | "license": "Apache-2.0", 26 | "bugs": { 27 | "url": "https://github.com/hashgraph/did-sdk-js/issues" 28 | }, 29 | "homepage": "https://github.com/hashgraph/did-sdk-js#readme", 30 | "devDependencies": { 31 | "@types/node": "^16.7.7", 32 | "chai": "^4.3.4", 33 | "mocha": "^9.0.1", 34 | "nodemon": "^2.0.7", 35 | "typescript": "^4.3.2" 36 | }, 37 | "dependencies": { 38 | "@hashgraph/sdk": "^2.0.20", 39 | "bs58": "^4.0.1", 40 | "js-base64": "^3.6.1", 41 | "moment": "^2.29.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/identity/did-document-base.ts: -------------------------------------------------------------------------------- 1 | import {DidSyntax} from "./did-syntax"; 2 | import {DidDocumentJsonProperties} from "./did-document-json-properties"; 3 | import {HcsDidRootKey} from "./hcs/did/hcs-did-root-key"; 4 | 5 | export class DidDocumentBase { 6 | private id: string; 7 | private context: string; 8 | private didRootKey: HcsDidRootKey; 9 | 10 | constructor(did: string) { 11 | this.id = did; 12 | this.context = DidSyntax.DID_DOCUMENT_CONTEXT; 13 | } 14 | 15 | public static fromJson(json: string): DidDocumentBase { 16 | let result: DidDocumentBase; 17 | 18 | try { 19 | const root = JSON.parse(json); 20 | result = new DidDocumentBase(root.id); 21 | if (root.hasOwnProperty(DidDocumentJsonProperties.PUBLIC_KEY)) { 22 | if (!Array.isArray(root[DidDocumentJsonProperties.PUBLIC_KEY])) { 23 | throw new Error(`${root[DidDocumentJsonProperties.PUBLIC_KEY]} is not an array`); 24 | } 25 | for (let publicKeyObj of root[DidDocumentJsonProperties.PUBLIC_KEY]) { 26 | if (publicKeyObj.hasOwnProperty(DidDocumentJsonProperties.ID) && (publicKeyObj[DidDocumentJsonProperties.ID] === 27 | (result.getId() + HcsDidRootKey.DID_ROOT_KEY_NAME))) { 28 | const didRootKey = HcsDidRootKey.fromJsonTree(publicKeyObj); 29 | result.setDidRootKey(didRootKey); 30 | break; 31 | } 32 | } 33 | } 34 | } catch (e) { 35 | throw new Error('Given JSON string is not a valid DID document ' + e.message); 36 | } 37 | 38 | return result; 39 | } 40 | 41 | public getContext(): string { 42 | return this.context; 43 | } 44 | 45 | public getId(): string { 46 | return this.id; 47 | } 48 | 49 | public getDidRootKey(): HcsDidRootKey { 50 | return this.didRootKey; 51 | } 52 | 53 | public setDidRootKey(rootKey: HcsDidRootKey): void { 54 | this.didRootKey = rootKey 55 | } 56 | 57 | public toJsonTree(): any { 58 | const rootObject = {}; 59 | rootObject[DidDocumentJsonProperties.CONTEXT] = this.context; 60 | rootObject[DidDocumentJsonProperties.ID] = this.id; 61 | rootObject[DidDocumentJsonProperties.PUBLIC_KEY] = [ 62 | this.didRootKey.toJsonTree() 63 | ]; 64 | rootObject[DidDocumentJsonProperties.AUTHENTICATION] = [ 65 | this.didRootKey.getId() 66 | ]; 67 | return rootObject; 68 | } 69 | 70 | public toJSON(): string { 71 | return JSON.stringify(this.toJsonTree()); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/identity/did-document-json-properties.ts: -------------------------------------------------------------------------------- 1 | export module DidDocumentJsonProperties { 2 | export const CONTEXT = '@context'; 3 | export const ID = 'id'; 4 | export const AUTHENTICATION = 'authentication'; 5 | export const PUBLIC_KEY = 'publicKey'; 6 | export const SERVICE = 'service'; 7 | export const CREATED = 'created'; 8 | export const UPDATED = 'updated'; 9 | export const PROOF = 'proof'; 10 | } 11 | -------------------------------------------------------------------------------- /src/identity/did-method-operation.ts: -------------------------------------------------------------------------------- 1 | export enum DidMethodOperation { 2 | CREATE = 'create', 3 | UPDATE = 'update', 4 | DELETE = 'delete' 5 | } 6 | -------------------------------------------------------------------------------- /src/identity/did-parser.ts: -------------------------------------------------------------------------------- 1 | import {HederaDid} from "./hedera-did"; 2 | import {DidSyntax} from "./did-syntax"; 3 | import {HcsDid} from "./hcs/did/hcs-did"; 4 | 5 | /** 6 | * Parses the given DID string into it's corresponding Hedera DID object. 7 | * 8 | * @param didString DID string. 9 | * @return {@link HederaDid} instance. 10 | */ 11 | export class DidParser { 12 | public static parse(didString: string): HederaDid { 13 | const methodIndex = DidSyntax.DID_PREFIX.length + 1; 14 | if (!didString || didString.length <= methodIndex) { 15 | throw new Error('DID string cannot be null'); 16 | } 17 | 18 | if (didString.startsWith(HcsDid.DID_METHOD + DidSyntax.DID_METHOD_SEPARATOR, methodIndex)) { 19 | return HcsDid.fromString(didString); 20 | } else { 21 | throw new Error('DID string is invalid.'); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/identity/did-syntax.ts: -------------------------------------------------------------------------------- 1 | export module DidSyntax { 2 | export const DID_PREFIX = 'did'; 3 | export const DID_DOCUMENT_CONTEXT = 'https://www.w3.org/ns/did/v1'; 4 | export const DID_METHOD_SEPARATOR = ':'; 5 | export const DID_PARAMETER_SEPARATOR = ';'; 6 | export const DID_PARAMETER_VALUE_SEPARATOR = '='; 7 | 8 | export enum Method { 9 | HEDERA_HCS = 'hedera', 10 | } 11 | 12 | export module MethodSpecificParameter { 13 | export const ADDRESS_BOOK_FILE_ID = 'fid'; 14 | export const DID_TOPIC_ID = 'tid'; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/identity/hcs/address-book.ts: -------------------------------------------------------------------------------- 1 | import {FileId} from "@hashgraph/sdk"; 2 | 3 | /** 4 | * Appnet's address book for HCS identity network. 5 | */ 6 | export class AddressBook { 7 | private fileId: FileId; 8 | private appnetName: string; 9 | private didTopicId: string; 10 | private vcTopicId: string; 11 | private appnetDidServers: string[]; 12 | 13 | /** 14 | * Converts an address book JSON string into address book object. 15 | * 16 | * @param json Address book JSON file. 17 | * @param addressBookFileId FileId of this address book in Hedera File Service. 18 | * @return The {@link AddressBook}. 19 | */ 20 | public static fromJson(json: string, addressBookFileId: FileId | string): AddressBook { 21 | const result = new AddressBook(); 22 | const item = JSON.parse(json); 23 | result.appnetName = item.appnetName; 24 | result.didTopicId = item.didTopicId; 25 | result.vcTopicId = item.vcTopicId; 26 | result.appnetDidServers = item.appnetDidServers; 27 | 28 | if (typeof addressBookFileId === 'string') { 29 | result.setFileId(FileId.fromString(addressBookFileId)); 30 | } else if (addressBookFileId instanceof FileId) { 31 | result.setFileId(addressBookFileId); 32 | } 33 | 34 | return result; 35 | } 36 | 37 | /** 38 | * Creates a new {@link AddressBook} instance. Does not create the file on Hedera File Service!. 39 | * 40 | * @param appnetName Name of the appnet. 41 | * @param didTopicId TopicID of the DID topic. 42 | * @param vcTopicId Topic ID of the Verifiable Credentials topic. 43 | * @param appnetDidServers List of appnet API servers. 44 | * @return The {@link AddressBook}. 45 | */ 46 | public static create(appnetName: string, didTopicId: string, vcTopicId: string, appnetDidServers: string[]) { 47 | const result = new AddressBook(); 48 | result.appnetDidServers = appnetDidServers; 49 | result.didTopicId = didTopicId; 50 | result.vcTopicId = vcTopicId; 51 | result.appnetName = appnetName; 52 | 53 | return result; 54 | } 55 | 56 | /** 57 | * Converts this address book file into JSON string. 58 | * 59 | * @return The JSON representation of this address book. 60 | */ 61 | public toJSON(): string { 62 | return JSON.stringify({ 63 | appnetName: this.appnetName, 64 | didTopicId: this.didTopicId, 65 | vcTopicId: this.vcTopicId, 66 | appnetDidServers: this.appnetDidServers 67 | }); 68 | } 69 | 70 | public getAppnetName(): string { 71 | return this.appnetName; 72 | } 73 | 74 | public setAppnetName(appnetName: string): void { 75 | this.appnetName = appnetName; 76 | } 77 | 78 | public getDidTopicId(): string { 79 | return this.didTopicId; 80 | } 81 | 82 | public setDidTopicId(didTopicId: string): void { 83 | this.didTopicId = didTopicId; 84 | } 85 | 86 | public getVcTopicId(): string { 87 | return this.vcTopicId; 88 | } 89 | 90 | public setVcTopicId(vcTopicId: string): void { 91 | this.vcTopicId = vcTopicId; 92 | } 93 | 94 | public getAppnetDidServers(): string[] { 95 | return this.appnetDidServers; 96 | } 97 | 98 | public setAppnetDidServers(appnetDidServers: string[]): void { 99 | this.appnetDidServers = appnetDidServers; 100 | } 101 | 102 | public getFileId(): FileId { 103 | return this.fileId; 104 | } 105 | 106 | public setFileId(fileId: FileId): void { 107 | this.fileId = fileId; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/identity/hcs/did/hcs-did-message.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey, Timestamp, TopicId } from "@hashgraph/sdk"; 2 | import { Hashing } from "../../../utils/hashing"; 3 | import { TimestampUtils } from "../../../utils/timestamp-utils"; 4 | import { DidDocumentBase } from "../../did-document-base"; 5 | import { DidDocumentJsonProperties } from "../../did-document-json-properties"; 6 | import { DidMethodOperation } from "../../did-method-operation"; 7 | import { Decrypter, Encrypter, Message } from "../message"; 8 | import { MessageEnvelope } from "../message-envelope"; 9 | import { HcsDid } from "./hcs-did"; 10 | 11 | /** 12 | * The DID document message submitted to appnet's DID Topic. 13 | */ 14 | export class HcsDidMessage extends Message { 15 | protected operation: DidMethodOperation; 16 | protected did: string; 17 | protected didDocumentBase64: string; 18 | /** 19 | * The date when the DID was created and published. 20 | * It is equal to consensus timestamp of the first creation message. 21 | * This property is set by the listener and injected into the DID document upon calling getDidDocument() method. 22 | */ 23 | 24 | protected created: Timestamp; 25 | /** 26 | * The date when the DID was updated and published. 27 | * It is equal to consensus timestamp of the last valid update or delete message. 28 | * This property is set by the listener and injected into the DID document upon calling getDidDocument() method. 29 | */ 30 | protected updated: Timestamp; 31 | 32 | /** 33 | * Creates a new instance of {@link HcsDidMessage}. 34 | * 35 | * @param operation The operation on DID document. 36 | * @param did The DID string. 37 | * @param didDocumentBase64 The Base64-encoded DID document. 38 | */ 39 | constructor(operation: DidMethodOperation, did: string, didDocumentBase64: string) { 40 | super(); 41 | this.operation = operation; 42 | this.did = did; 43 | this.didDocumentBase64 = didDocumentBase64; 44 | } 45 | 46 | public getOperation(): DidMethodOperation { 47 | return this.operation; 48 | } 49 | 50 | public getDid(): string { 51 | return this.did; 52 | } 53 | 54 | public getDidDocumentBase64(): string { 55 | return this.didDocumentBase64; 56 | } 57 | 58 | public getCreated(): Timestamp { 59 | return this.created; 60 | } 61 | 62 | public getUpdated(): Timestamp { 63 | return this.updated; 64 | } 65 | 66 | public setUpdated(updated: Timestamp): void { 67 | this.updated = updated; 68 | } 69 | 70 | public setCreated(created: Timestamp): void { 71 | this.created = created; 72 | } 73 | 74 | /** 75 | * Decodes didDocumentBase64 field and returns its content. 76 | * In case this message is in encrypted mode, it will return encrypted content, 77 | * so getPlainDidDocument method should be used instead. 78 | * If message consensus timestamps for creation and update are provided they will be injected into the result 79 | * document upon decoding. 80 | * 81 | * @return The decoded DID document as JSON string. 82 | */ 83 | public getDidDocument(): string { 84 | if (this.didDocumentBase64 == null) { 85 | return null; 86 | } 87 | 88 | let document: string = Hashing.base64.decode(this.didDocumentBase64); 89 | 90 | // inject timestamps 91 | if (this.created != null || this.updated != null) { 92 | const root = JSON.parse(document); 93 | if (this.created != null) { 94 | root[DidDocumentJsonProperties.CREATED] = TimestampUtils.toJSON(this.created); 95 | } 96 | 97 | if (this.updated != null) { 98 | root[DidDocumentJsonProperties.UPDATED] = TimestampUtils.toJSON(this.updated); 99 | } 100 | document = JSON.stringify(root); 101 | } 102 | 103 | return document; 104 | } 105 | 106 | /** 107 | * Validates this DID message by checking its completeness, signature and DID document. 108 | * 109 | * @return True if the message is valid, false otherwise. 110 | */ 111 | public isValid(): boolean; 112 | /** 113 | * Validates this DID message by checking its completeness, signature and DID document. 114 | * 115 | * @param didTopicId The DID topic ID against which the message is validated. 116 | * @return True if the message is valid, false otherwise. 117 | */ 118 | public isValid(didTopicId: TopicId): boolean; 119 | public isValid(...args: any[]): boolean { 120 | const didTopicId: TopicId = args[0] || null; 121 | if (this.did == null || this.didDocumentBase64 == null) { 122 | return false; 123 | } 124 | 125 | try { 126 | const doc: DidDocumentBase = DidDocumentBase.fromJson(this.getDidDocument()); 127 | 128 | // Validate if DID and DID document are present and match 129 | if (this.did != doc.getId()) { 130 | return false; 131 | } 132 | 133 | // Validate if DID root key is present in the document 134 | if (doc.getDidRootKey() == null || doc.getDidRootKey().getPublicKeyBase58() == null) { 135 | return false; 136 | } 137 | 138 | // Verify that DID was derived from this DID root key 139 | const hcsDid: HcsDid = HcsDid.fromString(this.did); 140 | 141 | // Extract public key from the DID document 142 | const publicKeyBytes: Uint8Array = Hashing.base58.decode(doc.getDidRootKey().getPublicKeyBase58()); 143 | const publicKey: PublicKey = PublicKey.fromBytes(publicKeyBytes); 144 | 145 | if (HcsDid.publicKeyToIdString(publicKey) != hcsDid.getIdString()) { 146 | return false; 147 | } 148 | 149 | // Verify that the message was sent to the right topic, if the DID contains the topic 150 | if (!!didTopicId && !!hcsDid.getDidTopicId() && (didTopicId.toString() != hcsDid.getDidTopicId().toString())) { 151 | return false; 152 | } 153 | } catch (e) { 154 | return false; 155 | } 156 | 157 | return true; 158 | } 159 | 160 | /** 161 | * Extracts #did-root-key from the DID document. 162 | * 163 | * @return Public key of the DID subject. 164 | */ 165 | public extractDidRootKey(): PublicKey { 166 | let result: PublicKey = null; 167 | try { 168 | const doc: DidDocumentBase = DidDocumentBase.fromJson(this.getDidDocument()); 169 | // Make sure that DID root key is present in the document 170 | if (doc.getDidRootKey() != null && doc.getDidRootKey().getPublicKeyBase58() != null) { 171 | const publicKeyBytes: Uint8Array = Hashing.base58.decode(doc.getDidRootKey().getPublicKeyBase58()); 172 | result = PublicKey.fromBytes(publicKeyBytes); 173 | } 174 | // ArrayIndexOutOfBoundsException is thrown in case public key is invalid in PublicKey.fromBytes 175 | } catch (e) { 176 | return null; 177 | } 178 | 179 | return result; 180 | } 181 | 182 | public toJsonTree(): any { 183 | const result: any = super.toJsonTree(); 184 | result.operation = this.operation; 185 | result.did = this.did; 186 | result.didDocumentBase64 = this.didDocumentBase64; 187 | return result; 188 | } 189 | 190 | public static fromJsonTree(tree: any, result?: HcsDidMessage): HcsDidMessage { 191 | if (!result) { 192 | result = new HcsDidMessage(tree.operation, tree.did, tree.didDocumentBase64); 193 | } else { 194 | result.operation = tree.operation; 195 | result.did = tree.did; 196 | result.didDocumentBase64 = tree.didDocumentBase64; 197 | } 198 | result = super.fromJsonTree(tree, result) as HcsDidMessage; 199 | return result; 200 | } 201 | 202 | public toJSON(): string { 203 | return JSON.stringify(this.toJsonTree()); 204 | } 205 | 206 | public static fromJson(json: string): Message { 207 | return Message.fromJsonTree(JSON.parse(json)); 208 | } 209 | 210 | /** 211 | * Creates a new DID message for submission to HCS topic. 212 | * 213 | * @param didDocumentJson DID document as JSON string. 214 | * @param operation The operation on DID document. 215 | * @return The HCS message wrapped in an envelope for the given DID document and method operation. 216 | */ 217 | public static fromDidDocumentJson(didDocumentJson: string, operation: DidMethodOperation): MessageEnvelope { 218 | const didDocumentBase: DidDocumentBase = DidDocumentBase.fromJson(didDocumentJson); 219 | const didDocumentBase64 = Hashing.base64.encode(didDocumentJson); 220 | const message: HcsDidMessage = new HcsDidMessage(operation, didDocumentBase.getId(), didDocumentBase64); 221 | return new MessageEnvelope(message); 222 | } 223 | 224 | /** 225 | * Provides an encryption operator that converts an {@link HcsDidMessage} into encrypted one. 226 | * 227 | * @param encryptionFunction The encryption function to use for encryption of single attributes. 228 | * @return The encryption operator instance. 229 | */ 230 | public static getEncrypter(encryptionFunction: Encrypter): Encrypter { 231 | if (encryptionFunction == null) { 232 | throw "Encryption function is missing or null."; 233 | } 234 | return function (message: HcsDidMessage): HcsDidMessage { 235 | const operation: DidMethodOperation = message.getOperation(); 236 | // Encrypt the DID 237 | const encryptedDid: string = encryptionFunction(message.getDid()); 238 | const did: string = Hashing.base64.encode(encryptedDid); 239 | // Encrypt the DID document 240 | const encryptedDoc: string = encryptionFunction(message.getDidDocumentBase64()); 241 | const didDocumentBase64: string = Hashing.base64.encode(encryptedDoc); 242 | return new HcsDidMessage(operation, did, didDocumentBase64); 243 | }; 244 | } 245 | 246 | /** 247 | * Provides a decryption function that converts {@link HcsDidMessage} in encrypted for into a plain form. 248 | * 249 | * @param decryptionFunction The decryption function to use for decryption of single attributes. 250 | * @return The Decryption function for the {@link HcsDidMessage} 251 | */ 252 | public static getDecrypter(decryptionFunction: Decrypter): Decrypter { 253 | if (decryptionFunction == null) { 254 | throw "Decryption function is missing or null."; 255 | } 256 | return function (encryptedMsg: HcsDidMessage, consensusTimestamp: Timestamp): HcsDidMessage { 257 | const operation: DidMethodOperation = encryptedMsg.getOperation(); 258 | // Decrypt DID string 259 | let decryptedDid: string = encryptedMsg.getDid(); 260 | if (decryptedDid != null) { 261 | const did: string = Hashing.base64.decode(decryptedDid); 262 | decryptedDid = decryptionFunction(did, consensusTimestamp); 263 | } 264 | // Decrypt DID document 265 | let decryptedDocBase64 = encryptedMsg.getDidDocumentBase64(); 266 | if (decryptedDocBase64 != null) { 267 | const doc: string = Hashing.base64.decode(decryptedDocBase64); 268 | decryptedDocBase64 = decryptionFunction(doc, consensusTimestamp); 269 | } 270 | return new HcsDidMessage(operation, decryptedDid, decryptedDocBase64); 271 | }; 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/identity/hcs/did/hcs-did-resolver.ts: -------------------------------------------------------------------------------- 1 | import { TopicId } from "@hashgraph/sdk"; 2 | import { TimestampUtils } from "../../../utils/timestamp-utils"; 3 | import { DidMethodOperation } from "../../did-method-operation"; 4 | import { MessageEnvelope } from "../message-envelope"; 5 | import { MessageListener } from "../message-listener"; 6 | import { MessageResolver } from "../message-resolver"; 7 | import { HcsDidMessage } from "./hcs-did-message"; 8 | import { HcsDidTopicListener } from "./hcs-did-topic-listener"; 9 | 10 | /** 11 | * Resolves the DID from Hedera network. 12 | */ 13 | export class HcsDidResolver extends MessageResolver { 14 | /** 15 | * Instantiates a new DID resolver for the given DID topic. 16 | * 17 | * @param topicId The HCS DID topic ID. 18 | */ 19 | constructor(topicId: TopicId) { 20 | super(topicId); 21 | } 22 | 23 | /** 24 | * Adds a DID to resolve. 25 | * 26 | * @param did The DID string. 27 | * @return This resolver instance. 28 | */ 29 | public addDid(did: string): HcsDidResolver { 30 | if (did != null) { 31 | this.results.set(did, null); 32 | } 33 | return this; 34 | } 35 | 36 | /** 37 | * Adds multiple DIDs to resolve. 38 | * 39 | * @param dids The set of DID strings. 40 | * @return This resolver instance. 41 | */ 42 | public addDids(dids: string[]): HcsDidResolver { 43 | if (dids) { 44 | dids.forEach(d => this.addDid(d)); 45 | } 46 | return this; 47 | } 48 | 49 | protected override matchesSearchCriteria(message: HcsDidMessage): boolean { 50 | return this.results.has(message.getDid()); 51 | } 52 | 53 | protected override processMessage(envelope: MessageEnvelope): void { 54 | const message: HcsDidMessage = envelope.open(); 55 | 56 | // Also skip messages that are older than the once collected or if we already have a DELETE message 57 | const existing: MessageEnvelope = this.results.get(message.getDid()); 58 | 59 | const chackOperation = ( 60 | (existing != null) && 61 | ( 62 | (TimestampUtils.lessThan(envelope.getConsensusTimestamp(), existing.getConsensusTimestamp())) || 63 | ( 64 | DidMethodOperation.DELETE == (existing.open().getOperation()) && 65 | DidMethodOperation.DELETE != (message.getOperation()) 66 | ) 67 | ) 68 | ) 69 | if (chackOperation) { 70 | return; 71 | } 72 | 73 | // Preserve created and updated timestamps 74 | message.setUpdated(envelope.getConsensusTimestamp()); 75 | if (DidMethodOperation.CREATE == message.getOperation()) { 76 | message.setCreated(envelope.getConsensusTimestamp()); 77 | } else if (existing != null) { 78 | message.setCreated(existing.open().getCreated()); 79 | } 80 | 81 | // Add valid message to the results 82 | this.results.set(message.getDid(), envelope); 83 | } 84 | 85 | protected override supplyMessageListener(): MessageListener { 86 | return new HcsDidTopicListener(this.topicId); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/identity/hcs/did/hcs-did-root-key.ts: -------------------------------------------------------------------------------- 1 | import {PublicKey} from "@hashgraph/sdk"; 2 | import bs58 from "bs58"; 3 | import {HcsDid} from "./hcs-did"; 4 | 5 | /** 6 | * Represents a root key of HCS Identity DID. 7 | * That is a public key of type Ed25519VerificationKey2018 compatible with a single publicKey entry of a DID Document. 8 | */ 9 | export class HcsDidRootKey { 10 | public static DID_ROOT_KEY_NAME = '#did-root-key'; 11 | public static DID_ROOT_KEY_TYPE = 'Ed25519VerificationKey2018'; 12 | 13 | private id: string; 14 | private type: string; 15 | private controller: string; 16 | private publicKeyBase58: string; 17 | 18 | /** 19 | * Creates a {@link HcsDidRootKey} object from the given {@link HcsDid} DID and it's root public key. 20 | * 21 | * @param did The {@link HcsDid} DID object. 22 | * @param didRootKey The public key from which the DID was derived. 23 | * @return The {@link HcsDidRootKey} object. 24 | */ 25 | public static fromHcsIdentity(did: HcsDid, didRootKey: PublicKey): HcsDidRootKey { 26 | if (!did) { 27 | throw new Error('DID cannot be ' + did); 28 | } 29 | if (!didRootKey) { 30 | throw new Error('DID root key cannot be ' + didRootKey); 31 | } 32 | if (HcsDid.publicKeyToIdString(didRootKey) !== did.getIdString()) { 33 | throw new Error('The specified DID does not correspond to the given DID root key'); 34 | } 35 | const result = new HcsDidRootKey(); 36 | result.controller = did.toDid(); 37 | result.id = result.controller + HcsDidRootKey.DID_ROOT_KEY_NAME; 38 | result.publicKeyBase58 = bs58.encode(didRootKey.toBytes()); 39 | result.type = HcsDidRootKey.DID_ROOT_KEY_TYPE; 40 | 41 | return result; 42 | } 43 | 44 | public static fromId(id: string): HcsDidRootKey { 45 | if (id == null) { 46 | throw new Error("id cannot be null"); 47 | } 48 | const didString = id.replace(new RegExp(HcsDidRootKey.DID_ROOT_KEY_NAME + "$"), ""); 49 | if (didString == null) { 50 | throw new Error("DID cannot be null"); 51 | } 52 | const did = HcsDid.fromString(didString); 53 | 54 | const result = new HcsDidRootKey(); 55 | result.controller = did.toDid(); 56 | result.id = result.controller + this.DID_ROOT_KEY_NAME; 57 | result.publicKeyBase58 = null; 58 | result.type = this.DID_ROOT_KEY_TYPE; 59 | return result; 60 | } 61 | 62 | public getId(): string { 63 | return this.id; 64 | } 65 | 66 | public getType(): string { 67 | return this.type; 68 | } 69 | 70 | public getController(): string { 71 | return this.controller; 72 | } 73 | 74 | public getPublicKeyBase58(): string { 75 | return this.publicKeyBase58; 76 | } 77 | 78 | public toJsonTree(): any { 79 | const result: any = {}; 80 | result.id = this.id; 81 | result.type = this.type; 82 | result.controller = this.controller; 83 | result.publicKeyBase58 = this.publicKeyBase58; 84 | return result; 85 | } 86 | 87 | public toJSON(): string { 88 | return JSON.stringify(this.toJsonTree()); 89 | } 90 | 91 | public static fromJsonTree(json: any): HcsDidRootKey { 92 | const result = new HcsDidRootKey(); 93 | result.id = json.id; 94 | result.type = json.type; 95 | result.controller = json.controller; 96 | result.publicKeyBase58 = json.publicKeyBase58; 97 | return result; 98 | } 99 | 100 | public static fromJson(json: string): HcsDidRootKey { 101 | return HcsDidRootKey.fromJsonTree(JSON.parse(json)); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/identity/hcs/did/hcs-did-topic-listener.ts: -------------------------------------------------------------------------------- 1 | import { TopicId, TopicMessage } from "@hashgraph/sdk"; 2 | import { MessageEnvelope } from "../message-envelope"; 3 | import { MessageListener } from "../message-listener"; 4 | import { HcsDidMessage } from "./hcs-did-message"; 5 | 6 | /** 7 | * A listener of confirmed {@link HcsDidMessage} messages from a DID topic. 8 | * Messages are received from a given mirror node, parsed and validated. 9 | */ 10 | export class HcsDidTopicListener extends MessageListener { 11 | /** 12 | * Creates a new instance of a DID topic listener for the given consensus topic. 13 | * By default, invalid messages are ignored and errors are not. 14 | * 15 | * @param didTopicId The DID consensus topic ID. 16 | */ 17 | constructor(didTopicId: TopicId) { 18 | super(didTopicId); 19 | } 20 | 21 | protected override extractMessage(response: TopicMessage): MessageEnvelope { 22 | let result: MessageEnvelope = null; 23 | try { 24 | result = MessageEnvelope.fromMirrorResponse(response, HcsDidMessage); 25 | } catch (err) { 26 | this.handleError(err); 27 | } 28 | 29 | return result; 30 | } 31 | 32 | protected override isMessageValid(envelope: MessageEnvelope, response: TopicMessage): boolean { 33 | try { 34 | const msgDecrypter = !!this.decrypter ? HcsDidMessage.getDecrypter(this.decrypter) : null; 35 | 36 | const message: HcsDidMessage = envelope.open(msgDecrypter); 37 | if (!message) { 38 | this.reportInvalidMessage(response, "Empty message received when opening envelope"); 39 | return false; 40 | } 41 | 42 | const key = message.extractDidRootKey(); 43 | if (!envelope.isSignatureValid(key)) { 44 | this.reportInvalidMessage(response, "Signature validation failed"); 45 | return false; 46 | } 47 | 48 | if (!message.isValid(this.topicId)) { 49 | this.reportInvalidMessage(response, "Message content validation failed."); 50 | return false; 51 | } 52 | 53 | return true; 54 | } catch (err) { 55 | this.handleError(err); 56 | this.reportInvalidMessage(response, "Exception while validating message: " + err.message); 57 | return false; 58 | } 59 | 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/identity/hcs/did/hcs-did-transaction.ts: -------------------------------------------------------------------------------- 1 | import {MessageTransaction} from "../message-transaction"; 2 | import {HcsDidMessage} from "./hcs-did-message"; 3 | import {DidMethodOperation} from "../../did-method-operation"; 4 | import {PublicKey, TopicId} from "@hashgraph/sdk"; 5 | import {MessageEnvelope} from "../message-envelope"; 6 | import {Validator} from "../../../utils/validator"; 7 | import {MessageListener} from "../message-listener"; 8 | import {HcsDidTopicListener} from "./hcs-did-topic-listener"; 9 | import {Encrypter} from "../message"; 10 | 11 | /** 12 | * The DID document creation, update or deletion transaction. 13 | * Builds a correct {@link HcsDidMessage} and send it to HCS DID topic. 14 | */ 15 | export class HcsDidTransaction extends MessageTransaction { 16 | private operation: DidMethodOperation; 17 | private didDocument: string; 18 | 19 | /** 20 | * Instantiates a new transaction object from a message that was already prepared. 21 | * 22 | * @param topicId The HCS DID topic ID where message will be submitted. 23 | * @param message The message envelope. 24 | */ 25 | constructor(message: MessageEnvelope, topicId: TopicId); 26 | 27 | /** 28 | * Instantiates a new transaction object. 29 | * 30 | * @param operation The operation to be performed on a DID document. 31 | * @param topicId The HCS DID topic ID where message will be submitted. 32 | */ 33 | constructor(operation: DidMethodOperation, topicId: TopicId); 34 | constructor(...args) { 35 | if ( 36 | (args[0] instanceof MessageEnvelope) && 37 | (args[1] instanceof TopicId) && 38 | (args.length === 2) 39 | ) { 40 | const [message, topicId] = args; 41 | super(topicId, message); 42 | this.operation = null; 43 | } else if (args.length === 2) { 44 | const [operation, topicId] = args; 45 | super(topicId); 46 | this.operation = operation; 47 | } else { 48 | throw new Error('Invalid arguments') 49 | } 50 | } 51 | 52 | /** 53 | * Sets a DID document as JSON string that will be submitted to HCS. 54 | * 55 | * @param didDocument The didDocument to be published. 56 | * @return This transaction instance. 57 | */ 58 | public setDidDocument(didDocument: string): HcsDidTransaction { 59 | this.didDocument = didDocument; 60 | return this; 61 | } 62 | 63 | protected validate(validator: Validator): void { 64 | super.validate(validator); 65 | validator.require(!!this.didDocument || !!this.message, 'DID document is mandatory.'); 66 | validator.require(!!this.operation || !!this.message, 'DID method operation is not defined.'); 67 | } 68 | 69 | protected buildMessage(): MessageEnvelope { 70 | return HcsDidMessage.fromDidDocumentJson(this.didDocument, this.operation); 71 | } 72 | 73 | protected provideTopicListener(topicIdToListen: TopicId): MessageListener { 74 | return new HcsDidTopicListener(topicIdToListen); 75 | } 76 | 77 | protected provideMessageEncrypter(encryptionFunction: Encrypter): (input: HcsDidMessage) => HcsDidMessage { 78 | return HcsDidMessage.getEncrypter(encryptionFunction); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/identity/hcs/hcs-identity-network-builder.ts: -------------------------------------------------------------------------------- 1 | import {Client, FileCreateTransaction, Hbar, PublicKey, TopicCreateTransaction, TopicId} from "@hashgraph/sdk"; 2 | import {HcsIdentityNetwork} from "./hcs-identity-network"; 3 | import {AddressBook} from "./address-book"; 4 | 5 | export class HcsIdentityNetworkBuilder { 6 | private appnetName: string; 7 | private didTopicId: TopicId; 8 | private vcTopicId: TopicId; 9 | private network: string; 10 | private didServers: string[]; 11 | private publicKey: PublicKey; 12 | private maxTransactionFee: Hbar = new Hbar(2); 13 | private didTopicMemo: string = ''; 14 | private vcTopicMemo: string = ''; 15 | 16 | public async execute(client: Client): Promise { 17 | const didTopicCreateTransaction = new TopicCreateTransaction() 18 | .setMaxTransactionFee(this.maxTransactionFee) 19 | .setTopicMemo(this.didTopicMemo); 20 | 21 | if (this.publicKey) { 22 | didTopicCreateTransaction.setAdminKey(this.publicKey); 23 | } 24 | 25 | const didTxId = await didTopicCreateTransaction.execute(client); 26 | this.didTopicId = (await didTxId.getReceipt(client)).topicId; 27 | 28 | const vcTopicCreateTransaction = new TopicCreateTransaction() 29 | .setMaxTransactionFee(this.maxTransactionFee) 30 | .setTopicMemo(this.vcTopicMemo); 31 | 32 | if (this.publicKey) { 33 | vcTopicCreateTransaction.setAdminKey(this.publicKey); 34 | } 35 | 36 | const vcTxId = await vcTopicCreateTransaction.execute(client); 37 | this.vcTopicId = (await vcTxId.getReceipt(client)).topicId; 38 | 39 | const addressBook = AddressBook.create(this.appnetName, this.didTopicId.toString(), this.vcTopicId.toString(), this.didServers); 40 | 41 | const fileCreateTx = new FileCreateTransaction().setContents(addressBook.toJSON()); 42 | 43 | const response = await fileCreateTx.execute(client); 44 | const receipt = await response.getReceipt(client); 45 | const fileId = receipt.fileId; 46 | 47 | addressBook.setFileId(fileId); 48 | 49 | return HcsIdentityNetwork.fromAddressBook(this.network, addressBook); 50 | } 51 | 52 | public addAppnetDidServer(serverUrl: string): HcsIdentityNetworkBuilder { 53 | if (!this.didServers) { 54 | this.didServers = []; 55 | } 56 | 57 | if (this.didServers.indexOf(serverUrl) == -1) { 58 | this.didServers.push(serverUrl); 59 | } 60 | 61 | return this; 62 | } 63 | 64 | public setAppnetName(appnetName: string): HcsIdentityNetworkBuilder { 65 | this.appnetName = appnetName; 66 | return this; 67 | } 68 | 69 | public setDidTopicMemo(didTopicMemo: string): HcsIdentityNetworkBuilder { 70 | this.didTopicMemo = didTopicMemo; 71 | return this; 72 | } 73 | 74 | public setVCTopicMemo(vcTopicMemo: string): HcsIdentityNetworkBuilder { 75 | this.vcTopicMemo = vcTopicMemo; 76 | return this; 77 | } 78 | 79 | public setDidTopicId(didTopicId: TopicId): HcsIdentityNetworkBuilder { 80 | this.didTopicId = didTopicId; 81 | return this; 82 | } 83 | 84 | public setVCTopicId(vcTopicId: TopicId): HcsIdentityNetworkBuilder { 85 | this.vcTopicId = vcTopicId; 86 | return this; 87 | } 88 | 89 | public setMaxTransactionFee(maxTransactionFee: Hbar): HcsIdentityNetworkBuilder { 90 | this.maxTransactionFee = maxTransactionFee; 91 | return this; 92 | } 93 | 94 | public setPublicKey(publicKey: PublicKey): HcsIdentityNetworkBuilder { 95 | this.publicKey = publicKey; 96 | return this; 97 | } 98 | 99 | public setNetwork(network: string): HcsIdentityNetworkBuilder { 100 | this.network = network; 101 | return this; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/identity/hcs/hcs-identity-network.ts: -------------------------------------------------------------------------------- 1 | import {AddressBook} from "./address-book"; 2 | import {Client, FileContentsQuery, FileId, PrivateKey, PublicKey, TopicId} from "@hashgraph/sdk"; 3 | import {HcsDid} from "./did/hcs-did"; 4 | import {DidMethodOperation} from "../did-method-operation"; 5 | import {HcsDidTransaction} from "./did/hcs-did-transaction"; 6 | import {MessageEnvelope} from "./message-envelope"; 7 | import {HcsVcMessage} from "./vc/hcs-vc-message"; 8 | import {HcsDidMessage} from "./did/hcs-did-message"; 9 | import {HcsVcTransaction} from "./vc/hcs-vc-transaction"; 10 | import {HcsVcOperation} from "./vc/hcs-vc-operation"; 11 | import {HcsDidResolver} from "./did/hcs-did-resolver"; 12 | import {HcsDidTopicListener} from "./did/hcs-did-topic-listener"; 13 | import {HcsVcStatusResolver} from "./vc/hcs-vc-status-resolver"; 14 | import {HcsVcTopicListener} from "./vc/hcs-vc-topic-listener"; 15 | 16 | /** 17 | * Appnet's identity network based on Hedera HCS DID method specification. 18 | */ 19 | export class HcsIdentityNetwork { 20 | /** 21 | * The address book of appnet's identity network. 22 | */ 23 | private addressBook: AddressBook; 24 | 25 | /** 26 | * The Hedera network on which this identity network is created. 27 | */ 28 | private network: string; 29 | 30 | /** 31 | * Instantiates existing identity network from a provided address book. 32 | * 33 | * @param network The Hedera network. 34 | * @param addressBook The {@link AddressBook} of the identity network. 35 | * @return The identity network instance. 36 | */ 37 | public static fromAddressBook(network: string, addressBook: AddressBook): HcsIdentityNetwork { 38 | const result = new HcsIdentityNetwork(); 39 | result.network = network; 40 | result.addressBook = addressBook; 41 | 42 | return result; 43 | } 44 | 45 | /** 46 | * Instantiates existing identity network using an address book file read from Hedera File Service. 47 | * 48 | * @param client The Hedera network client. 49 | * @param network The Hedera network. 50 | * @param addressBookFileId The FileID of {@link AddressBook} file stored on Hedera File Service. 51 | * @return The identity network instance. 52 | */ 53 | public static async fromAddressBookFile(client: Client, network: string, addressBookFileId: FileId): Promise { 54 | const fileContentsQueryCost = (new FileContentsQuery()).setFileId(addressBookFileId).getCost(client); 55 | const fileQuery = (new FileContentsQuery()).setFileId(addressBookFileId); 56 | 57 | const contents = await fileQuery.execute(client); 58 | 59 | const result = new HcsIdentityNetwork(); 60 | result.network = network; 61 | result.addressBook = AddressBook.fromJson(contents.toString(), addressBookFileId); 62 | 63 | return result; 64 | } 65 | 66 | /** 67 | * Instantiates existing identity network using a DID generated for this network. 68 | * 69 | * @param client The Hedera network client. 70 | * @param hcsDid The Hedera HCS DID. 71 | * @return The identity network instance. 72 | */ 73 | public static async fromHcsDid(client: Client, hcsDid: HcsDid): Promise { 74 | const addressBookFileId = hcsDid.getAddressBookFileId(); 75 | return await HcsIdentityNetwork.fromAddressBookFile(client, hcsDid.getNetwork(), addressBookFileId); 76 | } 77 | 78 | /** 79 | * Instantiates a {@link HcsDidTransaction} to perform the specified operation on the DID document. 80 | * 81 | * @param operation The operation to be performed on a DID document. 82 | * @return The {@link HcsDidTransaction} instance. 83 | */ 84 | public createDidTransaction(operation: DidMethodOperation): HcsDidTransaction; 85 | 86 | /** 87 | * Instantiates a {@link HcsDidTransaction} to perform the specified operation on the DID document. 88 | * 89 | * @param message The DID topic message ready to for sending. 90 | * @return The {@link HcsDidTransaction} instance. 91 | */ 92 | public createDidTransaction(message: MessageEnvelope): HcsDidTransaction; 93 | 94 | public createDidTransaction(...args): HcsDidTransaction { 95 | if ( 96 | (args.length === 1) && 97 | (args[0] instanceof MessageEnvelope) 98 | ) { 99 | const [message] = args; 100 | return new HcsDidTransaction(message, this.getDidTopicId()); 101 | } else if ( 102 | (args.length === 1) 103 | // (args[0] instanceof DidMethodOperation) 104 | ) { 105 | const [operation] = args; 106 | return new HcsDidTransaction(operation, this.getDidTopicId()); 107 | } else { 108 | throw new Error('Invalid arguments'); 109 | } 110 | } 111 | 112 | /** 113 | * Instantiates a {@link HcsVcTransaction} to perform the specified operation on the VC document. 114 | * 115 | * @param operation The type of operation. 116 | * @param credentialHash Credential hash. 117 | * @param signerPublicKey Public key of the signer (issuer). 118 | * @return The transaction instance. 119 | */ 120 | public createVcTransaction(operation: HcsVcOperation, credentialHash: string, signerPublicKey: PublicKey): HcsVcTransaction; 121 | 122 | /** 123 | * Instantiates a {@link HcsVcTransaction} to perform the specified operation on the VC document status. 124 | * 125 | * @param message The VC topic message ready to for sending. 126 | * @param signerPublicKey Public key of the signer (usually issuer). 127 | * @return The {@link HcsVcTransaction} instance. 128 | */ 129 | public createVcTransaction(message: MessageEnvelope, signerPublicKey: PublicKey): HcsVcTransaction; 130 | 131 | public createVcTransaction(...args): HcsVcTransaction { 132 | if ( 133 | (args.length === 3) && 134 | // (args[0] instanceof HcsVcOperation) && 135 | (typeof args[1] === 'string') && 136 | (args[2] instanceof PublicKey) 137 | ) { 138 | const [operation, credentialHash, signerPublicKey] = args; 139 | return new HcsVcTransaction(this.getVcTopicId(), operation, credentialHash, signerPublicKey); 140 | } else if ( 141 | (args.length === 2) && 142 | (args[0] instanceof MessageEnvelope) && 143 | (args[1] instanceof PublicKey) 144 | ) { 145 | const [message, signerPublicKey] = args; 146 | return new HcsVcTransaction(this.getVcTopicId(), message, signerPublicKey); 147 | } else { 148 | throw new Error('Invalid arguments'); 149 | } 150 | } 151 | 152 | /** 153 | * Returns the Hedera network on which this identity network runs. 154 | * 155 | * @return The Hedera network. 156 | */ 157 | public getNetwork(): string { 158 | return this.network; 159 | } 160 | 161 | /** 162 | * Generates a new DID and it's root key. 163 | * 164 | * @param withTid Indicates if DID topic ID should be added to the DID as tid parameter. 165 | * @return Generated {@link HcsDid} with it's private DID root key. 166 | */ 167 | public generateDid(withTid: boolean): HcsDid; 168 | 169 | public generateDid(privateKey: PrivateKey, withTid: boolean): HcsDid; 170 | 171 | /** 172 | * Generates a new DID from the given public DID root key. 173 | * 174 | * @param publicKey A DID root key. 175 | * @param withTid Indicates if DID topic ID should be added to the DID as tid parameter. 176 | * @return A newly generated DID. 177 | */ 178 | public generateDid(publicKey: PublicKey, withTid: boolean): HcsDid; 179 | public generateDid(...args): HcsDid { 180 | if ( 181 | (args.length === 1) && 182 | (typeof args[0] === 'boolean') 183 | ) { 184 | const [withTid] = args; 185 | const privateKey = HcsDid.generateDidRootKey(); 186 | const tid = withTid ? this.getDidTopicId() : null; 187 | 188 | return new HcsDid(this.getNetwork(), privateKey, this.addressBook.getFileId(), tid); 189 | } else if ( 190 | (args.length === 2) && 191 | (args[0] instanceof PublicKey) && 192 | (typeof args[1] === 'boolean') 193 | ) { 194 | const [publicKey, withTid] = args; 195 | const tid = withTid ? this.getDidTopicId() : null; 196 | 197 | return new HcsDid(this.getNetwork(), publicKey, this.addressBook.getFileId(), tid); 198 | } else if ( 199 | (args.length === 2) && 200 | (args[0] instanceof PrivateKey) && 201 | (typeof args[1] === 'boolean') 202 | ) { 203 | const [privateKey, withTid] = args; 204 | const tid = withTid ? this.getDidTopicId() : null; 205 | 206 | return new HcsDid(this.getNetwork(), privateKey, this.addressBook.getFileId(), tid); 207 | } 208 | } 209 | 210 | /** 211 | * Returns a DID resolver for this network. 212 | * 213 | * @return The DID resolver for this network. 214 | */ 215 | public getDidResolver(): HcsDidResolver { 216 | return new HcsDidResolver(this.getDidTopicId()); 217 | } 218 | 219 | /** 220 | * Returns DID topic ID for this network. 221 | * 222 | * @return The DID topic ID. 223 | */ 224 | public getDidTopicId(): TopicId { 225 | return TopicId.fromString(this.addressBook.getDidTopicId()); 226 | } 227 | 228 | /** 229 | * Returns a DID topic listener for this network. 230 | * 231 | * @return The DID topic listener. 232 | */ 233 | public getDidTopicListener(): HcsDidTopicListener { 234 | return new HcsDidTopicListener(this.getDidTopicId()); 235 | } 236 | 237 | /** 238 | * Returns Verifiable Credentials topic ID for this network. 239 | * 240 | * @return The VC topic ID. 241 | */ 242 | public getVcTopicId(): TopicId { 243 | return TopicId.fromString(this.addressBook.getVcTopicId()); 244 | } 245 | 246 | /** 247 | * Returns the address book of this identity network. 248 | * 249 | * @return The address book of this identity network. 250 | */ 251 | public getAddressBook(): AddressBook { 252 | return this.addressBook; 253 | } 254 | 255 | /** 256 | * Returns a VC status resolver for this network. 257 | * 258 | * @return The VC status resolver for this network. 259 | */ 260 | public getVcStatusResolver(): HcsVcStatusResolver; 261 | 262 | /** 263 | * Returns a VC status resolver for this network. 264 | * Resolver will validate signatures of topic messages against public keys supplied 265 | * by the given provider. 266 | * 267 | * @param publicKeysProvider Provider of a public keys acceptable for a given VC hash. 268 | * @return The VC status resolver for this network. 269 | */ 270 | public getVcStatusResolver(publicKeysProvider: (t: string) => PublicKey[]): HcsVcStatusResolver; 271 | 272 | public getVcStatusResolver(...args): HcsVcStatusResolver { 273 | if (args.length === 0) { 274 | return new HcsVcStatusResolver(this.getVcTopicId()); 275 | } else if (args.length === 1) { 276 | const [publicKeysProvider] = args; 277 | return new HcsVcStatusResolver(this.getVcTopicId(), publicKeysProvider); 278 | } else { 279 | throw Error('Invalid arguments'); 280 | } 281 | } 282 | 283 | /** 284 | * Returns a VC topic listener for this network. 285 | * 286 | * @return The VC topic listener. 287 | */ 288 | public getVcTopicListener(): HcsVcTopicListener; 289 | 290 | /** 291 | * Returns a VC topic listener for this network. 292 | * This listener will validate signatures of topic messages against public keys supplied 293 | * by the given provider. 294 | * 295 | * @param publicKeysProvider Provider of a public keys acceptable for a given VC hash. 296 | * @return The VC topic listener. 297 | */ 298 | public getVcTopicListener(publicKeysProvider: (t: string) => PublicKey[]): HcsVcTopicListener; 299 | 300 | public getVcTopicListener(...args): HcsVcTopicListener { 301 | if (args.length === 0) { 302 | return new HcsVcTopicListener(this.getVcTopicId()); 303 | } else if (args.length === 1) { 304 | const [publicKeysProvider] = args; 305 | return new HcsVcTopicListener(this.getVcTopicId(), publicKeysProvider); 306 | } else { 307 | throw new Error('Invalid arguments'); 308 | } 309 | } 310 | 311 | } 312 | -------------------------------------------------------------------------------- /src/identity/hcs/json-class.ts: -------------------------------------------------------------------------------- 1 | export type JsonClass = { 2 | fromJsonTree(json: any, result?: U): U; 3 | } -------------------------------------------------------------------------------- /src/identity/hcs/message-envelope.ts: -------------------------------------------------------------------------------- 1 | import { Decrypter, Encrypter, Message } from "./message"; 2 | import Long from "long"; 3 | import { MessageMode } from "./message-mode"; 4 | import { SerializableMirrorConsensusResponse } from "./serializable-mirror-consensus-response"; 5 | import { PublicKey, Timestamp, TopicMessage } from "@hashgraph/sdk"; 6 | import { JsonClass } from "./json-class"; 7 | import { Base64 } from "js-base64"; 8 | import { ArraysUtils } from "../../utils/arrays-utils"; 9 | 10 | export type PublicKeyProvider = (evn: MessageEnvelope) => PublicKey; 11 | export type SignFunction = (message: Uint8Array) => Uint8Array; 12 | 13 | /** 14 | * The envelope for Hedera identity messages sent to HCS DID or VC topics. 15 | */ 16 | export class MessageEnvelope { 17 | private static MESSAGE_KEY = 'message'; 18 | private static SIGNATURE_KEY = 'signature'; 19 | 20 | private static serialVersionUID = Long.fromInt(1); 21 | 22 | protected mode: MessageMode; 23 | protected message: T; 24 | protected signature: string; 25 | 26 | protected get messageJson(): string { 27 | if (!this.message) { 28 | return null; 29 | } 30 | return this.message.toJSON(); 31 | }; 32 | protected decryptedMessage: T; 33 | protected mirrorResponse: SerializableMirrorConsensusResponse; 34 | 35 | /** 36 | * Creates a new message envelope for the given message. 37 | * 38 | * @param message The message. 39 | */ 40 | constructor(message: T); 41 | constructor(); 42 | constructor(...args: any[]) { 43 | if (args.length === 0) { 44 | // do nothing 45 | } 46 | else if (args.length === 1) { 47 | const [message] = args; 48 | 49 | this.message = message; 50 | this.mode = MessageMode.PLAIN; 51 | } else { 52 | throw new Error('Wrong arguments passed to constructor'); 53 | } 54 | } 55 | 56 | 57 | /** 58 | * Signs this message envelope with the given signing function. 59 | * 60 | * @param signer The signing function. 61 | * @return This envelope signed and serialized to JSON, ready for submission to HCS topic. 62 | */ 63 | public sign(signer: SignFunction): Uint8Array { 64 | if (!signer) { 65 | throw new Error('Signing function is not provided.'); 66 | } 67 | 68 | if (this.signature) { 69 | throw new Error('Message is already signed.'); 70 | } 71 | 72 | const msgBytes = ArraysUtils.fromString(this.message.toJSON()); 73 | const signatureBytes = signer(msgBytes); 74 | this.signature = Base64.fromUint8Array(signatureBytes); 75 | 76 | return ArraysUtils.fromString(this.toJSON()); 77 | } 78 | 79 | public toJsonTree(): any { 80 | const result: any = {}; 81 | result.mode = this.mode; 82 | if (this.message) { 83 | result[MessageEnvelope.MESSAGE_KEY] = this.message.toJsonTree(); 84 | } 85 | if (this.signature) { 86 | result[MessageEnvelope.SIGNATURE_KEY] = this.signature; 87 | } 88 | return result; 89 | } 90 | 91 | /** 92 | * Converts this message envelope into a JSON string. 93 | * 94 | * @return The JSON string representing this message envelope. 95 | */ 96 | public toJSON(): string { 97 | return JSON.stringify(this.toJsonTree()); 98 | } 99 | 100 | /** 101 | * Converts a message from a DID or VC topic response into object instance. 102 | * 103 | * @param Type of the message inside envelope. 104 | * @param response Topic message as a response from mirror node. 105 | * @param messageClass Class type of the message inside envelope. 106 | * @return The {@link MessageEnvelope}. 107 | */ 108 | public static fromMirrorResponse(response: TopicMessage, messageClass: JsonClass): MessageEnvelope { 109 | const msgJson = ArraysUtils.toString(response.contents); 110 | const result = MessageEnvelope.fromJson(msgJson, messageClass); 111 | result.mirrorResponse = new SerializableMirrorConsensusResponse(response); 112 | 113 | return result; 114 | } 115 | 116 | 117 | /** 118 | * Converts a VC topic message from a JSON string into object instance. 119 | * 120 | * @param Type of the message inside envelope. 121 | * @param json VC topic message as JSON string. 122 | * @param messageClass Class of the message inside envelope. 123 | * @return The {@link MessageEnvelope}. 124 | */ 125 | public static fromJson(json: string, messageClass: JsonClass): MessageEnvelope { 126 | const result = new MessageEnvelope(); 127 | const root = JSON.parse(json) 128 | result.mode = root.mode; 129 | result.signature = root[MessageEnvelope.SIGNATURE_KEY]; 130 | if (root.hasOwnProperty(MessageEnvelope.MESSAGE_KEY)) { 131 | result.message = messageClass.fromJsonTree(root[MessageEnvelope.MESSAGE_KEY]); 132 | } else { 133 | result.message = null; 134 | } 135 | return result; 136 | } 137 | 138 | 139 | /** 140 | * Encrypts the message in this envelope and returns its encrypted instance. 141 | * 142 | * @param encrypter The function used to encrypt the message. 143 | * @return This envelope instance. 144 | */ 145 | public encrypt(encrypter: Encrypter): MessageEnvelope { 146 | if (!encrypter) { 147 | throw new Error('The encryption function is not provided.'); 148 | } 149 | 150 | this.decryptedMessage = this.message; 151 | this.message = encrypter(this.message); 152 | this.mode = MessageMode.ENCRYPTED; 153 | 154 | return this; 155 | } 156 | 157 | /** 158 | * Verifies the signature of the envelope against the public key of it's signer. 159 | * 160 | * @param publicKeyProvider Provider of a public key of this envelope signer. 161 | * @return True if the message is valid, false otherwise. 162 | */ 163 | public isSignatureValid(publicKeyProvider: PublicKeyProvider): boolean; 164 | public isSignatureValid(publicKey: PublicKey): boolean; 165 | public isSignatureValid(...args: any[]): boolean { 166 | if (!this.signature || !this.message) { 167 | return false; 168 | } 169 | 170 | let publicKey: PublicKey; 171 | if (typeof args[0] == "function") { 172 | const publicKeyProvider = args[0] as PublicKeyProvider; 173 | publicKey = publicKeyProvider(this); 174 | } else { 175 | publicKey = args[0] as PublicKey; 176 | } 177 | 178 | if (!publicKey) { 179 | return false; 180 | } 181 | 182 | const signatureToVerify = Base64.toUint8Array(this.signature); 183 | const messageBytes = ArraysUtils.fromString(this.message.toJSON()); 184 | 185 | return publicKey.verify(messageBytes, signatureToVerify); 186 | } 187 | 188 | 189 | /** 190 | * Opens a message in this envelope. 191 | * If the message is encrypted, the given decrypter will be used first to decrypt it. 192 | * If the message is not encrypted, it will be immediately returned. 193 | * 194 | * @param decrypter The function used to decrypt the message. 195 | * @return The message object in a plain mode. 196 | */ 197 | public open(decrypter: Decrypter = null): T { 198 | if (this.decryptedMessage != null) { 199 | return this.decryptedMessage; 200 | } 201 | 202 | if (MessageMode.ENCRYPTED !== this.mode) { 203 | this.decryptedMessage = this.message; 204 | } else if (!decrypter) { 205 | throw new Error("The message is encrypted, provide decryption function."); 206 | } else if (!this.decryptedMessage) { 207 | this.decryptedMessage = decrypter(this.message, this.getConsensusTimestamp()); 208 | } 209 | 210 | return this.decryptedMessage; 211 | } 212 | 213 | public getSignature(): string { 214 | return this.signature; 215 | } 216 | 217 | public getConsensusTimestamp(): Timestamp { 218 | return (!this.mirrorResponse) ? null : this.mirrorResponse.consensusTimestamp; 219 | } 220 | 221 | public getMode(): MessageMode { 222 | return this.mode; 223 | } 224 | 225 | public getMirrorResponse(): SerializableMirrorConsensusResponse { 226 | return this.mirrorResponse; 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/identity/hcs/message-listener.ts: -------------------------------------------------------------------------------- 1 | import { Decrypter, Message } from "./message"; 2 | import { Client, Timestamp, TopicId, TopicMessage, TopicMessageQuery } from "@hashgraph/sdk"; 3 | import SubscriptionHandle from "@hashgraph/sdk/lib/topic/SubscriptionHandle"; 4 | import { MessageEnvelope } from "./message-envelope"; 5 | import { MessageMode } from "./message-mode"; 6 | import Long from "long"; 7 | 8 | /** 9 | * A listener of confirmed messages from a HCS identity topic. 10 | * Messages are received from a given mirror node, parsed and validated. 11 | */ 12 | export abstract class MessageListener { 13 | protected topicId: TopicId; 14 | protected query: TopicMessageQuery; 15 | protected errorHandler: (input: Error) => void; 16 | protected ignoreErrors: boolean; 17 | protected decrypter: Decrypter; 18 | protected subscriptionHandle: SubscriptionHandle; 19 | protected filters: ((input: TopicMessage) => boolean)[]; 20 | protected invalidMessageHandler: (t: TopicMessage, u: string) => void; 21 | 22 | /** 23 | * Creates a new instance of a topic listener for the given consensus topic. 24 | * By default, invalid messages are ignored and errors are not. 25 | * 26 | * @param topicId The consensus topic ID. 27 | */ 28 | constructor(topicId: TopicId) { 29 | this.topicId = topicId; 30 | this.query = new TopicMessageQuery().setTopicId(topicId); 31 | this.ignoreErrors = false; 32 | } 33 | 34 | /** 35 | * Extracts and parses the message inside the response object into the given type. 36 | * 37 | * @param response Response message coming from the mirror node for for this listener's topic. 38 | * @return The message inside an envelope. 39 | */ 40 | protected abstract extractMessage(response: TopicMessage): MessageEnvelope; 41 | 42 | /** 43 | * Validates the message and its envelope signature. 44 | * 45 | * @param message The message inside an envelope. 46 | * @param response Response message coming from the mirror node for for this listener's topic. 47 | * @return True if the message is valid, False otherwise. 48 | */ 49 | protected abstract isMessageValid(message: MessageEnvelope, response: TopicMessage): boolean; 50 | 51 | /** 52 | * Adds a custom filter for topic responses from a mirror node. 53 | * Messages that do not pass the test are skipped before any other checks are run. 54 | * 55 | * @param filter The filter function. 56 | * @return This listener instance. 57 | */ 58 | public addFilter(filter: (input: TopicMessage) => boolean): MessageListener { 59 | if (!this.filters) { 60 | this.filters = []; 61 | } 62 | this.filters.push(filter); 63 | 64 | return this; 65 | } 66 | 67 | /** 68 | * Subscribes to mirror node topic messages stream. 69 | * 70 | * @param client Mirror client instance. 71 | * @param receiver Receiver of parsed messages. 72 | * @return This listener instance. 73 | */ 74 | public subscribe(client: Client, receiver: (input: MessageEnvelope) => void): MessageListener { 75 | const errorHandler = (message: TopicMessage, error: Error) => { 76 | this.handleError(error); 77 | }; 78 | const listener = (message: TopicMessage) => { 79 | this.handleResponse(message, receiver); 80 | }; 81 | 82 | this.subscriptionHandle = this.query.subscribe(client, errorHandler, listener); 83 | 84 | return this; 85 | } 86 | 87 | /** 88 | * Stops receiving messages from the topic. 89 | */ 90 | public unsubscribe(): void { 91 | if (this.subscriptionHandle) { 92 | this.subscriptionHandle.unsubscribe(); 93 | } 94 | } 95 | 96 | /** 97 | * Handles incoming messages from the topic on a mirror node. 98 | * 99 | * @param response Response message coming from the mirror node for the topic. 100 | * @param receiver Consumer of the result message. 101 | */ 102 | protected handleResponse(response: TopicMessage, receiver: (input: MessageEnvelope) => void) { 103 | if (this.filters) { 104 | for (let filter of this.filters) { 105 | if (!filter(response)) { 106 | this.reportInvalidMessage(response, "Message was rejected by external filter"); 107 | return; 108 | } 109 | } 110 | } 111 | 112 | const envelope = this.extractMessage(response); 113 | 114 | if (!envelope) { 115 | this.reportInvalidMessage(response, "Extracting envelope from the mirror response failed"); 116 | return; 117 | } 118 | 119 | if ((MessageMode.ENCRYPTED === envelope.getMode()) && !this.decrypter) { 120 | this.reportInvalidMessage(response, "Message is encrypted and no decryption function was provided"); 121 | return; 122 | } 123 | 124 | if (this.isMessageValid(envelope, response)) { 125 | receiver(envelope); 126 | } 127 | } 128 | 129 | /** 130 | * Handles the given error internally. 131 | * If external error handler is defined, passes the error there, otherwise raises RuntimeException or ignores it 132 | * depending on a ignoreErrors flag. 133 | * 134 | * @param err The error. 135 | * @throws RuntimeException Runtime exception with the given error in case external error handler is not defined 136 | * and errors were not requested to be ignored. 137 | */ 138 | protected handleError(err: Error): void { 139 | if (this.errorHandler) { 140 | this.errorHandler(err); 141 | } else if (!this.ignoreErrors) { 142 | throw new Error(err.message); 143 | } 144 | } 145 | 146 | /** 147 | * Reports invalid message to the handler. 148 | * 149 | * @param response The mirror response. 150 | * @param reason The reason why message validation failed. 151 | */ 152 | protected reportInvalidMessage(response: TopicMessage, reason: string): void { 153 | if (this.invalidMessageHandler) { 154 | this.invalidMessageHandler(response, reason); 155 | } 156 | } 157 | 158 | /** 159 | * Defines a handler for errors when they happen during execution. 160 | * 161 | * @param handler The error handler. 162 | * @return This transaction instance. 163 | */ 164 | public onError(handler: (input: Error) => void): MessageListener { 165 | this.errorHandler = handler; 166 | return this; 167 | } 168 | 169 | /** 170 | * Defines a handler for invalid messages received from the topic. 171 | * The first parameter of the handler is the mirror response. 172 | * The second parameter is the reason why the message failed validation (if available). 173 | * 174 | * @param handler The invalid message handler. 175 | * @return This transaction instance. 176 | */ 177 | public onInvalidMessageReceived(handler: (t: TopicMessage, u: string) => void): MessageListener { 178 | this.invalidMessageHandler = handler; 179 | return this; 180 | } 181 | 182 | /** 183 | * Defines decryption function that decrypts submitted message attributes after consensus is reached. 184 | * Decryption function must accept a byte array of encrypted message and an Timestamp that is its consensus timestamp, 185 | * If decrypter is not specified, encrypted messages will be ignored. 186 | * 187 | * @param decrypter The decryption function to use. 188 | * @return This transaction instance. 189 | */ 190 | public onDecrypt(decrypter: Decrypter): MessageListener { 191 | this.decrypter = decrypter; 192 | return this; 193 | } 194 | 195 | public setStartTime(startTime: Timestamp): MessageListener { 196 | this.query.setStartTime(startTime); 197 | return this; 198 | } 199 | 200 | public setEndTime(endTime: Timestamp): MessageListener { 201 | this.query.setEndTime(endTime); 202 | return this; 203 | } 204 | 205 | public setLimit(messagesLimit: Long): MessageListener { 206 | this.query.setLimit(messagesLimit); 207 | return this; 208 | } 209 | 210 | public setIgnoreErrors(ignoreErrors: boolean): MessageListener { 211 | this.ignoreErrors = ignoreErrors; 212 | return this; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/identity/hcs/message-mode.ts: -------------------------------------------------------------------------------- 1 | export enum MessageMode { 2 | PLAIN = 'plain', 3 | ENCRYPTED = 'encrypted' 4 | } 5 | -------------------------------------------------------------------------------- /src/identity/hcs/message-resolver.ts: -------------------------------------------------------------------------------- 1 | import { Decrypter, Message } from "./message"; 2 | import Long from "long"; 3 | import { Client, Timestamp, TopicId } from "@hashgraph/sdk"; 4 | import { MessageEnvelope } from "./message-envelope"; 5 | import { MessageListener } from "./message-listener"; 6 | import { Validator } from "../../utils/validator"; 7 | import { TimestampUtils } from "../../utils/timestamp-utils"; 8 | import { Sleep } from "../../utils/sleep"; 9 | 10 | export abstract class MessageResolver { 11 | /** 12 | * Default time to wait before finishing resolution and after the last message was received. 13 | */ 14 | public static DEFAULT_TIMEOUT: Long = Long.fromInt(30000); 15 | 16 | protected topicId: TopicId; 17 | protected results: Map>; 18 | 19 | private lastMessageArrivalTime: Long; 20 | private resultsHandler: (input: Map>) => void; 21 | private errorHandler: (input: Error) => void; 22 | private decrypter: Decrypter; 23 | private existingSignatures: string[]; 24 | private listener: MessageListener 25 | private noMoreMessagesTimeout: Long; 26 | 27 | /** 28 | * Instantiates a message resolver. 29 | * 30 | * @param topicId Consensus topic ID. 31 | */ 32 | constructor(topicId: TopicId) { 33 | this.topicId = topicId; 34 | this.results = new Map(); 35 | this.noMoreMessagesTimeout = MessageResolver.DEFAULT_TIMEOUT; 36 | this.lastMessageArrivalTime = Long.fromInt(Date.now()); 37 | } 38 | 39 | /** 40 | * Checks if the message matches preliminary search criteria. 41 | * 42 | * @param message The message read from the topic. 43 | * @return True if the message matches search criteria, false otherwise. 44 | */ 45 | protected abstract matchesSearchCriteria(message: T): boolean; 46 | 47 | /** 48 | * Applies custom filters on the message and if successfully verified, adds it to the results map. 49 | * 50 | * @param envelope Message inside an envelope in PLAIN mode. 51 | */ 52 | protected abstract processMessage(envelope: MessageEnvelope): void; 53 | 54 | /** 55 | * Supplies message listener for messages of specified type. 56 | * 57 | * @return The {@link MessageListener} instance. 58 | */ 59 | protected abstract supplyMessageListener(): MessageListener; 60 | 61 | /** 62 | * Resolves queries defined in implementing classes against a mirror node. 63 | * 64 | * @param client The mirror node client. 65 | */ 66 | public execute(client: Client): void { 67 | new Validator().checkValidationErrors('Resolver not executed: ', v => { 68 | return this.validate(v); 69 | }); 70 | 71 | this.existingSignatures = []; 72 | 73 | this.listener = this.supplyMessageListener(); 74 | 75 | this.listener.setStartTime(new Timestamp(0, 0)) 76 | .setEndTime(Timestamp.fromDate(new Date())) 77 | .setIgnoreErrors(false) 78 | .onError(this.errorHandler) 79 | .onDecrypt(this.decrypter) 80 | .subscribe(client, msg => { 81 | return this.handleMessage(msg); 82 | }); 83 | 84 | this.lastMessageArrivalTime = Long.fromInt(Date.now()); 85 | this.waitOrFinish(); 86 | } 87 | 88 | /** 89 | * Handles incoming DID messages from DID Topic on a mirror node. 90 | * 91 | * @param envelope The parsed message envelope in a PLAIN mode. 92 | */ 93 | private handleMessage(envelope: MessageEnvelope): void { 94 | this.lastMessageArrivalTime = Long.fromInt(Date.now()); 95 | 96 | if (!this.matchesSearchCriteria(envelope.open())) { 97 | return; 98 | } 99 | 100 | if (this.existingSignatures.indexOf(envelope.getSignature()) != -1) { 101 | return; 102 | } 103 | 104 | this.existingSignatures.push(envelope.getSignature()); 105 | this.processMessage(envelope); 106 | } 107 | 108 | /** 109 | * Waits for a new message from the topic for the configured amount of time. 110 | */ 111 | protected async waitOrFinish(): Promise { 112 | const timeDiff = Long.fromInt(Date.now()).sub(this.lastMessageArrivalTime); 113 | 114 | if (timeDiff.lt(this.noMoreMessagesTimeout)) { 115 | await Sleep(this.noMoreMessagesTimeout.sub(timeDiff).toNumber()); 116 | await this.waitOrFinish(); 117 | return; 118 | } 119 | 120 | this.resultsHandler(this.results); 121 | 122 | if (this.listener) { 123 | this.listener.unsubscribe(); 124 | } 125 | } 126 | 127 | /** 128 | * Defines a handler for resolution results. 129 | * This will be called when the resolution process is finished. 130 | * 131 | * @param handler The results handler. 132 | * @return This resolver instance. 133 | */ 134 | public whenFinished(handler: (input: Map>) => void): MessageResolver { 135 | this.resultsHandler = handler; 136 | return this; 137 | } 138 | 139 | /** 140 | * Defines a handler for errors when they happen during resolution. 141 | * 142 | * @param handler The error handler. 143 | * @return This resolver instance. 144 | */ 145 | public onError(handler: (input: Error) => void): MessageResolver { 146 | this.errorHandler = handler; 147 | return this; 148 | } 149 | 150 | /** 151 | * Defines a maximum time in milliseconds to wait for new messages from the topic. 152 | * Default is 30 seconds. 153 | * 154 | * @param timeout The timeout in milliseconds to wait for new messages from the topic. 155 | * @return This resolver instance. 156 | */ 157 | public setTimeout(timeout: Long | number): MessageResolver { 158 | this.noMoreMessagesTimeout = Long.fromValue(timeout); 159 | return this; 160 | } 161 | 162 | /** 163 | * Defines decryption function that decrypts submitted the message after consensus was reached. 164 | * Decryption function must accept a byte array of encrypted message and an Instant that is its consensus timestamp, 165 | * If decrypter is not specified, encrypted messages will be ignored. 166 | * 167 | * @param decrypter The decrypter to use. 168 | * @return This resolver instance. 169 | */ 170 | public onDecrypt(decrypter: Decrypter): MessageResolver { 171 | this.decrypter = decrypter; 172 | return this; 173 | } 174 | 175 | /** 176 | * Runs validation logic of the resolver's configuration. 177 | * 178 | * @param validator The errors validator. 179 | */ 180 | protected validate(validator: Validator): void { 181 | validator.require(this.results.size > 0, 'Nothing to resolve.'); 182 | validator.require(!!this.topicId, 'Consensus topic ID not defined.'); 183 | validator.require(!!this.resultsHandler, 'Results handler \'whenFinished\' not defined.'); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/identity/hcs/message-transaction.ts: -------------------------------------------------------------------------------- 1 | import { Decrypter, Encrypter, Message, Signer } from "./message"; 2 | import { Client, Timestamp, TopicId, TopicMessageSubmitTransaction, Transaction, TransactionId } from "@hashgraph/sdk"; 3 | import { MessageEnvelope } from "./message-envelope"; 4 | import { MessageListener } from "./message-listener"; 5 | import { Validator } from "../../utils/validator"; 6 | import moment from "moment"; 7 | import { ArraysUtils } from "../../utils/arrays-utils"; 8 | 9 | export abstract class MessageTransaction { 10 | private static SUBTRACT_TIME = 1; // seconds 11 | 12 | protected topicId: TopicId; 13 | protected message: MessageEnvelope; 14 | 15 | private encrypter: Encrypter; 16 | private decrypter: Decrypter; 17 | private buildTransactionFunction: (input: TopicMessageSubmitTransaction) => Transaction; 18 | private receiver: (input: MessageEnvelope) => void; 19 | private errorHandler: (input: Error) => void; 20 | private executed: boolean; 21 | private signer: Signer; 22 | private listener: MessageListener; 23 | 24 | /** 25 | * Creates a new instance of a message transaction. 26 | * 27 | * @param topicId Consensus topic ID to which message will be submitted. 28 | */ 29 | constructor(topicId: TopicId); 30 | /** 31 | * Creates a new instance of a message transaction with already prepared message. 32 | * 33 | * @param topicId Consensus topic ID to which message will be submitted. 34 | * @param message The message signed and ready to be sent. 35 | */ 36 | constructor(topicId: TopicId, message: MessageEnvelope); 37 | constructor(...args) { 38 | if (args.length === 1) { 39 | const [topicId] = args; 40 | this.topicId = topicId; 41 | this.executed = false; 42 | } else if (args.length === 2) { 43 | const [topicId, message] = args; 44 | this.topicId = topicId; 45 | this.message = message; 46 | this.executed = false; 47 | } else { 48 | throw new Error('Invalid arguments'); 49 | } 50 | } 51 | 52 | /** 53 | * Method that constructs a message envelope with a message of type T. 54 | * 55 | * @return The message envelope with a message inside ready to sign. 56 | */ 57 | protected abstract buildMessage(): MessageEnvelope; 58 | 59 | /** 60 | * Provides an instance of a message encrypter. 61 | * 62 | * @param encryptionFunction Encryption function used to encrypt single message property. 63 | * @return The message encrypter instance. 64 | */ 65 | protected abstract provideMessageEncrypter(encryptionFunction: Encrypter): (input: T) => T; 66 | 67 | /** 68 | * Provides a {@link MessageListener} instance specific to the submitted message type. 69 | * 70 | * @param topicIdToListen ID of the HCS topic. 71 | * @return The topic listener for this message on a mirror node. 72 | */ 73 | protected abstract provideTopicListener(topicIdToListen: TopicId): MessageListener; 74 | 75 | /** 76 | * Handles the error. 77 | * If external error handler is defined, passes the error there, otherwise raises RuntimeException. 78 | * 79 | * @param err The error. 80 | * @throws RuntimeException Runtime exception with the given error in case external error handler is not defined. 81 | */ 82 | protected handleError(err: Error): void { 83 | if (this.errorHandler) { 84 | this.errorHandler(err); 85 | } else { 86 | throw new Error(err.message); 87 | } 88 | } 89 | 90 | /** 91 | * Defines encryption function that encrypts the message attributes before submission. 92 | * 93 | * @param encrypter The encrypter to use. 94 | * @return This transaction instance. 95 | */ 96 | public onEncrypt(encrypter: Encrypter): MessageTransaction { 97 | this.encrypter = encrypter; 98 | return this; 99 | } 100 | 101 | /** 102 | * Handles event from a mirror node when a message was consensus was reached and message received. 103 | * 104 | * @param receiver The receiver handling incoming message. 105 | * @return This transaction instance. 106 | */ 107 | public onMessageConfirmed(receiver: (input: MessageEnvelope) => void): MessageTransaction { 108 | this.receiver = receiver; 109 | return this; 110 | } 111 | 112 | /** 113 | * Defines a handler for errors when they happen during execution. 114 | * 115 | * @param handler The error handler. 116 | * @return This transaction instance. 117 | */ 118 | public onError(handler: (input: Error) => void): MessageTransaction { 119 | this.errorHandler = handler; 120 | return this; 121 | } 122 | 123 | /** 124 | * Defines decryption function that decrypts message attributes after consensus is reached. 125 | * Decryption function must accept a byte array of encrypted message and an Timestamp that is its consensus timestamp, 126 | * 127 | * @param decrypter The decrypter to use. 128 | * @return This transaction instance. 129 | */ 130 | public onDecrypt(decrypter: Decrypter): MessageTransaction { 131 | this.decrypter = decrypter; 132 | return this; 133 | } 134 | 135 | /** 136 | * Defines a function that signs the message. 137 | * 138 | * @param signer The signing function to set. 139 | * @return This transaction instance. 140 | */ 141 | public signMessage(signer: Signer): MessageTransaction { 142 | this.signer = signer; 143 | return this; 144 | } 145 | 146 | /** 147 | * Sets {@link TopicMessageSubmitTransaction} parameters, builds and signs it without executing it. 148 | * Topic ID and transaction message content are already set in the incoming transaction. 149 | * 150 | * @param builderFunction The transaction builder function. 151 | * @return This transaction instance. 152 | */ 153 | public buildAndSignTransaction(builderFunction: (input: TopicMessageSubmitTransaction) => Transaction): MessageTransaction { 154 | this.buildTransactionFunction = builderFunction; 155 | return this; 156 | } 157 | 158 | /** 159 | * Builds the message and submits it to appnet's topic. 160 | * 161 | * @param client The hedera network client. 162 | * @return Transaction ID. 163 | */ 164 | public async execute(client: Client): Promise { 165 | new Validator().checkValidationErrors('MessageTransaction execution failed: ', v => { 166 | return this.validate(v); 167 | }); 168 | 169 | const envelope = !this.message ? this.buildMessage() : this.message; 170 | 171 | if (this.encrypter) { 172 | envelope.encrypt(this.provideMessageEncrypter(this.encrypter)); 173 | } 174 | 175 | const messageContent = !envelope.getSignature() ? envelope.sign(this.signer) : ArraysUtils.fromString(envelope.toJSON()); 176 | 177 | if (this.receiver) { 178 | this.listener = this.provideTopicListener(this.topicId); 179 | this.listener.setStartTime(Timestamp.fromDate(moment().subtract(MessageTransaction.SUBTRACT_TIME, 'seconds').toDate())) 180 | .setIgnoreErrors(false) 181 | .addFilter((response) => { 182 | return ArraysUtils.equals(messageContent, response.contents); 183 | }) 184 | .onError(err => { 185 | return this.handleError(err); 186 | }) 187 | .onInvalidMessageReceived((response, reason) => { 188 | if (!ArraysUtils.equals(messageContent, response.contents)) { 189 | return; 190 | } 191 | 192 | this.handleError(new Error(reason + ': ' + ArraysUtils.toString(response.contents))); 193 | this.listener.unsubscribe(); 194 | }) 195 | .onDecrypt(this.decrypter) 196 | .subscribe(client, msg => { 197 | this.listener.unsubscribe(); 198 | this.receiver(msg); 199 | }); 200 | } 201 | 202 | const tx = new TopicMessageSubmitTransaction().setTopicId(this.topicId).setMessage(messageContent); 203 | 204 | let transactionId; 205 | 206 | try { 207 | const response = await this.buildTransactionFunction(tx).execute(client); 208 | transactionId = response.transactionId; 209 | this.executed = true; 210 | } catch (e) { 211 | this.handleError(e); 212 | if (this.listener) { 213 | this.listener.unsubscribe(); 214 | } 215 | } 216 | 217 | return transactionId; 218 | } 219 | 220 | /** 221 | * Runs validation logic. 222 | * 223 | * @param validator The errors validator. 224 | */ 225 | protected validate(validator: Validator): void { 226 | validator.require(!this.executed, 'This transaction has already been executed.'); 227 | validator.require(!!this.signer || (!!this.message && !!this.message.getSignature()), 'Signing function is missing.'); 228 | validator.require(!!this.buildTransactionFunction, 'Transaction builder is missing.'); 229 | validator.require((!!this.encrypter && !!this.decrypter) || (!this.decrypter && !this.encrypter), 'Either both encrypter and decrypter must be specified or none.') 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/identity/hcs/message.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Serialize { 2 | toJsonTree: () => any; 3 | toJSON(): () => string; 4 | } 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/identity/hcs/message.ts: -------------------------------------------------------------------------------- 1 | import Long from "long"; 2 | import { Timestamp } from "@hashgraph/sdk"; 3 | import { TimestampUtils } from "../../utils/timestamp-utils"; 4 | 5 | export type Encrypter = (message: T) => T; 6 | export type Decrypter = (message: T, consensusTime: Timestamp) => T; 7 | export type Signer = (message: T) => T; 8 | 9 | export class Message { 10 | private static serialVersionUID = Long.fromInt(1); 11 | 12 | protected timestamp: Timestamp; 13 | 14 | constructor() { 15 | this.timestamp = TimestampUtils.now(); 16 | } 17 | 18 | public getTimestamp(): Timestamp { 19 | return this.timestamp; 20 | } 21 | 22 | public toJsonTree(): any { 23 | const result: any = {}; 24 | result.timestamp = TimestampUtils.toJSON(this.timestamp); 25 | return result; 26 | } 27 | 28 | public toJSON(): string { 29 | return JSON.stringify(this.toJsonTree()); 30 | } 31 | 32 | public static fromJsonTree(tree: any, result?: Message): Message { 33 | if (!result) 34 | result = new Message(); 35 | result.timestamp = TimestampUtils.fromJson(tree.timestamp); 36 | return result; 37 | } 38 | 39 | public static fromJson(json: string): Message { 40 | return Message.fromJsonTree(JSON.parse(json)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/identity/hcs/serializable-mirror-consensus-response.ts: -------------------------------------------------------------------------------- 1 | import Long from "long"; 2 | import { Timestamp, TopicMessage } from "@hashgraph/sdk"; 3 | import { ArraysUtils } from "../../utils/arrays-utils"; 4 | import { TimestampUtils } from "../../utils/timestamp-utils"; 5 | 6 | export class SerializableMirrorConsensusResponse { 7 | private static serialVersionUID = Long.fromInt(1); 8 | 9 | public consensusTimestamp: Timestamp; 10 | public message: Uint8Array; 11 | public runningHash: Uint8Array; 12 | public sequenceNumber: Long; 13 | 14 | constructor(response: TopicMessage) { 15 | this.consensusTimestamp = response.consensusTimestamp; 16 | this.message = response.contents; 17 | this.runningHash = response.runningHash; 18 | this.sequenceNumber = response.sequenceNumber; 19 | } 20 | 21 | public toString(): string { 22 | return "ConsensusMessage{" 23 | + "consensusTimestamp=" + TimestampUtils.toJSON(this.consensusTimestamp) 24 | + ", message=" + ArraysUtils.toString(this.message) 25 | + ", runningHash=" + ArraysUtils.toString(this.runningHash) 26 | + ", sequenceNumber=" + this.sequenceNumber.toNumber() 27 | + '}'; 28 | } 29 | 30 | public toJsonTree(): any { 31 | const result: any = {}; 32 | result.consensusTimestamp = { 33 | seconds: this.consensusTimestamp.seconds, 34 | nanos: this.consensusTimestamp.nanos 35 | }; 36 | result.message = this.message.toString(); 37 | result.runningHash = this.runningHash.toString(); 38 | result.sequenceNumber = this.sequenceNumber.toString(); 39 | return result; 40 | } 41 | 42 | public toJSON(): string { 43 | return JSON.stringify(this.toJsonTree()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/identity/hcs/vc/credential-subject.ts: -------------------------------------------------------------------------------- 1 | import { HcsVcDocumentJsonProperties } from "./hcs-vc-document-json-properties"; 2 | 3 | export class CredentialSubject { 4 | protected id: string; 5 | 6 | public getId(): string { 7 | return this.id; 8 | } 9 | 10 | public setId(id: string): void { 11 | this.id = id; 12 | } 13 | 14 | 15 | // JsonClass 16 | 17 | public toJsonTree(): any { 18 | const rootObject = {}; 19 | rootObject[HcsVcDocumentJsonProperties.ID] = this.id; 20 | return rootObject; 21 | } 22 | 23 | public static fromJsonTree(root: any, result?: CredentialSubject): CredentialSubject { 24 | if (!result) 25 | result = new CredentialSubject(); 26 | result.id = root[HcsVcDocumentJsonProperties.ID]; 27 | return result; 28 | 29 | } 30 | 31 | public toJSON(): string { 32 | return JSON.stringify(this.toJsonTree()); 33 | } 34 | 35 | public static fromJson(json: string): CredentialSubject { 36 | let result: CredentialSubject; 37 | 38 | try { 39 | const root = JSON.parse(json); 40 | result = this.fromJsonTree(root); 41 | 42 | } catch (e) { 43 | throw new Error('Given JSON string is not a valid CredentialSubject ' + e.message); 44 | } 45 | 46 | return result; 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /src/identity/hcs/vc/hcs-vc-document-base.ts: -------------------------------------------------------------------------------- 1 | import { Timestamp } from "@hashgraph/sdk"; 2 | import { Hashing } from "../../../utils/hashing"; 3 | import { TimestampUtils } from "../../../utils/timestamp-utils"; 4 | import { HcsDid } from "../did/hcs-did"; 5 | import { JsonClass } from "../json-class"; 6 | import { CredentialSubject } from "./credential-subject"; 7 | import { HcsVcDocumentHashBase } from "./hcs-vc-document-hash-base"; 8 | import { HcsVcDocumentJsonProperties } from "./hcs-vc-document-json-properties"; 9 | import { Issuer } from "./issuer"; 10 | 11 | /** 12 | * The base for a VC document generation in JSON-LD format. 13 | * VC documents according to W3C draft specification must be compatible with JSON-LD version 1.1 Up until now there is 14 | * no Java implementation library of JSON-LD version 1.1. For that reason this object represents only the most basic and 15 | * mandatory attributes from the VC specification and Hedera HCS DID method specification point of view. Applications 16 | * shall extend it with any VC document properties or custom properties they require. 17 | */ 18 | export class HcsVcDocumentBase extends HcsVcDocumentHashBase { 19 | protected context: string[]; 20 | protected credentialSubject: T[]; 21 | 22 | /** 23 | * Creates a new VC Document instance. 24 | */ 25 | constructor() { 26 | super(); 27 | this.context = [HcsVcDocumentJsonProperties.FIRST_CONTEXT_ENTRY]; 28 | } 29 | 30 | /** 31 | * Constructs a credential hash that uniquely identifies this verifiable credential. 32 | * This is not a credential ID, but a hash composed of the properties included in HcsVcDocumentHashBase class 33 | * (excluding issuer name). 34 | * Credential hash is used to find the credential on Hedera VC registry. 35 | * Due to the nature of the VC document the hash taken from the base mandatory fields in this class 36 | * and shall produce a unique constant. 37 | * W3C specification defines ID field of a verifiable credential as not mandatory, however Hedera requires issuers to 38 | * define this property for each VC. 39 | * 40 | * @return The credential hash uniquely identifying this verifiable credential. 41 | */ 42 | public toCredentialHash(): string { 43 | const map = {}; 44 | map[HcsVcDocumentJsonProperties.ID] = this.id; 45 | map[HcsVcDocumentJsonProperties.TYPE] = this.type; 46 | map[HcsVcDocumentJsonProperties.ISSUER] = this.issuer.getId(); 47 | map[HcsVcDocumentJsonProperties.ISSUANCE_DATE] = TimestampUtils.toJSON(this.issuanceDate); 48 | const json: string = JSON.stringify(map); 49 | const hash: Uint8Array = Hashing.sha256.digest(json); 50 | return Hashing.base58.encode(hash); 51 | } 52 | 53 | public getContext(): string[] { 54 | return this.context; 55 | } 56 | 57 | public getId(): string { 58 | return this.id; 59 | } 60 | 61 | public getType(): string[] { 62 | return this.type; 63 | } 64 | 65 | public getIssuer(): Issuer { 66 | return this.issuer; 67 | } 68 | 69 | public getIssuanceDate(): Timestamp { 70 | return this.issuanceDate; 71 | } 72 | 73 | public getCredentialSubject(): T[] { 74 | return this.credentialSubject; 75 | } 76 | 77 | public setId(id: string): void { 78 | this.id = id; 79 | } 80 | 81 | public setIssuer(issuerDid: string): void; 82 | public setIssuer(issuer: Issuer): void; 83 | public setIssuer(issuerDid: HcsDid): void; 84 | public setIssuer(...args: any[]): void { 85 | if (typeof args[0] === 'string') { 86 | this.issuer = new Issuer(args[0]); 87 | return; 88 | } 89 | if (args[0] instanceof Issuer) { 90 | this.issuer = args[0]; 91 | return; 92 | } 93 | if (args[0] instanceof HcsDid) { 94 | this.issuer = new Issuer(args[0].toDid()); 95 | return; 96 | } 97 | } 98 | 99 | public setIssuanceDate(issuanceDate: Timestamp): void { 100 | this.issuanceDate = issuanceDate; 101 | } 102 | 103 | 104 | /** 105 | * Adds an additional context to @context field of the VC document. 106 | * 107 | * @param context The context to add. 108 | */ 109 | public addContext(context: string): void { 110 | this.context.push(context); 111 | } 112 | 113 | /** 114 | * Adds an additional type to `type` field of the VC document. 115 | * 116 | * @param type The type to add. 117 | */ 118 | public addType(type: string): void { 119 | this.type.push(type); 120 | } 121 | 122 | /** 123 | * Adds a credential subject. 124 | * 125 | * @param credentialSubject The credential subject to add. 126 | */ 127 | public addCredentialSubject(credentialSubject: T): void { 128 | if (this.credentialSubject == null) { 129 | this.credentialSubject = []; 130 | } 131 | 132 | this.credentialSubject.push(credentialSubject); 133 | } 134 | 135 | /** 136 | * Checks if all mandatory fields of a VC document are filled in. 137 | * 138 | * @return True if the document is complete and false otherwise. 139 | */ 140 | public isComplete(): boolean { 141 | return ( 142 | (this.context != null) && 143 | (!!this.context.length) && 144 | (HcsVcDocumentJsonProperties.FIRST_CONTEXT_ENTRY == this.context[0]) && 145 | (this.type != null) && 146 | (!!this.type.length) && 147 | (this.type.indexOf(HcsVcDocumentJsonProperties.VERIFIABLE_CREDENTIAL_TYPE) > -1) && 148 | (this.issuanceDate != null) && 149 | (this.issuer != null) && 150 | (!!this.issuer.getId()) && 151 | (this.credentialSubject != null) && 152 | (!!this.credentialSubject.length) 153 | ); 154 | } 155 | 156 | // JsonClass 157 | 158 | public toJsonTree(): any { 159 | const rootObject = super.toJsonTree(); 160 | 161 | const context = []; 162 | if (this.context) { 163 | for (let index = 0; index < this.context.length; index++) { 164 | const element = this.context[index]; 165 | context.push(element); 166 | } 167 | } 168 | rootObject[HcsVcDocumentJsonProperties.CONTEXT] = context; 169 | 170 | const credentialSubject = []; 171 | if (this.credentialSubject) { 172 | for (let index = 0; index < this.credentialSubject.length; index++) { 173 | const element = this.credentialSubject[index]; 174 | credentialSubject.push(element.toJsonTree()); 175 | } 176 | } 177 | rootObject[HcsVcDocumentJsonProperties.CREDENTIAL_SUBJECT] = credentialSubject; 178 | 179 | return rootObject; 180 | } 181 | 182 | public static fromJsonTree(root: any, result?: HcsVcDocumentBase, credentialSubjectClass?: JsonClass): HcsVcDocumentBase { 183 | if (!result) 184 | result = new HcsVcDocumentBase(); 185 | result = HcsVcDocumentHashBase.fromJsonTree(root, result) as HcsVcDocumentBase; 186 | const jsonCredentialSubject = root[HcsVcDocumentJsonProperties.CREDENTIAL_SUBJECT] as any[]; 187 | const credentialSubject: U[] = []; 188 | for (let i = 0; i < jsonCredentialSubject.length; i++) { 189 | const item = jsonCredentialSubject[i]; 190 | const subject: U = credentialSubjectClass.fromJsonTree(item); 191 | credentialSubject.push(subject) 192 | } 193 | result.credentialSubject = credentialSubject; 194 | return result; 195 | } 196 | 197 | /** 198 | * Converts this document into a JSON string. 199 | * 200 | * @return The JSON representation of this document. 201 | */ 202 | public toJSON(): string { 203 | return JSON.stringify(this.toJsonTree()); 204 | } 205 | 206 | /** 207 | * Converts a VC document in JSON format into a {@link HcsVcDocumentBase} object. 208 | * Please note this conversion respects only the fields of the base VC document. All other fields are ignored. 209 | * 210 | * @param The type of the credential subject. 211 | * @param json The VC document as JSON string. 212 | * @param credentialSubjectClass The type of the credential subject inside. 213 | * @return The {@link HcsVcDocumentBase} object. 214 | */ 215 | public static fromJson(json: string, credentialSubjectClass?: JsonClass): HcsVcDocumentBase { 216 | let result: HcsVcDocumentBase; 217 | try { 218 | const root = JSON.parse(json); 219 | result = this.fromJsonTree(root, null, credentialSubjectClass); 220 | 221 | } catch (e) { 222 | throw new Error('Given JSON string is not a valid HcsVcDocumentBase ' + e.message); 223 | } 224 | return result; 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/identity/hcs/vc/hcs-vc-document-hash-base.ts: -------------------------------------------------------------------------------- 1 | import { Timestamp } from "@hashgraph/sdk"; 2 | import { TimestampUtils } from "../../../utils/timestamp-utils"; 3 | import { HcsVcDocumentJsonProperties } from "./hcs-vc-document-json-properties"; 4 | import { Issuer } from "./issuer"; 5 | 6 | /** 7 | * The part of the VC document that is used for hash calculation. 8 | */ 9 | export class HcsVcDocumentHashBase { 10 | protected id: string; 11 | protected type: string[]; 12 | protected issuer: Issuer; 13 | protected issuanceDate: Timestamp; 14 | 15 | /** 16 | * Creates a new VC document instance. 17 | */ 18 | constructor() { 19 | this.type = [HcsVcDocumentJsonProperties.VERIFIABLE_CREDENTIAL_TYPE]; 20 | } 21 | 22 | // JsonClass 23 | 24 | public toJsonTree(): any { 25 | const rootObject = {}; 26 | if (this.id) 27 | rootObject[HcsVcDocumentJsonProperties.ID] = this.id; 28 | if (this.type) 29 | rootObject[HcsVcDocumentJsonProperties.TYPE] = this.type; 30 | if (this.issuer) 31 | rootObject[HcsVcDocumentJsonProperties.ISSUER] = this.issuer.toJsonTree(); 32 | if (this.issuanceDate) 33 | rootObject[HcsVcDocumentJsonProperties.ISSUANCE_DATE] = TimestampUtils.toJSON(this.issuanceDate); 34 | return rootObject; 35 | } 36 | 37 | public static fromJsonTree(root: any, result?: HcsVcDocumentHashBase): HcsVcDocumentHashBase { 38 | if (!result) 39 | result = new HcsVcDocumentHashBase(); 40 | if (root[HcsVcDocumentJsonProperties.ID]) 41 | result.id = root[HcsVcDocumentJsonProperties.ID]; 42 | if (root[HcsVcDocumentJsonProperties.TYPE]) 43 | result.type = root[HcsVcDocumentJsonProperties.TYPE]; 44 | if (root[HcsVcDocumentJsonProperties.ISSUER]) 45 | result.issuer = Issuer.fromJsonTree(root[HcsVcDocumentJsonProperties.ISSUER]); 46 | if (root[HcsVcDocumentJsonProperties.ISSUANCE_DATE]) 47 | result.issuanceDate = TimestampUtils.fromJson(root[HcsVcDocumentJsonProperties.ISSUANCE_DATE]); 48 | return result; 49 | } 50 | 51 | public toJSON(): string { 52 | return JSON.stringify(this.toJsonTree()); 53 | } 54 | 55 | public static fromJson(json: string): HcsVcDocumentHashBase { 56 | let result: HcsVcDocumentHashBase; 57 | try { 58 | const root = JSON.parse(json); 59 | result = this.fromJsonTree(root); 60 | 61 | } catch (e) { 62 | throw new Error('Given JSON string is not a valid HcsVcDocumentHashBase ' + e.message); 63 | } 64 | return result; 65 | } 66 | } -------------------------------------------------------------------------------- /src/identity/hcs/vc/hcs-vc-document-json-properties.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Key property names in VC document standard. 3 | */ 4 | export module HcsVcDocumentJsonProperties { 5 | export const CONTEXT = '@context'; 6 | export const FIRST_CONTEXT_ENTRY = 'https://www.w3.org/2018/credentials/v1'; 7 | export const ID = 'id'; 8 | export const CREDENTIAL_SUBJECT = 'credentialSubject'; 9 | export const TYPE = 'type'; 10 | export const VERIFIABLE_CREDENTIAL_TYPE = 'VerifiableCredential'; 11 | export const ISSUER = 'issuer'; 12 | export const ISSUANCE_DATE = 'issuanceDate'; 13 | export const CREDENTIAL_STATUS = 'credentialStatus'; 14 | export const PROOF = 'proof'; 15 | } 16 | -------------------------------------------------------------------------------- /src/identity/hcs/vc/hcs-vc-message.ts: -------------------------------------------------------------------------------- 1 | import { Timestamp } from "@hashgraph/sdk"; 2 | import { Hashing } from "../../../utils/hashing"; 3 | import { Decrypter, Encrypter, Message } from "../message"; 4 | import { MessageEnvelope } from "../message-envelope"; 5 | import { HcsVcOperation } from "./hcs-vc-operation"; 6 | 7 | export class HcsVcMessage extends Message { 8 | private operation: HcsVcOperation; 9 | private credentialHash: string; 10 | 11 | /** 12 | * Creates a new message instance. 13 | * 14 | * @param operation Operation type. 15 | * @param credentialHash Credential hash. 16 | */ 17 | constructor(operation: HcsVcOperation, credentialHash: string) { 18 | super(); 19 | this.operation = operation; 20 | this.credentialHash = credentialHash; 21 | } 22 | 23 | /** 24 | * Checks if the message is valid from content point of view. 25 | * Does not verify hash nor any signatures. 26 | * 27 | * @return True if the message is valid and False otherwise. 28 | */ 29 | public isValid(): boolean { 30 | return (!!this.credentialHash && !!this.operation); 31 | } 32 | 33 | public getOperation(): HcsVcOperation { 34 | return this.operation; 35 | } 36 | 37 | public getCredentialHash(): string { 38 | return this.credentialHash; 39 | } 40 | 41 | public toJsonTree(): any { 42 | const result: any = super.toJsonTree(); 43 | result.operation = this.operation; 44 | result.credentialHash = this.credentialHash; 45 | return result; 46 | } 47 | 48 | public static fromJsonTree(tree: any, result?: HcsVcMessage): HcsVcMessage { 49 | if (!result) { 50 | result = new HcsVcMessage(tree.operation, tree.credentialHash); 51 | } else { 52 | result.operation = tree.operation; 53 | result.credentialHash = tree.credentialHash; 54 | } 55 | result = super.fromJsonTree(tree, result) as HcsVcMessage; 56 | return result; 57 | } 58 | 59 | public toJSON(): string { 60 | return JSON.stringify(this.toJsonTree()); 61 | } 62 | 63 | public static fromJson(json: string): Message { 64 | return Message.fromJsonTree(JSON.parse(json)); 65 | } 66 | 67 | /** 68 | * Creates a new VC message for submission to HCS topic. 69 | * 70 | * @param credentialHash VC hash. 71 | * @param operation The operation on a VC document. 72 | * @return The HCS message wrapped in an envelope for the given VC and operation. 73 | */ 74 | public static fromCredentialHash(credentialHash: string, operation: HcsVcOperation): MessageEnvelope { 75 | const message: HcsVcMessage = new HcsVcMessage(operation, credentialHash); 76 | return new MessageEnvelope(message); 77 | } 78 | 79 | /** 80 | * Provides an encryption operator that converts an {@link HcsVcMessage} into encrypted one. 81 | * 82 | * @param encryptionFunction The encryption function to use for encryption of single attributes. 83 | * @return The encryption operator instance. 84 | */ 85 | public static getEncrypter(encryptionFunction: Encrypter): Encrypter { 86 | if (encryptionFunction == null) { 87 | throw "Encryption function is missing or null."; 88 | } 89 | return function (message: HcsVcMessage) { 90 | // Encrypt the credential hash 91 | const encryptedHash: string = encryptionFunction(message.getCredentialHash()); 92 | const hash = Hashing.base64.encode(encryptedHash); 93 | return new HcsVcMessage(message.getOperation(), hash); 94 | }; 95 | } 96 | 97 | /** 98 | * Provides a decryption function that converts {@link HcsVcMessage} in encrypted for into a plain form. 99 | * 100 | * @param decryptionFunction The decryption function to use for decryption of single attributes. 101 | * @return The decryption function for the {@link HcsVcMessage} 102 | */ 103 | public static getDecrypter(decryptionFunction: Decrypter): Decrypter { 104 | if (decryptionFunction == null) { 105 | throw "Decryption function is missing or null."; 106 | } 107 | return function (encryptedMsg: HcsVcMessage, consensusTimestamp: Timestamp) { 108 | // Decrypt DID string 109 | let decryptedHash: string = encryptedMsg.getCredentialHash(); 110 | if (decryptedHash != null) { 111 | const hash: string = Hashing.base64.decode(decryptedHash); 112 | decryptedHash = decryptionFunction(hash, consensusTimestamp); 113 | } 114 | return new HcsVcMessage(encryptedMsg.getOperation(), decryptedHash); 115 | }; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/identity/hcs/vc/hcs-vc-operation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The operation type to be performed on the DID document. 3 | */ 4 | export enum HcsVcOperation { 5 | ISSUE = 'issue', 6 | REVOKE = 'revoke', 7 | SUSPEND = 'suspend', 8 | RESUME = 'resume' 9 | } 10 | -------------------------------------------------------------------------------- /src/identity/hcs/vc/hcs-vc-status-resolver.ts: -------------------------------------------------------------------------------- 1 | import {TopicId} from "@hashgraph/sdk"; 2 | import {TimestampUtils} from "../../../utils/timestamp-utils"; 3 | import {MessageEnvelope} from "../message-envelope"; 4 | import {MessageListener} from "../message-listener"; 5 | import {MessageResolver} from "../message-resolver"; 6 | import {HcsVcMessage} from "./hcs-vc-message"; 7 | import {HcsVcOperation} from "./hcs-vc-operation"; 8 | import {HcsVcTopicListener, PublicKeysProvider} from "./hcs-vc-topic-listener"; 9 | 10 | /** 11 | * Resolves the DID from Hedera network. 12 | */ 13 | export class HcsVcStatusResolver extends MessageResolver { 14 | /** 15 | * A function providing a collection of public keys accepted for a given credential hash. 16 | * If the function is not supplied, the listener will not validate signatures. 17 | */ 18 | private publicKeysProvider: PublicKeysProvider; 19 | 20 | /** 21 | * Instantiates a new status resolver for the given VC topic. 22 | * 23 | * @param topicId The HCS VC topic ID. 24 | */ 25 | constructor(topicId: TopicId); 26 | /** 27 | * Instantiates a new status resolver for the given VC topic with signature validation. 28 | * 29 | * @param topicId The VC consensus topic ID. 30 | * @param publicKeysProvider Provider of a public keys acceptable for a given VC hash. 31 | */ 32 | constructor(topicId: TopicId, publicKeysProvider: PublicKeysProvider); 33 | constructor(...args: any[]) { 34 | const topicId = args[0] as TopicId; 35 | super(topicId); 36 | if (args[1]) { 37 | this.publicKeysProvider = args[1]; 38 | } else { 39 | this.publicKeysProvider = null; 40 | } 41 | } 42 | 43 | /** 44 | * Adds a credential hash to resolve its status. 45 | * 46 | * @param credentialHash The credential hash string. 47 | * @return This resolver instance. 48 | */ 49 | public addCredentialHash(credentialHash: string): HcsVcStatusResolver { 50 | if (credentialHash != null) { 51 | this.results.set(credentialHash, null); 52 | } 53 | return this; 54 | } 55 | 56 | /** 57 | * Adds multiple VC hashes to resolve. 58 | * 59 | * @param hashes The set of VC hash strings. 60 | * @return This resolver instance. 61 | */ 62 | public addCredentialHashes(hashes: string[]): HcsVcStatusResolver { 63 | if (hashes != null) { 64 | hashes.forEach(d => this.addCredentialHash(d)); 65 | } 66 | 67 | return this; 68 | } 69 | 70 | protected override matchesSearchCriteria(message: HcsVcMessage): boolean { 71 | return this.results.has(message.getCredentialHash()); 72 | } 73 | 74 | 75 | protected override supplyMessageListener(): MessageListener { 76 | return new HcsVcTopicListener(this.topicId, this.publicKeysProvider); 77 | } 78 | 79 | protected override processMessage(envelope: MessageEnvelope): void { 80 | const message: HcsVcMessage = envelope.open(); 81 | 82 | // Skip messages that are older than the once collected or if we already have a REVOKED message 83 | const existing: MessageEnvelope = this.results.get(message.getCredentialHash()); 84 | 85 | const chackOperation = ( 86 | (existing != null) && 87 | ( 88 | (TimestampUtils.lessThan(envelope.getConsensusTimestamp(), existing.getConsensusTimestamp())) || 89 | ( 90 | HcsVcOperation.REVOKE == (existing.open().getOperation()) && 91 | HcsVcOperation.REVOKE != (message.getOperation()) 92 | ) 93 | ) 94 | ) 95 | if (chackOperation) { 96 | return; 97 | } 98 | 99 | // Add valid message to the results 100 | this.results.set(message.getCredentialHash(), envelope); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/identity/hcs/vc/hcs-vc-topic-listener.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey, TopicId, TopicMessage } from "@hashgraph/sdk"; 2 | import { MessageEnvelope } from "../message-envelope"; 3 | import { MessageListener } from "../message-listener"; 4 | import { HcsVcMessage } from "./hcs-vc-message"; 5 | 6 | export type PublicKeysProvider = (t: string) => PublicKey[]; 7 | 8 | /** 9 | * A listener of confirmed {@link HcsVcMessage} messages from a VC topic. 10 | * Messages are received from a given mirror node, parsed and validated. 11 | */ 12 | export class HcsVcTopicListener extends MessageListener { 13 | /** 14 | * A function providing a collection of public keys accepted for a given credential hash. 15 | * If the function is not supplied, the listener will not validate signatures. 16 | */ 17 | private publicKeysProvider: PublicKeysProvider; 18 | 19 | /** 20 | * Creates a new instance of a VC topic listener for the given consensus topic. 21 | * By default, invalid messages are ignored and errors are not. 22 | * Listener without a public key provider will not validate message signatures. 23 | * 24 | * @param vcTopicId The VC consensus topic ID. 25 | */ 26 | constructor(vcTopicId: TopicId) 27 | /** 28 | * Creates a new instance of a VC topic listener for the given consensus topic. 29 | * By default, invalid messages are ignored and errors are not. 30 | * 31 | * @param vcTopicId The VC consensus topic ID. 32 | * @param publicKeysProvider Provider of a public keys acceptable for a given VC hash. 33 | */ 34 | constructor(vcTopicId: TopicId, publicKeysProvider: PublicKeysProvider); 35 | constructor(...args: any[]) { 36 | const vcTopicId = args[0] as TopicId; 37 | super(vcTopicId); 38 | if (args[1]) { 39 | this.publicKeysProvider = args[1]; 40 | } else { 41 | this.publicKeysProvider = null; 42 | } 43 | } 44 | 45 | protected override extractMessage(response: TopicMessage): MessageEnvelope { 46 | let result: MessageEnvelope = null; 47 | try { 48 | result = MessageEnvelope.fromMirrorResponse(response, HcsVcMessage); 49 | } catch (err) { 50 | this.handleError(err); 51 | } 52 | return result; 53 | } 54 | 55 | protected override isMessageValid(envelope: MessageEnvelope, response: TopicMessage): boolean { 56 | try { 57 | const msgDecrypter = !!this.decrypter ? HcsVcMessage.getDecrypter(this.decrypter) : null; 58 | 59 | const message: HcsVcMessage = envelope.open(msgDecrypter); 60 | if (message == null) { 61 | this.reportInvalidMessage(response, "Empty message received when opening envelope"); 62 | return false; 63 | } 64 | 65 | if (!message.isValid()) { 66 | this.reportInvalidMessage(response, "Message content validation failed."); 67 | return false; 68 | } 69 | 70 | // Validate signature only if public key provider has been supplied. 71 | if (!!this.publicKeysProvider && !this.isSignatureAccepted(envelope)) { 72 | this.reportInvalidMessage(response, "Signature validation failed"); 73 | return false; 74 | } 75 | 76 | return true; 77 | } catch (err) { 78 | this.handleError(err); 79 | this.reportInvalidMessage(response, "Exception while validating message: " + err.message); 80 | return false; 81 | } 82 | } 83 | 84 | /** 85 | * Checks if the signature on the envelope is accepted by any public key supplied for the credential hash. 86 | * 87 | * @param envelope The message envelope. 88 | * @return True if signature is accepted, false otherwise. 89 | */ 90 | private isSignatureAccepted(envelope: MessageEnvelope): boolean { 91 | if (!this.publicKeysProvider) { 92 | return false; 93 | } 94 | 95 | const message: HcsVcMessage = envelope.open(); 96 | const acceptedKeys: PublicKey[] = this.publicKeysProvider(message.getCredentialHash()); 97 | if (!acceptedKeys || !acceptedKeys.length) { 98 | return false; 99 | } 100 | 101 | for (let publicKey of acceptedKeys) { 102 | if (envelope.isSignatureValid(publicKey)) { 103 | return true; 104 | } 105 | } 106 | 107 | return false; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/identity/hcs/vc/hcs-vc-transaction.ts: -------------------------------------------------------------------------------- 1 | import {MessageTransaction} from "../message-transaction"; 2 | import {HcsVcMessage} from "./hcs-vc-message"; 3 | import {HcsVcOperation} from "./hcs-vc-operation"; 4 | import {PublicKey, TopicId} from "@hashgraph/sdk"; 5 | import {MessageEnvelope} from "../message-envelope"; 6 | import {Validator} from "../../../utils/validator"; 7 | import {MessageListener} from "../message-listener"; 8 | import {HcsVcTopicListener} from "./hcs-vc-topic-listener"; 9 | import {Encrypter} from "../message"; 10 | 11 | /** 12 | * The DID document creation, update or deletion transaction. 13 | * Builds a correct {@link HcsDidMessage} and send it to HCS DID topic. 14 | */ 15 | export class HcsVcTransaction extends MessageTransaction { 16 | private operation: HcsVcOperation; 17 | private credentialHash: string; 18 | private signerPublicKey: PublicKey; 19 | 20 | /** 21 | * Instantiates a new transaction object. 22 | * 23 | * @param topicId The HCS VC topic ID where message will be submitted. 24 | * @param operation The operation to be performed on a verifiable credential. 25 | * @param credentialHash The hash of a credential. 26 | * @param signerPublicKey Public key of the signer of this operation. 27 | */ 28 | constructor(topicId: TopicId, operation: HcsVcOperation, credentialHash: string, signerPublicKey: PublicKey); 29 | 30 | /** 31 | * Instantiates a new transaction object from a message that was already prepared. 32 | * 33 | * @param topicId The HCS VC topic ID where message will be submitted. 34 | * @param message The message envelope. 35 | * @param signerPublicKey Public key of the signer of this operation. 36 | */ 37 | constructor(topicId: TopicId, message: MessageEnvelope, signerPublicKey: PublicKey); 38 | constructor(...args) { 39 | if ( 40 | (args.length === 4) && 41 | (args[0] instanceof TopicId) && 42 | // (args[1] instanceof HcsVcOperation) && 43 | (typeof args[2] === 'string') && 44 | (args[3] instanceof PublicKey) 45 | ) { 46 | const [topicId, operation, credentialHash, signerPublicKey] = args; 47 | super(topicId); 48 | this.operation = operation; 49 | this.credentialHash = credentialHash; 50 | this.signerPublicKey = signerPublicKey; 51 | } else if ( 52 | (args.length === 3) && 53 | (args[0] instanceof TopicId) && 54 | (args[1] instanceof MessageEnvelope) && 55 | (args[2] instanceof PublicKey) 56 | ) { 57 | const [topicId, message, signerPublicKey] = args; 58 | super(topicId, message); 59 | this.signerPublicKey = signerPublicKey; 60 | this.operation = null; 61 | this.credentialHash = null; 62 | } 63 | } 64 | 65 | protected validate(validator: Validator): void { 66 | super.validate(validator); 67 | validator.require(!!this.credentialHash || !!this.message, 'Verifiable credential hash is null or empty.'); 68 | validator.require(!!this.operation || !!this.message, 'Operation on verifiable credential is not defined.'); 69 | } 70 | 71 | protected buildMessage(): MessageEnvelope { 72 | return HcsVcMessage.fromCredentialHash(this.credentialHash, this.operation); 73 | } 74 | 75 | protected provideTopicListener(topicIdToListen: TopicId): MessageListener { 76 | return new HcsVcTopicListener(topicIdToListen, (s) => { 77 | return [this.signerPublicKey] 78 | }); 79 | } 80 | 81 | protected provideMessageEncrypter(encryptionFunction: Encrypter): (input: HcsVcMessage) => HcsVcMessage { 82 | return HcsVcMessage.getEncrypter(encryptionFunction); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/identity/hcs/vc/issuer.ts: -------------------------------------------------------------------------------- 1 | import { HcsVcDocumentJsonProperties } from "./hcs-vc-document-json-properties"; 2 | 3 | export class Issuer { 4 | protected id: string; 5 | protected name: string; 6 | 7 | constructor(id: string) 8 | constructor(id: string, name: string); 9 | constructor(...args: any[]) { 10 | this.id = args[0]; 11 | this.name = args[1] || null; 12 | } 13 | 14 | public getId(): string { 15 | return this.id; 16 | } 17 | 18 | public getName(): string { 19 | return this.name; 20 | } 21 | 22 | // JsonClass 23 | 24 | public toJsonTree(): any { 25 | if (this.name) { 26 | const rootObject = {}; 27 | rootObject[HcsVcDocumentJsonProperties.ID] = this.id; 28 | rootObject['name'] = this.name; 29 | return rootObject; 30 | } 31 | return this.id; 32 | } 33 | 34 | public static fromJsonTree(root: any, result?: Issuer): Issuer { 35 | let id: string, name: string; 36 | if (typeof root == "string") { 37 | id = root; 38 | } else { 39 | id = root[HcsVcDocumentJsonProperties.ID]; 40 | name = root["name"]; 41 | } 42 | if (result) { 43 | result.id = id; 44 | result.name = name 45 | return result; 46 | } else { 47 | return new Issuer(id, name); 48 | } 49 | } 50 | 51 | public toJSON(): string { 52 | return JSON.stringify(this.toJsonTree()); 53 | } 54 | 55 | public static fromJson(json: string): Issuer { 56 | let result: Issuer; 57 | 58 | try { 59 | const root = JSON.parse(json); 60 | result = this.fromJsonTree(root); 61 | 62 | } catch (e) { 63 | throw new Error('Given JSON string is not a valid Issuer ' + e.message); 64 | } 65 | 66 | return result; 67 | } 68 | } -------------------------------------------------------------------------------- /src/identity/hedera-did.ts: -------------------------------------------------------------------------------- 1 | import {DidDocumentBase} from "./did-document-base"; 2 | import {DidSyntax} from "./did-syntax"; 3 | 4 | export interface HederaDid { 5 | // fromString(string): HederaDid; 6 | toDid(): string; 7 | generateDidDocument(): DidDocumentBase; 8 | getNetwork(): string; 9 | getMethod(): DidSyntax.Method 10 | } 11 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {AddressBook} from "./identity/hcs/address-book"; 2 | import {ArraysUtils} from "./utils/arrays-utils"; 3 | import {CredentialSubject} from "./identity/hcs/vc/credential-subject"; 4 | import {DidDocumentBase} from "./identity/did-document-base"; 5 | import {DidDocumentJsonProperties} from "./identity/did-document-json-properties"; 6 | import {DidMethodOperation} from "./identity/did-method-operation"; 7 | import {DidParser} from "./identity/did-parser"; 8 | import {DidSyntax} from "./identity/did-syntax"; 9 | import {Hashing} from "./utils/hashing"; 10 | import {HcsDidMessage} from "./identity/hcs/did/hcs-did-message"; 11 | import {HcsDidResolver} from "./identity/hcs/did/hcs-did-resolver"; 12 | import {HcsDidRootKey} from "./identity/hcs/did/hcs-did-root-key"; 13 | import {HcsDidTopicListener} from "./identity/hcs/did/hcs-did-topic-listener"; 14 | import {HcsDidTransaction} from "./identity/hcs/did/hcs-did-transaction"; 15 | import {HcsDid} from "./identity/hcs/did/hcs-did"; 16 | import {HcsIdentityNetworkBuilder} from "./identity/hcs/hcs-identity-network-builder"; 17 | import {HcsIdentityNetwork} from "./identity/hcs/hcs-identity-network"; 18 | import {HcsVcDocumentBase} from "./identity/hcs/vc/hcs-vc-document-base"; 19 | import {HcsVcDocumentHashBase} from "./identity/hcs/vc/hcs-vc-document-hash-base"; 20 | import {HcsVcDocumentJsonProperties} from "./identity/hcs/vc/hcs-vc-document-json-properties"; 21 | import {HcsVcMessage} from "./identity/hcs/vc/hcs-vc-message"; 22 | import {HcsVcOperation} from "./identity/hcs/vc/hcs-vc-operation"; 23 | import {HcsVcStatusResolver} from "./identity/hcs/vc/hcs-vc-status-resolver"; 24 | import {HcsVcTopicListener} from "./identity/hcs/vc/hcs-vc-topic-listener"; 25 | import {HederaDid} from "./identity/hedera-did"; 26 | import {JsonClass} from "./identity/hcs/json-class"; 27 | import {MessageEnvelope} from "./identity/hcs/message-envelope"; 28 | import {MessageListener} from "./identity/hcs/message-listener"; 29 | import {MessageMode} from "./identity/hcs/message-mode"; 30 | import {MessageResolver} from "./identity/hcs/message-resolver"; 31 | import {MessageTransaction} from "./identity/hcs/message-transaction"; 32 | import {Message} from "./identity/hcs/message"; 33 | import {SerializableMirrorConsensusResponse} from "./identity/hcs/serializable-mirror-consensus-response"; 34 | import {TimestampUtils} from "./utils/timestamp-utils"; 35 | import {Validator} from "./utils/validator"; 36 | import {Issuer} from "./identity/hcs/vc/issuer"; 37 | 38 | export { 39 | AddressBook, 40 | ArraysUtils, 41 | CredentialSubject, 42 | DidDocumentBase, 43 | DidDocumentJsonProperties, 44 | DidMethodOperation, 45 | DidParser, 46 | DidSyntax, 47 | Hashing, 48 | HcsDid, 49 | HcsDidMessage, 50 | HcsDidResolver, 51 | HcsDidRootKey, 52 | HcsDidTopicListener, 53 | HcsDidTransaction, 54 | HcsIdentityNetwork, 55 | HcsIdentityNetworkBuilder, 56 | HcsVcDocumentBase, 57 | HcsVcDocumentHashBase, 58 | HcsVcDocumentJsonProperties, 59 | HcsVcMessage, 60 | HcsVcOperation, 61 | HcsVcStatusResolver, 62 | HcsVcTopicListener, 63 | HederaDid, 64 | JsonClass, 65 | Message, 66 | MessageEnvelope, 67 | MessageListener, 68 | MessageMode, 69 | MessageResolver, 70 | MessageTransaction, 71 | SerializableMirrorConsensusResponse, 72 | TimestampUtils, 73 | Validator, 74 | Issuer 75 | } 76 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashgraph/did-sdk-js/fd8ad26b1ab282e55a7b963258c84540133a8362/src/typings.d.ts -------------------------------------------------------------------------------- /src/utils/arrays-utils.ts: -------------------------------------------------------------------------------- 1 | 2 | export class ArraysUtils { 3 | public static equals(a: Uint8Array, b: Uint8Array): boolean { 4 | if (a == b) { 5 | return true 6 | } 7 | if (!a || !b) { 8 | return false 9 | } 10 | if (a.length != b.length) { 11 | return false 12 | } 13 | for (let i = 0; i < a.length; i++) { 14 | if (a[i] != b[i]) return false; 15 | } 16 | return true; 17 | } 18 | 19 | public static toString(array: number[] | Uint8Array): string { 20 | return Buffer.from(array).toString("utf8"); 21 | } 22 | 23 | public static fromString(text: string): Uint8Array { 24 | return new Uint8Array(Buffer.from(text, "utf8")) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/hashing.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from "crypto"; 2 | import bs58 from "bs58"; 3 | import { Base64 } from "js-base64"; 4 | 5 | export class Hashing { 6 | public static readonly base58 = { 7 | encode: function (data: Uint8Array): string { 8 | return bs58.encode(data); 9 | }, 10 | decode: function (data: string): Uint8Array { 11 | return bs58.decode(data); 12 | } 13 | } 14 | public static readonly sha256 = { 15 | digest: function (data: Uint8Array | string): Uint8Array { 16 | const sha256 = crypto 17 | .createHash('sha256') // may need to change in the future. 18 | .update(data) 19 | .digest(); 20 | return sha256; 21 | } 22 | } 23 | 24 | public static readonly base64 = { 25 | decode: function (encodedString: string): string { 26 | return Base64.fromBase64(encodedString);; 27 | }, 28 | encode: function (decodedBytes: string): string { 29 | return Base64.toBase64(decodedBytes); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/sleep.ts: -------------------------------------------------------------------------------- 1 | export function Sleep(sleepTime: number): Promise { 2 | return new Promise(resolve => { 3 | setTimeout(() => { 4 | resolve(); 5 | }, sleepTime) 6 | }) 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/timestamp-utils.ts: -------------------------------------------------------------------------------- 1 | import { Timestamp } from "@hashgraph/sdk"; 2 | import moment from 'moment'; 3 | 4 | export class TimestampUtils { 5 | public static ISO = "YYYY-MM-DDTHH:mm:ss.SSS[Z]"; 6 | public static ISO8601 = "YYYY-MM-DDTHH:mm:ss[Z]"; 7 | 8 | public static toJSON(item: Timestamp, format: string = this.ISO): string { 9 | const d = item.toDate(); 10 | return moment(d).utc().format(format); 11 | } 12 | 13 | public static fromJson(json: string, format: string = this.ISO): Timestamp { 14 | const d = moment.utc(json, format).toDate(); 15 | return Timestamp.fromDate(d); 16 | } 17 | 18 | public static now(): Timestamp { 19 | return Timestamp.fromDate(new Date()); 20 | } 21 | 22 | public static equals(a: Timestamp, b: Timestamp): boolean { 23 | if (a == b) { 24 | return true; 25 | } 26 | if (!a || !b) { 27 | return false; 28 | } 29 | return a.seconds.equals(b.seconds) && a.nanos.equals(b.nanos); 30 | } 31 | 32 | public static lessThan(a: Timestamp, b: Timestamp): boolean { 33 | if (a == b) { 34 | return false; 35 | } 36 | if (!a || !b) { 37 | return false; 38 | } 39 | if (a.seconds.equals(b.seconds)) { 40 | return a.nanos.lessThan(b.nanos); 41 | } 42 | a.seconds.lessThan(b.seconds); 43 | } 44 | } -------------------------------------------------------------------------------- /src/utils/validator.ts: -------------------------------------------------------------------------------- 1 | export class Validator { 2 | protected validationErrors: string[]; 3 | 4 | public addValidationError(errorMessage: string): void { 5 | if (!this.validationErrors) { 6 | this.validationErrors = []; 7 | } 8 | this.validationErrors.push(errorMessage); 9 | } 10 | 11 | public checkValidationErrors(prologue: string, validationFunction: (input: Validator) => void): void { 12 | this.validationErrors = null; 13 | 14 | validationFunction(this); 15 | 16 | if (!this.validationErrors) { 17 | return; 18 | } 19 | 20 | const errors = this.validationErrors; 21 | this.validationErrors = null; 22 | 23 | throw new Error(prologue + ':\n' + errors.join('\n')); 24 | } 25 | 26 | public require(condition: boolean, errorMessage: string): void { 27 | if (!condition) { 28 | this.addValidationError(errorMessage); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/aes-encryption-util.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | 3 | const encrypt = function (plainText, key, outputEncoding = "base64") { 4 | const cipher = crypto.createCipheriv("aes-128-ecb", crypto.createHash('sha256').update(String(key)).digest('base64').substr(0, 16), null); 5 | return Buffer.concat([cipher.update(plainText, 'utf8'), cipher.final()]).toString(outputEncoding); 6 | } 7 | 8 | const decrypt = function (cipherText, key, outputEncoding = "utf8") { 9 | const cipher = crypto.createDecipheriv("aes-128-ecb", crypto.createHash('sha256').update(String(key)).digest('base64').substr(0, 16), null); 10 | return Buffer.concat([cipher.update(cipherText, 'base64'), cipher.final()]).toString(outputEncoding); 11 | 12 | } 13 | 14 | exports.encrypt = encrypt; 15 | exports.decrypt = decrypt; -------------------------------------------------------------------------------- /test/did-document-base.js: -------------------------------------------------------------------------------- 1 | const { 2 | HcsDid, 3 | DidDocumentBase, 4 | DidDocumentJsonProperties, 5 | DidSyntax, 6 | HcsDidRootKey 7 | } = require("../dist"); 8 | const {FileId} = require("@hashgraph/sdk"); 9 | const bs58 = require('bs58'); 10 | const {expect, assert} = require('chai'); 11 | 12 | const network = 'testnet'; 13 | 14 | describe("DidDocumentBase", function() { 15 | it('Test Serialization', async function() { 16 | const privateKey = HcsDid.generateDidRootKey(); 17 | const did = new HcsDid(network, privateKey.publicKey, FileId.fromString('0.0.1')); 18 | const doc = did.generateDidDocument(); 19 | 20 | const didJson = doc.toJSON(); 21 | 22 | const root = JSON.parse(didJson); 23 | 24 | expect(root).to.have.keys([ 25 | DidDocumentJsonProperties.CONTEXT, 26 | DidDocumentJsonProperties.ID, 27 | DidDocumentJsonProperties.PUBLIC_KEY, 28 | DidDocumentJsonProperties.AUTHENTICATION, 29 | ]); 30 | assert.equal(root[DidDocumentJsonProperties.CONTEXT], DidSyntax.DID_DOCUMENT_CONTEXT); 31 | assert.equal(root[DidDocumentJsonProperties.ID], did.toDid()); 32 | 33 | const didRootKey = root[DidDocumentJsonProperties.PUBLIC_KEY][0]; 34 | assert.equal(didRootKey['type'], HcsDidRootKey.DID_ROOT_KEY_TYPE); 35 | assert.equal(didRootKey[DidDocumentJsonProperties.ID], did.toDid() + HcsDidRootKey.DID_ROOT_KEY_NAME); 36 | assert.equal(didRootKey['controller'], did.toDid()); 37 | assert.equal(didRootKey['publicKeyBase58'], bs58.encode(privateKey.publicKey.toBytes())); 38 | }); 39 | 40 | it('Test Deserialization', async function() { 41 | const privateKey = HcsDid.generateDidRootKey(); 42 | const did = new HcsDid(network, privateKey.publicKey, FileId.fromString('0.0.1')); 43 | const doc = did.generateDidDocument(); 44 | 45 | const didJson = doc.toJSON(); 46 | 47 | const parsedDoc = DidDocumentBase.fromJson(didJson); 48 | assert.equal(parsedDoc.getId(), doc.getId()); 49 | 50 | const didRootKey = parsedDoc.getDidRootKey(); 51 | assert.exists(didRootKey); 52 | assert.equal(didRootKey.getPublicKeyBase58(), doc.getDidRootKey().getPublicKeyBase58()); 53 | assert.equal(didRootKey.getController(), doc.getDidRootKey().getController()); 54 | assert.equal(didRootKey.getId(), doc.getDidRootKey().getId()); 55 | assert.equal(didRootKey.getType(), doc.getDidRootKey().getType()); 56 | }); 57 | 58 | it('Test Invalid Deserialization', async function() { 59 | const didJson = "{" 60 | + " \"@context\": \"https://www.w3.org/ns/did/v1\"," 61 | + " \"id\": \"did:hedera:mainnet:7Prd74ry1Uct87nZqL3ny7aR7Cg46JamVbJgk8azVgUm;hedera:mainnet:fid=0.0.1\"," 62 | + " \"authentication\": [" 63 | + " \"did:hedera:mainnet:7Prd74ry1Uct87nZqL3ny7aR7Cg46JamVbJgk8azVgUm;hedera:mainnet:fid=0.0.1#did-root-key\"" 64 | + " ]," 65 | + " \"publicKey\":\"invalidPublicKey\"," 66 | + " \"service\": [" 67 | + " {" 68 | + " \"id\":\"did:hedera:mainnet:7Prd74ry1Uct87nZqL3ny7aR7Cg46JamVbJgk8azVgUm;hedera:mainnet:fid=0.0.1#vcs\"," 69 | + " \"type\": \"VerifiableCredentialService\"," 70 | + " \"serviceEndpoint\": \"https://example.com/vc/\"" 71 | + " }" 72 | + " ]" 73 | + "}"; 74 | 75 | assert.throw(() => { 76 | DidDocumentBase.fromJson(didJson); 77 | }); 78 | }); 79 | 80 | it('Test Incomplete Json Deserialization', async function() { 81 | const didJsonMissingPublicKeys = "{" 82 | + " \"@context\": \"https://www.w3.org/ns/did/v1\"," 83 | + " \"id\": \"did:hedera:mainnet:7Prd74ry1Uct87nZqL3ny7aR7Cg46JamVbJgk8azVgUm;hedera:mainnet:fid=0.0.1\"," 84 | + " \"authentication\": [" 85 | + " \"did:hedera:mainnet:7Prd74ry1Uct87nZqL3ny7aR7Cg46JamVbJgk8azVgUm;hedera:mainnet:fid=0.0.1#did-root-key\"" 86 | + " ]" 87 | + "}"; 88 | 89 | const didJsonMissingRootKey = "{" 90 | + " \"@context\": \"https://www.w3.org/ns/did/v1\"," 91 | + " \"id\": \"did:hedera:mainnet:7Prd74ry1Uct87nZqL3ny7aR7Cg46JamVbJgk8azVgUm;hedera:mainnet:fid=0.0.1\"," 92 | + " \"authentication\": [" 93 | + " \"did:hedera:mainnet:7Prd74ry1Uct87nZqL3ny7aR7Cg46JamVbJgk8azVgUm;hedera:mainnet:fid=0.0.1#did-root-key\"" 94 | + " ]," 95 | + " \"publicKey\": [" 96 | + " {" 97 | + " \"id\": \"did:hedera:mainnet:7Prd74ry1Uct87nZqL3ny7aR7Cg46JamVbJgk8azVgUm;hedera:mainnet:fid=0.0.1#key-1\"," 98 | + " \"type\": \"Ed25519VerificationKey2018\"," 99 | + " \"publicKeyBase58\": \"H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV\"" 100 | + " }" 101 | + " ]," 102 | + " \"service\": [" 103 | + " {" 104 | + " \"id\": \"did:hedera:mainnet:7Prd74ry1Uct87nZqL3ny7aR7Cg46JamVbJgk8azVgUm;hedera:mainnet:fid=0.0.1#vcs\"," 105 | + " \"type\": \"VerifiableCredentialService\"," 106 | + " \"serviceEndpoint\": \"https://example.com/vc/\"" 107 | + " }" 108 | + " ]" 109 | + "}"; 110 | 111 | const didJsonMissingPublicKeyId = "{" 112 | + " \"@context\": \"https://www.w3.org/ns/did/v1\"," 113 | + " \"id\": \"did:hedera:mainnet:7Prd74ry1Uct87nZqL3ny7aR7Cg46JamVbJgk8azVgUm;hedera:mainnet:fid=0.0.1\"," 114 | + " \"authentication\": [" 115 | + " \"did:hedera:mainnet:7Prd74ry1Uct87nZqL3ny7aR7Cg46JamVbJgk8azVgUm;hedera:mainnet:fid=0.0.1#did-root-key\"" 116 | + " ]," 117 | + " \"publicKey\": [" 118 | + " {" 119 | + " \"type\": \"Ed25519VerificationKey2018\"," 120 | + " \"publicKeyBase58\": \"H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV\"" 121 | + " }" 122 | + " ]," 123 | + " \"service\": [" 124 | + " {" 125 | + " \"id\": \"did:hedera:mainnet:7Prd74ry1Uct87nZqL3ny7aR7Cg46JamVbJgk8azVgUm;hedera:mainnet:fid=0.0.1#vcs\"," 126 | + " \"type\": \"VerifiableCredentialService\"," 127 | + " \"serviceEndpoint\": \"https://example.com/vc/\"" 128 | + " }" 129 | + " ]" 130 | + "}"; 131 | 132 | let doc = DidDocumentBase.fromJson(didJsonMissingPublicKeys); 133 | assert.exists(doc); 134 | assert.notExists(doc.getDidRootKey()); 135 | 136 | doc = DidDocumentBase.fromJson(didJsonMissingRootKey); 137 | assert.exists(doc); 138 | assert.notExists(doc.getDidRootKey()); 139 | 140 | doc = DidDocumentBase.fromJson(didJsonMissingPublicKeyId); 141 | assert.exists(doc); 142 | assert.notExists(doc.getDidRootKey()); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /test/did/hcs-did-message.js: -------------------------------------------------------------------------------- 1 | const {encrypt, decrypt} = require("../aes-encryption-util"); 2 | const { 3 | FileId, 4 | TopicId, 5 | } = require('@hashgraph/sdk'); 6 | const { 7 | HcsDidMessage, 8 | MessageEnvelope, 9 | DidMethodOperation, 10 | HcsDid, 11 | ArraysUtils 12 | } = require("../../dist"); 13 | 14 | const {assert} = require('chai'); 15 | 16 | const network = 'network'; 17 | const ADDRESS_BOOK_FID = FileId.fromString('0.0.1'); 18 | const DID_TOPIC_ID1 = TopicId.fromString('0.0.2'); 19 | const DID_TOPIC_ID2 = TopicId.fromString('0.0.3'); 20 | 21 | describe('HcsDidMessage', function() { 22 | it('Test Valid Message', async function() { 23 | const privateKey = HcsDid.generateDidRootKey(); 24 | 25 | const did = new HcsDid(network, privateKey.publicKey, ADDRESS_BOOK_FID); 26 | const doc = did.generateDidDocument(); 27 | const didJson = doc.toJSON(); 28 | const originalEnvelope = HcsDidMessage.fromDidDocumentJson(didJson, DidMethodOperation.CREATE); 29 | const message = originalEnvelope.sign(msg => privateKey.sign(msg)); 30 | 31 | const envelope = MessageEnvelope.fromJson(Buffer.from(message).toString("utf8"), HcsDidMessage); 32 | 33 | assert.isTrue(envelope.isSignatureValid(e => e.open().extractDidRootKey())); 34 | assert.isTrue(envelope.open().isValid(DID_TOPIC_ID1)); 35 | assert.deepEqual(originalEnvelope.open().getTimestamp(), envelope.open().getTimestamp()); 36 | }); 37 | 38 | it('Test Encrypted Message', async function() { 39 | const secret = 'Secret encryption password'; 40 | 41 | const privateKey = HcsDid.generateDidRootKey(); 42 | const did = new HcsDid(network, privateKey.publicKey, ADDRESS_BOOK_FID); 43 | const doc = did.generateDidDocument(); 44 | const didJson = doc.toJSON(); 45 | 46 | const originalEnvelope = HcsDidMessage.fromDidDocumentJson(didJson, DidMethodOperation.CREATE); 47 | const encryptedMsg = originalEnvelope.encrypt(HcsDidMessage.getEncrypter(m => encrypt(m, secret))); 48 | const encryptedSignedMsg = MessageEnvelope.fromJson(ArraysUtils.toString(encryptedMsg.sign(m => privateKey.sign(m))), HcsDidMessage); 49 | 50 | assert.exists(encryptedSignedMsg); 51 | assert.throw(() => {encryptedSignedMsg.open()}); 52 | 53 | const decryptedMsg = await encryptedSignedMsg.open(HcsDidMessage.getDecrypter((m, t) => decrypt(m, secret))); 54 | 55 | assert.exists(decryptedMsg); 56 | assert.equal(originalEnvelope.open().getDidDocumentBase64(), decryptedMsg.getDidDocumentBase64()); 57 | assert.equal(originalEnvelope.open().getDid(), decryptedMsg.getDid()); 58 | }); 59 | 60 | it('Test Invalid Did', async function() { 61 | const privateKey = HcsDid.generateDidRootKey(); 62 | const did = new HcsDid(network, privateKey.publicKey, ADDRESS_BOOK_FID); 63 | const doc = did.generateDidDocument(); 64 | 65 | const didJson = doc.toJSON(); 66 | const message = HcsDidMessage.fromDidDocumentJson(didJson, DidMethodOperation.CREATE).sign(msg => privateKey.sign(msg)); 67 | const msg = MessageEnvelope.fromJson(Buffer.from(message).toString("utf8"), HcsDidMessage).open(); 68 | 69 | const differentDid = new HcsDid(network, HcsDid.generateDidRootKey().publicKey, ADDRESS_BOOK_FID); 70 | msg.did = differentDid.toDid(); 71 | 72 | assert.isFalse(msg.isValid()); 73 | }); 74 | 75 | it('Test Invalid Topic', async function() { 76 | const privateKey = HcsDid.generateDidRootKey(); 77 | const did = new HcsDid(network, privateKey.publicKey, ADDRESS_BOOK_FID, DID_TOPIC_ID1); 78 | const doc = did.generateDidDocument(); 79 | 80 | const didJson = doc.toJSON(); 81 | const message = HcsDidMessage.fromDidDocumentJson(didJson, DidMethodOperation.CREATE).sign(msg => privateKey.sign(msg)); 82 | const msg = await MessageEnvelope.fromJson(Buffer.from(message).toString("utf8"), HcsDidMessage).open(); 83 | 84 | assert.isTrue(msg.isValid(DID_TOPIC_ID1)); 85 | assert.isFalse(msg.isValid(DID_TOPIC_ID2)); 86 | }); 87 | 88 | it('Test Missing Data', async function() { 89 | const privateKey = HcsDid.generateDidRootKey(); 90 | const did = new HcsDid(network, privateKey.publicKey, ADDRESS_BOOK_FID, DID_TOPIC_ID1); 91 | const doc = did.generateDidDocument(); 92 | const operation = DidMethodOperation.CREATE; 93 | 94 | const didJson = doc.toJSON(); 95 | const message = HcsDidMessage.fromDidDocumentJson(didJson, DidMethodOperation.CREATE).sign(msg => privateKey.sign(msg)); 96 | 97 | const validMsg = MessageEnvelope.fromJson(Buffer.from(message).toString("utf8"), HcsDidMessage).open(); 98 | 99 | let msg = new HcsDidMessage(operation, null, validMsg.getDidDocumentBase64()); 100 | assert.isFalse(msg.isValid()); 101 | 102 | msg = new HcsDidMessage(operation, validMsg.getDid(), null); 103 | assert.isFalse(msg.isValid()); 104 | assert.notExists(msg.getDidDocument()); 105 | assert.exists(msg.getDid()); 106 | assert.equal(operation, msg.getOperation()); 107 | }); 108 | 109 | it('Test Invalid Signature', async function() { 110 | const privateKey = HcsDid.generateDidRootKey(); 111 | const did = new HcsDid(network, privateKey.publicKey, ADDRESS_BOOK_FID, DID_TOPIC_ID1); 112 | const doc = did.generateDidDocument(); 113 | 114 | const didJson = doc.toJSON(); 115 | const message = HcsDidMessage.fromDidDocumentJson(didJson, DidMethodOperation.CREATE).sign(msg => HcsDid.generateDidRootKey().sign(msg)); 116 | const envelope = MessageEnvelope.fromJson(Buffer.from(message).toString("utf8"), HcsDidMessage); 117 | 118 | assert.isFalse(envelope.isSignatureValid(e => e.open().extractDidRootKey())); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /test/did/hcs-did-method-operations.js: -------------------------------------------------------------------------------- 1 | const {DidMethodOperation, DidDocumentJsonProperties} = require("../../dist"); 2 | const {NetworkReadyTestBase} = require("../network-ready-test-base"); 3 | 4 | const {assert} = require('chai'); 5 | 6 | const testBase = new NetworkReadyTestBase(); 7 | 8 | let hcsDid, 9 | didDocument; 10 | 11 | const EXPECT_NO_ERROR = function(err) { 12 | throw err; 13 | } 14 | 15 | describe('Hcs Did Method Operations', function() { 16 | before(async function() { 17 | this.timeout(60000); 18 | await testBase.setup(); 19 | hcsDid = testBase.didNetwork.generateDid(false); 20 | didDocument = hcsDid.generateDidDocument().toJSON(); 21 | }); 22 | 23 | after(async function() { 24 | testBase.cleanup(); 25 | }); 26 | 27 | it('Test Create', async function() { 28 | this.timeout(60000); 29 | const op = DidMethodOperation.CREATE; 30 | 31 | const envelope = await testBase.sendDidTransaction(hcsDid, didDocument, op, EXPECT_NO_ERROR); 32 | assert.exists(envelope); 33 | 34 | const msg = envelope.open(); 35 | assert.exists(msg); 36 | assert.exists(msg.getDidDocument()); 37 | 38 | assert.equal(hcsDid.toDid(), msg.getDid()); 39 | assert.isTrue(msg.isValid()); 40 | assert.equal(DidMethodOperation.CREATE, msg.getOperation()); 41 | }); 42 | 43 | it('Test Resolve After Create', async function() { 44 | this.timeout(60000); 45 | const didString = hcsDid.toDid(); 46 | 47 | const envelope = await testBase.resolveDid(didString, EXPECT_NO_ERROR); 48 | const msg = envelope.open(); 49 | 50 | assert.exists(msg); 51 | assert.equal(didString, msg.getDid()); 52 | assert.isTrue(msg.isValid()); 53 | assert.equal(DidMethodOperation.CREATE, msg.getOperation()); 54 | }); 55 | 56 | it('Test Update', async function() { 57 | this.timeout(60000); 58 | const rootObject = JSON.parse(didDocument); 59 | 60 | const publicKeys = rootObject[DidDocumentJsonProperties.PUBLIC_KEY]; 61 | publicKeys.push(JSON.parse("{" 62 | + "\"id\": \"did:example:123456789abcdefghi#keys-2\"," 63 | + "\"type\": \"Ed25519VerificationKey2018\"," 64 | + "\"controller\": \"did:example:pqrstuvwxyz0987654321\"," 65 | + "\"publicKeyBase58\": \"H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV\"" 66 | + "}")); 67 | rootObject[DidDocumentJsonProperties.PUBLIC_KEY] = publicKeys; 68 | 69 | const newDoc = JSON.stringify(rootObject); 70 | 71 | const operation = DidMethodOperation.UPDATE; 72 | const envelope = await testBase.sendDidTransaction(hcsDid, newDoc, operation, EXPECT_NO_ERROR); 73 | assert.exists(envelope); 74 | 75 | const msg = envelope.open(); 76 | 77 | assert.exists(msg); 78 | assert.equal(newDoc, msg.getDidDocument()); 79 | assert.equal(operation, msg.getOperation()); 80 | }); 81 | 82 | it('Test Resolve After Update', async function() { 83 | this.timeout(60000); 84 | const didString = hcsDid.toDid(); 85 | 86 | const envelope = await testBase.resolveDid(didString, EXPECT_NO_ERROR); 87 | assert.exists(envelope); 88 | 89 | const msg = envelope.open(); 90 | 91 | assert.exists(msg); 92 | assert.equal(didString, msg.getDid()); 93 | assert.isTrue(msg.isValid()); 94 | assert.equal(DidMethodOperation.UPDATE, msg.getOperation()); 95 | assert.notEqual(didDocument, msg.getDidDocument()); 96 | }); 97 | 98 | it('Test Delete', async function() { 99 | this.timeout(60000); 100 | const rootObject = JSON.parse(didDocument); 101 | 102 | if(rootObject.hasOwnProperty(DidDocumentJsonProperties.AUTHENTICATION)) { 103 | rootObject[DidDocumentJsonProperties.AUTHENTICATION] = []; 104 | } 105 | 106 | const deletedDoc = JSON.stringify(rootObject); 107 | 108 | const operation = DidMethodOperation.DELETE; 109 | const envelope = await testBase.sendDidTransaction(hcsDid, deletedDoc, operation, EXPECT_NO_ERROR); 110 | assert.exists(envelope); 111 | 112 | const msg = envelope.open(); 113 | 114 | assert.exists(msg); 115 | assert.equal(deletedDoc, msg.getDidDocument()); 116 | assert.equal(operation, msg.getOperation()); 117 | }); 118 | 119 | it('Test Resolve After Delete', async function() { 120 | this.timeout(60000); 121 | const didString = hcsDid.toDid(); 122 | 123 | const envelope = await testBase.resolveDid(didString, EXPECT_NO_ERROR); 124 | assert.exists(envelope); 125 | 126 | const msg = envelope.open(); 127 | 128 | assert.exists(msg); 129 | assert.equal(didString, msg.getDid()); 130 | assert.isTrue(msg.isValid()); 131 | assert.equal(DidMethodOperation.DELETE, msg.getOperation()); 132 | assert.notEqual(didDocument, msg.getDidDocument()); 133 | }); 134 | 135 | it('Test Resolve After Delete And Another Invalid Submit', async function() { 136 | this.timeout(60000); 137 | await testBase.sendDidTransaction(hcsDid, didDocument, DidMethodOperation.UPDATE, EXPECT_NO_ERROR); 138 | 139 | const didString = hcsDid.toDid(); 140 | const envelope = await testBase.resolveDid(didString, EXPECT_NO_ERROR); 141 | assert.exists(envelope); 142 | 143 | const msg = envelope.open(); 144 | 145 | assert.exists(msg); 146 | assert.equal(didString, msg.getDid()); 147 | assert.isTrue(msg.isValid()); 148 | assert.equal(DidMethodOperation.DELETE, msg.getOperation()); 149 | assert.notEqual(didDocument, msg.getDidDocument()); 150 | }); 151 | }) 152 | -------------------------------------------------------------------------------- /test/did/hcs-did-root-key.js: -------------------------------------------------------------------------------- 1 | const { 2 | FileId 3 | } = require('@hashgraph/sdk'); 4 | const bs58 = require('bs58'); 5 | const { 6 | HcsDid, 7 | HcsDidRootKey 8 | } = require("../../dist"); 9 | 10 | const {assert} = require('chai'); 11 | 12 | const network = 'network'; 13 | 14 | describe('HcsDidRootKey', function() { 15 | it('Test Generate', async function() { 16 | const addressBook = '0.0.1'; 17 | 18 | const privateKey = HcsDid.generateDidRootKey(); 19 | 20 | const did = new HcsDid(network, privateKey.publicKey, FileId.fromString(addressBook)); 21 | 22 | assert.throw(() => {HcsDidRootKey.fromHcsIdentity(null, null);}); 23 | assert.throw(() => {HcsDidRootKey.fromHcsIdentity(did, null);}); 24 | assert.throw(() => {HcsDidRootKey.fromHcsIdentity(null, privateKey.publicKey);}); 25 | 26 | const differentPublicKey = HcsDid.generateDidRootKey().publicKey; 27 | assert.throw(() => {HcsDidRootKey.fromHcsIdentity(did, differentPublicKey);}); 28 | 29 | const didRootKey = HcsDidRootKey.fromHcsIdentity(did, privateKey.publicKey); 30 | assert.exists(didRootKey); 31 | 32 | assert.equal(didRootKey.getType(), 'Ed25519VerificationKey2018'); 33 | assert.equal(didRootKey.getId(), did.toDid() + HcsDidRootKey.DID_ROOT_KEY_NAME); 34 | assert.equal(didRootKey.getController(), did.toDid()); 35 | assert.equal(didRootKey.getPublicKeyBase58(), bs58.encode(privateKey.publicKey.toBytes())); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/did/hcs-did.js: -------------------------------------------------------------------------------- 1 | const { 2 | FileId, 3 | TopicId 4 | } = require('@hashgraph/sdk'); 5 | const bs58 = require('bs58'); 6 | const { 7 | DidSyntax, 8 | HcsDid 9 | } = require("../../dist"); 10 | 11 | const {assert} = require('chai'); 12 | 13 | const network = 'network'; 14 | 15 | describe('HcsDid', function() { 16 | it('Test Generate And Parse Did Without Tid', async function() { 17 | const addressBook = '0.0.24352'; 18 | 19 | const privKey = HcsDid.generateDidRootKey(); 20 | const pubKey = privKey.publicKey; 21 | 22 | const did = new HcsDid(network, pubKey, FileId.fromString(addressBook)); 23 | 24 | const didString = did.toString(); 25 | 26 | assert.exists(didString); 27 | 28 | const parsedDid = HcsDid.fromString(didString); 29 | 30 | assert.exists(parsedDid); 31 | assert.exists(parsedDid.getAddressBookFileId()); 32 | 33 | assert.notExists(parsedDid.getDidTopicId()); 34 | 35 | assert.equal(parsedDid.toString(), didString); 36 | assert.equal(parsedDid.getMethod(), DidSyntax.Method.HEDERA_HCS); 37 | assert.equal(parsedDid.getNetwork(), network); 38 | assert.equal(parsedDid.getAddressBookFileId().toString(), addressBook); 39 | assert.equal(parsedDid.getIdString(), did.getIdString()); 40 | }); 41 | 42 | it('Test Generate And Parse Did With Tid', async function() { 43 | const addressBook = '0.0.24352'; 44 | const didTopicId = '1.5.23462345'; 45 | 46 | const privateKey = HcsDid.generateDidRootKey(); 47 | 48 | const fileId = FileId.fromString(addressBook); 49 | const topicId = TopicId.fromString(didTopicId); 50 | const did = new HcsDid(network, privateKey.publicKey, fileId, topicId); 51 | 52 | const didString = did.toString(); 53 | 54 | assert.exists(didString); 55 | 56 | const parsedDid = HcsDid.fromString(didString); 57 | 58 | assert.exists(parsedDid); 59 | assert.exists(parsedDid.getAddressBookFileId()); 60 | assert.exists(parsedDid.getDidTopicId()); 61 | 62 | assert.equal(parsedDid.toDid(), didString); 63 | assert.equal(parsedDid.getMethod(), DidSyntax.Method.HEDERA_HCS); 64 | assert.equal(parsedDid.getNetwork(), network); 65 | assert.equal(parsedDid.getAddressBookFileId().toString(), addressBook); 66 | assert.equal(parsedDid.getDidTopicId().toString(), didTopicId); 67 | assert.equal(parsedDid.getIdString(), did.getIdString()); 68 | 69 | const parsedDocument = parsedDid.generateDidDocument(); 70 | 71 | assert.exists(parsedDocument); 72 | assert.equal(parsedDocument.getId(), parsedDid.toString()); 73 | assert.equal(parsedDocument.getContext(), DidSyntax.DID_DOCUMENT_CONTEXT); 74 | assert.notExists(parsedDocument.getDidRootKey()); 75 | 76 | const document = did.generateDidDocument(); 77 | 78 | assert.exists(document); 79 | assert.equal(document.getId(), parsedDid.toString()); 80 | assert.equal(document.getContext(), DidSyntax.DID_DOCUMENT_CONTEXT); 81 | assert.exists(document.getDidRootKey()); 82 | assert.equal(document.getDidRootKey().getPublicKeyBase58(), bs58.encode(privateKey.publicKey.toBytes())); 83 | }); 84 | 85 | it('Test Parse Predefined Dids', async function() { 86 | const addressBook = '0.0.24352'; 87 | const didTopicId = '1.5.23462345'; 88 | 89 | const validDidWithSwitchedParamsOrder = "did:hedera:testnet:8LjUL78kFVnWV9rFnNCTE5bZdRmjm2obqJwS892jVLak" 90 | + ";hedera:testnet:tid=" + didTopicId 91 | + ";hedera:testnet:fid=" + addressBook; 92 | 93 | const invalidDids = [ 94 | null, 95 | "invalidDid1", 96 | "did:invalid", 97 | "did:invalidMethod:8LjUL78kFVnWV9rFnNCTE5bZdRmjm2obqJwS892jVLak;hedera:testnet:fid=0.0.24352", 98 | "did:hedera:invalidNetwork:8LjUL78kFVnWV9rFnNCTE5bZdRmjm2obqJwS892jVLak;hedera:testnet:fid=0.0.24352", 99 | "did:hedera:testnet:invalidAddress;hedera:testnet:fid=0.0.24352;hedera:testnet:tid=1.5.23462345", 100 | "did:hedera:testnet;hedera:testnet:fid=0.0.24352;hedera:testnet:tid=1.5.23462345", 101 | "did:hedera:testnet:8LjUL78kFVnWV9rFnNCTE5bZdRmjm2obqJwS892jVLak;missing:fid=0.0.24352;" 102 | + "hedera:testnet:tid=1.5.2", 103 | "did:hedera:testnet:8LjUL78kFVnWV9rFnNCTE5bZdRmjm2obqJwS892jVLak;missing:fid=0.0.1;" 104 | + "hedera:testnet:tid=1.5.2;unknown:parameter=1", 105 | "did:hedera:testnet:8LjUL78kFVnWV9rFnNCTE5bZdRmjm2obqJwS892jVLak;hedera:testnet:fid=0.0.1=1", 106 | "did:hedera:testnet:8LjUL78kFVnWV9rFnNCTE5bZdRmjm2obqJwS892jVLak;hedera:testnet:fid", 107 | "did:hedera:testnet:8LjUL78kFVnWV9rFnNCTE5bZdRmjm2obqJwS892jVLak:unknownPart;hedera:testnet:fid=0.0.1", 108 | "did:notHedera:testnet:8LjUL78kFVnWV9rFnNCTE5bZdRmjm2obqJwS892jVLak;hedera:testnet:fid=0.0.1" 109 | ]; 110 | 111 | for (let did of invalidDids) { 112 | assert.throw(() => { 113 | HcsDid.fromString(did); 114 | }); 115 | } 116 | 117 | const validDid = HcsDid.fromString(validDidWithSwitchedParamsOrder); 118 | 119 | assert.exists(validDid); 120 | assert.exists(validDid.getAddressBookFileId()); 121 | assert.exists(validDid.getDidTopicId()); 122 | 123 | assert.equal(validDid.getAddressBookFileId().toString(), addressBook); 124 | assert.equal(validDid.getDidTopicId().toString(), didTopicId); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /test/hcs-identity-network.js: -------------------------------------------------------------------------------- 1 | const {OPERATOR_KEY, OPERATOR_ID, NETWORK} = require("./variables"); 2 | const { 3 | AccountId, 4 | PrivateKey, 5 | Client, 6 | FileCreateTransaction, 7 | Hbar, 8 | TopicInfoQuery 9 | } = require('@hashgraph/sdk'); 10 | 11 | const { 12 | AddressBook, 13 | HcsDid, 14 | HcsIdentityNetworkBuilder, 15 | HcsIdentityNetwork 16 | } = require("../dist"); 17 | 18 | const {assert} = require('chai'); 19 | 20 | const FEE = new Hbar(2); 21 | const ADDRESS_BOOK_JSON = "{\"appnetName\":\"Test Identity SDK appnet\",\"didTopicId\":\"0.0.214919\",\"vcTopicId\":\"0.0.214920\",\"appnetDidServers\":[\"http://localhost:3000/api/v1\"]}"; 22 | 23 | let client, 24 | operatorId, 25 | operatorKey, 26 | addressBookFileId, 27 | network; 28 | 29 | describe('HcsIdentityNetwork', function() { 30 | before(async function() { 31 | this.timeout(60000); 32 | 33 | operatorId = AccountId.fromString(OPERATOR_ID); 34 | operatorKey = PrivateKey.fromString(OPERATOR_KEY); 35 | network = NETWORK; 36 | client = Client.forTestnet(); 37 | client.setMirrorNetwork(["hcs." + network + ".mirrornode.hedera.com:5600"]); 38 | client.setOperator(operatorId, operatorKey); 39 | 40 | const response = await new FileCreateTransaction() 41 | .setContents(ADDRESS_BOOK_JSON) 42 | .setKeys([operatorKey.publicKey]) 43 | .setMaxTransactionFee(FEE) 44 | .execute(client); 45 | 46 | const receipt = await response.getReceipt(client); 47 | addressBookFileId = receipt.fileId; 48 | }); 49 | 50 | it('Test Create Identity Network', async function() { 51 | this.timeout(60000); 52 | const appnetName = 'Test Identity SDK appnet'; 53 | const didServerUrl = 'http://localhost:3000/api/v1'; 54 | const didTopicMemo = 'Test Identity SDK appnet DID topic'; 55 | const vcTopicMemo = 'Test Identity SDK appnet VC topic'; 56 | 57 | const didNetwork = await new HcsIdentityNetworkBuilder() 58 | .setNetwork(network) 59 | .setAppnetName(appnetName) 60 | .addAppnetDidServer(didServerUrl) 61 | .setPublicKey(operatorKey.publicKey) 62 | .setMaxTransactionFee(FEE) 63 | .setDidTopicMemo(didTopicMemo) 64 | .setVCTopicMemo(vcTopicMemo) 65 | .execute(client); 66 | 67 | assert.exists(didNetwork); 68 | assert.exists(didNetwork.getAddressBook()); 69 | 70 | const addressBook = didNetwork.getAddressBook(); 71 | assert.exists(addressBook.getDidTopicId()); 72 | assert.exists(addressBook.getVcTopicId()); 73 | assert.exists(addressBook.getAppnetDidServers()); 74 | assert.exists(addressBook.getFileId()); 75 | assert.equal(addressBook.getAppnetName(), appnetName); 76 | assert.equal(didNetwork.getNetwork(), network); 77 | 78 | const didTopicInfo = await new TopicInfoQuery() 79 | .setTopicId(didNetwork.getDidTopicId()) 80 | .execute(client); 81 | 82 | assert.exists(didTopicInfo); 83 | assert.equal(didTopicInfo.topicMemo, didTopicMemo); 84 | 85 | const vcTopicInfo = await new TopicInfoQuery() 86 | .setTopicId(didNetwork.getVcTopicId()) 87 | .execute(client); 88 | 89 | assert.exists(vcTopicInfo); 90 | assert.equal(vcTopicInfo.topicMemo, vcTopicMemo); 91 | 92 | const createdNetwork = await HcsIdentityNetwork.fromAddressBookFile(client, network, addressBook.getFileId()); 93 | assert.exists(createdNetwork); 94 | assert.equal(addressBook.toJSON(), createdNetwork.getAddressBook().toJSON()); 95 | }); 96 | 97 | it('Test Init Network From Json AddressBook', async function() { 98 | this.timeout(60000); 99 | const addressBook = AddressBook.fromJson(ADDRESS_BOOK_JSON, addressBookFileId); 100 | const didNetwork = HcsIdentityNetwork.fromAddressBook(network, addressBook); 101 | 102 | assert.exists(didNetwork); 103 | assert.exists(didNetwork.getAddressBook().getFileId()); 104 | assert.equal(didNetwork.getNetwork(), network); 105 | }); 106 | 107 | it('Test Init Network From Did', async function() { 108 | this.timeout(60000); 109 | const did = new HcsDid(network, HcsDid.generateDidRootKey().publicKey, addressBookFileId); 110 | 111 | const didNetwork = await HcsIdentityNetwork.fromHcsDid(client, did); 112 | 113 | assert.exists(didNetwork); 114 | assert.exists(didNetwork.getAddressBook().getFileId()); 115 | assert.equal(didNetwork.getNetwork(), network); 116 | assert.equal(ADDRESS_BOOK_JSON, didNetwork.getAddressBook().toJSON()); 117 | }); 118 | 119 | it('Test Generate Did For Network', async function() { 120 | this.timeout(60000); 121 | 122 | function checkTestGenerateDidForNetwork(did, publicKey, didTopicId, withTid) { 123 | assert.exists(did); 124 | assert.equal(HcsDid.publicKeyToIdString(publicKey), did.getIdString()); 125 | assert.equal(did.getNetwork(), network); 126 | assert.equal(did.getAddressBookFileId(), addressBookFileId); 127 | if (withTid) { 128 | assert.equal(did.getDidTopicId().toString(), didTopicId) 129 | } else { 130 | assert.notExists(did.getDidTopicId()); 131 | } 132 | assert.equal(did.getMethod(), HcsDid.DID_METHOD); 133 | } 134 | 135 | const addressBook = AddressBook.fromJson(ADDRESS_BOOK_JSON, addressBookFileId); 136 | const didNetwork = HcsIdentityNetwork.fromAddressBook(network, addressBook); 137 | 138 | let did = didNetwork.generateDid(true); 139 | assert.exists(did.getPrivateDidRootKey()); 140 | 141 | let publicKey = did.getPrivateDidRootKey().publicKey; 142 | checkTestGenerateDidForNetwork(did, publicKey, addressBook.getDidTopicId(), true); 143 | 144 | did = didNetwork.generateDid(false); 145 | assert.exists(did.getPrivateDidRootKey()); 146 | 147 | publicKey = did.getPrivateDidRootKey().publicKey; 148 | checkTestGenerateDidForNetwork(did, publicKey, addressBook.getDidTopicId(), false); 149 | 150 | did = didNetwork.generateDid(true); 151 | assert.exists(did.getPrivateDidRootKey()); 152 | publicKey = did.getPrivateDidRootKey().publicKey; 153 | checkTestGenerateDidForNetwork(did, publicKey, addressBook.getDidTopicId(), true); 154 | 155 | did = didNetwork.generateDid(false); 156 | assert.exists(did.getPrivateDidRootKey()); 157 | publicKey = did.getPrivateDidRootKey().publicKey; 158 | checkTestGenerateDidForNetwork(did, publicKey, addressBook.getDidTopicId(), false); 159 | 160 | publicKey = HcsDid.generateDidRootKey().publicKey; 161 | did = didNetwork.generateDid(publicKey, true); 162 | checkTestGenerateDidForNetwork(did, publicKey, addressBook.getDidTopicId(), true); 163 | 164 | publicKey = HcsDid.generateDidRootKey().publicKey; 165 | did = didNetwork.generateDid(publicKey, false); 166 | checkTestGenerateDidForNetwork(did, publicKey, addressBook.getDidTopicId(), false); 167 | }); 168 | }); 169 | -------------------------------------------------------------------------------- /test/network-ready-test-base.js: -------------------------------------------------------------------------------- 1 | const {OPERATOR_KEY, OPERATOR_ID, NETWORK} = require("./variables"); 2 | const { 3 | AccountId, 4 | PrivateKey, 5 | Client, 6 | FileId, 7 | Hbar 8 | } = require('@hashgraph/sdk'); 9 | 10 | const { 11 | AddressBook, 12 | HcsIdentityNetworkBuilder, 13 | HcsIdentityNetwork 14 | } = require("../dist"); 15 | 16 | const MIRROR_NODE_TIMEOUT = 30 * 1000; 17 | const NO_MORE_MESSAGES_TIMEOUT = 15 * 1000; 18 | const FEE = new Hbar(2); 19 | 20 | const EXISTING_ADDRESS_BOOK_FILE_ID = null; 21 | const EXISTING_ADDRESS_BOOK_JSON = null; 22 | 23 | const sleep = function (sleepTime) { 24 | return new Promise(resolve => { 25 | setTimeout(() => { 26 | resolve(); 27 | }, sleepTime) 28 | }) 29 | } 30 | 31 | const until = function (maxTime, untilFunction) { 32 | return new Promise((resolve, reject) => { 33 | let t, i; 34 | i = setInterval(() => { 35 | if (untilFunction()) { 36 | clearInterval(i); 37 | clearTimeout(t); 38 | resolve(); 39 | } 40 | }, 100); 41 | t = setTimeout(() => { 42 | clearInterval(i); 43 | clearTimeout(t); 44 | resolve(); 45 | }, maxTime) 46 | }) 47 | } 48 | 49 | 50 | /** 51 | * Base class for test classes that need a hedera identity network set up before running. 52 | */ 53 | class NetworkReadyTestBase { 54 | operatorId; 55 | operatorKey; 56 | network; 57 | didNetwork; 58 | client; 59 | 60 | /** 61 | * Initialize hedera clients and accounts. 62 | */ 63 | // BeforeAll 64 | async setup() { 65 | this.operatorId = AccountId.fromString(OPERATOR_ID); 66 | this.operatorKey = PrivateKey.fromString(OPERATOR_KEY); 67 | this.network = NETWORK; 68 | 69 | // Build Hedera testnet client 70 | switch (this.network.toUpperCase()) { 71 | case "MAINNET": 72 | this.client = Client.forMainnet(); 73 | break; 74 | case "TESTNET": 75 | this.client = Client.forTestnet(); 76 | break; 77 | case "PREVIEWNET": 78 | this.client = Client.forPreviewnet(); 79 | break; 80 | default: 81 | throw "Illegal argument for network."; 82 | } 83 | 84 | // Set the operator account ID and operator private key 85 | this.client.setOperator(this.operatorId, this.operatorKey); 86 | 87 | // If identity network is provided as environment variable read from there, otherwise setup new one: 88 | const abJson = EXISTING_ADDRESS_BOOK_JSON; 89 | const abFileId = EXISTING_ADDRESS_BOOK_FILE_ID; 90 | if (!(abJson) || !(abFileId)) { 91 | await this.setupIdentityNetwork(); 92 | } else { 93 | const addressBook = AddressBook.fromJson(abJson, FileId.fromString(abFileId)); 94 | this.didNetwork = HcsIdentityNetwork.fromAddressBook(this.network, addressBook); 95 | } 96 | } 97 | 98 | async setupIdentityNetwork() { 99 | const appnetName = "Test Identity SDK appnet"; 100 | const didServerUrl = "http://localhost:3000/api/v1"; 101 | const didTopicMemo = "Test Identity SDK appnet DID topic"; 102 | const vcTopicMemo = "Test Identity SDK appnet VC topic"; 103 | 104 | this.didNetwork = await new HcsIdentityNetworkBuilder() 105 | .setNetwork(this.network) 106 | .setAppnetName(appnetName) 107 | .addAppnetDidServer(didServerUrl) 108 | .setPublicKey(this.operatorKey.publicKey) 109 | .setMaxTransactionFee(FEE) 110 | .setDidTopicMemo(didTopicMemo) 111 | .setVCTopicMemo(vcTopicMemo) 112 | .execute(this.client); 113 | console.info("New identity network created: " + appnetName); 114 | console.info("Sleeping 10s to allow propagation of new topics to mirror node"); 115 | 116 | await sleep(10000); 117 | } 118 | 119 | //AfterAll 120 | cleanup() { 121 | try { 122 | if (this.client != null) { 123 | this.client.close(); 124 | } 125 | 126 | if (this.client != null) { 127 | this.client.close(); 128 | } 129 | } catch (e) { 130 | // ignore 131 | } 132 | } 133 | 134 | async sendDidTransaction(did, didDocumentJson, operation, onError) { 135 | const messageRef = []; 136 | 137 | // Build and execute transaction 138 | await this.didNetwork.createDidTransaction(operation) 139 | .setDidDocument(didDocumentJson) 140 | .signMessage(doc => did.getPrivateDidRootKey().sign(doc)) 141 | .buildAndSignTransaction(tx => tx.setMaxTransactionFee(FEE)) 142 | .onMessageConfirmed(msg => messageRef.push(msg)) 143 | .onError(onError) 144 | .execute(this.client); 145 | 146 | // Wait until consensus is reached and mirror node received the DID document, but with max. time limit. 147 | await until(MIRROR_NODE_TIMEOUT, () => !!messageRef.length); 148 | 149 | try { 150 | return messageRef[0]; 151 | } catch (error) { 152 | return undefined 153 | } 154 | } 155 | 156 | async resolveDid(didString, onError) { 157 | const mapRef = []; 158 | 159 | // Now resolve the DID. 160 | this.didNetwork.getDidResolver() 161 | .addDid(didString) 162 | .setTimeout(NO_MORE_MESSAGES_TIMEOUT) 163 | .onError(onError) 164 | .whenFinished(m => mapRef.push(m)) 165 | .execute(this.client); 166 | 167 | // Wait until mirror node resolves the DID. 168 | await until(MIRROR_NODE_TIMEOUT, () => !!mapRef.length); 169 | 170 | try { 171 | return mapRef[0].get(didString); 172 | } catch (error) { 173 | return undefined 174 | } 175 | } 176 | 177 | async sendVcTransaction(operation, credentialHash, signingKey, onError) { 178 | const messageRef = []; 179 | 180 | // Build and execute transaction 181 | await this.didNetwork.createVcTransaction(operation, credentialHash, signingKey.publicKey) 182 | .signMessage(doc => signingKey.sign(doc)) 183 | .buildAndSignTransaction(tx => tx.setMaxTransactionFee(FEE)) 184 | .onMessageConfirmed(msg => messageRef.push(msg)) 185 | .onError(onError) 186 | .execute(this.client); 187 | 188 | // Wait until consensus is reached and mirror node received the DID document, but with max. time limit. 189 | await until(MIRROR_NODE_TIMEOUT, () => !!messageRef.length); 190 | 191 | try { 192 | return messageRef[0]; 193 | } catch (error) { 194 | return undefined 195 | } 196 | } 197 | 198 | async resolveVcStatus(credentialHash, provider, onError) { 199 | const mapRef = []; 200 | 201 | // Now resolve the DID. 202 | this.didNetwork.getVcStatusResolver(provider) 203 | .addCredentialHash(credentialHash) 204 | .setTimeout(NO_MORE_MESSAGES_TIMEOUT) 205 | .onError(onError) 206 | .whenFinished(m => mapRef.push(m)) 207 | .execute(this.client); 208 | 209 | // Wait until mirror node resolves the DID. 210 | await until(MIRROR_NODE_TIMEOUT, () => !!mapRef.length); 211 | 212 | try { 213 | return mapRef[0].get(credentialHash); 214 | } catch (error) { 215 | return undefined 216 | } 217 | } 218 | } 219 | 220 | exports.NetworkReadyTestBase = NetworkReadyTestBase; 221 | exports.until = until; 222 | exports.sleep = sleep; 223 | -------------------------------------------------------------------------------- /test/variables.js: -------------------------------------------------------------------------------- 1 | const OPERATOR_ID = process.env.OPERATOR_ID; 2 | const OPERATOR_KEY = process.env.OPERATOR_KEY; 3 | // testnet, previewnet, mainnet 4 | const NETWORK = process.env.NETWORK || 'testnet'; 5 | 6 | // hedera, kabuto (note kabuto not available on previewnet) 7 | const MIRROR_PROVIDER = process.env.MIRROR_PROVIDER || 'hedera'; 8 | 9 | if (!OPERATOR_ID || !/^\d+\.\d+\.\d+$/.test(OPERATOR_ID)) { 10 | console.error('Missing or invalid OPERATOR_ID'); 11 | process.exit(1); 12 | } 13 | 14 | if (!OPERATOR_KEY) { 15 | console.error('Missing required OPERATOR_KEY'); 16 | process.exit(1); 17 | } 18 | 19 | if (!NETWORK || !/^(mainnet|previewnet|testnet)$/.test(NETWORK)) { 20 | console.error('Missing or invalid NETWORK'); 21 | process.exit(1); 22 | } 23 | 24 | if (!MIRROR_PROVIDER || !/^(hedera|kabuto)$/.test(MIRROR_PROVIDER)) { 25 | console.error('Missing or invalid MIRROR_PROVIDER'); 26 | process.exit(1); 27 | } 28 | 29 | module.exports = { 30 | OPERATOR_ID, 31 | OPERATOR_KEY, 32 | NETWORK, 33 | MIRROR_PROVIDER 34 | } 35 | -------------------------------------------------------------------------------- /test/vc/demo-access-credential.js: -------------------------------------------------------------------------------- 1 | const { 2 | CredentialSubject 3 | } = require("../../dist"); 4 | 5 | /** 6 | * Example Credential. 7 | */ 8 | class DemoAccessCredential extends CredentialSubject { 9 | static ACCESS_GRANTED = "granted"; 10 | static ACCESS_DENIED = "denied"; 11 | 12 | blueLevel; 13 | greenLevel; 14 | redLevel; 15 | 16 | /** 17 | * Creates a new credential instance. 18 | * 19 | * @param did Credential Subject DID. 20 | * @param blue Access to blue level granted or denied. 21 | * @param green Access to green level granted or denied. 22 | * @param red Access to red level granted or denied. 23 | */ 24 | constructor(did, blue, green, red) { 25 | super(); 26 | this.id = did; 27 | this.blueLevel = blue ? DemoAccessCredential.ACCESS_GRANTED : DemoAccessCredential.ACCESS_DENIED; 28 | this.greenLevel = green ? DemoAccessCredential.ACCESS_GRANTED : DemoAccessCredential.ACCESS_DENIED; 29 | this.redLevel = red ? DemoAccessCredential.ACCESS_GRANTED : DemoAccessCredential.ACCESS_DENIED; 30 | } 31 | 32 | getBlueLevel() { 33 | return this.blueLevel; 34 | } 35 | 36 | getGreenLevel() { 37 | return this.greenLevel; 38 | } 39 | 40 | getRedLevel() { 41 | return this.redLevel; 42 | } 43 | 44 | toJsonTree() { 45 | const json = super.toJsonTree(); 46 | json["blueLevel"] = this.blueLevel; 47 | json["greenLevel"] = this.greenLevel; 48 | json["redLevel"] = this.redLevel; 49 | return json; 50 | } 51 | 52 | toJson() { 53 | return JSON.stringify(this.toJsonTree()); 54 | } 55 | 56 | static fromJsonTree(json) { 57 | const result = new DemoAccessCredential(null, null, null, null); 58 | super.fromJsonTree(json, result); 59 | result.blueLevel = json["blueLevel"]; 60 | result.greenLevel = json["greenLevel"]; 61 | result.redLevel = json["redLevel"]; 62 | return result; 63 | } 64 | 65 | static fromJson(json) { 66 | const root = JSON.parse(json); 67 | return this.fromJsonTree(root); 68 | } 69 | 70 | static toJsonTree(item) { 71 | return item ? item.toJsonTree() : null; 72 | } 73 | 74 | static toJson(item) { 75 | return JSON.stringify(this.toJsonTree(item)); 76 | } 77 | } 78 | 79 | exports.DemoAccessCredential = DemoAccessCredential; 80 | -------------------------------------------------------------------------------- /test/vc/demo-verifiable-credential-document.js: -------------------------------------------------------------------------------- 1 | const { 2 | HcsVcDocumentBase 3 | } = require("../../dist"); 4 | 5 | /** 6 | * Custom VC document for tests. 7 | */ 8 | class DemoVerifiableCredentialDocument extends HcsVcDocumentBase { 9 | customProperty; 10 | 11 | constructor() { 12 | super(); 13 | } 14 | 15 | getCustomProperty() { 16 | return this.customProperty; 17 | } 18 | 19 | setCustomProperty(customProperty) { 20 | this.customProperty = customProperty; 21 | } 22 | } 23 | 24 | exports.DemoVerifiableCredentialDocument = DemoVerifiableCredentialDocument; -------------------------------------------------------------------------------- /test/vc/hcs-vc-document-base-test.js: -------------------------------------------------------------------------------- 1 | const { 2 | HcsVcDocumentBase, 3 | Issuer 4 | } = require("../../dist"); 5 | const { 6 | Timestamp 7 | } = require('@hashgraph/sdk'); 8 | const { 9 | DemoAccessCredential 10 | } = require("./demo-access-credential"); 11 | const { 12 | DemoVerifiableCredentialDocument 13 | } = require("./demo-verifiable-credential-document"); 14 | const { 15 | NetworkReadyTestBase 16 | } = require("../network-ready-test-base"); 17 | 18 | const { expect, assert } = require('chai'); 19 | 20 | describe("HcsVcDocumentBaseTest", function () { 21 | const network = new NetworkReadyTestBase(); 22 | let issuer, owner; 23 | 24 | before(async function () { 25 | this.timeout(60000); 26 | await network.setup(); 27 | 28 | issuer = network.didNetwork.generateDid(false); 29 | owner = network.didNetwork.generateDid(false); 30 | }); 31 | 32 | after(async function () { 33 | network.cleanup(); 34 | }); 35 | 36 | it('Test VcDocumentConstruction', async function () { 37 | const vc = new HcsVcDocumentBase(); 38 | 39 | // Should fail as no issuer is set. 40 | assert.isFalse(vc.isComplete()); 41 | 42 | vc.setIssuer(issuer); 43 | 44 | // Should fail as no issuance date is set. 45 | assert.isFalse(vc.isComplete()); 46 | 47 | vc.setIssuanceDate(Timestamp.fromDate(new Date())); 48 | 49 | // Should fail as no credential subject is set. 50 | assert.isFalse(vc.isComplete()); 51 | 52 | // Default VC type should be set. 53 | assert.exists(vc.getType()); 54 | assert.equal(1, vc.getType().length); 55 | 56 | // Add a custom type 57 | vc.addType("TestVC"); 58 | assert.equal(2, vc.getType().length); 59 | 60 | // Default context should be set 61 | assert.exists(vc.getContext()); 62 | assert.equal(1, vc.getContext().length); 63 | 64 | // Add a custom context 65 | vc.addContext("https://www.example.com/testContext"); 66 | assert.equal(2, vc.getContext().length); 67 | 68 | // Add a credential subject. 69 | assert.notExists(vc.getCredentialSubject()); 70 | const credential = new DemoAccessCredential(owner.toDid(), true, false, false); 71 | vc.addCredentialSubject(credential); 72 | 73 | // Make sure it's there 74 | assert.exists(vc.getCredentialSubject()); 75 | assert.equal(1, vc.getCredentialSubject().length); 76 | 77 | // Now all mandatory fields should be set 78 | assert.isTrue(vc.isComplete()); 79 | }); 80 | 81 | 82 | it('Test VcJsonConversion', async function () { 83 | const vc = new HcsVcDocumentBase(); 84 | vc.setId("example:test:vc:id"); 85 | vc.setIssuer(new Issuer(issuer.toDid(), "My Company Ltd.")); 86 | vc.setIssuanceDate(Timestamp.fromDate(new Date())); 87 | 88 | const subject = new DemoAccessCredential(owner.toDid(), true, false, false); 89 | vc.addCredentialSubject(subject); 90 | 91 | // Convert to JSON 92 | const json = vc.toJSON(); 93 | assert.isFalse(!(json)); 94 | 95 | // Convert back to VC document and compare 96 | const vcFromJson = HcsVcDocumentBase.fromJson(json, DemoAccessCredential); 97 | // Test simple properties 98 | assert.exists(vcFromJson); 99 | assert.deepEqual(vc.getType(), vcFromJson.getType()); 100 | assert.deepEqual(vc.getContext(), vcFromJson.getContext()); 101 | assert.deepEqual(vc.getIssuanceDate(), vcFromJson.getIssuanceDate()); 102 | assert.equal(vc.getId(), vcFromJson.getId()); 103 | 104 | // Test issuer object 105 | assert.exists(vcFromJson.getIssuer()); 106 | assert.equal(vc.getIssuer().getId(), vcFromJson.getIssuer().getId()); 107 | assert.equal(vc.getIssuer().getName(), vcFromJson.getIssuer().getName()); 108 | 109 | // Test credential subject 110 | assert.exists(vcFromJson.getCredentialSubject()); 111 | 112 | const subjectFromJson = vcFromJson.getCredentialSubject()[0]; 113 | assert.equal(subject.getId(), subjectFromJson.getId()); 114 | assert.equal(subject.getBlueLevel(), subjectFromJson.getBlueLevel()); 115 | assert.equal(subject.getGreenLevel(), subjectFromJson.getGreenLevel()); 116 | assert.equal(subject.getRedLevel(), subjectFromJson.getRedLevel()); 117 | }); 118 | 119 | it('Test CredentialHash', async function () { 120 | const vc = new DemoVerifiableCredentialDocument(); 121 | vc.setId("example:test:vc:id"); 122 | vc.setIssuer(issuer); 123 | vc.setIssuanceDate(Timestamp.fromDate(new Date())); 124 | vc.addCredentialSubject(new DemoAccessCredential(owner.toDid(), true, false, false)); 125 | vc.setCustomProperty("Custom property value 1"); 126 | 127 | const credentialHash = vc.toCredentialHash(); 128 | assert.isFalse(!credentialHash); 129 | 130 | // Recalculation should give the same value 131 | assert.equal(credentialHash, vc.toCredentialHash()); 132 | 133 | // Hash shall not change if we don't change anything in the document 134 | vc.setCustomProperty("Another value for custom property"); 135 | vc.addCredentialSubject(new DemoAccessCredential(owner.toDid(), false, false, true)); 136 | 137 | assert.equal(credentialHash, vc.toCredentialHash()); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /test/vc/hcs-vc-document-operations-test.js: -------------------------------------------------------------------------------- 1 | 2 | const { 3 | HcsVcDocumentBase, 4 | HcsVcOperation 5 | } = require("../../dist"); 6 | const { 7 | Timestamp 8 | } = require('@hashgraph/sdk'); 9 | const { 10 | DemoAccessCredential 11 | } = require("./demo-access-credential"); 12 | const { 13 | NetworkReadyTestBase 14 | } = require("../network-ready-test-base"); 15 | 16 | const { expect, assert } = require('chai'); 17 | 18 | 19 | /** 20 | * Tests operations on verifiable credentials and their status resolution. 21 | */ 22 | describe("HcsVcDocumentOperationsTest", function () { 23 | const network = new NetworkReadyTestBase(); 24 | let issuer, owner, vc, credentialHash, issuersPrivateKey; 25 | 26 | before(async function () { 27 | this.timeout(60000); 28 | await network.setup(); 29 | 30 | issuer = network.didNetwork.generateDid(false); 31 | issuersPrivateKey = issuer.getPrivateDidRootKey(); 32 | 33 | owner = network.didNetwork.generateDid(false); 34 | 35 | // For tests only we do not need to submit DID documents, as we will not validate them. 36 | // final DidMethodOperation op = DidMethodOperation.CREATE; 37 | // sendDidTransaction(issuer, issuer.generateDidDocument().toJson(), op, EXPECT_NO_ERROR); 38 | // sendDidTransaction(owner, owner.generateDidDocument().toJson(), op, EXPECT_NO_ERROR); 39 | 40 | // Create an example Verifiable Credential. 41 | vc = new HcsVcDocumentBase(); 42 | vc.setIssuer(issuer); 43 | vc.setIssuanceDate(Timestamp.fromDate(new Date())); 44 | vc.addCredentialSubject(new DemoAccessCredential(owner.toDid(), true, false, false)); 45 | 46 | credentialHash = vc.toCredentialHash(); 47 | }); 48 | 49 | after(async function () { 50 | network.cleanup(); 51 | }); 52 | 53 | const EXPECT_NO_ERROR = function (err) { 54 | assert.notExists(err); 55 | }; 56 | 57 | const testVcOperation = async function (op) { 58 | const envelope = await network.sendVcTransaction(op, credentialHash, issuersPrivateKey, EXPECT_NO_ERROR); 59 | 60 | assert.exists(envelope); 61 | 62 | const msg = envelope.open(); 63 | 64 | // Check results 65 | assert.exists(msg); 66 | assert.isTrue(msg.isValid()); 67 | assert.equal(op, msg.getOperation()); 68 | assert.equal(credentialHash, msg.getCredentialHash()); 69 | } 70 | 71 | const testVcStatusResolution = async function (expectedOperation) { 72 | const envelope = await network.resolveVcStatus( 73 | credentialHash, 74 | (m) => [issuersPrivateKey.publicKey], 75 | EXPECT_NO_ERROR 76 | ); 77 | 78 | assert.exists(envelope); 79 | 80 | const msg = envelope.open(); 81 | 82 | assert.exists(msg); 83 | assert.isTrue(msg.isValid()); 84 | assert.equal(credentialHash, msg.getCredentialHash()); 85 | assert.equal(expectedOperation, msg.getOperation()); 86 | } 87 | 88 | it('Test Issue', async function () { 89 | this.timeout(60000); 90 | await testVcOperation(HcsVcOperation.ISSUE); 91 | await testVcStatusResolution(HcsVcOperation.ISSUE); 92 | }); 93 | 94 | it('Test Suspend', async function () { 95 | this.timeout(60000); 96 | await testVcOperation(HcsVcOperation.SUSPEND); 97 | await testVcStatusResolution(HcsVcOperation.SUSPEND); 98 | }); 99 | 100 | it('Test Resume', async function () { 101 | this.timeout(60000); 102 | await testVcOperation(HcsVcOperation.RESUME); 103 | await testVcStatusResolution(HcsVcOperation.RESUME); 104 | }); 105 | 106 | it('Test Revoke', async function () { 107 | this.timeout(60000); 108 | await testVcOperation(HcsVcOperation.REVOKE); 109 | await testVcStatusResolution(HcsVcOperation.REVOKE); 110 | }); 111 | 112 | it('Test InvalidResumeAfterRevoke', async function () { 113 | this.timeout(120000); 114 | await testVcOperation(HcsVcOperation.RESUME); 115 | // Status should still be revoked 116 | await testVcStatusResolution(HcsVcOperation.REVOKE); 117 | 118 | await testVcOperation(HcsVcOperation.SUSPEND); 119 | // Status should still be revoked 120 | await testVcStatusResolution(HcsVcOperation.REVOKE); 121 | }); 122 | }); -------------------------------------------------------------------------------- /test/vc/hcs-vc-encryption-test.js: -------------------------------------------------------------------------------- 1 | const { 2 | Hbar, Timestamp, 3 | } = require('@hashgraph/sdk'); 4 | 5 | const { 6 | HcsVcDocumentBase, 7 | HcsVcOperation, 8 | HcsVcMessage, 9 | MessageEnvelope, 10 | ArraysUtils 11 | } = require("../../dist"); 12 | const { 13 | DemoAccessCredential 14 | } = require("./demo-access-credential"); 15 | const { 16 | DemoVerifiableCredentialDocument 17 | } = require("./demo-verifiable-credential-document"); 18 | const { 19 | NetworkReadyTestBase, until, sleep 20 | } = require("../network-ready-test-base"); 21 | const { 22 | encrypt, decrypt 23 | } = require("../aes-encryption-util"); 24 | 25 | const { expect, assert } = require('chai'); 26 | 27 | /** 28 | * Tests operations on verifiable credentials and their status resolution. 29 | */ 30 | describe("HcsVcEncryptionTest", function () { 31 | const MIRROR_NODE_TIMEOUT = 30 * 1000; 32 | const NO_MORE_MESSAGES_TIMEOUT = 15 * 1000; 33 | const FEE = new Hbar(2); 34 | const SECRET = "Secret message used for encryption"; 35 | const INVALID_SECRET = "Invalid secret message used for decryption"; 36 | const network = new NetworkReadyTestBase(); 37 | let issuer, owner, vc, credentialHash, issuersPrivateKey; 38 | 39 | const EXPECT_NO_ERROR = function (err) { 40 | assert.notExists(err); 41 | }; 42 | 43 | before(async function () { 44 | this.timeout(120000); 45 | await network.setup(); 46 | 47 | issuer = network.didNetwork.generateDid(false); 48 | issuersPrivateKey = issuer.getPrivateDidRootKey(); 49 | 50 | owner = network.didNetwork.generateDid(false); 51 | 52 | // For tests only we do not need to submit DID documents, as we will not validate them. 53 | // final DidMethodOperation op = DidMethodOperation.CREATE; 54 | // sendDidTransaction(issuer, issuer.generateDidDocument().toJson(), op, EXPECT_NO_ERROR); 55 | // sendDidTransaction(owner, owner.generateDidDocument().toJson(), op, EXPECT_NO_ERROR); 56 | 57 | // Create an example Verifiable Credential. 58 | vc = new HcsVcDocumentBase(); 59 | vc.setIssuer(issuer); 60 | vc.setIssuanceDate(Timestamp.fromDate(new Date())); 61 | vc.addCredentialSubject(new DemoAccessCredential(owner.toDid(), true, false, false)); 62 | 63 | credentialHash = vc.toCredentialHash(); 64 | 65 | await sleep(1000); 66 | }); 67 | 68 | after(async function () { 69 | network.cleanup(); 70 | }); 71 | 72 | it('Test IssueValidEncryptedMessage', async function () { 73 | this.timeout(60000); 74 | 75 | const messageRef = []; 76 | 77 | // Build and execute transaction with encrypted message 78 | await network.didNetwork.createVcTransaction(HcsVcOperation.ISSUE, credentialHash, issuersPrivateKey.publicKey) 79 | .signMessage(doc => issuersPrivateKey.sign(doc)) 80 | .buildAndSignTransaction(tx => tx.setMaxTransactionFee(FEE)) 81 | .onMessageConfirmed(msg => messageRef.push(msg)) 82 | .onError(EXPECT_NO_ERROR) 83 | .onEncrypt(m => encrypt(m, SECRET)) 84 | .onDecrypt((m, i) => decrypt(m, SECRET)) 85 | .execute(network.client); 86 | 87 | // Wait until consensus is reached and mirror node received the DID document, but with max. time limit. 88 | await until(MIRROR_NODE_TIMEOUT, () => !!messageRef.length); 89 | 90 | const envelope = messageRef[0]; 91 | 92 | assert.exists(envelope); 93 | 94 | const msg = envelope.open(); 95 | 96 | // Check results 97 | assert.exists(msg); 98 | assert.isTrue(msg.isValid()); 99 | assert.equal(credentialHash, msg.getCredentialHash()); 100 | 101 | await sleep(1000); 102 | }); 103 | 104 | it('Test ResolveWithValidDecrypter', async function () { 105 | this.timeout(60000); 106 | 107 | const mapRef = []; 108 | 109 | // Resolve encrypted message 110 | network.didNetwork.getVcStatusResolver(m => [issuersPrivateKey.publicKey]) 111 | .addCredentialHash(credentialHash) 112 | .setTimeout(NO_MORE_MESSAGES_TIMEOUT) 113 | .onError(EXPECT_NO_ERROR) 114 | .onDecrypt((m, i) => decrypt(m, SECRET)) 115 | .whenFinished(m => mapRef.push(m)) 116 | .execute(network.client); 117 | 118 | // Wait until mirror node resolves the DID. 119 | await until(MIRROR_NODE_TIMEOUT, () => !!mapRef.length); 120 | 121 | const envelope = mapRef[0] ? mapRef[0].get(credentialHash) : null; 122 | 123 | assert.exists(envelope); 124 | 125 | const msg = envelope.open(); 126 | 127 | // Check results 128 | assert.exists(msg); 129 | assert.isTrue(msg.isValid()); 130 | assert.equal(credentialHash, msg.getCredentialHash()); 131 | 132 | await sleep(1000); 133 | }); 134 | 135 | it('Test ResolveWithInvalidDecrypter', async function () { 136 | this.timeout(60000); 137 | 138 | const mapRef = []; 139 | const errorRef = []; 140 | 141 | // Try to resolve encrypted message with a wrong secret 142 | network.didNetwork.getVcStatusResolver(m => [issuersPrivateKey.publicKey]) 143 | .addCredentialHash(credentialHash) 144 | .setTimeout(NO_MORE_MESSAGES_TIMEOUT) 145 | .onError(e => errorRef.push(String(e))) 146 | .onDecrypt((m, i) => decrypt(m, INVALID_SECRET)) 147 | .whenFinished(m => mapRef.push(m)) 148 | .execute(network.client); 149 | 150 | // Wait until mirror node resolves the DID. 151 | await until(MIRROR_NODE_TIMEOUT, () => !!mapRef.length); 152 | 153 | const envelope = mapRef[0] ? mapRef[0].get(credentialHash) : null; 154 | const error = errorRef[0]; 155 | 156 | assert.notExists(envelope); 157 | assert.exists(error); 158 | 159 | await sleep(1000); 160 | }); 161 | 162 | it('Test MessageEncryptionDecryption', async function () { 163 | this.timeout(60000); 164 | 165 | const msg = HcsVcMessage.fromCredentialHash(credentialHash, HcsVcOperation.ISSUE); 166 | 167 | const encryptedMsg = msg 168 | .encrypt(HcsVcMessage.getEncrypter(m => encrypt(m, SECRET))); 169 | 170 | assert.exists(encryptedMsg); 171 | 172 | 173 | const msgJson = ArraysUtils.toString(encryptedMsg.sign(m => issuersPrivateKey.sign(m))); 174 | const encryptedSignedMsg = MessageEnvelope.fromJson(msgJson, HcsVcMessage); 175 | 176 | assert.exists(encryptedSignedMsg); 177 | // Throw error if decrypter is not provided 178 | try { 179 | encryptedSignedMsg.open(); 180 | assert.fail("Throw error if decrypter is not provided"); 181 | } catch (error) { 182 | assert.exists(error); 183 | } 184 | 185 | const decryptedMsg = encryptedSignedMsg 186 | .open(HcsVcMessage.getDecrypter((m, i) => decrypt(m, SECRET))); 187 | 188 | assert.exists(decryptedMsg); 189 | assert.equal(credentialHash, decryptedMsg.getCredentialHash()); 190 | assert.equal(encryptedSignedMsg.open().getTimestamp(), decryptedMsg.getTimestamp()); 191 | }); 192 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "lib": [ 6 | "es5", 7 | "es6" 8 | ], 9 | "moduleResolution": "node", 10 | "outDir": "dist/", 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "esModuleInterop": true, 14 | "declaration": true 15 | }, 16 | "include": [ 17 | "src/**/*" 18 | ], 19 | "exclude": [ 20 | "node_modules" 21 | ], 22 | "compileOnSave": true 23 | } 24 | --------------------------------------------------------------------------------