├── .github ├── dependabot.yml └── workflows │ ├── automerge.yml │ ├── js-test-and-release.yml │ ├── semantic-pull-request.yml │ └── stale.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── doc ├── private-key.png └── private-key.xml ├── package.json ├── src ├── errors.ts ├── index.ts └── util.ts ├── test ├── keychain.spec.ts └── peerid.spec.ts └── tsconfig.json /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "10:00" 8 | open-pull-requests-limit: 10 9 | commit-message: 10 | prefix: "deps" 11 | prefix-development: "deps(dev)" 12 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | # File managed by web3-bot. DO NOT EDIT. 2 | # See https://github.com/protocol/.github/ for details. 3 | 4 | name: Automerge 5 | on: [ pull_request ] 6 | 7 | jobs: 8 | automerge: 9 | uses: protocol/.github/.github/workflows/automerge.yml@master 10 | with: 11 | job: 'automerge' 12 | -------------------------------------------------------------------------------- /.github/workflows/js-test-and-release.yml: -------------------------------------------------------------------------------- 1 | # File managed by web3-bot. DO NOT EDIT. 2 | # See https://github.com/protocol/.github/ for details. 3 | 4 | name: test & maybe release 5 | on: 6 | push: 7 | branches: 8 | - master 9 | pull_request: 10 | 11 | jobs: 12 | 13 | check: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-node@v3 18 | with: 19 | node-version: lts/* 20 | - uses: ipfs/aegir/actions/cache-node-modules@master 21 | - run: npm run --if-present lint 22 | - run: npm run --if-present dep-check 23 | 24 | test-node: 25 | needs: check 26 | runs-on: ${{ matrix.os }} 27 | strategy: 28 | matrix: 29 | os: [windows-latest, ubuntu-latest, macos-latest] 30 | node: [lts/*] 31 | fail-fast: true 32 | steps: 33 | - uses: actions/checkout@v3 34 | - uses: actions/setup-node@v3 35 | with: 36 | node-version: ${{ matrix.node }} 37 | - uses: ipfs/aegir/actions/cache-node-modules@master 38 | - run: npm run --if-present test:node 39 | - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 40 | with: 41 | flags: node 42 | 43 | test-chrome: 44 | needs: check 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v3 48 | - uses: actions/setup-node@v3 49 | with: 50 | node-version: lts/* 51 | - uses: ipfs/aegir/actions/cache-node-modules@master 52 | - run: npm run --if-present test:chrome 53 | - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 54 | with: 55 | flags: chrome 56 | 57 | test-chrome-webworker: 58 | needs: check 59 | runs-on: ubuntu-latest 60 | steps: 61 | - uses: actions/checkout@v3 62 | - uses: actions/setup-node@v3 63 | with: 64 | node-version: lts/* 65 | - uses: ipfs/aegir/actions/cache-node-modules@master 66 | - run: npm run --if-present test:chrome-webworker 67 | - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 68 | with: 69 | flags: chrome-webworker 70 | 71 | test-firefox: 72 | needs: check 73 | runs-on: ubuntu-latest 74 | steps: 75 | - uses: actions/checkout@v3 76 | - uses: actions/setup-node@v3 77 | with: 78 | node-version: lts/* 79 | - uses: ipfs/aegir/actions/cache-node-modules@master 80 | - run: npm run --if-present test:firefox 81 | - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 82 | with: 83 | flags: firefox 84 | 85 | test-firefox-webworker: 86 | needs: check 87 | runs-on: ubuntu-latest 88 | steps: 89 | - uses: actions/checkout@v3 90 | - uses: actions/setup-node@v3 91 | with: 92 | node-version: lts/* 93 | - uses: ipfs/aegir/actions/cache-node-modules@master 94 | - run: npm run --if-present test:firefox-webworker 95 | - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 96 | with: 97 | flags: firefox-webworker 98 | 99 | test-webkit: 100 | needs: check 101 | runs-on: ${{ matrix.os }} 102 | strategy: 103 | matrix: 104 | os: [ubuntu-latest, macos-latest] 105 | node: [lts/*] 106 | fail-fast: true 107 | steps: 108 | - uses: actions/checkout@v3 109 | - uses: actions/setup-node@v3 110 | with: 111 | node-version: lts/* 112 | - uses: ipfs/aegir/actions/cache-node-modules@master 113 | - run: npm run --if-present test:webkit 114 | - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 115 | with: 116 | flags: webkit 117 | 118 | test-webkit-webworker: 119 | needs: check 120 | runs-on: ${{ matrix.os }} 121 | strategy: 122 | matrix: 123 | os: [ubuntu-latest, macos-latest] 124 | node: [lts/*] 125 | fail-fast: true 126 | steps: 127 | - uses: actions/checkout@v3 128 | - uses: actions/setup-node@v3 129 | with: 130 | node-version: lts/* 131 | - uses: ipfs/aegir/actions/cache-node-modules@master 132 | - run: npm run --if-present test:webkit-webworker 133 | - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 134 | with: 135 | flags: webkit-webworker 136 | 137 | test-electron-main: 138 | needs: check 139 | runs-on: ubuntu-latest 140 | steps: 141 | - uses: actions/checkout@v3 142 | - uses: actions/setup-node@v3 143 | with: 144 | node-version: lts/* 145 | - uses: ipfs/aegir/actions/cache-node-modules@master 146 | - run: npx xvfb-maybe npm run --if-present test:electron-main 147 | - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 148 | with: 149 | flags: electron-main 150 | 151 | test-electron-renderer: 152 | needs: check 153 | runs-on: ubuntu-latest 154 | steps: 155 | - uses: actions/checkout@v3 156 | - uses: actions/setup-node@v3 157 | with: 158 | node-version: lts/* 159 | - uses: ipfs/aegir/actions/cache-node-modules@master 160 | - run: npx xvfb-maybe npm run --if-present test:electron-renderer 161 | - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 162 | with: 163 | flags: electron-renderer 164 | 165 | release: 166 | needs: [test-node, test-chrome, test-chrome-webworker, test-firefox, test-firefox-webworker, test-webkit, test-webkit-webworker, test-electron-main, test-electron-renderer] 167 | runs-on: ubuntu-latest 168 | if: github.event_name == 'push' && github.ref == 'refs/heads/master' 169 | steps: 170 | - uses: actions/checkout@v3 171 | with: 172 | fetch-depth: 0 173 | - uses: actions/setup-node@v3 174 | with: 175 | node-version: lts/* 176 | - uses: ipfs/aegir/actions/cache-node-modules@master 177 | - uses: ipfs/aegir/actions/docker-login@master 178 | with: 179 | docker-token: ${{ secrets.DOCKER_TOKEN }} 180 | docker-username: ${{ secrets.DOCKER_USERNAME }} 181 | - run: npm run --if-present release 182 | env: 183 | GITHUB_TOKEN: ${{ secrets.UCI_GITHUB_TOKEN || github.token }} 184 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 185 | -------------------------------------------------------------------------------- /.github/workflows/semantic-pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Semantic PR 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | main: 12 | uses: pl-strflt/.github/.github/workflows/reusable-semantic-pull-request.yml@v0.3 13 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Close and mark stale issue 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | jobs: 8 | stale: 9 | uses: pl-strflt/.github/.github/workflows/reusable-stale-issue.yml@v0.3 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | dist 4 | .docs 5 | .coverage 6 | node_modules 7 | package-lock.json 8 | yarn.lock 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.0.1](https://github.com/libp2p/js-libp2p-keychain/compare/v2.0.0...v2.0.1) (2023-06-15) 2 | 3 | 4 | ### Trivial Changes 5 | 6 | * Update .github/workflows/semantic-pull-request.yml [skip ci] ([7fd8023](https://github.com/libp2p/js-libp2p-keychain/commit/7fd80233db0b8706eb0ffe5372c6bad584ec211f)) 7 | * Update .github/workflows/stale.yml [skip ci] ([c185b0d](https://github.com/libp2p/js-libp2p-keychain/commit/c185b0de456611ca42ec49bc7d52f803e4a76930)) 8 | 9 | 10 | ### Dependencies 11 | 12 | * **dev:** bump aegir from 38.1.8 to 39.0.10 ([#70](https://github.com/libp2p/js-libp2p-keychain/issues/70)) ([4da4a08](https://github.com/libp2p/js-libp2p-keychain/commit/4da4a08b86f436c36e2fae48ecc48817e9b8066f)) 13 | 14 | ## [2.0.0](https://github.com/libp2p/js-libp2p-keychain/compare/v1.0.1...v2.0.0) (2023-03-13) 15 | 16 | 17 | ### ⚠ BREAKING CHANGES 18 | 19 | * requires most recent datastore implementation 20 | 21 | ### Bug Fixes 22 | 23 | * update datastore dependency ([#58](https://github.com/libp2p/js-libp2p-keychain/issues/58)) ([a8a1628](https://github.com/libp2p/js-libp2p-keychain/commit/a8a162875e48f23611190c3fb31e439da1d2d64b)) 24 | 25 | ## [1.0.1](https://github.com/libp2p/js-libp2p-keychain/compare/v1.0.0...v1.0.1) (2023-03-13) 26 | 27 | 28 | ### Bug Fixes 29 | 30 | * replace err-code with CodeError ([#57](https://github.com/libp2p/js-libp2p-keychain/issues/57)) ([cc752d9](https://github.com/libp2p/js-libp2p-keychain/commit/cc752d9349a622f013cb3b713d09a663b1169766)) 31 | 32 | 33 | ### Trivial Changes 34 | 35 | * Update .github/workflows/semantic-pull-request.yml [skip ci] ([f3985cc](https://github.com/libp2p/js-libp2p-keychain/commit/f3985cc47ae966a33537af3f58c071f6c58184c9)) 36 | * Update .github/workflows/semantic-pull-request.yml [skip ci] ([d8b81ff](https://github.com/libp2p/js-libp2p-keychain/commit/d8b81ff5e03ca56541ae2117a928dedf180e85ac)) 37 | * Update .github/workflows/semantic-pull-request.yml [skip ci] ([a0a6972](https://github.com/libp2p/js-libp2p-keychain/commit/a0a6972d7af40488344e619e116f4d665190db6e)) 38 | * Update .github/workflows/stale.yml [skip ci] ([b2cf129](https://github.com/libp2p/js-libp2p-keychain/commit/b2cf129fb1a3e0263a03d5a8a0e1ee74cd543004)) 39 | 40 | ## [1.0.0](https://github.com/libp2p/js-libp2p-keychain/compare/v0.6.1...v1.0.0) (2023-01-27) 41 | 42 | 43 | ### ⚠ BREAKING CHANGES 44 | 45 | * this module is now typescript and does not store the self key on startup. cms operations have also been moved to [@libp2p/cms](https://www.npmjs.com/@libp2p/cms) 46 | 47 | ### Features 48 | 49 | * convert to typescript ([#53](https://github.com/libp2p/js-libp2p-keychain/issues/53)) ([3544df7](https://github.com/libp2p/js-libp2p-keychain/commit/3544df7c119b8cebded3f5c483e9f44bf499280f)) 50 | 51 | 52 | ### Trivial Changes 53 | 54 | * add deprecation notice ([#50](https://github.com/libp2p/js-libp2p-keychain/issues/50)) ([2a9b99c](https://github.com/libp2p/js-libp2p-keychain/commit/2a9b99cd402ed7260ebcac49d9e44905697beee0)) 55 | 56 | 57 | ## [0.6.1](https://github.com/libp2p/js-libp2p-keychain/compare/v0.6.0...v0.6.1) (2020-06-09) 58 | 59 | 60 | 61 | 62 | # [0.6.0](https://github.com/libp2p/js-libp2p-keychain/compare/v0.5.4...v0.6.0) (2019-12-18) 63 | 64 | 65 | 66 | 67 | ## [0.5.4](https://github.com/libp2p/js-libp2p-keychain/compare/v0.5.3...v0.5.4) (2019-12-18) 68 | 69 | 70 | 71 | 72 | ## [0.5.3](https://github.com/libp2p/js-libp2p-keychain/compare/v0.5.2...v0.5.3) (2019-12-18) 73 | 74 | 75 | 76 | 77 | ## [0.5.2](https://github.com/libp2p/js-libp2p-keychain/compare/v0.5.1...v0.5.2) (2019-12-02) 78 | 79 | 80 | 81 | 82 | ## [0.5.1](https://github.com/libp2p/js-libp2p-keychain/compare/v0.5.0...v0.5.1) (2019-09-25) 83 | 84 | 85 | 86 | 87 | # [0.5.0](https://github.com/libp2p/js-libp2p-keychain/compare/v0.4.2...v0.5.0) (2019-08-16) 88 | 89 | 90 | * refactor: use async/await instead of callbacks (#37) ([dda315a](https://github.com/libp2p/js-libp2p-keychain/commit/dda315a)), closes [#37](https://github.com/libp2p/js-libp2p-keychain/issues/37) 91 | 92 | 93 | ### BREAKING CHANGES 94 | 95 | * The api now uses async/await instead of callbacks. 96 | 97 | Co-Authored-By: Vasco Santos 98 | 99 | 100 | 101 | 102 | ## [0.4.2](https://github.com/libp2p/js-libp2p-keychain/compare/v0.4.1...v0.4.2) (2019-06-13) 103 | 104 | 105 | ### Bug Fixes 106 | 107 | * throw errors with correct stack trace ([#35](https://github.com/libp2p/js-libp2p-keychain/issues/35)) ([7051b9c](https://github.com/libp2p/js-libp2p-keychain/commit/7051b9c)) 108 | 109 | 110 | 111 | 112 | ## [0.4.1](https://github.com/libp2p/js-libp2p-keychain/compare/v0.4.0...v0.4.1) (2019-03-14) 113 | 114 | 115 | 116 | 117 | # [0.4.0](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.6...v0.4.0) (2019-02-26) 118 | 119 | 120 | ### Features 121 | 122 | * adds support for ed25199 and secp256k1 ([#31](https://github.com/libp2p/js-libp2p-keychain/issues/31)) ([9eb11f4](https://github.com/libp2p/js-libp2p-keychain/commit/9eb11f4)) 123 | 124 | 125 | 126 | 127 | ## [0.3.6](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.5...v0.3.6) (2019-01-10) 128 | 129 | 130 | ### Bug Fixes 131 | 132 | * reduce bundle size ([#28](https://github.com/libp2p/js-libp2p-keychain/issues/28)) ([7eeed87](https://github.com/libp2p/js-libp2p-keychain/commit/7eeed87)) 133 | 134 | 135 | 136 | 137 | ## [0.3.5](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.4...v0.3.5) (2019-01-10) 138 | 139 | 140 | 141 | 142 | ## [0.3.4](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.3...v0.3.4) (2019-01-04) 143 | 144 | 145 | 146 | 147 | ## [0.3.3](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.2...v0.3.3) (2018-10-25) 148 | 149 | 150 | 151 | 152 | ## [0.3.2](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.1...v0.3.2) (2018-09-18) 153 | 154 | 155 | ### Bug Fixes 156 | 157 | * validate createKey params properly ([#26](https://github.com/libp2p/js-libp2p-keychain/issues/26)) ([8dfaab1](https://github.com/libp2p/js-libp2p-keychain/commit/8dfaab1)) 158 | 159 | 160 | 161 | 162 | ## [0.3.1](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.0...v0.3.1) (2018-01-29) 163 | 164 | 165 | 166 | 167 | # [0.3.0](https://github.com/libp2p/js-libp2p-keychain/compare/v0.2.1...v0.3.0) (2018-01-29) 168 | 169 | 170 | ### Bug Fixes 171 | 172 | * deepmerge 2.0.1 fails in browser, stay with 1.5.2 ([2ce4444](https://github.com/libp2p/js-libp2p-keychain/commit/2ce4444)) 173 | 174 | 175 | 176 | 177 | ## [0.2.1](https://github.com/libp2p/js-libp2p-keychain/compare/v0.2.0...v0.2.1) (2017-12-28) 178 | 179 | 180 | ### Features 181 | 182 | * generate unique options for a key chain ([#20](https://github.com/libp2p/js-libp2p-keychain/issues/20)) ([89a451c](https://github.com/libp2p/js-libp2p-keychain/commit/89a451c)) 183 | 184 | 185 | 186 | 187 | # 0.2.0 (2017-12-20) 188 | 189 | 190 | ### Bug Fixes 191 | 192 | * error message ([8305d20](https://github.com/libp2p/js-libp2p-keychain/commit/8305d20)) 193 | * lint errors ([06917f7](https://github.com/libp2p/js-libp2p-keychain/commit/06917f7)) 194 | * lint errors ([ff4f656](https://github.com/libp2p/js-libp2p-keychain/commit/ff4f656)) 195 | * linting ([409a999](https://github.com/libp2p/js-libp2p-keychain/commit/409a999)) 196 | * maps an IPFS hash name to its forge equivalent ([f71d3a6](https://github.com/libp2p/js-libp2p-keychain/commit/f71d3a6)), closes [#12](https://github.com/libp2p/js-libp2p-keychain/issues/12) 197 | * more linting ([7c44c91](https://github.com/libp2p/js-libp2p-keychain/commit/7c44c91)) 198 | * return info on removed key [#10](https://github.com/libp2p/js-libp2p-keychain/issues/10) ([f49e753](https://github.com/libp2p/js-libp2p-keychain/commit/f49e753)) 199 | 200 | 201 | ### Features 202 | 203 | * move bits from https://github.com/richardschneider/ipfs-encryption ([1a96ae8](https://github.com/libp2p/js-libp2p-keychain/commit/1a96ae8)) 204 | * use libp2p-crypto ([#18](https://github.com/libp2p/js-libp2p-keychain/issues/18)) ([c1627a9](https://github.com/libp2p/js-libp2p-keychain/commit/c1627a9)) 205 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This project is dual licensed under MIT and Apache-2.0. 2 | 3 | MIT: https://www.opensource.org/licenses/mit 4 | Apache-2.0: https://www.apache.org/licenses/license-2.0 5 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 2 | 3 | http://www.apache.org/licenses/LICENSE-2.0 4 | 5 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 6 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📁 Archived - this module has been merged into [js-libp2p](https://github.com/libp2p/js-libp2p/tree/master/packages/keychain) 2 | 3 | # @libp2p/keychain 4 | 5 | [![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) 6 | [![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) 7 | [![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p-keychain.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-keychain) 8 | [![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p-keychain/js-test-and-release.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p-keychain/actions/workflows/js-test-and-release.yml?query=branch%3Amaster) 9 | 10 | > Key management and cryptographically protected messages 11 | 12 | ## Table of contents 13 | 14 | - [Install](#install) 15 | - [Browser ` 36 | ``` 37 | 38 | ## Features 39 | 40 | - Manages the lifecycle of a key 41 | - Keys are encrypted at rest 42 | - Enforces the use of safe key names 43 | - Uses encrypted PKCS 8 for key storage 44 | - Uses PBKDF2 for a "stetched" key encryption key 45 | - Enforces NIST SP 800-131A and NIST SP 800-132 46 | - Delays reporting errors to slow down brute force attacks 47 | 48 | ### KeyInfo 49 | 50 | The key management and naming service API all return a `KeyInfo` object. The `id` is a universally unique identifier for the key. The `name` is local to the key chain. 51 | 52 | ```js 53 | { 54 | name: 'rsa-key', 55 | id: 'QmYWYSUZ4PV6MRFYpdtEDJBiGs4UrmE6g8wmAWSePekXVW' 56 | } 57 | ``` 58 | 59 | The **key id** is the SHA-256 [multihash](https://github.com/multiformats/multihash) of its public key. The *public key* is a [protobuf encoding](https://github.com/libp2p/js-libp2p-crypto/blob/master/src/keys/keys.proto.js) containing a type and the [DER encoding](https://en.wikipedia.org/wiki/X.690) of the PKCS [SubjectPublicKeyInfo](https://www.ietf.org/rfc/rfc3279.txt). 60 | 61 | ### Private key storage 62 | 63 | A private key is stored as an encrypted PKCS 8 structure in the PEM format. It is protected by a key generated from the key chain's *passPhrase* using **PBKDF2**. 64 | 65 | The default options for generating the derived encryption key are in the `dek` object. This, along with the passPhrase, is the input to a `PBKDF2` function. 66 | 67 | ```js 68 | const defaultOptions = { 69 | //See https://cryptosense.com/parameter-choice-for-pbkdf2/ 70 | dek: { 71 | keyLength: 512 / 8, 72 | iterationCount: 1000, 73 | salt: 'at least 16 characters long', 74 | hash: 'sha2-512' 75 | } 76 | } 77 | ``` 78 | 79 | ![key storage](./doc/private-key.png?raw=true) 80 | 81 | ### Physical storage 82 | 83 | The actual physical storage of an encrypted key is left to implementations of [interface-datastore](https://github.com/ipfs/interface-datastore/). A key benefit is that now the key chain can be used in browser with the [js-datastore-level](https://github.com/ipfs/js-datastore-level) implementation. 84 | 85 | ## API Docs 86 | 87 | - 88 | 89 | ## License 90 | 91 | Licensed under either of 92 | 93 | - Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) 94 | - MIT ([LICENSE-MIT](LICENSE-MIT) / ) 95 | 96 | ## Contribution 97 | 98 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 99 | -------------------------------------------------------------------------------- /doc/private-key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/libp2p/js-libp2p-keychain/b6046140dade0194db582affdb4c4d3b348c8fb3/doc/private-key.png -------------------------------------------------------------------------------- /doc/private-key.xml: -------------------------------------------------------------------------------- 1 | 7VlNb6MwEP01HLfCGBJ6bNJ2V9pdqVIP2x4dcMAKYGScJumvXxNsvkw+SmgSVe2hMs9mbL839swQA07j9U+G0vAv9XFkWKa/NuC9YVmua4n/ObApAOjCAggY8QsIVMAzeccSNCW6JD7OGgM5pREnaRP0aJJgjzcwxBhdNYfNadScNUUB1oBnD0U6+o/4PJTbssYV/guTIFQzg9Ft0TND3iJgdJnI+QwLzrd/RXeMlC250SxEPl3VIPhgwCmjlBeteD3FUU6toq1473FHb7luhhN+zAtSpzcULeXWU5RluYmQoQzLRfKNIobjtbA7CXkcCQCIZsYZXeApjSgTSEITMXIyJ1HUglBEgkQ8emJlWOCTN8w4EZTfyY6Y+H4+zWQVEo6fU+Tlc66EfwlsSynOF22KJ7loYQCvd24clHQKL8U0xpxtxBDlolIA6aBgJJ9Xldy2hMKa0ko3JB0sKA1XJIuG5Lmbc6hx/jT5ff9oaWQL50jzZsqoh4Uq3dTUtBiAF9AmxtaJAVYHM6MBmLE1Zny8EABNOaFJ9nW9sfQryfr4fN7oaJxrNOPEv8sv1ZyvSFwPxGuSLjbJNi85GzcmGCvgdQvAUQk8YUbE8nK6a7xhX7uKD7JWo8XpoEVhDEeIk7em+S6u5AxPlIiJq6PQEgWMraaJjC6Zh+Vb9Uu2bUiFw12GOGIB5pqhrXTlto9SczSomk5Dyw9IJsL1dku1C+9SKpYHR5Fvmj1VhE1D2ukbTkX3WlQsuGmErbqw4KLnE5oHBDlWWbt10K22i+xQVgiANrVhaT4g271g22xfKI3kTDQKi33d5rY7fB4Mmgxn5B3NtgNy/5D7EKOdieHcfyhcRmiGo0mZBauwW+XBe+KlzOblSoxSz7pjunvj6A8RgcpaY9Mw3tfZ1BA6n2f41IOt6puaRAucrz/AiSbUNaR/Fjxj+geAxk668PJqRLiPexX8QPuS/OjVmo84yjhleqV2CXac9o18Vnb06uEm3e01PvWW8XZfh4iZFdn+n9mQTLWSCQhcjanRntB5ElF6yl9cQl++zGpfbo7unp9VZgE9M2dJoFFdbRmc5cRarRMLLd0P3S5KnAEoGWuUaHwcTHPXhL/U2q/NjPdF+k6tIHV6J8AqeF9PBtzyZxu2HLVvaQPdlqHhShswaG0zmLQdVWsRbb+lPV5avf44Qdpm2Vo/67JLnfb+oo86RDeNKxLdHkr0208TXcXGz/pW0S066C+61SG6/S36x0TXC7VTRP9SH43VLahyzHZpc/xHY7DfUG85xWP1A2MxvPoRFz78Bw== -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@libp2p/keychain", 3 | "version": "2.0.1", 4 | "description": "Key management and cryptographically protected messages", 5 | "license": "Apache-2.0 OR MIT", 6 | "homepage": "https://github.com/libp2p/js-libp2p-keychain#readme", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/libp2p/js-libp2p-keychain.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/libp2p/js-libp2p-keychain/issues" 13 | }, 14 | "keywords": [ 15 | "IPFS", 16 | "crypto", 17 | "encryption", 18 | "keys", 19 | "libp2p", 20 | "secure" 21 | ], 22 | "engines": { 23 | "node": ">=16.0.0", 24 | "npm": ">=7.0.0" 25 | }, 26 | "type": "module", 27 | "types": "./dist/src/index.d.ts", 28 | "files": [ 29 | "src", 30 | "dist", 31 | "!dist/test", 32 | "!**/*.tsbuildinfo" 33 | ], 34 | "exports": { 35 | ".": { 36 | "types": "./src/index.d.ts", 37 | "import": "./dist/src/index.js" 38 | } 39 | }, 40 | "eslintConfig": { 41 | "extends": "ipfs", 42 | "parserOptions": { 43 | "sourceType": "module" 44 | } 45 | }, 46 | "release": { 47 | "branches": [ 48 | "master" 49 | ], 50 | "plugins": [ 51 | [ 52 | "@semantic-release/commit-analyzer", 53 | { 54 | "preset": "conventionalcommits", 55 | "releaseRules": [ 56 | { 57 | "breaking": true, 58 | "release": "major" 59 | }, 60 | { 61 | "revert": true, 62 | "release": "patch" 63 | }, 64 | { 65 | "type": "feat", 66 | "release": "minor" 67 | }, 68 | { 69 | "type": "fix", 70 | "release": "patch" 71 | }, 72 | { 73 | "type": "docs", 74 | "release": "patch" 75 | }, 76 | { 77 | "type": "test", 78 | "release": "patch" 79 | }, 80 | { 81 | "type": "deps", 82 | "release": "patch" 83 | }, 84 | { 85 | "scope": "no-release", 86 | "release": false 87 | } 88 | ] 89 | } 90 | ], 91 | [ 92 | "@semantic-release/release-notes-generator", 93 | { 94 | "preset": "conventionalcommits", 95 | "presetConfig": { 96 | "types": [ 97 | { 98 | "type": "feat", 99 | "section": "Features" 100 | }, 101 | { 102 | "type": "fix", 103 | "section": "Bug Fixes" 104 | }, 105 | { 106 | "type": "chore", 107 | "section": "Trivial Changes" 108 | }, 109 | { 110 | "type": "docs", 111 | "section": "Documentation" 112 | }, 113 | { 114 | "type": "deps", 115 | "section": "Dependencies" 116 | }, 117 | { 118 | "type": "test", 119 | "section": "Tests" 120 | } 121 | ] 122 | } 123 | } 124 | ], 125 | "@semantic-release/changelog", 126 | "@semantic-release/npm", 127 | "@semantic-release/github", 128 | "@semantic-release/git" 129 | ] 130 | }, 131 | "scripts": { 132 | "clean": "aegir clean", 133 | "lint": "aegir lint", 134 | "dep-check": "aegir dep-check", 135 | "build": "aegir build", 136 | "test": "aegir test", 137 | "test:chrome": "aegir test -t browser --cov", 138 | "test:chrome-webworker": "aegir test -t webworker", 139 | "test:firefox": "aegir test -t browser -- --browser firefox", 140 | "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", 141 | "test:node": "aegir test -t node --cov", 142 | "test:electron-main": "aegir test -t electron-main", 143 | "release": "aegir release", 144 | "docs": "aegir docs" 145 | }, 146 | "dependencies": { 147 | "@libp2p/crypto": "^1.0.11", 148 | "@libp2p/interface-keychain": "^2.0.3", 149 | "@libp2p/interface-peer-id": "^2.0.1", 150 | "@libp2p/interfaces": "^3.3.1", 151 | "@libp2p/logger": "^2.0.5", 152 | "@libp2p/peer-id": "^2.0.1", 153 | "interface-datastore": "^8.0.0", 154 | "merge-options": "^3.0.4", 155 | "sanitize-filename": "^1.6.3", 156 | "uint8arrays": "^4.0.3" 157 | }, 158 | "devDependencies": { 159 | "@libp2p/peer-id-factory": "^2.0.1", 160 | "aegir": "^39.0.10", 161 | "datastore-core": "^9.0.1", 162 | "multiformats": "^11.0.1" 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | 2 | export enum codes { 3 | ERR_INVALID_PARAMETERS = 'ERR_INVALID_PARAMETERS', 4 | ERR_INVALID_KEY_NAME = 'ERR_INVALID_KEY_NAME', 5 | ERR_INVALID_KEY_TYPE = 'ERR_INVALID_KEY_TYPE', 6 | ERR_KEY_ALREADY_EXISTS = 'ERR_KEY_ALREADY_EXISTS', 7 | ERR_INVALID_KEY_SIZE = 'ERR_INVALID_KEY_SIZE', 8 | ERR_KEY_NOT_FOUND = 'ERR_KEY_NOT_FOUND', 9 | ERR_OLD_KEY_NAME_INVALID = 'ERR_OLD_KEY_NAME_INVALID', 10 | ERR_NEW_KEY_NAME_INVALID = 'ERR_NEW_KEY_NAME_INVALID', 11 | ERR_PASSWORD_REQUIRED = 'ERR_PASSWORD_REQUIRED', 12 | ERR_PEM_REQUIRED = 'ERR_PEM_REQUIRED', 13 | ERR_CANNOT_READ_KEY = 'ERR_CANNOT_READ_KEY', 14 | ERR_MISSING_PRIVATE_KEY = 'ERR_MISSING_PRIVATE_KEY', 15 | ERR_INVALID_OLD_PASS_TYPE = 'ERR_INVALID_OLD_PASS_TYPE', 16 | ERR_INVALID_NEW_PASS_TYPE = 'ERR_INVALID_NEW_PASS_TYPE', 17 | ERR_INVALID_PASS_LENGTH = 'ERR_INVALID_PASS_LENGTH' 18 | } 19 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint max-nested-callbacks: ["error", 5] */ 2 | 3 | import { pbkdf2, randomBytes } from '@libp2p/crypto' 4 | import { generateKeyPair, importKey, unmarshalPrivateKey } from '@libp2p/crypto/keys' 5 | import { CodeError } from '@libp2p/interfaces/errors' 6 | import { logger } from '@libp2p/logger' 7 | import { peerIdFromKeys } from '@libp2p/peer-id' 8 | import { Key } from 'interface-datastore/key' 9 | import mergeOptions from 'merge-options' 10 | import sanitize from 'sanitize-filename' 11 | import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' 12 | import { toString as uint8ArrayToString } from 'uint8arrays/to-string' 13 | import { codes } from './errors.js' 14 | import type { KeyChain, KeyInfo, KeyType } from '@libp2p/interface-keychain' 15 | import type { PeerId } from '@libp2p/interface-peer-id' 16 | import type { Datastore } from 'interface-datastore' 17 | 18 | const log = logger('libp2p:keychain') 19 | 20 | export interface DEKConfig { 21 | hash: string 22 | salt: string 23 | iterationCount: number 24 | keyLength: number 25 | } 26 | 27 | export interface KeyChainInit { 28 | pass?: string 29 | dek?: DEKConfig 30 | } 31 | 32 | const keyPrefix = '/pkcs8/' 33 | const infoPrefix = '/info/' 34 | const privates = new WeakMap() 35 | 36 | // NIST SP 800-132 37 | const NIST = { 38 | minKeyLength: 112 / 8, 39 | minSaltLength: 128 / 8, 40 | minIterationCount: 1000 41 | } 42 | 43 | const defaultOptions = { 44 | // See https://cryptosense.com/parametesr-choice-for-pbkdf2/ 45 | dek: { 46 | keyLength: 512 / 8, 47 | iterationCount: 10000, 48 | salt: 'you should override this value with a crypto secure random number', 49 | hash: 'sha2-512' 50 | } 51 | } 52 | 53 | function validateKeyName (name: string): boolean { 54 | if (name == null) { 55 | return false 56 | } 57 | if (typeof name !== 'string') { 58 | return false 59 | } 60 | return name === sanitize(name.trim()) && name.length > 0 61 | } 62 | 63 | /** 64 | * Throws an error after a delay 65 | * 66 | * This assumes than an error indicates that the keychain is under attack. Delay returning an 67 | * error to make brute force attacks harder. 68 | */ 69 | async function randomDelay (): Promise { 70 | const min = 200 71 | const max = 1000 72 | const delay = Math.random() * (max - min) + min 73 | 74 | await new Promise(resolve => setTimeout(resolve, delay)) 75 | } 76 | 77 | /** 78 | * Converts a key name into a datastore name 79 | */ 80 | function DsName (name: string): Key { 81 | return new Key(keyPrefix + name) 82 | } 83 | 84 | /** 85 | * Converts a key name into a datastore info name 86 | */ 87 | function DsInfoName (name: string): Key { 88 | return new Key(infoPrefix + name) 89 | } 90 | 91 | export interface KeyChainComponents { 92 | datastore: Datastore 93 | } 94 | 95 | /** 96 | * Manages the lifecycle of a key. Keys are encrypted at rest using PKCS #8. 97 | * 98 | * A key in the store has two entries 99 | * - '/info/*key-name*', contains the KeyInfo for the key 100 | * - '/pkcs8/*key-name*', contains the PKCS #8 for the key 101 | * 102 | */ 103 | export class DefaultKeyChain implements KeyChain { 104 | private readonly components: KeyChainComponents 105 | private readonly init: KeyChainInit 106 | 107 | /** 108 | * Creates a new instance of a key chain 109 | */ 110 | constructor (components: KeyChainComponents, init: KeyChainInit) { 111 | this.components = components 112 | this.init = mergeOptions(defaultOptions, init) 113 | 114 | // Enforce NIST SP 800-132 115 | if (this.init.pass != null && this.init.pass?.length < 20) { 116 | throw new Error('pass must be least 20 characters') 117 | } 118 | if (this.init.dek?.keyLength != null && this.init.dek.keyLength < NIST.minKeyLength) { 119 | throw new Error(`dek.keyLength must be least ${NIST.minKeyLength} bytes`) 120 | } 121 | if (this.init.dek?.salt?.length != null && this.init.dek.salt.length < NIST.minSaltLength) { 122 | throw new Error(`dek.saltLength must be least ${NIST.minSaltLength} bytes`) 123 | } 124 | if (this.init.dek?.iterationCount != null && this.init.dek.iterationCount < NIST.minIterationCount) { 125 | throw new Error(`dek.iterationCount must be least ${NIST.minIterationCount}`) 126 | } 127 | 128 | const dek = this.init.pass != null && this.init.dek?.salt != null 129 | ? pbkdf2( 130 | this.init.pass, 131 | this.init.dek?.salt, 132 | this.init.dek?.iterationCount, 133 | this.init.dek?.keyLength, 134 | this.init.dek?.hash) 135 | : '' 136 | 137 | privates.set(this, { dek }) 138 | } 139 | 140 | /** 141 | * Generates the options for a keychain. A random salt is produced. 142 | * 143 | * @returns {object} 144 | */ 145 | static generateOptions (): KeyChainInit { 146 | const options = Object.assign({}, defaultOptions) 147 | const saltLength = Math.ceil(NIST.minSaltLength / 3) * 3 // no base64 padding 148 | options.dek.salt = uint8ArrayToString(randomBytes(saltLength), 'base64') 149 | return options 150 | } 151 | 152 | /** 153 | * Gets an object that can encrypt/decrypt protected data. 154 | * The default options for a keychain. 155 | * 156 | * @returns {object} 157 | */ 158 | static get options (): typeof defaultOptions { 159 | return defaultOptions 160 | } 161 | 162 | /** 163 | * Create a new key. 164 | * 165 | * @param {string} name - The local key name; cannot already exist. 166 | * @param {string} type - One of the key types; 'rsa'. 167 | * @param {number} [size = 2048] - The key size in bits. Used for rsa keys only 168 | */ 169 | async createKey (name: string, type: KeyType, size = 2048): Promise { 170 | if (!validateKeyName(name) || name === 'self') { 171 | await randomDelay() 172 | throw new CodeError('Invalid key name', codes.ERR_INVALID_KEY_NAME) 173 | } 174 | 175 | if (typeof type !== 'string') { 176 | await randomDelay() 177 | throw new CodeError('Invalid key type', codes.ERR_INVALID_KEY_TYPE) 178 | } 179 | 180 | const dsname = DsName(name) 181 | const exists = await this.components.datastore.has(dsname) 182 | if (exists) { 183 | await randomDelay() 184 | throw new CodeError('Key name already exists', codes.ERR_KEY_ALREADY_EXISTS) 185 | } 186 | 187 | switch (type.toLowerCase()) { 188 | case 'rsa': 189 | if (!Number.isSafeInteger(size) || size < 2048) { 190 | await randomDelay() 191 | throw new CodeError('Invalid RSA key size', codes.ERR_INVALID_KEY_SIZE) 192 | } 193 | break 194 | default: 195 | break 196 | } 197 | 198 | let keyInfo 199 | try { 200 | const keypair = await generateKeyPair(type, size) 201 | const kid = await keypair.id() 202 | const cached = privates.get(this) 203 | 204 | if (cached == null) { 205 | throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) 206 | } 207 | 208 | const dek = cached.dek 209 | const pem = await keypair.export(dek) 210 | keyInfo = { 211 | name, 212 | id: kid 213 | } 214 | const batch = this.components.datastore.batch() 215 | batch.put(dsname, uint8ArrayFromString(pem)) 216 | batch.put(DsInfoName(name), uint8ArrayFromString(JSON.stringify(keyInfo))) 217 | 218 | await batch.commit() 219 | } catch (err: any) { 220 | await randomDelay() 221 | throw err 222 | } 223 | 224 | return keyInfo 225 | } 226 | 227 | /** 228 | * List all the keys. 229 | * 230 | * @returns {Promise} 231 | */ 232 | async listKeys (): Promise { 233 | const query = { 234 | prefix: infoPrefix 235 | } 236 | 237 | const info = [] 238 | for await (const value of this.components.datastore.query(query)) { 239 | info.push(JSON.parse(uint8ArrayToString(value.value))) 240 | } 241 | 242 | return info 243 | } 244 | 245 | /** 246 | * Find a key by it's id 247 | */ 248 | async findKeyById (id: string): Promise { 249 | try { 250 | const keys = await this.listKeys() 251 | const key = keys.find((k) => k.id === id) 252 | 253 | if (key == null) { 254 | throw new CodeError(`Key with id '${id}' does not exist.`, codes.ERR_KEY_NOT_FOUND) 255 | } 256 | 257 | return key 258 | } catch (err: any) { 259 | await randomDelay() 260 | throw err 261 | } 262 | } 263 | 264 | /** 265 | * Find a key by it's name. 266 | * 267 | * @param {string} name - The local key name. 268 | * @returns {Promise} 269 | */ 270 | async findKeyByName (name: string): Promise { 271 | if (!validateKeyName(name)) { 272 | await randomDelay() 273 | throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) 274 | } 275 | 276 | const dsname = DsInfoName(name) 277 | try { 278 | const res = await this.components.datastore.get(dsname) 279 | return JSON.parse(uint8ArrayToString(res)) 280 | } catch (err: any) { 281 | await randomDelay() 282 | log.error(err) 283 | throw new CodeError(`Key '${name}' does not exist.`, codes.ERR_KEY_NOT_FOUND) 284 | } 285 | } 286 | 287 | /** 288 | * Remove an existing key. 289 | * 290 | * @param {string} name - The local key name; must already exist. 291 | * @returns {Promise} 292 | */ 293 | async removeKey (name: string): Promise { 294 | if (!validateKeyName(name) || name === 'self') { 295 | await randomDelay() 296 | throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) 297 | } 298 | const dsname = DsName(name) 299 | const keyInfo = await this.findKeyByName(name) 300 | const batch = this.components.datastore.batch() 301 | batch.delete(dsname) 302 | batch.delete(DsInfoName(name)) 303 | await batch.commit() 304 | return keyInfo 305 | } 306 | 307 | /** 308 | * Rename a key 309 | * 310 | * @param {string} oldName - The old local key name; must already exist. 311 | * @param {string} newName - The new local key name; must not already exist. 312 | * @returns {Promise} 313 | */ 314 | async renameKey (oldName: string, newName: string): Promise { 315 | if (!validateKeyName(oldName) || oldName === 'self') { 316 | await randomDelay() 317 | throw new CodeError(`Invalid old key name '${oldName}'`, codes.ERR_OLD_KEY_NAME_INVALID) 318 | } 319 | if (!validateKeyName(newName) || newName === 'self') { 320 | await randomDelay() 321 | throw new CodeError(`Invalid new key name '${newName}'`, codes.ERR_NEW_KEY_NAME_INVALID) 322 | } 323 | const oldDsname = DsName(oldName) 324 | const newDsname = DsName(newName) 325 | const oldInfoName = DsInfoName(oldName) 326 | const newInfoName = DsInfoName(newName) 327 | 328 | const exists = await this.components.datastore.has(newDsname) 329 | if (exists) { 330 | await randomDelay() 331 | throw new CodeError(`Key '${newName}' already exists`, codes.ERR_KEY_ALREADY_EXISTS) 332 | } 333 | 334 | try { 335 | const pem = await this.components.datastore.get(oldDsname) 336 | const res = await this.components.datastore.get(oldInfoName) 337 | 338 | const keyInfo = JSON.parse(uint8ArrayToString(res)) 339 | keyInfo.name = newName 340 | const batch = this.components.datastore.batch() 341 | batch.put(newDsname, pem) 342 | batch.put(newInfoName, uint8ArrayFromString(JSON.stringify(keyInfo))) 343 | batch.delete(oldDsname) 344 | batch.delete(oldInfoName) 345 | await batch.commit() 346 | return keyInfo 347 | } catch (err: any) { 348 | await randomDelay() 349 | throw err 350 | } 351 | } 352 | 353 | /** 354 | * Export an existing key as a PEM encrypted PKCS #8 string 355 | */ 356 | async exportKey (name: string, password: string): Promise { 357 | if (!validateKeyName(name)) { 358 | await randomDelay() 359 | throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) 360 | } 361 | if (password == null) { 362 | await randomDelay() 363 | throw new CodeError('Password is required', codes.ERR_PASSWORD_REQUIRED) 364 | } 365 | 366 | const dsname = DsName(name) 367 | try { 368 | const res = await this.components.datastore.get(dsname) 369 | const pem = uint8ArrayToString(res) 370 | const cached = privates.get(this) 371 | 372 | if (cached == null) { 373 | throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) 374 | } 375 | 376 | const dek = cached.dek 377 | const privateKey = await importKey(pem, dek) 378 | return await privateKey.export(password) 379 | } catch (err: any) { 380 | await randomDelay() 381 | throw err 382 | } 383 | } 384 | 385 | /** 386 | * Export an existing key as a PeerId 387 | */ 388 | async exportPeerId (name: string): Promise { 389 | const password = 'temporary-password' 390 | const pem = await this.exportKey(name, password) 391 | const privateKey = await importKey(pem, password) 392 | 393 | return peerIdFromKeys(privateKey.public.bytes, privateKey.bytes) 394 | } 395 | 396 | /** 397 | * Import a new key from a PEM encoded PKCS #8 string 398 | * 399 | * @param {string} name - The local key name; must not already exist. 400 | * @param {string} pem - The PEM encoded PKCS #8 string 401 | * @param {string} password - The password. 402 | * @returns {Promise} 403 | */ 404 | async importKey (name: string, pem: string, password: string): Promise { 405 | if (!validateKeyName(name) || name === 'self') { 406 | await randomDelay() 407 | throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) 408 | } 409 | if (pem == null) { 410 | await randomDelay() 411 | throw new CodeError('PEM encoded key is required', codes.ERR_PEM_REQUIRED) 412 | } 413 | const dsname = DsName(name) 414 | const exists = await this.components.datastore.has(dsname) 415 | if (exists) { 416 | await randomDelay() 417 | throw new CodeError(`Key '${name}' already exists`, codes.ERR_KEY_ALREADY_EXISTS) 418 | } 419 | 420 | let privateKey 421 | try { 422 | privateKey = await importKey(pem, password) 423 | } catch (err: any) { 424 | await randomDelay() 425 | throw new CodeError('Cannot read the key, most likely the password is wrong', codes.ERR_CANNOT_READ_KEY) 426 | } 427 | 428 | let kid 429 | try { 430 | kid = await privateKey.id() 431 | const cached = privates.get(this) 432 | 433 | if (cached == null) { 434 | throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) 435 | } 436 | 437 | const dek = cached.dek 438 | pem = await privateKey.export(dek) 439 | } catch (err: any) { 440 | await randomDelay() 441 | throw err 442 | } 443 | 444 | const keyInfo = { 445 | name, 446 | id: kid 447 | } 448 | const batch = this.components.datastore.batch() 449 | batch.put(dsname, uint8ArrayFromString(pem)) 450 | batch.put(DsInfoName(name), uint8ArrayFromString(JSON.stringify(keyInfo))) 451 | await batch.commit() 452 | 453 | return keyInfo 454 | } 455 | 456 | /** 457 | * Import a peer key 458 | */ 459 | async importPeer (name: string, peer: PeerId): Promise { 460 | try { 461 | if (!validateKeyName(name)) { 462 | throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) 463 | } 464 | if (peer == null) { 465 | throw new CodeError('PeerId is required', codes.ERR_MISSING_PRIVATE_KEY) 466 | } 467 | if (peer.privateKey == null) { 468 | throw new CodeError('PeerId.privKey is required', codes.ERR_MISSING_PRIVATE_KEY) 469 | } 470 | 471 | const privateKey = await unmarshalPrivateKey(peer.privateKey) 472 | 473 | const dsname = DsName(name) 474 | const exists = await this.components.datastore.has(dsname) 475 | if (exists) { 476 | await randomDelay() 477 | throw new CodeError(`Key '${name}' already exists`, codes.ERR_KEY_ALREADY_EXISTS) 478 | } 479 | 480 | const cached = privates.get(this) 481 | 482 | if (cached == null) { 483 | throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) 484 | } 485 | 486 | const dek = cached.dek 487 | const pem = await privateKey.export(dek) 488 | const keyInfo: KeyInfo = { 489 | name, 490 | id: peer.toString() 491 | } 492 | const batch = this.components.datastore.batch() 493 | batch.put(dsname, uint8ArrayFromString(pem)) 494 | batch.put(DsInfoName(name), uint8ArrayFromString(JSON.stringify(keyInfo))) 495 | await batch.commit() 496 | return keyInfo 497 | } catch (err: any) { 498 | await randomDelay() 499 | throw err 500 | } 501 | } 502 | 503 | /** 504 | * Gets the private key as PEM encoded PKCS #8 string 505 | */ 506 | async getPrivateKey (name: string): Promise { 507 | if (!validateKeyName(name)) { 508 | await randomDelay() 509 | throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) 510 | } 511 | 512 | try { 513 | const dsname = DsName(name) 514 | const res = await this.components.datastore.get(dsname) 515 | return uint8ArrayToString(res) 516 | } catch (err: any) { 517 | await randomDelay() 518 | log.error(err) 519 | throw new CodeError(`Key '${name}' does not exist.`, codes.ERR_KEY_NOT_FOUND) 520 | } 521 | } 522 | 523 | /** 524 | * Rotate keychain password and re-encrypt all associated keys 525 | */ 526 | async rotateKeychainPass (oldPass: string, newPass: string): Promise { 527 | if (typeof oldPass !== 'string') { 528 | await randomDelay() 529 | throw new CodeError(`Invalid old pass type '${typeof oldPass}'`, codes.ERR_INVALID_OLD_PASS_TYPE) 530 | } 531 | if (typeof newPass !== 'string') { 532 | await randomDelay() 533 | throw new CodeError(`Invalid new pass type '${typeof newPass}'`, codes.ERR_INVALID_NEW_PASS_TYPE) 534 | } 535 | if (newPass.length < 20) { 536 | await randomDelay() 537 | throw new CodeError(`Invalid pass length ${newPass.length}`, codes.ERR_INVALID_PASS_LENGTH) 538 | } 539 | log('recreating keychain') 540 | const cached = privates.get(this) 541 | 542 | if (cached == null) { 543 | throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) 544 | } 545 | 546 | const oldDek = cached.dek 547 | this.init.pass = newPass 548 | const newDek = newPass != null && this.init.dek?.salt != null 549 | ? pbkdf2( 550 | newPass, 551 | this.init.dek.salt, 552 | this.init.dek?.iterationCount, 553 | this.init.dek?.keyLength, 554 | this.init.dek?.hash) 555 | : '' 556 | privates.set(this, { dek: newDek }) 557 | const keys = await this.listKeys() 558 | for (const key of keys) { 559 | const res = await this.components.datastore.get(DsName(key.name)) 560 | const pem = uint8ArrayToString(res) 561 | const privateKey = await importKey(pem, oldDek) 562 | const password = newDek.toString() 563 | const keyAsPEM = await privateKey.export(password) 564 | 565 | // Update stored key 566 | const batch = this.components.datastore.batch() 567 | const keyInfo = { 568 | name: key.name, 569 | id: key.id 570 | } 571 | batch.put(DsName(key.name), uint8ArrayFromString(keyAsPEM)) 572 | batch.put(DsInfoName(key.name), uint8ArrayFromString(JSON.stringify(keyInfo))) 573 | await batch.commit() 574 | } 575 | log('keychain reconstructed') 576 | } 577 | } 578 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Finds the first item in a collection that is matched in the 3 | * `asyncCompare` function. 4 | * 5 | * `asyncCompare` is an async function that must 6 | * resolve to either `true` or `false`. 7 | * 8 | * @param {Array} array 9 | * @param {function(*)} asyncCompare - An async function that returns a boolean 10 | */ 11 | export async function findAsync (array: T[], asyncCompare: (val: T) => Promise): Promise { 12 | const promises = array.map(asyncCompare) 13 | const results = await Promise.all(promises) 14 | const index = results.findIndex(result => result) 15 | return array[index] 16 | } 17 | -------------------------------------------------------------------------------- /test/keychain.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint max-nested-callbacks: ["error", 8] */ 2 | /* eslint-env mocha */ 3 | 4 | import { pbkdf2 } from '@libp2p/crypto' 5 | import { unmarshalPrivateKey } from '@libp2p/crypto/keys' 6 | import { createFromPrivKey } from '@libp2p/peer-id-factory' 7 | import { expect } from 'aegir/chai' 8 | import { MemoryDatastore } from 'datastore-core/memory' 9 | import { Key } from 'interface-datastore/key' 10 | import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' 11 | import { toString as uint8ArrayToString } from 'uint8arrays/to-string' 12 | import { DefaultKeyChain, type KeyChainInit } from '../src/index.js' 13 | import type { KeyChain, KeyInfo } from '@libp2p/interface-keychain' 14 | import type { PeerId } from '@libp2p/interface-peer-id' 15 | import type { Datastore } from 'interface-datastore' 16 | 17 | describe('keychain', () => { 18 | const passPhrase = 'this is not a secure phrase' 19 | const rsaKeyName = 'tajné jméno' 20 | const renamedRsaKeyName = 'ชื่อลับ' 21 | let rsaKeyInfo: KeyInfo 22 | let ks: DefaultKeyChain 23 | let datastore2: Datastore 24 | 25 | before(async () => { 26 | datastore2 = new MemoryDatastore() 27 | 28 | ks = new DefaultKeyChain({ 29 | datastore: datastore2 30 | }, { pass: passPhrase }) 31 | }) 32 | 33 | it('can start without a password', async () => { 34 | await expect(async function () { 35 | return new DefaultKeyChain({ 36 | datastore: datastore2 37 | }, {}) 38 | }()).to.eventually.be.ok() 39 | }) 40 | 41 | it('needs a NIST SP 800-132 non-weak pass phrase', async () => { 42 | await expect(async function () { 43 | return new DefaultKeyChain({ 44 | datastore: datastore2 45 | }, { pass: '< 20 character' }) 46 | }()).to.eventually.be.rejected() 47 | }) 48 | 49 | it('has default options', () => { 50 | expect(DefaultKeyChain.options).to.exist() 51 | }) 52 | 53 | it('supports supported hashing alorithms', async () => { 54 | const ok = new DefaultKeyChain({ 55 | datastore: datastore2 56 | }, { pass: passPhrase, dek: { hash: 'sha2-256', salt: 'salt-salt-salt-salt', iterationCount: 1000, keyLength: 14 } }) 57 | expect(ok).to.exist() 58 | }) 59 | 60 | it('does not support unsupported hashing alorithms', async () => { 61 | await expect(async function () { 62 | return new DefaultKeyChain({ 63 | datastore: datastore2 64 | }, { pass: passPhrase, dek: { hash: 'my-hash', salt: 'salt-salt-salt-salt', iterationCount: 1000, keyLength: 14 } }) 65 | }()).to.eventually.be.rejected() 66 | }) 67 | 68 | it('can list keys without a password', async () => { 69 | const keychain = new DefaultKeyChain({ 70 | datastore: datastore2 71 | }, {}) 72 | 73 | expect(await keychain.listKeys()).to.have.lengthOf(0) 74 | }) 75 | 76 | it('can find a key without a password', async () => { 77 | const keychain = new DefaultKeyChain({ 78 | datastore: datastore2 79 | }, {}) 80 | const keychainWithPassword = new DefaultKeyChain({ 81 | datastore: datastore2 82 | }, { pass: `hello-${Date.now()}-${Date.now()}` }) 83 | const name = `key-${Math.random()}` 84 | 85 | const { id } = await keychainWithPassword.createKey(name, 'Ed25519') 86 | 87 | await expect(keychain.findKeyById(id)).to.eventually.be.ok() 88 | }) 89 | 90 | it('can remove a key without a password', async () => { 91 | const keychainWithoutPassword = new DefaultKeyChain({ 92 | datastore: datastore2 93 | }, {}) 94 | const keychainWithPassword = new DefaultKeyChain({ 95 | datastore: datastore2 96 | }, { pass: `hello-${Date.now()}-${Date.now()}` }) 97 | const name = `key-${Math.random()}` 98 | 99 | expect(await keychainWithPassword.createKey(name, 'Ed25519')).to.have.property('name', name) 100 | expect(await keychainWithoutPassword.findKeyByName(name)).to.have.property('name', name) 101 | await keychainWithoutPassword.removeKey(name) 102 | await expect(keychainWithoutPassword.findKeyByName(name)).to.be.rejectedWith(/does not exist/) 103 | }) 104 | 105 | it('requires a name to create a password', async () => { 106 | const keychain = new DefaultKeyChain({ 107 | datastore: datastore2 108 | }, {}) 109 | 110 | // @ts-expect-error invalid parameters 111 | await expect(keychain.createKey(undefined, 'derp')).to.eventually.be.rejected() 112 | }) 113 | 114 | it('can generate options', async () => { 115 | const options = DefaultKeyChain.generateOptions() 116 | options.pass = passPhrase 117 | const chain = new DefaultKeyChain({ 118 | datastore: datastore2 119 | }, options) 120 | expect(chain).to.exist() 121 | }) 122 | 123 | describe('key name', () => { 124 | it('is a valid filename and non-ASCII', async () => { 125 | const errors = await Promise.all([ 126 | ks.removeKey('../../nasty').catch(err => err), 127 | ks.removeKey('').catch(err => err), 128 | ks.removeKey(' ').catch(err => err), 129 | // @ts-expect-error invalid parameters 130 | ks.removeKey(null).catch(err => err), 131 | // @ts-expect-error invalid parameters 132 | ks.removeKey(undefined).catch(err => err) 133 | ]) 134 | 135 | expect(errors).to.have.length(5) 136 | errors.forEach(error => { 137 | expect(error).to.have.property('code', 'ERR_INVALID_KEY_NAME') 138 | }) 139 | }) 140 | }) 141 | 142 | describe('key', () => { 143 | it('can be an RSA key', async () => { 144 | rsaKeyInfo = await ks.createKey(rsaKeyName, 'RSA', 2048) 145 | expect(rsaKeyInfo).to.exist() 146 | expect(rsaKeyInfo).to.have.property('name', rsaKeyName) 147 | expect(rsaKeyInfo).to.have.property('id') 148 | }) 149 | 150 | it('is encrypted PEM encoded PKCS #8', async () => { 151 | const pem = await ks.getPrivateKey(rsaKeyName) 152 | return expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') 153 | }) 154 | 155 | it('throws if an invalid private key name is given', async () => { 156 | // @ts-expect-error invalid parameters 157 | await expect(ks.getPrivateKey(undefined)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') 158 | }) 159 | 160 | it('throws if a private key cant be found', async () => { 161 | await expect(ks.getPrivateKey('not real')).to.eventually.be.rejected.with.property('code', 'ERR_KEY_NOT_FOUND') 162 | }) 163 | 164 | it('does not overwrite existing key', async () => { 165 | await expect(ks.createKey(rsaKeyName, 'RSA', 2048)).to.eventually.be.rejected.with.property('code', 'ERR_KEY_ALREADY_EXISTS') 166 | }) 167 | 168 | it('cannot create the "self" key', async () => { 169 | await expect(ks.createKey('self', 'RSA', 2048)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') 170 | }) 171 | 172 | it('should validate name is string', async () => { 173 | // @ts-expect-error invalid parameters 174 | await expect(ks.createKey(5, 'rsa', 2048)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') 175 | }) 176 | 177 | it('should validate type is string', async () => { 178 | // @ts-expect-error invalid parameters 179 | await expect(ks.createKey(`TEST-${Date.now()}`, null, 2048)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_TYPE') 180 | }) 181 | 182 | it('should validate size is integer', async () => { 183 | // @ts-expect-error invalid parameters 184 | await expect(ks.createKey(`TEST-${Date.now()}`, 'RSA', 'string')).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_SIZE') 185 | }) 186 | 187 | describe('implements NIST SP 800-131A', () => { 188 | it('disallows RSA length < 2048', async () => { 189 | await expect(ks.createKey('bad-nist-rsa', 'RSA', 1024)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_SIZE') 190 | }) 191 | }) 192 | }) 193 | 194 | describe('Ed25519 keys', () => { 195 | const keyName = 'my custom key' 196 | it('can be an Ed25519 key', async () => { 197 | const keyInfo = await ks.createKey(keyName, 'Ed25519') 198 | expect(keyInfo).to.exist() 199 | expect(keyInfo).to.have.property('name', keyName) 200 | expect(keyInfo).to.have.property('id') 201 | }) 202 | 203 | it('does not overwrite existing key', async () => { 204 | await expect(ks.createKey(keyName, 'Ed25519')).to.eventually.be.rejected.with.property('code', 'ERR_KEY_ALREADY_EXISTS') 205 | }) 206 | 207 | it('can export/import a key', async () => { 208 | const keyName = 'a new key' 209 | const password = 'my sneaky password' 210 | const keyInfo = await ks.createKey(keyName, 'Ed25519') 211 | const exportedKey = await ks.exportKey(keyName, password) 212 | // remove it so we can import it 213 | await ks.removeKey(keyName) 214 | const importedKey = await ks.importKey(keyName, exportedKey, password) 215 | expect(importedKey.id).to.eql(keyInfo.id) 216 | }) 217 | 218 | it('cannot create the "self" key', async () => { 219 | await expect(ks.createKey('self', 'Ed25519')).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') 220 | }) 221 | }) 222 | 223 | describe('query', () => { 224 | it('finds all existing keys', async () => { 225 | const keys = await ks.listKeys() 226 | expect(keys).to.exist() 227 | const mykey = keys.find((k) => k.name.normalize() === rsaKeyName.normalize()) 228 | expect(mykey).to.exist() 229 | }) 230 | 231 | it('finds a key by name', async () => { 232 | const key = await ks.findKeyByName(rsaKeyName) 233 | expect(key).to.exist() 234 | expect(key).to.deep.equal(rsaKeyInfo) 235 | }) 236 | 237 | it('finds a key by id', async () => { 238 | const key = await ks.findKeyById(rsaKeyInfo.id) 239 | expect(key).to.exist() 240 | expect(key).to.deep.equal(rsaKeyInfo) 241 | }) 242 | 243 | it('returns the key\'s name and id', async () => { 244 | const keys = await ks.listKeys() 245 | expect(keys).to.exist() 246 | keys.forEach((key) => { 247 | expect(key).to.have.property('name') 248 | expect(key).to.have.property('id') 249 | }) 250 | }) 251 | }) 252 | 253 | describe('exported key', () => { 254 | let pemKey: string 255 | 256 | it('requires the password', async () => { 257 | // @ts-expect-error invalid parameters 258 | await expect(ks.exportKey(rsaKeyName)).to.eventually.be.rejected.with.property('code', 'ERR_PASSWORD_REQUIRED') 259 | }) 260 | 261 | it('requires the key name', async () => { 262 | // @ts-expect-error invalid parameters 263 | await expect(ks.exportKey(undefined, 'password')).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') 264 | }) 265 | 266 | it('is a PKCS #8 encrypted pem', async () => { 267 | pemKey = await ks.exportKey(rsaKeyName, 'password') 268 | expect(pemKey).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----') 269 | }) 270 | 271 | it('can be imported', async () => { 272 | const key = await ks.importKey('imported-key', pemKey, 'password') 273 | expect(key.name).to.equal('imported-key') 274 | expect(key.id).to.equal(rsaKeyInfo.id) 275 | }) 276 | 277 | it('requires the pem', async () => { 278 | // @ts-expect-error invalid parameters 279 | await expect(ks.importKey('imported-key', undefined, 'password')).to.eventually.be.rejected.with.property('code', 'ERR_PEM_REQUIRED') 280 | }) 281 | 282 | it('cannot be imported as an existing key name', async () => { 283 | await expect(ks.importKey(rsaKeyName, pemKey, 'password')).to.eventually.be.rejected.with.property('code', 'ERR_KEY_ALREADY_EXISTS') 284 | }) 285 | 286 | it('cannot be imported with the wrong password', async () => { 287 | await expect(ks.importKey('a-new-name-for-import', pemKey, 'not the password')).to.eventually.be.rejected.with.property('code', 'ERR_CANNOT_READ_KEY') 288 | }) 289 | }) 290 | 291 | describe('peer id', () => { 292 | const alicePrivKey = 'CAASpgkwggSiAgEAAoIBAQC2SKo/HMFZeBml1AF3XijzrxrfQXdJzjePBZAbdxqKR1Mc6juRHXij6HXYPjlAk01BhF1S3Ll4Lwi0cAHhggf457sMg55UWyeGKeUv0ucgvCpBwlR5cQ020i0MgzjPWOLWq1rtvSbNcAi2ZEVn6+Q2EcHo3wUvWRtLeKz+DZSZfw2PEDC+DGPJPl7f8g7zl56YymmmzH9liZLNrzg/qidokUv5u1pdGrcpLuPNeTODk0cqKB+OUbuKj9GShYECCEjaybJDl9276oalL9ghBtSeEv20kugatTvYy590wFlJkkvyl+nPxIH0EEYMKK9XRWlu9XYnoSfboiwcv8M3SlsjAgMBAAECggEAZtju/bcKvKFPz0mkHiaJcpycy9STKphorpCT83srBVQi59CdFU6Mj+aL/xt0kCPMVigJw8P3/YCEJ9J+rS8BsoWE+xWUEsJvtXoT7vzPHaAtM3ci1HZd302Mz1+GgS8Epdx+7F5p80XAFLDUnELzOzKftvWGZmWfSeDnslwVONkL/1VAzwKy7Ce6hk4SxRE7l2NE2OklSHOzCGU1f78ZzVYKSnS5Ag9YrGjOAmTOXDbKNKN/qIorAQ1bovzGoCwx3iGIatQKFOxyVCyO1PsJYT7JO+kZbhBWRRE+L7l+ppPER9bdLFxs1t5CrKc078h+wuUr05S1P1JjXk68pk3+kQKBgQDeK8AR11373Mzib6uzpjGzgNRMzdYNuExWjxyxAzz53NAR7zrPHvXvfIqjDScLJ4NcRO2TddhXAfZoOPVH5k4PJHKLBPKuXZpWlookCAyENY7+Pd55S8r+a+MusrMagYNljb5WbVTgN8cgdpim9lbbIFlpN6SZaVjLQL3J8TWH6wKBgQDSChzItkqWX11CNstJ9zJyUE20I7LrpyBJNgG1gtvz3ZMUQCn3PxxHtQzN9n1P0mSSYs+jBKPuoSyYLt1wwe10/lpgL4rkKWU3/m1Myt0tveJ9WcqHh6tzcAbb/fXpUFT/o4SWDimWkPkuCb+8j//2yiXk0a/T2f36zKMuZvujqQKBgC6B7BAQDG2H2B/ijofp12ejJU36nL98gAZyqOfpLJ+FeMz4TlBDQ+phIMhnHXA5UkdDapQ+zA3SrFk+6yGk9Vw4Hf46B+82SvOrSbmnMa+PYqKYIvUzR4gg34rL/7AhwnbEyD5hXq4dHwMNsIDq+l2elPjwm/U9V0gdAl2+r50HAoGALtsKqMvhv8HucAMBPrLikhXP/8um8mMKFMrzfqZ+otxfHzlhI0L08Bo3jQrb0Z7ByNY6M8epOmbCKADsbWcVre/AAY0ZkuSZK/CaOXNX/AhMKmKJh8qAOPRY02LIJRBCpfS4czEdnfUhYV/TYiFNnKRj57PPYZdTzUsxa/yVTmECgYBr7slQEjb5Onn5mZnGDh+72BxLNdgwBkhO0OCdpdISqk0F0Pxby22DFOKXZEpiyI9XYP1C8wPiJsShGm2yEwBPWXnrrZNWczaVuCbXHrZkWQogBDG3HGXNdU4MAWCyiYlyinIBpPpoAJZSzpGLmWbMWh28+RJS6AQX6KHrK1o2uw==' 293 | let alice: PeerId 294 | 295 | before(async function () { 296 | const encoded = uint8ArrayFromString(alicePrivKey, 'base64pad') 297 | const privateKey = await unmarshalPrivateKey(encoded) 298 | alice = await createFromPrivKey(privateKey) 299 | }) 300 | 301 | it('private key can be imported', async () => { 302 | const key = await ks.importPeer('alice', alice) 303 | expect(key.name).to.equal('alice') 304 | expect(key.id).to.equal(alice.toString()) 305 | }) 306 | 307 | it('private key can be exported', async () => { 308 | const alice2 = await ks.exportPeerId('alice') 309 | 310 | expect(alice.equals(alice2)).to.be.true() 311 | expect(alice2).to.have.property('privateKey').that.is.ok() 312 | expect(alice2).to.have.property('publicKey').that.is.ok() 313 | }) 314 | 315 | it('private key import requires a valid name', async () => { 316 | // @ts-expect-error invalid parameters 317 | await expect(ks.importPeer(undefined, alice)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') 318 | }) 319 | 320 | it('private key import requires the peer', async () => { 321 | // @ts-expect-error invalid parameters 322 | await expect(ks.importPeer('alice')).to.eventually.be.rejected.with.property('code', 'ERR_MISSING_PRIVATE_KEY') 323 | }) 324 | 325 | it('key id exists', async () => { 326 | const key = await ks.findKeyById(alice.toString()) 327 | expect(key).to.exist() 328 | expect(key).to.have.property('name', 'alice') 329 | expect(key).to.have.property('id', alice.toString()) 330 | }) 331 | 332 | it('key name exists', async () => { 333 | const key = await ks.findKeyByName('alice') 334 | expect(key).to.exist() 335 | expect(key).to.have.property('name', 'alice') 336 | expect(key).to.have.property('id', alice.toString()) 337 | }) 338 | 339 | it('can create Ed25519 peer id', async () => { 340 | const name = 'ed-key' 341 | await ks.createKey(name, 'Ed25519') 342 | const peer = await ks.exportPeerId(name) 343 | 344 | expect(peer).to.have.property('type', 'Ed25519') 345 | expect(peer).to.have.property('privateKey').that.is.ok() 346 | expect(peer).to.have.property('publicKey').that.is.ok() 347 | }) 348 | 349 | it('can create RSA peer id', async () => { 350 | const name = 'rsa-key' 351 | await ks.createKey(name, 'RSA', 2048) 352 | const peer = await ks.exportPeerId(name) 353 | 354 | expect(peer).to.have.property('type', 'RSA') 355 | expect(peer).to.have.property('privateKey').that.is.ok() 356 | expect(peer).to.have.property('publicKey').that.is.ok() 357 | }) 358 | 359 | it('can create secp256k1 peer id', async () => { 360 | const name = 'secp256k1-key' 361 | await ks.createKey(name, 'secp256k1') 362 | const peer = await ks.exportPeerId(name) 363 | 364 | expect(peer).to.have.property('type', 'secp256k1') 365 | expect(peer).to.have.property('privateKey').that.is.ok() 366 | expect(peer).to.have.property('publicKey').that.is.ok() 367 | }) 368 | }) 369 | 370 | describe('rename', () => { 371 | it('requires an existing key name', async () => { 372 | await expect(ks.renameKey('not-there', renamedRsaKeyName)).to.eventually.be.rejected.with.property('code', 'ERR_NOT_FOUND') 373 | }) 374 | 375 | it('requires a valid new key name', async () => { 376 | await expect(ks.renameKey(rsaKeyName, '..\not-valid')).to.eventually.be.rejected.with.property('code', 'ERR_NEW_KEY_NAME_INVALID') 377 | }) 378 | 379 | it('does not overwrite existing key', async () => { 380 | await expect(ks.renameKey(rsaKeyName, rsaKeyName)).to.eventually.be.rejected.with.property('code', 'ERR_KEY_ALREADY_EXISTS') 381 | }) 382 | 383 | it('cannot create the "self" key', async () => { 384 | await expect(ks.renameKey(rsaKeyName, 'self')).to.eventually.be.rejected.with.property('code', 'ERR_NEW_KEY_NAME_INVALID') 385 | }) 386 | 387 | it('removes the existing key name', async () => { 388 | const key = await ks.renameKey(rsaKeyName, renamedRsaKeyName) 389 | expect(key).to.exist() 390 | expect(key).to.have.property('name', renamedRsaKeyName) 391 | expect(key).to.have.property('id', rsaKeyInfo.id) 392 | // Try to find the changed key 393 | await expect(ks.findKeyByName(rsaKeyName)).to.eventually.be.rejected() 394 | }) 395 | 396 | it('creates the new key name', async () => { 397 | const key = await ks.findKeyByName(renamedRsaKeyName) 398 | expect(key).to.exist() 399 | expect(key).to.have.property('name', renamedRsaKeyName) 400 | }) 401 | 402 | it('does not change the key ID', async () => { 403 | const key = await ks.findKeyByName(renamedRsaKeyName) 404 | expect(key).to.exist() 405 | expect(key).to.have.property('name', renamedRsaKeyName) 406 | expect(key).to.have.property('id', rsaKeyInfo.id) 407 | }) 408 | 409 | it('throws with invalid key names', async () => { 410 | // @ts-expect-error invalid parameters 411 | await expect(ks.findKeyByName(undefined)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') 412 | }) 413 | }) 414 | 415 | describe('key removal', () => { 416 | it('cannot remove the "self" key', async () => { 417 | await expect(ks.removeKey('self')).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') 418 | }) 419 | 420 | it('cannot remove an unknown key', async () => { 421 | await expect(ks.removeKey('not-there')).to.eventually.be.rejected.with.property('code', 'ERR_KEY_NOT_FOUND') 422 | }) 423 | 424 | it('can remove a known key', async () => { 425 | const key = await ks.removeKey(renamedRsaKeyName) 426 | expect(key).to.exist() 427 | expect(key).to.have.property('name', renamedRsaKeyName) 428 | expect(key).to.have.property('id', rsaKeyInfo.id) 429 | }) 430 | }) 431 | 432 | describe('rotate keychain passphrase', () => { 433 | let oldPass: string 434 | let kc: KeyChain 435 | let options: KeyChainInit 436 | let ds: Datastore 437 | before(async () => { 438 | ds = new MemoryDatastore() 439 | oldPass = `hello-${Date.now()}-${Date.now()}` 440 | options = { 441 | pass: oldPass, 442 | dek: { 443 | salt: '3Nd/Ya4ENB3bcByNKptb4IR', 444 | iterationCount: 10000, 445 | keyLength: 64, 446 | hash: 'sha2-512' 447 | } 448 | } 449 | kc = new DefaultKeyChain({ 450 | datastore: ds 451 | }, options) 452 | }) 453 | 454 | it('should validate newPass is a string', async () => { 455 | // @ts-expect-error invalid parameters 456 | await expect(kc.rotateKeychainPass(oldPass, 1234567890)).to.eventually.be.rejected() 457 | }) 458 | 459 | it('should validate oldPass is a string', async () => { 460 | // @ts-expect-error invalid parameters 461 | await expect(kc.rotateKeychainPass(1234, 'newInsecurePassword1')).to.eventually.be.rejected() 462 | }) 463 | 464 | it('should validate newPass is at least 20 characters', async () => { 465 | try { 466 | await kc.rotateKeychainPass(oldPass, 'not20Chars') 467 | } catch (err: any) { 468 | expect(err).to.exist() 469 | } 470 | }) 471 | 472 | it('can rotate keychain passphrase', async () => { 473 | await kc.createKey('keyCreatedWithOldPassword', 'RSA', 2048) 474 | await kc.rotateKeychainPass(oldPass, 'newInsecurePassphrase') 475 | 476 | // Get Key PEM from datastore 477 | const dsname = new Key('/pkcs8/' + 'keyCreatedWithOldPassword') 478 | const res = await ds.get(dsname) 479 | const pem = uint8ArrayToString(res) 480 | 481 | const oldDek = options.pass != null 482 | ? pbkdf2( 483 | options.pass, 484 | options.dek?.salt ?? 'salt', 485 | options.dek?.iterationCount ?? 0, 486 | options.dek?.keyLength ?? 0, 487 | options.dek?.hash ?? 'sha2-256' 488 | ) 489 | : '' 490 | 491 | const newDek = pbkdf2( 492 | 'newInsecurePassphrase', 493 | options.dek?.salt ?? 'salt', 494 | options.dek?.iterationCount ?? 0, 495 | options.dek?.keyLength ?? 0, 496 | options.dek?.hash ?? 'sha2-256' 497 | ) 498 | 499 | // Dek with old password should not work: 500 | await expect(kc.importKey('keyWhosePassChanged', pem, oldDek)) 501 | .to.eventually.be.rejected() 502 | // Dek with new password should work: 503 | await expect(kc.importKey('keyWhosePasswordChanged', pem, newDek)) 504 | .to.eventually.have.property('name', 'keyWhosePasswordChanged') 505 | }).timeout(10000) 506 | }) 507 | }) 508 | 509 | describe('libp2p.keychain', () => { 510 | it('needs a passphrase to be used, otherwise throws an error', async () => { 511 | expect(() => { 512 | return new DefaultKeyChain({ 513 | datastore: new MemoryDatastore() 514 | }, { 515 | pass: '' 516 | }) 517 | }).to.throw() 518 | }) 519 | 520 | it('can be used when a passphrase is provided', async () => { 521 | const keychain = new DefaultKeyChain({ 522 | datastore: new MemoryDatastore() 523 | }, { 524 | pass: '12345678901234567890' 525 | }) 526 | 527 | const kInfo = await keychain.createKey('keyName', 'Ed25519') 528 | expect(kInfo).to.exist() 529 | }) 530 | 531 | it('can reload keys', async () => { 532 | const datastore = new MemoryDatastore() 533 | const keychain = new DefaultKeyChain({ 534 | datastore 535 | }, { 536 | pass: '12345678901234567890' 537 | }) 538 | 539 | const kInfo = await keychain.createKey('keyName', 'Ed25519') 540 | expect(kInfo).to.exist() 541 | 542 | const keychain2 = new DefaultKeyChain({ 543 | datastore 544 | }, { 545 | pass: '12345678901234567890' 546 | }) 547 | 548 | const key = await keychain2.findKeyByName('keyName') 549 | 550 | expect(key).to.exist() 551 | }) 552 | }) 553 | -------------------------------------------------------------------------------- /test/peerid.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import { supportedKeys, unmarshalPrivateKey, unmarshalPublicKey } from '@libp2p/crypto/keys' 4 | import { createFromPrivKey } from '@libp2p/peer-id-factory' 5 | import { expect } from 'aegir/chai' 6 | import { base58btc } from 'multiformats/bases/base58' 7 | import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' 8 | import type { PeerId } from '@libp2p/interface-peer-id' 9 | 10 | const sample = { 11 | id: '122019318b6e5e0cf93a2314bf01269a2cc23cd3dcd452d742cdb9379d8646f6e4a9', 12 | privKey: 'CAASpgkwggSiAgEAAoIBAQC2SKo/HMFZeBml1AF3XijzrxrfQXdJzjePBZAbdxqKR1Mc6juRHXij6HXYPjlAk01BhF1S3Ll4Lwi0cAHhggf457sMg55UWyeGKeUv0ucgvCpBwlR5cQ020i0MgzjPWOLWq1rtvSbNcAi2ZEVn6+Q2EcHo3wUvWRtLeKz+DZSZfw2PEDC+DGPJPl7f8g7zl56YymmmzH9liZLNrzg/qidokUv5u1pdGrcpLuPNeTODk0cqKB+OUbuKj9GShYECCEjaybJDl9276oalL9ghBtSeEv20kugatTvYy590wFlJkkvyl+nPxIH0EEYMKK9XRWlu9XYnoSfboiwcv8M3SlsjAgMBAAECggEAZtju/bcKvKFPz0mkHiaJcpycy9STKphorpCT83srBVQi59CdFU6Mj+aL/xt0kCPMVigJw8P3/YCEJ9J+rS8BsoWE+xWUEsJvtXoT7vzPHaAtM3ci1HZd302Mz1+GgS8Epdx+7F5p80XAFLDUnELzOzKftvWGZmWfSeDnslwVONkL/1VAzwKy7Ce6hk4SxRE7l2NE2OklSHOzCGU1f78ZzVYKSnS5Ag9YrGjOAmTOXDbKNKN/qIorAQ1bovzGoCwx3iGIatQKFOxyVCyO1PsJYT7JO+kZbhBWRRE+L7l+ppPER9bdLFxs1t5CrKc078h+wuUr05S1P1JjXk68pk3+kQKBgQDeK8AR11373Mzib6uzpjGzgNRMzdYNuExWjxyxAzz53NAR7zrPHvXvfIqjDScLJ4NcRO2TddhXAfZoOPVH5k4PJHKLBPKuXZpWlookCAyENY7+Pd55S8r+a+MusrMagYNljb5WbVTgN8cgdpim9lbbIFlpN6SZaVjLQL3J8TWH6wKBgQDSChzItkqWX11CNstJ9zJyUE20I7LrpyBJNgG1gtvz3ZMUQCn3PxxHtQzN9n1P0mSSYs+jBKPuoSyYLt1wwe10/lpgL4rkKWU3/m1Myt0tveJ9WcqHh6tzcAbb/fXpUFT/o4SWDimWkPkuCb+8j//2yiXk0a/T2f36zKMuZvujqQKBgC6B7BAQDG2H2B/ijofp12ejJU36nL98gAZyqOfpLJ+FeMz4TlBDQ+phIMhnHXA5UkdDapQ+zA3SrFk+6yGk9Vw4Hf46B+82SvOrSbmnMa+PYqKYIvUzR4gg34rL/7AhwnbEyD5hXq4dHwMNsIDq+l2elPjwm/U9V0gdAl2+r50HAoGALtsKqMvhv8HucAMBPrLikhXP/8um8mMKFMrzfqZ+otxfHzlhI0L08Bo3jQrb0Z7ByNY6M8epOmbCKADsbWcVre/AAY0ZkuSZK/CaOXNX/AhMKmKJh8qAOPRY02LIJRBCpfS4czEdnfUhYV/TYiFNnKRj57PPYZdTzUsxa/yVTmECgYBr7slQEjb5Onn5mZnGDh+72BxLNdgwBkhO0OCdpdISqk0F0Pxby22DFOKXZEpiyI9XYP1C8wPiJsShGm2yEwBPWXnrrZNWczaVuCbXHrZkWQogBDG3HGXNdU4MAWCyiYlyinIBpPpoAJZSzpGLmWbMWh28+RJS6AQX6KHrK1o2uw==', 13 | pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2SKo/HMFZeBml1AF3XijzrxrfQXdJzjePBZAbdxqKR1Mc6juRHXij6HXYPjlAk01BhF1S3Ll4Lwi0cAHhggf457sMg55UWyeGKeUv0ucgvCpBwlR5cQ020i0MgzjPWOLWq1rtvSbNcAi2ZEVn6+Q2EcHo3wUvWRtLeKz+DZSZfw2PEDC+DGPJPl7f8g7zl56YymmmzH9liZLNrzg/qidokUv5u1pdGrcpLuPNeTODk0cqKB+OUbuKj9GShYECCEjaybJDl9276oalL9ghBtSeEv20kugatTvYy590wFlJkkvyl+nPxIH0EEYMKK9XRWlu9XYnoSfboiwcv8M3SlsjAgMBAAE=' 14 | } 15 | 16 | describe('peer ID', () => { 17 | let peer: PeerId 18 | let publicKeyDer: Uint8Array // a buffer 19 | 20 | before(async () => { 21 | const encoded = uint8ArrayFromString(sample.privKey, 'base64pad') 22 | peer = await createFromPrivKey(await unmarshalPrivateKey(encoded)) 23 | }) 24 | 25 | it('decoded public key', async () => { 26 | if (peer.publicKey == null) { 27 | throw new Error('PublicKey missing from PeerId') 28 | } 29 | 30 | if (peer.privateKey == null) { 31 | throw new Error('PrivateKey missing from PeerId') 32 | } 33 | 34 | // get protobuf version of the public key 35 | const publicKeyProtobuf = peer.publicKey 36 | const publicKey = unmarshalPublicKey(publicKeyProtobuf) 37 | publicKeyDer = publicKey.marshal() 38 | 39 | // get protobuf version of the private key 40 | const privateKeyProtobuf = peer.privateKey 41 | const key = await unmarshalPrivateKey(privateKeyProtobuf) 42 | expect(key).to.exist() 43 | }) 44 | 45 | it('encoded public key with DER', async () => { 46 | const rsa = supportedKeys.rsa.unmarshalRsaPublicKey(publicKeyDer) 47 | const keyId = await rsa.hash() 48 | const kids = base58btc.encode(keyId).substring(1) 49 | expect(kids).to.equal(peer.toString()) 50 | }) 51 | 52 | it('encoded public key with JWT', async () => { 53 | const jwk = { 54 | kty: 'RSA', 55 | n: 'tkiqPxzBWXgZpdQBd14o868a30F3Sc43jwWQG3caikdTHOo7kR14o-h12D45QJNNQYRdUty5eC8ItHAB4YIH-Oe7DIOeVFsnhinlL9LnILwqQcJUeXENNtItDIM4z1ji1qta7b0mzXAItmRFZ-vkNhHB6N8FL1kbS3is_g2UmX8NjxAwvgxjyT5e3_IO85eemMpppsx_ZYmSza84P6onaJFL-btaXRq3KS7jzXkzg5NHKigfjlG7io_RkoWBAghI2smyQ5fdu-qGpS_YIQbUnhL9tJLoGrU72MufdMBZSZJL8pfpz8SB9BBGDCivV0VpbvV2J6En26IsHL_DN0pbIw', 56 | e: 'AQAB', 57 | alg: 'RS256', 58 | kid: '2011-04-29' 59 | } 60 | const rsa = new supportedKeys.rsa.RsaPublicKey(jwk) 61 | const keyId = await rsa.hash() 62 | const kids = base58btc.encode(keyId).substring(1) 63 | expect(kids).to.equal(peer.toString()) 64 | }) 65 | 66 | it('decoded private key', async () => { 67 | if (peer.privateKey == null) { 68 | throw new Error('PrivateKey missing from PeerId') 69 | } 70 | 71 | // get protobuf version of the private key 72 | const privateKeyProtobuf = peer.privateKey 73 | const key = await unmarshalPrivateKey(privateKeyProtobuf) 74 | expect(key).to.exist() 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "aegir/src/config/tsconfig.aegir.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "emitDeclarationOnly": false, 6 | "module": "ES2020" 7 | }, 8 | "include": [ 9 | "src", 10 | "test" 11 | ] 12 | } 13 | --------------------------------------------------------------------------------