diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..ef41d4fa
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+*.png binary
+* crlf=input
diff --git a/keychain/CHANGELOG.md b/keychain/CHANGELOG.md
new file mode 100644
index 00000000..f661d419
--- /dev/null
+++ b/keychain/CHANGELOG.md
@@ -0,0 +1,147 @@
+
+# [0.6.0](https://github.com/libp2p/js-libp2p-keychain/compare/v0.5.4...v0.6.0) (2019-12-18)
+
+
+
+
+## [0.5.4](https://github.com/libp2p/js-libp2p-keychain/compare/v0.5.3...v0.5.4) (2019-12-18)
+
+
+
+
+## [0.5.3](https://github.com/libp2p/js-libp2p-keychain/compare/v0.5.2...v0.5.3) (2019-12-18)
+
+
+
+
+## [0.5.2](https://github.com/libp2p/js-libp2p-keychain/compare/v0.5.1...v0.5.2) (2019-12-02)
+
+
+
+
+## [0.5.1](https://github.com/libp2p/js-libp2p-keychain/compare/v0.5.0...v0.5.1) (2019-09-25)
+
+
+
+
+# [0.5.0](https://github.com/libp2p/js-libp2p-keychain/compare/v0.4.2...v0.5.0) (2019-08-16)
+
+
+* 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)
+
+
+### BREAKING CHANGES
+
+* The api now uses async/await instead of callbacks.
+
+Co-Authored-By: Vasco Santos
+
+
+
+
+## [0.4.2](https://github.com/libp2p/js-libp2p-keychain/compare/v0.4.1...v0.4.2) (2019-06-13)
+
+
+### Bug Fixes
+
+* 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))
+
+
+
+
+## [0.4.1](https://github.com/libp2p/js-libp2p-keychain/compare/v0.4.0...v0.4.1) (2019-03-14)
+
+
+
+
+# [0.4.0](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.6...v0.4.0) (2019-02-26)
+
+
+### Features
+
+* 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))
+
+
+
+
+## [0.3.6](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.5...v0.3.6) (2019-01-10)
+
+
+### Bug Fixes
+
+* reduce bundle size ([#28](https://github.com/libp2p/js-libp2p-keychain/issues/28)) ([7eeed87](https://github.com/libp2p/js-libp2p-keychain/commit/7eeed87))
+
+
+
+
+## [0.3.5](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.4...v0.3.5) (2019-01-10)
+
+
+
+
+## [0.3.4](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.3...v0.3.4) (2019-01-04)
+
+
+
+
+## [0.3.3](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.2...v0.3.3) (2018-10-25)
+
+
+
+
+## [0.3.2](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.1...v0.3.2) (2018-09-18)
+
+
+### Bug Fixes
+
+* validate createKey params properly ([#26](https://github.com/libp2p/js-libp2p-keychain/issues/26)) ([8dfaab1](https://github.com/libp2p/js-libp2p-keychain/commit/8dfaab1))
+
+
+
+
+## [0.3.1](https://github.com/libp2p/js-libp2p-keychain/compare/v0.3.0...v0.3.1) (2018-01-29)
+
+
+
+
+# [0.3.0](https://github.com/libp2p/js-libp2p-keychain/compare/v0.2.1...v0.3.0) (2018-01-29)
+
+
+### Bug Fixes
+
+* deepmerge 2.0.1 fails in browser, stay with 1.5.2 ([2ce4444](https://github.com/libp2p/js-libp2p-keychain/commit/2ce4444))
+
+
+
+
+## [0.2.1](https://github.com/libp2p/js-libp2p-keychain/compare/v0.2.0...v0.2.1) (2017-12-28)
+
+
+### Features
+
+* 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))
+
+
+
+
+# 0.2.0 (2017-12-20)
+
+
+### Bug Fixes
+
+* error message ([8305d20](https://github.com/libp2p/js-libp2p-keychain/commit/8305d20))
+* lint errors ([06917f7](https://github.com/libp2p/js-libp2p-keychain/commit/06917f7))
+* lint errors ([ff4f656](https://github.com/libp2p/js-libp2p-keychain/commit/ff4f656))
+* linting ([409a999](https://github.com/libp2p/js-libp2p-keychain/commit/409a999))
+* 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)
+* more linting ([7c44c91](https://github.com/libp2p/js-libp2p-keychain/commit/7c44c91))
+* 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))
+
+
+### Features
+
+* move bits from https://github.com/richardschneider/ipfs-encryption ([1a96ae8](https://github.com/libp2p/js-libp2p-keychain/commit/1a96ae8))
+* use libp2p-crypto ([#18](https://github.com/libp2p/js-libp2p-keychain/issues/18)) ([c1627a9](https://github.com/libp2p/js-libp2p-keychain/commit/c1627a9))
+
+
+
diff --git a/keychain/LICENSE b/keychain/LICENSE
new file mode 100644
index 00000000..bbfffbf9
--- /dev/null
+++ b/keychain/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 libp2p
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/keychain/README.md b/keychain/README.md
new file mode 100644
index 00000000..37829b48
--- /dev/null
+++ b/keychain/README.md
@@ -0,0 +1,123 @@
+# js-libp2p-keychain
+
+[](http://protocol.ai)
+[](http://libp2p.io/)
+[](http://webchat.freenode.net/?channels=%23libp2p)
+[](https://discuss.libp2p.io)
+[](https://codecov.io/gh/libp2p/js-libp2p-keychain)
+[](https://travis-ci.com/libp2p/js-libp2p-keychain)
+[](https://david-dm.org/libp2p/js-libp2p-keychain)
+[](https://github.com/feross/standard)
+
+> A secure key chain for libp2p in JavaScript
+
+## Lead Maintainer
+
+[Vasco Santos](https://github.com/vasco-santos).
+
+## Features
+
+- Manages the lifecycle of a key
+- Keys are encrypted at rest
+- Enforces the use of safe key names
+- Uses encrypted PKCS 8 for key storage
+- Uses PBKDF2 for a "stetched" key encryption key
+- Enforces NIST SP 800-131A and NIST SP 800-132
+- Uses PKCS 7: CMS (aka RFC 5652) to provide cryptographically protected messages
+- Delays reporting errors to slow down brute force attacks
+
+## Table of Contents
+
+## Install
+
+```sh
+npm install --save libp2p-keychain
+```
+
+### Usage
+
+```js
+const Keychain = require('libp2p-keychain')
+const FsStore = require('datastore-fs')
+
+const datastore = new FsStore('./a-keystore')
+const opts = {
+ passPhrase: 'some long easily remembered phrase'
+}
+const keychain = new Keychain(datastore, opts)
+```
+
+## API
+
+Managing a key
+
+- `async createKey (name, type, size)`
+- `async renameKey (oldName, newName)`
+- `async removeKey (name)`
+- `async exportKey (name, password)`
+- `async importKey (name, pem, password)`
+- `async importPeer (name, peer)`
+
+A naming service for a key
+
+- `async listKeys ()`
+- `async findKeyById (id)`
+- `async findKeyByName (name)`
+
+Cryptographically protected messages
+
+- `async cms.encrypt (name, plain)`
+- `async cms.decrypt (cmsData)`
+
+### KeyInfo
+
+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.
+
+```js
+{
+ name: 'rsa-key',
+ id: 'QmYWYSUZ4PV6MRFYpdtEDJBiGs4UrmE6g8wmAWSePekXVW'
+}
+```
+
+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).
+
+### Private key storage
+
+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**.
+
+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.
+
+```js
+const defaultOptions = {
+ //See https://cryptosense.com/parameter-choice-for-pbkdf2/
+ dek: {
+ keyLength: 512 / 8,
+ iterationCount: 1000,
+ salt: 'at least 16 characters long',
+ hash: 'sha2-512'
+ }
+}
+```
+
+
+
+### Physical storage
+
+The actual physical storage of an encrypted key is left to implementations of [interface-datastore](https://github.com/ipfs/interface-datastore/). A key benifit is that now the key chain can be used in browser with the [js-datastore-level](https://github.com/ipfs/js-datastore-level) implementation.
+
+### Cryptographic Message Syntax (CMS)
+
+CMS, aka [PKCS #7](https://en.wikipedia.org/wiki/PKCS) and [RFC 5652](https://tools.ietf.org/html/rfc5652), describes an encapsulation syntax for data protection. It is used to digitally sign, digest, authenticate, or encrypt arbitrary message content. Basically, `cms.encrypt` creates a DER message that can be only be read by someone holding the private key.
+
+## Contribute
+
+Feel free to join in. All welcome. Open an [issue](https://github.com/libp2p/js-libp2p-keychain/issues)!
+
+This repository falls under the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md).
+
+[](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md)
+
+## License
+
+[MIT](LICENSE)
diff --git a/keychain/doc/private-key.png b/keychain/doc/private-key.png
new file mode 100644
index 00000000..4c85dc61
Binary files /dev/null and b/keychain/doc/private-key.png differ
diff --git a/keychain/doc/private-key.xml b/keychain/doc/private-key.xml
new file mode 100644
index 00000000..51cb8c5a
--- /dev/null
+++ b/keychain/doc/private-key.xml
@@ -0,0 +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==
\ No newline at end of file
diff --git a/keychain/package.json b/keychain/package.json
new file mode 100644
index 00000000..29b8873a
--- /dev/null
+++ b/keychain/package.json
@@ -0,0 +1,77 @@
+{
+ "name": "libp2p-keychain",
+ "version": "0.6.0",
+ "description": "Key management and cryptographically protected messages",
+ "leadMaintainer": "Vasco Santos ",
+ "main": "src/index.js",
+ "scripts": {
+ "lint": "aegir lint",
+ "build": "aegir build",
+ "coverage": "nyc --reporter=text --reporter=lcov npm run test:node",
+ "test": "aegir test -t node -t browser",
+ "test:node": "aegir test -t node",
+ "test:browser": "aegir test -t browser",
+ "release": "aegir release",
+ "release-minor": "aegir release --type minor",
+ "release-major": "aegir release --type major"
+ },
+ "pre-push": [
+ "lint"
+ ],
+ "engines": {
+ "node": ">=10.0.0",
+ "npm": ">=3.0.0"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/libp2p/js-libp2p-keychain.git"
+ },
+ "keywords": [
+ "IPFS",
+ "libp2p",
+ "keys",
+ "encryption",
+ "secure",
+ "crypto"
+ ],
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/libp2p/js-libp2p-keychain/issues"
+ },
+ "homepage": "https://github.com/libp2p/js-libp2p-keychain#readme",
+ "dependencies": {
+ "err-code": "^2.0.0",
+ "interface-datastore": "^0.8.0",
+ "libp2p-crypto": "^0.17.1",
+ "merge-options": "^2.0.0",
+ "node-forge": "^0.9.1",
+ "sanitize-filename": "^1.6.1"
+ },
+ "devDependencies": {
+ "aegir": "^20.0.0",
+ "chai": "^4.2.0",
+ "chai-string": "^1.5.0",
+ "datastore-fs": "^0.9.0",
+ "datastore-level": "^0.14.0",
+ "dirty-chai": "^2.0.1",
+ "level": "^6.0.0",
+ "multihashes": "^0.4.15",
+ "peer-id": "^0.13.5",
+ "promisify-es6": "^1.0.3",
+ "rimraf": "^3.0.0"
+ },
+ "contributors": [
+ "Alan Shaw ",
+ "Alberto Elias ",
+ "Alex Potsides ",
+ "David Dias ",
+ "Hugo Dias ",
+ "Jacob Heun ",
+ "Maciej Krüger ",
+ "Masahiro Saito ",
+ "Richard Schneider ",
+ "Vasco Santos ",
+ "Vasco Santos ",
+ "Victor Bjelkholm "
+ ]
+}
diff --git a/keychain/src/cms.js b/keychain/src/cms.js
new file mode 100644
index 00000000..9bec4b94
--- /dev/null
+++ b/keychain/src/cms.js
@@ -0,0 +1,122 @@
+'use strict'
+
+require('node-forge/lib/pkcs7')
+require('node-forge/lib/pbe')
+const forge = require('node-forge/lib/forge')
+const { certificateForKey, findAsync } = require('./util')
+const errcode = require('err-code')
+
+/**
+ * Cryptographic Message Syntax (aka PKCS #7)
+ *
+ * CMS describes an encapsulation syntax for data protection. It
+ * is used to digitally sign, digest, authenticate, or encrypt
+ * arbitrary message content.
+ *
+ * See RFC 5652 for all the details.
+ */
+class CMS {
+ /**
+ * Creates a new instance with a keychain
+ *
+ * @param {Keychain} keychain - the available keys
+ */
+ constructor (keychain) {
+ if (!keychain) {
+ throw errcode(new Error('keychain is required'), 'ERR_KEYCHAIN_REQUIRED')
+ }
+
+ this.keychain = keychain
+ }
+
+ /**
+ * Creates some protected data.
+ *
+ * The output Buffer contains the PKCS #7 message in DER.
+ *
+ * @param {string} name - The local key name.
+ * @param {Buffer} plain - The data to encrypt.
+ * @returns {undefined}
+ */
+ async encrypt (name, plain) {
+ if (!Buffer.isBuffer(plain)) {
+ throw errcode(new Error('Plain data must be a Buffer'), 'ERR_INVALID_PARAMS')
+ }
+
+ const key = await this.keychain.findKeyByName(name)
+ const pem = await this.keychain._getPrivateKey(name)
+ const privateKey = forge.pki.decryptRsaPrivateKey(pem, this.keychain._())
+ const certificate = await certificateForKey(key, privateKey)
+
+ // create a p7 enveloped message
+ const p7 = forge.pkcs7.createEnvelopedData()
+ p7.addRecipient(certificate)
+ p7.content = forge.util.createBuffer(plain)
+ p7.encrypt()
+
+ // convert message to DER
+ const der = forge.asn1.toDer(p7.toAsn1()).getBytes()
+ return Buffer.from(der, 'binary')
+ }
+
+ /**
+ * Reads some protected data.
+ *
+ * The keychain must contain one of the keys used to encrypt the data. If none of the keys
+ * exists, an Error is returned with the property 'missingKeys'. It is array of key ids.
+ *
+ * @param {Buffer} cmsData - The CMS encrypted data to decrypt.
+ * @returns {undefined}
+ */
+ async decrypt (cmsData) {
+ if (!Buffer.isBuffer(cmsData)) {
+ throw errcode(new Error('CMS data is required'), 'ERR_INVALID_PARAMS')
+ }
+
+ let cms
+ try {
+ const buf = forge.util.createBuffer(cmsData.toString('binary'))
+ const obj = forge.asn1.fromDer(buf)
+ cms = forge.pkcs7.messageFromAsn1(obj)
+ } catch (err) {
+ throw errcode(new Error('Invalid CMS: ' + err.message), 'ERR_INVALID_CMS')
+ }
+
+ // Find a recipient whose key we hold. We only deal with recipient certs
+ // issued by ipfs (O=ipfs).
+ const recipients = cms.recipients
+ .filter(r => r.issuer.find(a => a.shortName === 'O' && a.value === 'ipfs'))
+ .filter(r => r.issuer.find(a => a.shortName === 'CN'))
+ .map(r => {
+ return {
+ recipient: r,
+ keyId: r.issuer.find(a => a.shortName === 'CN').value
+ }
+ })
+
+ const r = await findAsync(recipients, async (recipient) => {
+ try {
+ const key = await this.keychain.findKeyById(recipient.keyId)
+ if (key) return true
+ } catch (err) {
+ return false
+ }
+ return false
+ })
+
+ if (!r) {
+ const missingKeys = recipients.map(r => r.keyId)
+ throw errcode(new Error('Decryption needs one of the key(s): ' + missingKeys.join(', ')), 'ERR_MISSING_KEYS', {
+ missingKeys
+ })
+ }
+
+ const key = await this.keychain.findKeyById(r.keyId)
+ const pem = await this.keychain._getPrivateKey(key.name)
+ const privateKey = forge.pki.decryptRsaPrivateKey(pem, this.keychain._())
+ cms.decrypt(r.recipient, privateKey)
+ return Buffer.from(cms.content.getBytes(), 'binary')
+ }
+}
+
+module.exports = CMS
diff --git a/keychain/src/index.js b/keychain/src/index.js
new file mode 100644
index 00000000..2704d626
--- /dev/null
+++ b/keychain/src/index.js
@@ -0,0 +1,3 @@
+'use strict'
+
+module.exports = require('./keychain')
diff --git a/keychain/src/keychain.js b/keychain/src/keychain.js
new file mode 100644
index 00000000..aae78972
--- /dev/null
+++ b/keychain/src/keychain.js
@@ -0,0 +1,469 @@
+/* eslint max-nested-callbacks: ["error", 5] */
+'use strict'
+
+const sanitize = require('sanitize-filename')
+const mergeOptions = require('merge-options')
+const crypto = require('libp2p-crypto')
+const DS = require('interface-datastore')
+const CMS = require('./cms')
+const errcode = require('err-code')
+
+const keyPrefix = '/pkcs8/'
+const infoPrefix = '/info/'
+
+// NIST SP 800-132
+const NIST = {
+ minKeyLength: 112 / 8,
+ minSaltLength: 128 / 8,
+ minIterationCount: 1000
+}
+
+const defaultOptions = {
+ // See https://cryptosense.com/parametesr-choice-for-pbkdf2/
+ dek: {
+ keyLength: 512 / 8,
+ iterationCount: 10000,
+ salt: 'you should override this value with a crypto secure random number',
+ hash: 'sha2-512'
+ }
+}
+
+function validateKeyName (name) {
+ if (!name) return false
+ if (typeof name !== 'string') return false
+ return name === sanitize(name.trim())
+}
+
+/**
+ * Throws an error after a delay
+ *
+ * This assumes than an error indicates that the keychain is under attack. Delay returning an
+ * error to make brute force attacks harder.
+ *
+ * @param {string | Error} err - The error
+ * @private
+ */
+async function throwDelayed (err) {
+ const min = 200
+ const max = 1000
+ const delay = Math.random() * (max - min) + min
+
+ await new Promise(resolve => setTimeout(resolve, delay))
+ throw err
+}
+
+/**
+ * Converts a key name into a datastore name.
+ *
+ * @param {string} name
+ * @returns {DS.Key}
+ * @private
+ */
+function DsName (name) {
+ return new DS.Key(keyPrefix + name)
+}
+
+/**
+ * Converts a key name into a datastore info name.
+ *
+ * @param {string} name
+ * @returns {DS.Key}
+ * @private
+ */
+function DsInfoName (name) {
+ return new DS.Key(infoPrefix + name)
+}
+
+/**
+ * Information about a key.
+ *
+ * @typedef {Object} KeyInfo
+ *
+ * @property {string} id - The universally unique key id.
+ * @property {string} name - The local key name.
+ */
+
+/**
+ * Manages the lifecycle of a key. Keys are encrypted at rest using PKCS #8.
+ *
+ * A key in the store has two entries
+ * - '/info/*key-name*', contains the KeyInfo for the key
+ * - '/pkcs8/*key-name*', contains the PKCS #8 for the key
+ *
+ */
+class Keychain {
+ /**
+ * Creates a new instance of a key chain.
+ *
+ * @param {DS} store - where the key are.
+ * @param {object} options - ???
+ */
+ constructor (store, options) {
+ if (!store) {
+ throw new Error('store is required')
+ }
+ this.store = store
+
+ const opts = mergeOptions(defaultOptions, options)
+
+ // Enforce NIST SP 800-132
+ if (!opts.passPhrase || opts.passPhrase.length < 20) {
+ throw new Error('passPhrase must be least 20 characters')
+ }
+ if (opts.dek.keyLength < NIST.minKeyLength) {
+ throw new Error(`dek.keyLength must be least ${NIST.minKeyLength} bytes`)
+ }
+ if (opts.dek.salt.length < NIST.minSaltLength) {
+ throw new Error(`dek.saltLength must be least ${NIST.minSaltLength} bytes`)
+ }
+ if (opts.dek.iterationCount < NIST.minIterationCount) {
+ throw new Error(`dek.iterationCount must be least ${NIST.minIterationCount}`)
+ }
+
+ // Create the derived encrypting key
+ const dek = crypto.pbkdf2(
+ opts.passPhrase,
+ opts.dek.salt,
+ opts.dek.iterationCount,
+ opts.dek.keyLength,
+ opts.dek.hash)
+ Object.defineProperty(this, '_', { value: () => dek })
+ }
+
+ /**
+ * Gets an object that can encrypt/decrypt protected data
+ * using the Cryptographic Message Syntax (CMS).
+ *
+ * CMS describes an encapsulation syntax for data protection. It
+ * is used to digitally sign, digest, authenticate, or encrypt
+ * arbitrary message content.
+ *
+ * @returns {CMS}
+ */
+ get cms () {
+ return new CMS(this)
+ }
+
+ /**
+ * Generates the options for a keychain. A random salt is produced.
+ *
+ * @returns {object}
+ */
+ static generateOptions () {
+ const options = Object.assign({}, defaultOptions)
+ const saltLength = Math.ceil(NIST.minSaltLength / 3) * 3 // no base64 padding
+ options.dek.salt = crypto.randomBytes(saltLength).toString('base64')
+ return options
+ }
+
+ /**
+ * Gets an object that can encrypt/decrypt protected data.
+ * The default options for a keychain.
+ *
+ * @returns {object}
+ */
+ static get options () {
+ return defaultOptions
+ }
+
+ /**
+ * Create a new key.
+ *
+ * @param {string} name - The local key name; cannot already exist.
+ * @param {string} type - One of the key types; 'rsa'.
+ * @param {int} size - The key size in bits.
+ * @returns {KeyInfo}
+ */
+ async createKey (name, type, size) {
+ const self = this
+
+ if (!validateKeyName(name) || name === 'self') {
+ return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME'))
+ }
+
+ if (typeof type !== 'string') {
+ return throwDelayed(errcode(new Error(`Invalid key type '${type}'`), 'ERR_INVALID_KEY_TYPE'))
+ }
+
+ if (!Number.isSafeInteger(size)) {
+ return throwDelayed(errcode(new Error(`Invalid key size '${size}'`), 'ERR_INVALID_KEY_SIZE'))
+ }
+
+ const dsname = DsName(name)
+ const exists = await self.store.has(dsname)
+ if (exists) return throwDelayed(errcode(new Error(`Key '${name}' already exists`), 'ERR_KEY_ALREADY_EXISTS'))
+
+ switch (type.toLowerCase()) {
+ case 'rsa':
+ if (size < 2048) {
+ return throwDelayed(errcode(new Error(`Invalid RSA key size ${size}`), 'ERR_INVALID_KEY_SIZE'))
+ }
+ break
+ default:
+ break
+ }
+
+ let keyInfo
+ try {
+ const keypair = await crypto.keys.generateKeyPair(type, size)
+ const kid = await keypair.id()
+ const pem = await keypair.export(this._())
+ keyInfo = {
+ name: name,
+ id: kid
+ }
+ const batch = self.store.batch()
+ batch.put(dsname, pem)
+ batch.put(DsInfoName(name), JSON.stringify(keyInfo))
+
+ await batch.commit()
+ } catch (err) {
+ return throwDelayed(err)
+ }
+
+ return keyInfo
+ }
+
+ /**
+ * List all the keys.
+ *
+ * @returns {KeyInfo[]}
+ */
+ async listKeys () {
+ const self = this
+ const query = {
+ prefix: infoPrefix
+ }
+
+ const info = []
+ for await (const value of self.store.query(query)) {
+ info.push(JSON.parse(value.value))
+ }
+
+ return info
+ }
+
+ /**
+ * Find a key by it's id.
+ *
+ * @param {string} id - The universally unique key identifier.
+ * @returns {KeyInfo}
+ */
+ async findKeyById (id) {
+ try {
+ const keys = await this.listKeys()
+ return keys.find((k) => k.id === id)
+ } catch (err) {
+ return throwDelayed(err)
+ }
+ }
+
+ /**
+ * Find a key by it's name.
+ *
+ * @param {string} name - The local key name.
+ * @returns {KeyInfo}
+ */
+ async findKeyByName (name) {
+ if (!validateKeyName(name)) {
+ return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME'))
+ }
+
+ const dsname = DsInfoName(name)
+ try {
+ const res = await this.store.get(dsname)
+ return JSON.parse(res.toString())
+ } catch (err) {
+ return throwDelayed(errcode(new Error(`Key '${name}' does not exist. ${err.message}`), 'ERR_KEY_NOT_FOUND'))
+ }
+ }
+
+ /**
+ * Remove an existing key.
+ *
+ * @param {string} name - The local key name; must already exist.
+ * @returns {KeyInfo}
+ */
+ async removeKey (name) {
+ const self = this
+ if (!validateKeyName(name) || name === 'self') {
+ return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME'))
+ }
+ const dsname = DsName(name)
+ const keyInfo = await self.findKeyByName(name)
+ const batch = self.store.batch()
+ batch.delete(dsname)
+ batch.delete(DsInfoName(name))
+ await batch.commit()
+ return keyInfo
+ }
+
+ /**
+ * Rename a key
+ *
+ * @param {string} oldName - The old local key name; must already exist.
+ * @param {string} newName - The new local key name; must not already exist.
+ * @returns {KeyInfo}
+ */
+ async renameKey (oldName, newName) {
+ const self = this
+ if (!validateKeyName(oldName) || oldName === 'self') {
+ return throwDelayed(errcode(new Error(`Invalid old key name '${oldName}'`), 'ERR_OLD_KEY_NAME_INVALID'))
+ }
+ if (!validateKeyName(newName) || newName === 'self') {
+ return throwDelayed(errcode(new Error(`Invalid new key name '${newName}'`), 'ERR_NEW_KEY_NAME_INVALID'))
+ }
+ const oldDsname = DsName(oldName)
+ const newDsname = DsName(newName)
+ const oldInfoName = DsInfoName(oldName)
+ const newInfoName = DsInfoName(newName)
+
+ const exists = await self.store.has(newDsname)
+ if (exists) return throwDelayed(errcode(new Error(`Key '${newName}' already exists`), 'ERR_KEY_ALREADY_EXISTS'))
+
+ try {
+ let res = await this.store.get(oldDsname)
+ const pem = res.toString()
+ res = await self.store.get(oldInfoName)
+
+ const keyInfo = JSON.parse(res.toString())
+ keyInfo.name = newName
+ const batch = self.store.batch()
+ batch.put(newDsname, pem)
+ batch.put(newInfoName, JSON.stringify(keyInfo))
+ batch.delete(oldDsname)
+ batch.delete(oldInfoName)
+ await batch.commit()
+ return keyInfo
+ } catch (err) {
+ return throwDelayed(err)
+ }
+ }
+
+ /**
+ * Export an existing key as a PEM encrypted PKCS #8 string
+ *
+ * @param {string} name - The local key name; must already exist.
+ * @param {string} password - The password
+ * @returns {string}
+ */
+ async exportKey (name, password) {
+ if (!validateKeyName(name)) {
+ return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME'))
+ }
+ if (!password) {
+ return throwDelayed(errcode(new Error('Password is required'), 'ERR_PASSWORD_REQUIRED'))
+ }
+
+ const dsname = DsName(name)
+ try {
+ const res = await this.store.get(dsname)
+ const pem = res.toString()
+ const privateKey = await crypto.keys.import(pem, this._())
+ return privateKey.export(password)
+ } catch (err) {
+ return throwDelayed(err)
+ }
+ }
+
+ /**
+ * Import a new key from a PEM encoded PKCS #8 string
+ *
+ * @param {string} name - The local key name; must not already exist.
+ * @param {string} pem - The PEM encoded PKCS #8 string
+ * @param {string} password - The password.
+ * @returns {KeyInfo}
+ */
+ async importKey (name, pem, password) {
+ const self = this
+ if (!validateKeyName(name) || name === 'self') {
+ return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME'))
+ }
+ if (!pem) {
+ return throwDelayed(errcode(new Error('PEM encoded key is required'), 'ERR_PEM_REQUIRED'))
+ }
+ const dsname = DsName(name)
+ const exists = await self.store.has(dsname)
+ if (exists) return throwDelayed(errcode(new Error(`Key '${name}' already exists`), 'ERR_KEY_ALREADY_EXISTS'))
+
+ let privateKey
+ try {
+ privateKey = await crypto.keys.import(pem, password)
+ } catch (err) {
+ return throwDelayed(errcode(new Error('Cannot read the key, most likely the password is wrong'), 'ERR_CANNOT_READ_KEY'))
+ }
+
+ let kid
+ try {
+ kid = await privateKey.id()
+ pem = await privateKey.export(this._())
+ } catch (err) {
+ return throwDelayed(err)
+ }
+
+ const keyInfo = {
+ name: name,
+ id: kid
+ }
+ const batch = self.store.batch()
+ batch.put(dsname, pem)
+ batch.put(DsInfoName(name), JSON.stringify(keyInfo))
+ await batch.commit()
+
+ return keyInfo
+ }
+
+ async importPeer (name, peer) {
+ const self = this
+ if (!validateKeyName(name)) {
+ return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME'))
+ }
+ if (!peer || !peer.privKey) {
+ return throwDelayed(errcode(new Error('Peer.privKey is required'), 'ERR_MISSING_PRIVATE_KEY'))
+ }
+
+ const privateKey = peer.privKey
+ const dsname = DsName(name)
+ const exists = await self.store.has(dsname)
+ if (exists) return throwDelayed(errcode(new Error(`Key '${name}' already exists`), 'ERR_KEY_ALREADY_EXISTS'))
+
+ try {
+ const kid = await privateKey.id()
+ const pem = await privateKey.export(this._())
+ const keyInfo = {
+ name: name,
+ id: kid
+ }
+ const batch = self.store.batch()
+ batch.put(dsname, pem)
+ batch.put(DsInfoName(name), JSON.stringify(keyInfo))
+ await batch.commit()
+ return keyInfo
+ } catch (err) {
+ return throwDelayed(err)
+ }
+ }
+
+ /**
+ * Gets the private key as PEM encoded PKCS #8 string.
+ *
+ * @param {string} name
+ * @returns {string}
+ * @private
+ */
+ async _getPrivateKey (name) {
+ if (!validateKeyName(name)) {
+ return throwDelayed(errcode(new Error(`Invalid key name '${name}'`), 'ERR_INVALID_KEY_NAME'))
+ }
+
+ try {
+ const dsname = DsName(name)
+ const res = await this.store.get(dsname)
+ return res.toString()
+ } catch (err) {
+ return throwDelayed(errcode(new Error(`Key '${name}' does not exist. ${err.message}`), 'ERR_KEY_NOT_FOUND'))
+ }
+ }
+}
+
+module.exports = Keychain
diff --git a/keychain/src/util.js b/keychain/src/util.js
new file mode 100644
index 00000000..50ce4174
--- /dev/null
+++ b/keychain/src/util.js
@@ -0,0 +1,89 @@
+'use strict'
+
+require('node-forge/lib/x509')
+const forge = require('node-forge/lib/forge')
+const pki = forge.pki
+exports = module.exports
+
+/**
+ * Gets a self-signed X.509 certificate for the key.
+ *
+ * The output Buffer contains the PKCS #7 message in DER.
+ *
+ * TODO: move to libp2p-crypto package
+ *
+ * @param {KeyInfo} key - The id and name of the key
+ * @param {RsaPrivateKey} privateKey - The naked key
+ * @returns {undefined}
+ */
+exports.certificateForKey = (key, privateKey) => {
+ const publicKey = pki.setRsaPublicKey(privateKey.n, privateKey.e)
+ const cert = pki.createCertificate()
+ cert.publicKey = publicKey
+ cert.serialNumber = '01'
+ cert.validity.notBefore = new Date()
+ cert.validity.notAfter = new Date()
+ cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10)
+ const attrs = [{
+ name: 'organizationName',
+ value: 'ipfs'
+ }, {
+ shortName: 'OU',
+ value: 'keystore'
+ }, {
+ name: 'commonName',
+ value: key.id
+ }]
+ cert.setSubject(attrs)
+ cert.setIssuer(attrs)
+ cert.setExtensions([{
+ name: 'basicConstraints',
+ cA: true
+ }, {
+ name: 'keyUsage',
+ keyCertSign: true,
+ digitalSignature: true,
+ nonRepudiation: true,
+ keyEncipherment: true,
+ dataEncipherment: true
+ }, {
+ name: 'extKeyUsage',
+ serverAuth: true,
+ clientAuth: true,
+ codeSigning: true,
+ emailProtection: true,
+ timeStamping: true
+ }, {
+ name: 'nsCertType',
+ client: true,
+ server: true,
+ email: true,
+ objsign: true,
+ sslCA: true,
+ emailCA: true,
+ objCA: true
+ }])
+ // self-sign certificate
+ cert.sign(privateKey)
+
+ return cert
+}
+
+/**
+ * Finds the first item in a collection that is matched in the
+ * `asyncCompare` function.
+ *
+ * `asyncCompare` is an async function that must
+ * resolve to either `true` or `false`.
+ *
+ * @param {Array} array
+ * @param {function(*)} asyncCompare An async function that returns a boolean
+ */
+async function findAsync (array, asyncCompare) {
+ const promises = array.map(asyncCompare)
+ const results = await Promise.all(promises)
+ const index = results.findIndex(result => result)
+ return array[index]
+}
+
+module.exports.findAsync = findAsync
diff --git a/keychain/test/browser.js b/keychain/test/browser.js
new file mode 100644
index 00000000..02222fb3
--- /dev/null
+++ b/keychain/test/browser.js
@@ -0,0 +1,27 @@
+/* eslint-env mocha */
+'use strict'
+
+const LevelStore = require('datastore-level')
+
+describe('browser', () => {
+ const datastore1 = new LevelStore('test-keystore-1', { db: require('level') })
+ const datastore2 = new LevelStore('test-keystore-2', { db: require('level') })
+
+ before(() => {
+ return Promise.all([
+ datastore1.open(),
+ datastore2.open()
+ ])
+ })
+
+ after(() => {
+ return Promise.all([
+ datastore1.close(),
+ datastore2.close()
+ ])
+ })
+
+ require('./keychain.spec')(datastore1, datastore2)
+ require('./cms-interop')(datastore2)
+ require('./peerid')
+})
diff --git a/keychain/test/cms-interop.js b/keychain/test/cms-interop.js
new file mode 100644
index 00000000..06eb6312
--- /dev/null
+++ b/keychain/test/cms-interop.js
@@ -0,0 +1,66 @@
+/* eslint max-nested-callbacks: ["error", 8] */
+/* eslint-env mocha */
+'use strict'
+
+const chai = require('chai')
+const dirtyChai = require('dirty-chai')
+const expect = chai.expect
+chai.use(dirtyChai)
+chai.use(require('chai-string'))
+const Keychain = require('..')
+
+module.exports = (datastore) => {
+ describe('cms interop', () => {
+ const passPhrase = 'this is not a secure phrase'
+ const aliceKeyName = 'cms-interop-alice'
+ let ks
+
+ before(() => {
+ ks = new Keychain(datastore, { passPhrase: passPhrase })
+ })
+
+ const plainData = Buffer.from('This is a message from Alice to Bob')
+
+ it('imports openssl key', async function () {
+ this.timeout(10 * 1000)
+ const aliceKid = 'QmNzBqPwp42HZJccsLtc4ok6LjZAspckgs2du5tTmjPfFA'
+ const alice = `-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIICxjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIMhYqiVoLJMICAggA
+MBQGCCqGSIb3DQMHBAhU7J9bcJPLDQSCAoDzi0dP6z97wJBs3jK2hDvZYdoScknG
+QMPOnpG1LO3IZ7nFha1dta5liWX+xRFV04nmVYkkNTJAPS0xjJOG9B5Hm7wm8uTd
+1rOaYKOW5S9+1sD03N+fAx9DDFtB7OyvSdw9ty6BtHAqlFk3+/APASJS12ak2pg7
+/Ei6hChSYYRS9WWGw4lmSitOBxTmrPY1HmODXkR3txR17LjikrMTd6wyky9l/u7A
+CgkMnj1kn49McOBJ4gO14c9524lw9OkPatyZK39evFhx8AET73LrzCnsf74HW9Ri
+dKq0FiKLVm2wAXBZqdd5ll/TPj3wmFqhhLSj/txCAGg+079gq2XPYxxYC61JNekA
+ATKev5zh8x1Mf1maarKN72sD28kS/J+aVFoARIOTxbG3g+1UbYs/00iFcuIaM4IY
+zB1kQUFe13iWBsJ9nfvN7TJNSVnh8NqHNbSg0SdzKlpZHHSWwOUrsKmxmw/XRVy/
+ufvN0hZQ3BuK5MZLixMWAyKc9zbZSOB7E7VNaK5Fmm85FRz0L1qRjHvoGcEIhrOt
+0sjbsRvjs33J8fia0FF9nVfOXvt/67IGBKxIMF9eE91pY5wJNwmXcBk8jghTZs83
+GNmMB+cGH1XFX4cT4kUGzvqTF2zt7IP+P2cQTS1+imKm7r8GJ7ClEZ9COWWdZIcH
+igg5jozKCW82JsuWSiW9tu0F/6DuvYiZwHS3OLiJP0CuLfbOaRw8Jia1RTvXEH7m
+3N0/kZ8hJIK4M/t/UAlALjeNtFxYrFgsPgLxxcq7al1ruG7zBq8L/G3RnkSjtHqE
+cn4oisOvxCprs4aM9UVjtZTCjfyNpX8UWwT1W3rySV+KQNhxuMy3RzmL
+-----END ENCRYPTED PRIVATE KEY-----
+`
+ const key = await ks.importKey(aliceKeyName, alice, 'mypassword')
+ expect(key.name).to.equal(aliceKeyName)
+ expect(key.id).to.equal(aliceKid)
+ })
+
+ it('decrypts node-forge example', async () => {
+ const example = `
+MIIBcwYJKoZIhvcNAQcDoIIBZDCCAWACAQAxgfowgfcCAQAwYDBbMQ0wCwYDVQQK
+EwRpcGZzMREwDwYDVQQLEwhrZXlzdG9yZTE3MDUGA1UEAxMuUW1OekJxUHdwNDJI
+WkpjY3NMdGM0b2s2TGpaQXNwY2tnczJkdTV0VG1qUGZGQQIBATANBgkqhkiG9w0B
+AQEFAASBgLKXCZQYmMLuQ8m0Ex/rr3KNK+Q2+QG1zIbIQ9MFPUNQ7AOgGOHyL40k
+d1gr188EHuiwd90PafZoQF9VRSX9YtwGNqAE8+LD8VaITxCFbLGRTjAqeOUHR8cO
+knU1yykWGkdlbclCuu0NaAfmb8o0OX50CbEKZB7xmsv8tnqn0H0jMF4GCSqGSIb3
+DQEHATAdBglghkgBZQMEASoEEP/PW1JWehQx6/dsLkp/Mf+gMgQwFM9liLTqC56B
+nHILFmhac/+a/StQOKuf9dx5qXeGvt9LnwKuGGSfNX4g+dTkoa6N
+`
+ const plain = await ks.cms.decrypt(Buffer.from(example, 'base64'))
+ expect(plain).to.exist()
+ expect(plain.toString()).to.equal(plainData.toString())
+ })
+ })
+}
diff --git a/keychain/test/keychain.spec.js b/keychain/test/keychain.spec.js
new file mode 100644
index 00000000..c455f2d7
--- /dev/null
+++ b/keychain/test/keychain.spec.js
@@ -0,0 +1,383 @@
+/* eslint max-nested-callbacks: ["error", 8] */
+/* eslint-env mocha */
+'use strict'
+
+const chai = require('chai')
+const expect = chai.expect
+const fail = expect.fail
+chai.use(require('dirty-chai'))
+chai.use(require('chai-string'))
+const Keychain = require('../')
+const PeerId = require('peer-id')
+
+module.exports = (datastore1, datastore2) => {
+ describe('keychain', () => {
+ const passPhrase = 'this is not a secure phrase'
+ const rsaKeyName = 'tajné jméno'
+ const renamedRsaKeyName = 'ชื่อลับ'
+ let rsaKeyInfo
+ let emptyKeystore
+ let ks
+
+ before((done) => {
+ ks = new Keychain(datastore2, { passPhrase: passPhrase })
+ emptyKeystore = new Keychain(datastore1, { passPhrase: passPhrase })
+ done()
+ })
+
+ it('needs a pass phrase to encrypt a key', () => {
+ expect(() => new Keychain(datastore2)).to.throw()
+ })
+
+ it('needs a NIST SP 800-132 non-weak pass phrase', () => {
+ expect(() => new Keychain(datastore2, { passPhrase: '< 20 character' })).to.throw()
+ })
+
+ it('needs a store to persist a key', () => {
+ expect(() => new Keychain(null, { passPhrase: passPhrase })).to.throw()
+ })
+
+ it('has default options', () => {
+ expect(Keychain.options).to.exist()
+ })
+
+ it('needs a supported hashing alorithm', () => {
+ const ok = new Keychain(datastore2, { passPhrase: passPhrase, dek: { hash: 'sha2-256' } })
+ expect(ok).to.exist()
+ expect(() => new Keychain(datastore2, { passPhrase: passPhrase, dek: { hash: 'my-hash' } })).to.throw()
+ })
+
+ it('can generate options', () => {
+ const options = Keychain.generateOptions()
+ options.passPhrase = passPhrase
+ const chain = new Keychain(datastore2, options)
+ expect(chain).to.exist()
+ })
+
+ describe('key name', () => {
+ it('is a valid filename and non-ASCII', async () => {
+ const errors = await Promise.all([
+ ks.removeKey('../../nasty').then(fail, err => err),
+ ks.removeKey('').then(fail, err => err),
+ ks.removeKey(' ').then(fail, err => err),
+ ks.removeKey(null).then(fail, err => err),
+ ks.removeKey(undefined).then(fail, err => err)
+ ])
+
+ expect(errors).to.have.length(5)
+ errors.forEach(error => {
+ expect(error).to.have.property('code', 'ERR_INVALID_KEY_NAME')
+ })
+ })
+ })
+
+ describe('key', () => {
+ it('can be an RSA key', async () => {
+ rsaKeyInfo = await ks.createKey(rsaKeyName, 'rsa', 2048)
+ expect(rsaKeyInfo).to.exist()
+ expect(rsaKeyInfo).to.have.property('name', rsaKeyName)
+ expect(rsaKeyInfo).to.have.property('id')
+ })
+
+ it('is encrypted PEM encoded PKCS #8', async () => {
+ const pem = await ks._getPrivateKey(rsaKeyName)
+ return expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----')
+ })
+
+ it('throws if an invalid private key name is given', async () => {
+ const err = await ks._getPrivateKey(undefined).then(fail, err => err)
+ expect(err).to.exist()
+ expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME')
+ })
+
+ it('throws if a private key cant be found', async () => {
+ const err = await ks._getPrivateKey('not real').then(fail, err => err)
+ expect(err).to.exist()
+ expect(err).to.have.property('code', 'ERR_KEY_NOT_FOUND')
+ })
+
+ it('does not overwrite existing key', async () => {
+ const err = await ks.createKey(rsaKeyName, 'rsa', 2048).then(fail, err => err)
+ expect(err).to.have.property('code', 'ERR_KEY_ALREADY_EXISTS')
+ })
+
+ it('cannot create the "self" key', async () => {
+ const err = await ks.createKey('self', 'rsa', 2048).then(fail, err => err)
+ expect(err).to.exist()
+ expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME')
+ })
+
+ it('should validate name is string', async () => {
+ const err = await ks.createKey(5, 'rsa', 2048).then(fail, err => err)
+ expect(err).to.exist()
+ expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME')
+ })
+
+ it('should validate type is string', async () => {
+ const err = await ks.createKey('TEST' + Date.now(), null, 2048).then(fail, err => err)
+ expect(err).to.exist()
+ expect(err).to.have.property('code', 'ERR_INVALID_KEY_TYPE')
+ })
+
+ it('should validate size is integer', async () => {
+ const err = await ks.createKey('TEST' + Date.now(), 'rsa', 'string').then(fail, err => err)
+ expect(err).to.exist()
+ expect(err).to.have.property('code', 'ERR_INVALID_KEY_SIZE')
+ })
+
+ describe('implements NIST SP 800-131A', () => {
+ it('disallows RSA length < 2048', async () => {
+ const err = await ks.createKey('bad-nist-rsa', 'rsa', 1024).then(fail, err => err)
+ expect(err).to.exist()
+ expect(err).to.have.property('code', 'ERR_INVALID_KEY_SIZE')
+ })
+ })
+ })
+
+ describe('query', () => {
+ it('finds all existing keys', async () => {
+ const keys = await ks.listKeys()
+ expect(keys).to.exist()
+ const mykey = keys.find((k) => k.name.normalize() === rsaKeyName.normalize())
+ expect(mykey).to.exist()
+ })
+
+ it('finds a key by name', async () => {
+ const key = await ks.findKeyByName(rsaKeyName)
+ expect(key).to.exist()
+ expect(key).to.deep.equal(rsaKeyInfo)
+ })
+
+ it('finds a key by id', async () => {
+ const key = await ks.findKeyById(rsaKeyInfo.id)
+ expect(key).to.exist()
+ expect(key).to.deep.equal(rsaKeyInfo)
+ })
+
+ it('returns the key\'s name and id', async () => {
+ const keys = await ks.listKeys()
+ expect(keys).to.exist()
+ keys.forEach((key) => {
+ expect(key).to.have.property('name')
+ expect(key).to.have.property('id')
+ })
+ })
+ })
+
+ describe('CMS protected data', () => {
+ const plainData = Buffer.from('This is a message from Alice to Bob')
+ let cms
+
+ it('service is available', () => {
+ expect(ks).to.have.property('cms')
+ })
+
+ it('requires a key', async () => {
+ const err = await ks.cms.encrypt('no-key', plainData).then(fail, err => err)
+ expect(err).to.exist()
+ expect(err).to.have.property('code', 'ERR_KEY_NOT_FOUND')
+ })
+
+ it('requires plain data as a Buffer', async () => {
+ const err = await ks.cms.encrypt(rsaKeyName, 'plain data').then(fail, err => err)
+ expect(err).to.exist()
+ expect(err).to.have.property('code', 'ERR_INVALID_PARAMS')
+ })
+
+ it('encrypts', async () => {
+ cms = await ks.cms.encrypt(rsaKeyName, plainData)
+ expect(cms).to.exist()
+ expect(cms).to.be.instanceOf(Buffer)
+ })
+
+ it('is a PKCS #7 message', async () => {
+ const err = await ks.cms.decrypt('not CMS').then(fail, err => err)
+ expect(err).to.exist()
+ expect(err).to.have.property('code', 'ERR_INVALID_PARAMS')
+ })
+
+ it('is a PKCS #7 binary message', async () => {
+ const err = await ks.cms.decrypt(plainData).then(fail, err => err)
+ expect(err).to.exist()
+ expect(err).to.have.property('code', 'ERR_INVALID_CMS')
+ })
+
+ it('cannot be read without the key', async () => {
+ const err = await emptyKeystore.cms.decrypt(cms).then(fail, err => err)
+ expect(err).to.exist()
+ expect(err).to.have.property('missingKeys')
+ expect(err.missingKeys).to.eql([rsaKeyInfo.id])
+ expect(err).to.have.property('code', 'ERR_MISSING_KEYS')
+ })
+
+ it('can be read with the key', async () => {
+ const plain = await ks.cms.decrypt(cms)
+ expect(plain).to.exist()
+ expect(plain.toString()).to.equal(plainData.toString())
+ })
+ })
+
+ describe('exported key', () => {
+ let pemKey
+
+ it('requires the password', async () => {
+ const err = await ks.exportKey(rsaKeyName).then(fail, err => err)
+ expect(err).to.exist()
+ expect(err).to.have.property('code', 'ERR_PASSWORD_REQUIRED')
+ })
+
+ it('requires the key name', async () => {
+ const err = await ks.exportKey(undefined, 'password').then(fail, err => err)
+ expect(err).to.exist()
+ expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME')
+ })
+
+ it('is a PKCS #8 encrypted pem', async () => {
+ pemKey = await ks.exportKey(rsaKeyName, 'password')
+ expect(pemKey).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----')
+ })
+
+ it('can be imported', async () => {
+ const key = await ks.importKey('imported-key', pemKey, 'password')
+ expect(key.name).to.equal('imported-key')
+ expect(key.id).to.equal(rsaKeyInfo.id)
+ })
+
+ it('requires the pem', async () => {
+ const err = await ks.importKey('imported-key', undefined, 'password').then(fail, err => err)
+ expect(err).to.exist()
+ expect(err).to.have.property('code', 'ERR_PEM_REQUIRED')
+ })
+
+ it('cannot be imported as an existing key name', async () => {
+ const err = await ks.importKey(rsaKeyName, pemKey, 'password').then(fail, err => err)
+ expect(err).to.exist()
+ expect(err).to.have.property('code', 'ERR_KEY_ALREADY_EXISTS')
+ })
+
+ it('cannot be imported with the wrong password', async () => {
+ const err = await ks.importKey('a-new-name-for-import', pemKey, 'not the password').then(fail, err => err)
+ expect(err).to.exist()
+ expect(err).to.have.property('code', 'ERR_CANNOT_READ_KEY')
+ })
+ })
+
+ describe('peer id', () => {
+ 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=='
+ let alice
+
+ before(async function () {
+ const encoded = Buffer.from(alicePrivKey, 'base64')
+ alice = await PeerId.createFromPrivKey(encoded)
+ })
+
+ it('private key can be imported', async () => {
+ const key = await ks.importPeer('alice', alice)
+ expect(key.name).to.equal('alice')
+ expect(key.id).to.equal(alice.toB58String())
+ })
+
+ it('private key import requires a valid name', async () => {
+ const err = await ks.importPeer(undefined, alice).then(fail, err => err)
+ expect(err).to.exist()
+ expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME')
+ })
+
+ it('private key import requires the peer', async () => {
+ const err = await ks.importPeer('alice').then(fail, err => err)
+ expect(err).to.exist()
+ expect(err).to.have.property('code', 'ERR_MISSING_PRIVATE_KEY')
+ })
+
+ it('key id exists', async () => {
+ const key = await ks.findKeyById(alice.toB58String())
+ expect(key).to.exist()
+ expect(key).to.have.property('name', 'alice')
+ expect(key).to.have.property('id', alice.toB58String())
+ })
+
+ it('key name exists', async () => {
+ const key = await ks.findKeyByName('alice')
+ expect(key).to.exist()
+ expect(key).to.have.property('name', 'alice')
+ expect(key).to.have.property('id', alice.toB58String())
+ })
+ })
+
+ describe('rename', () => {
+ it('requires an existing key name', async () => {
+ const err = await ks.renameKey('not-there', renamedRsaKeyName).then(fail, err => err)
+ expect(err).to.exist()
+ expect(err).to.have.property('code', 'ERR_NOT_FOUND')
+ })
+
+ it('requires a valid new key name', async () => {
+ const err = await ks.renameKey(rsaKeyName, '..\not-valid').then(fail, err => err)
+ expect(err).to.exist()
+ expect(err).to.have.property('code', 'ERR_NEW_KEY_NAME_INVALID')
+ })
+
+ it('does not overwrite existing key', async () => {
+ const err = await ks.renameKey(rsaKeyName, rsaKeyName).then(fail, err => err)
+ expect(err).to.exist()
+ expect(err).to.have.property('code', 'ERR_KEY_ALREADY_EXISTS')
+ })
+
+ it('cannot create the "self" key', async () => {
+ const err = await ks.renameKey(rsaKeyName, 'self').then(fail, err => err)
+ expect(err).to.exist()
+ expect(err).to.have.property('code', 'ERR_NEW_KEY_NAME_INVALID')
+ })
+
+ it('removes the existing key name', async () => {
+ const key = await ks.renameKey(rsaKeyName, renamedRsaKeyName)
+ expect(key).to.exist()
+ expect(key).to.have.property('name', renamedRsaKeyName)
+ expect(key).to.have.property('id', rsaKeyInfo.id)
+ // Try to find the changed key
+ const err = await ks.findKeyByName(rsaKeyName).then(fail, err => err)
+ expect(err).to.exist()
+ })
+
+ it('creates the new key name', async () => {
+ const key = await ks.findKeyByName(renamedRsaKeyName)
+ expect(key).to.exist()
+ expect(key).to.have.property('name', renamedRsaKeyName)
+ })
+
+ it('does not change the key ID', async () => {
+ const key = await ks.findKeyByName(renamedRsaKeyName)
+ expect(key).to.exist()
+ expect(key).to.have.property('name', renamedRsaKeyName)
+ expect(key).to.have.property('id', rsaKeyInfo.id)
+ })
+
+ it('throws with invalid key names', async () => {
+ const err = await ks.findKeyByName(undefined).then(fail, err => err)
+ expect(err).to.exist()
+ expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME')
+ })
+ })
+
+ describe('key removal', () => {
+ it('cannot remove the "self" key', async () => {
+ const err = await ks.removeKey('self').then(fail, err => err)
+ expect(err).to.exist()
+ expect(err).to.have.property('code', 'ERR_INVALID_KEY_NAME')
+ })
+
+ it('cannot remove an unknown key', async () => {
+ const err = await ks.removeKey('not-there').then(fail, err => err)
+ expect(err).to.exist()
+ expect(err).to.have.property('code', 'ERR_KEY_NOT_FOUND')
+ })
+
+ it('can remove a known key', async () => {
+ const key = await ks.removeKey(renamedRsaKeyName)
+ expect(key).to.exist()
+ expect(key).to.have.property('name', renamedRsaKeyName)
+ expect(key).to.have.property('id', rsaKeyInfo.id)
+ })
+ })
+ })
+}
diff --git a/keychain/test/node.js b/keychain/test/node.js
new file mode 100644
index 00000000..bbb25089
--- /dev/null
+++ b/keychain/test/node.js
@@ -0,0 +1,31 @@
+/* eslint-env mocha */
+'use strict'
+
+const os = require('os')
+const path = require('path')
+const promisify = require('promisify-es6')
+const rimraf = promisify(require('rimraf'))
+const FsStore = require('datastore-fs')
+
+describe('node', () => {
+ const store1 = path.join(os.tmpdir(), 'test-keystore-1-' + Date.now())
+ const store2 = path.join(os.tmpdir(), 'test-keystore-2-' + Date.now())
+ const datastore1 = new FsStore(store1)
+ const datastore2 = new FsStore(store2)
+
+ before(async () => {
+ await datastore1.open()
+ await datastore2.open()
+ })
+
+ after(async () => {
+ await datastore1.close()
+ await datastore2.close()
+ await rimraf(store1)
+ await rimraf(store2)
+ })
+
+ require('./keychain.spec')(datastore1, datastore2)
+ require('./cms-interop')(datastore2)
+ require('./peerid')
+})
diff --git a/keychain/test/peerid.js b/keychain/test/peerid.js
new file mode 100644
index 00000000..4360e538
--- /dev/null
+++ b/keychain/test/peerid.js
@@ -0,0 +1,69 @@
+/* eslint-env mocha */
+'use strict'
+
+const chai = require('chai')
+const dirtyChai = require('dirty-chai')
+const expect = chai.expect
+chai.use(dirtyChai)
+const PeerId = require('peer-id')
+const multihash = require('multihashes')
+const crypto = require('libp2p-crypto')
+const rsaUtils = require('libp2p-crypto/src/keys/rsa-utils')
+const rsaClass = require('libp2p-crypto/src/keys/rsa-class')
+
+const sample = {
+ id: '122019318b6e5e0cf93a2314bf01269a2cc23cd3dcd452d742cdb9379d8646f6e4a9',
+ 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==',
+ pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2SKo/HMFZeBml1AF3XijzrxrfQXdJzjePBZAbdxqKR1Mc6juRHXij6HXYPjlAk01BhF1S3Ll4Lwi0cAHhggf457sMg55UWyeGKeUv0ucgvCpBwlR5cQ020i0MgzjPWOLWq1rtvSbNcAi2ZEVn6+Q2EcHo3wUvWRtLeKz+DZSZfw2PEDC+DGPJPl7f8g7zl56YymmmzH9liZLNrzg/qidokUv5u1pdGrcpLuPNeTODk0cqKB+OUbuKj9GShYECCEjaybJDl9276oalL9ghBtSeEv20kugatTvYy590wFlJkkvyl+nPxIH0EEYMKK9XRWlu9XYnoSfboiwcv8M3SlsjAgMBAAE='
+}
+
+describe('peer ID', () => {
+ let peer
+ let publicKeyDer // a buffer
+
+ before(async () => {
+ const encoded = Buffer.from(sample.privKey, 'base64')
+ peer = await PeerId.createFromPrivKey(encoded)
+ })
+
+ it('decoded public key', async () => {
+ // get protobuf version of the public key
+ const publicKeyProtobuf = peer.marshalPubKey()
+ const publicKey = crypto.keys.unmarshalPublicKey(publicKeyProtobuf)
+ publicKeyDer = publicKey.marshal()
+
+ // get protobuf version of the private key
+ const privateKeyProtobuf = peer.marshalPrivKey()
+ const key = await crypto.keys.unmarshalPrivateKey(privateKeyProtobuf)
+ expect(key).to.exist()
+ })
+
+ it('encoded public key with DER', async () => {
+ const jwk = rsaUtils.pkixToJwk(publicKeyDer)
+ const rsa = new rsaClass.RsaPublicKey(jwk)
+ const keyId = await rsa.hash()
+ const kids = multihash.toB58String(keyId)
+ expect(kids).to.equal(peer.toB58String())
+ })
+
+ it('encoded public key with JWT', async () => {
+ const jwk = {
+ kty: 'RSA',
+ n: 'tkiqPxzBWXgZpdQBd14o868a30F3Sc43jwWQG3caikdTHOo7kR14o-h12D45QJNNQYRdUty5eC8ItHAB4YIH-Oe7DIOeVFsnhinlL9LnILwqQcJUeXENNtItDIM4z1ji1qta7b0mzXAItmRFZ-vkNhHB6N8FL1kbS3is_g2UmX8NjxAwvgxjyT5e3_IO85eemMpppsx_ZYmSza84P6onaJFL-btaXRq3KS7jzXkzg5NHKigfjlG7io_RkoWBAghI2smyQ5fdu-qGpS_YIQbUnhL9tJLoGrU72MufdMBZSZJL8pfpz8SB9BBGDCivV0VpbvV2J6En26IsHL_DN0pbIw',
+ e: 'AQAB',
+ alg: 'RS256',
+ kid: '2011-04-29'
+ }
+ const rsa = new rsaClass.RsaPublicKey(jwk)
+ const keyId = await rsa.hash()
+ const kids = multihash.toB58String(keyId)
+ expect(kids).to.equal(peer.toB58String())
+ })
+
+ it('decoded private key', async () => {
+ // get protobuf version of the private key
+ const privateKeyProtobuf = peer.marshalPrivKey()
+ const key = await crypto.keys.unmarshalPrivateKey(privateKeyProtobuf)
+ expect(key).to.exist()
+ })
+})