diff --git a/package.json b/package.json index 8f340b2262..2b6aec5a1d 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "@libp2p/interface-connection-manager": "^1.1.1", "@libp2p/interface-content-routing": "^2.0.0", "@libp2p/interface-dht": "^2.0.0", + "@libp2p/interface-keychain": "^2.0.4", "@libp2p/interface-libp2p": "^1.0.0", "@libp2p/interface-metrics": "^4.0.0", "@libp2p/interface-peer-discovery": "^1.0.1", @@ -114,6 +115,7 @@ "@libp2p/interface-stream-muxer": "^3.0.0", "@libp2p/interface-transport": "^2.1.0", "@libp2p/interfaces": "^3.0.3", + "@libp2p/keychain": "^1.0.0", "@libp2p/logger": "^2.0.1", "@libp2p/multistream-select": "^3.0.0", "@libp2p/peer-collections": "^3.0.0", @@ -136,7 +138,6 @@ "it-drain": "^2.0.0", "it-filter": "^2.0.0", "it-first": "^2.0.0", - "it-foreach": "^1.0.0", "it-handshake": "^4.1.2", "it-length-prefixed": "^8.0.2", "it-map": "^2.0.0", @@ -147,7 +148,6 @@ "it-stream-types": "^1.0.4", "merge-options": "^3.0.4", "multiformats": "^11.0.0", - "node-forge": "^1.3.1", "p-fifo": "^1.0.0", "p-retry": "^5.0.0", "p-settle": "^5.0.0", @@ -155,7 +155,6 @@ "protons-runtime": "^4.0.1", "rate-limiter-flexible": "^2.3.11", "retimer": "^3.0.0", - "sanitize-filename": "^1.6.3", "set-delayed-interval": "^1.0.0", "timeout-abort-controller": "^3.0.0", "uint8arraylist": "^2.3.2", @@ -183,7 +182,6 @@ "@libp2p/topology": "^4.0.0", "@libp2p/webrtc-star": "^6.0.0", "@libp2p/websockets": "^5.0.0", - "@types/node-forge": "^1.0.0", "@types/p-fifo": "^1.0.0", "@types/varint": "^6.0.0", "@types/xsalsa20": "^1.1.0", diff --git a/src/index.ts b/src/index.ts index cd0ee8992a..db2a467ee9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,7 +37,7 @@ import type { PingServiceInit } from './ping/index.js' import type { FetchServiceInit } from './fetch/index.js' import type { Components } from './components.js' import type { Libp2p } from '@libp2p/interface-libp2p' -import type { KeyChainInit } from './keychain/index.js' +import type { KeyChainInit } from '@libp2p/keychain' import type { NatManagerInit } from './nat-manager.js' import type { AddressManagerInit } from './address-manager/index.js' import type { PeerRoutingInit } from './peer-routing.js' diff --git a/src/keychain/README.md b/src/keychain/README.md deleted file mode 100644 index 1a5a6ce387..0000000000 --- a/src/keychain/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# js-libp2p-keychain - -> A secure key chain for libp2p in JavaScript - -## 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 - -### 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' - } -} -``` - -![key storage](./doc/private-key.png?raw=true) - -### 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. diff --git a/src/keychain/cms.ts b/src/keychain/cms.ts deleted file mode 100644 index 8a26c33868..0000000000 --- a/src/keychain/cms.ts +++ /dev/null @@ -1,150 +0,0 @@ -import 'node-forge/lib/pkcs7.js' -import 'node-forge/lib/pbe.js' -// @ts-expect-error types are missing -import forge from 'node-forge/lib/forge.js' -import { certificateForKey, findAsync } from './util.js' -import errCode from 'err-code' -import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' -import { toString as uint8ArrayToString } from 'uint8arrays/to-string' -import { codes } from '../errors.js' -import { logger } from '@libp2p/logger' -import type { KeyChain } from './index.js' - -const log = logger('libp2p:keychain:cms') - -const privates = new WeakMap() - -/** - * 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. - */ -export class CMS { - private readonly keychain: KeyChain - - /** - * Creates a new instance with a keychain - */ - constructor (keychain: KeyChain, dek: string) { - if (keychain == null) { - throw errCode(new Error('keychain is required'), codes.ERR_KEYCHAIN_REQUIRED) - } - - this.keychain = keychain - privates.set(this, { dek }) - } - - /** - * Creates some protected data. - * - * The output Uint8Array contains the PKCS #7 message in DER. - */ - async encrypt (name: string, plain: Uint8Array): Promise { - if (!(plain instanceof Uint8Array)) { - throw errCode(new Error('Plain data must be a Uint8Array'), codes.ERR_INVALID_PARAMETERS) - } - - const key = await this.keychain.findKeyByName(name) - const pem = await this.keychain.getPrivateKey(name) - const cached = privates.get(this) - - if (cached == null) { - throw errCode(new Error('dek missing'), codes.ERR_INVALID_PARAMETERS) - } - - const dek = cached.dek - const privateKey = forge.pki.decryptRsaPrivateKey(pem, dek) - 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 uint8ArrayFromString(der, 'ascii') - } - - /** - * 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. - */ - async decrypt (cmsData: Uint8Array): Promise { - if (!(cmsData instanceof Uint8Array)) { - throw errCode(new Error('CMS data is required'), codes.ERR_INVALID_PARAMETERS) - } - - let cms: any - try { - const buf = forge.util.createBuffer(uint8ArrayToString(cmsData, 'ascii')) - const obj = forge.asn1.fromDer(buf) - - cms = forge.pkcs7.messageFromAsn1(obj) - } catch (err: any) { - log.error(err) - throw errCode(new Error('Invalid CMS'), codes.ERR_INVALID_CMS) - } - - // Find a recipient whose key we hold. We only deal with recipient certs - // issued by ipfs (O=ipfs). - const recipients: any = cms.recipients - // @ts-expect-error cms types not defined - .filter(r => r.issuer.find(a => a.shortName === 'O' && a.value === 'ipfs')) - // @ts-expect-error cms types not defined - .filter(r => r.issuer.find(a => a.shortName === 'CN')) - // @ts-expect-error cms types not defined - .map(r => { - return { - recipient: r, - // @ts-expect-error cms types not defined - keyId: r.issuer.find(a => a.shortName === 'CN').value - } - }) - - const r = await findAsync(recipients, async (recipient: any) => { - try { - const key = await this.keychain.findKeyById(recipient.keyId) - if (key != null) { - return true - } - } catch (err: any) { - return false - } - return false - }) - - if (r == null) { - // @ts-expect-error cms types not defined - const missingKeys: string[] = recipients.map(r => r.keyId) - throw errCode(new Error(`Decryption needs one of the key(s): ${missingKeys.join(', ')}`), codes.ERR_MISSING_KEYS, { - missingKeys - }) - } - - const key = await this.keychain.findKeyById(r.keyId) - - if (key == null) { - throw errCode(new Error('No key available to decrypto'), codes.ERR_NO_KEY) - } - - const pem = await this.keychain.getPrivateKey(key.name) - const cached = privates.get(this) - - if (cached == null) { - throw errCode(new Error('dek missing'), codes.ERR_INVALID_PARAMETERS) - } - - const dek = cached.dek - const privateKey = forge.pki.decryptRsaPrivateKey(pem, dek) - cms.decrypt(r.recipient, privateKey) - return uint8ArrayFromString(cms.content.getBytes(), 'ascii') - } -} diff --git a/src/keychain/doc/private-key.png b/src/keychain/doc/private-key.png deleted file mode 100644 index 4c85dc610c..0000000000 Binary files a/src/keychain/doc/private-key.png and /dev/null differ diff --git a/src/keychain/doc/private-key.xml b/src/keychain/doc/private-key.xml deleted file mode 100644 index 51cb8c5a9b..0000000000 --- a/src/keychain/doc/private-key.xml +++ /dev/null @@ -1 +0,0 @@ -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/src/keychain/index.ts b/src/keychain/index.ts deleted file mode 100644 index d508c26007..0000000000 --- a/src/keychain/index.ts +++ /dev/null @@ -1,629 +0,0 @@ -/* eslint max-nested-callbacks: ["error", 5] */ - -import { logger } from '@libp2p/logger' -import sanitize from 'sanitize-filename' -import mergeOptions from 'merge-options' -import { Key } from 'interface-datastore/key' -import { CMS } from './cms.js' -import errCode from 'err-code' -import { codes } from '../errors.js' -import { toString as uint8ArrayToString } from 'uint8arrays/to-string' -import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' -import { generateKeyPair, importKey, unmarshalPrivateKey } from '@libp2p/crypto/keys' -import type { PeerId } from '@libp2p/interface-peer-id' -import { pbkdf2, randomBytes } from '@libp2p/crypto' -import type { Startable } from '@libp2p/interfaces/dist/src/startable' -import type { Datastore } from 'interface-datastore' -import { peerIdFromKeys } from '@libp2p/peer-id' -import type { KeyTypes } from '@libp2p/crypto/keys' - -const log = logger('libp2p:keychain') - -export interface DEKConfig { - hash: string - salt: string - iterationCount: number - keyLength: number -} - -export interface KeyChainInit { - pass?: string - dek?: DEKConfig -} - -/** - * Information about a key. - */ -export interface KeyInfo { - /** - * The universally unique key id - */ - id: string - - /** - * The local key name. - */ - name: string -} - -const keyPrefix = '/pkcs8/' -const infoPrefix = '/info/' -const privates = new WeakMap() - -// 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: string) { - if (name == null) { - return false - } - if (typeof name !== 'string') { - return false - } - return name === sanitize(name.trim()) && name.length > 0 -} - -/** - * 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. - */ -async function randomDelay () { - const min = 200 - const max = 1000 - const delay = Math.random() * (max - min) + min - - await new Promise(resolve => setTimeout(resolve, delay)) -} - -/** - * Converts a key name into a datastore name - */ -function DsName (name: string) { - return new Key(keyPrefix + name) -} - -/** - * Converts a key name into a datastore info name - */ -function DsInfoName (name: string) { - return new Key(infoPrefix + name) -} - -export interface KeyChainComponents { - peerId: PeerId - datastore: Datastore -} - -/** - * 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 - * - */ -export class KeyChain implements Startable { - private readonly components: KeyChainComponents - private readonly init: KeyChainInit - private started: boolean - - /** - * Creates a new instance of a key chain - */ - constructor (components: KeyChainComponents, init: KeyChainInit) { - this.components = components - this.init = mergeOptions(defaultOptions, init) - - // Enforce NIST SP 800-132 - if (this.init.pass != null && this.init.pass?.length < 20) { - throw new Error('pass must be least 20 characters') - } - if (this.init.dek?.keyLength != null && this.init.dek.keyLength < NIST.minKeyLength) { - throw new Error(`dek.keyLength must be least ${NIST.minKeyLength} bytes`) - } - if (this.init.dek?.salt?.length != null && this.init.dek.salt.length < NIST.minSaltLength) { - throw new Error(`dek.saltLength must be least ${NIST.minSaltLength} bytes`) - } - if (this.init.dek?.iterationCount != null && this.init.dek.iterationCount < NIST.minIterationCount) { - throw new Error(`dek.iterationCount must be least ${NIST.minIterationCount}`) - } - - const dek = this.init.pass != null && this.init.dek?.salt != null - ? pbkdf2( - this.init.pass, - this.init.dek?.salt, - this.init.dek?.iterationCount, - this.init.dek?.keyLength, - this.init.dek?.hash) - : '' - - privates.set(this, { dek }) - this.started = false - } - - isStarted () { - return this.started - } - - async start () { - const dsname = DsInfoName('self') - - if (!(await this.components.datastore.has(dsname))) { - await this.importPeer('self', this.components.peerId) - } - - this.started = true - } - - stop () { - this.started = false - } - - /** - * 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 - */ - get cms () { - const cached = privates.get(this) - - if (cached == null) { - throw errCode(new Error('dek missing'), codes.ERR_INVALID_PARAMETERS) - } - - const dek = cached.dek - - return new CMS(this, dek) - } - - /** - * Generates the options for a keychain. A random salt is produced. - * - * @returns {object} - */ - static generateOptions (): KeyChainInit { - const options = Object.assign({}, defaultOptions) - const saltLength = Math.ceil(NIST.minSaltLength / 3) * 3 // no base64 padding - options.dek.salt = uint8ArrayToString(randomBytes(saltLength), '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 {number} [size = 2048] - The key size in bits. Used for rsa keys only - */ - async createKey (name: string, type: KeyTypes, size = 2048): Promise { - if (!validateKeyName(name) || name === 'self') { - await randomDelay() - throw errCode(new Error('Invalid key name'), codes.ERR_INVALID_KEY_NAME) - } - - if (typeof type !== 'string') { - await randomDelay() - throw errCode(new Error('Invalid key type'), codes.ERR_INVALID_KEY_TYPE) - } - - const dsname = DsName(name) - const exists = await this.components.datastore.has(dsname) - if (exists) { - await randomDelay() - throw errCode(new Error('Key name already exists'), codes.ERR_KEY_ALREADY_EXISTS) - } - - switch (type.toLowerCase()) { - case 'rsa': - if (!Number.isSafeInteger(size) || size < 2048) { - await randomDelay() - throw errCode(new Error('Invalid RSA key size'), codes.ERR_INVALID_KEY_SIZE) - } - break - default: - break - } - - let keyInfo - try { - const keypair = await generateKeyPair(type, size) - const kid = await keypair.id() - const cached = privates.get(this) - - if (cached == null) { - throw errCode(new Error('dek missing'), codes.ERR_INVALID_PARAMETERS) - } - - const dek = cached.dek - const pem = await keypair.export(dek) - keyInfo = { - name: name, - id: kid - } - const batch = this.components.datastore.batch() - batch.put(dsname, uint8ArrayFromString(pem)) - batch.put(DsInfoName(name), uint8ArrayFromString(JSON.stringify(keyInfo))) - - await batch.commit() - } catch (err: any) { - await randomDelay() - throw err - } - - return keyInfo - } - - /** - * List all the keys. - * - * @returns {Promise} - */ - async listKeys () { - const query = { - prefix: infoPrefix - } - - const info = [] - for await (const value of this.components.datastore.query(query)) { - info.push(JSON.parse(uint8ArrayToString(value.value))) - } - - return info - } - - /** - * Find a key by it's id - */ - async findKeyById (id: string): Promise { - try { - const keys = await this.listKeys() - return keys.find((k) => k.id === id) - } catch (err: any) { - await randomDelay() - throw err - } - } - - /** - * Find a key by it's name. - * - * @param {string} name - The local key name. - * @returns {Promise} - */ - async findKeyByName (name: string): Promise { - if (!validateKeyName(name)) { - await randomDelay() - throw errCode(new Error(`Invalid key name '${name}'`), codes.ERR_INVALID_KEY_NAME) - } - - const dsname = DsInfoName(name) - try { - const res = await this.components.datastore.get(dsname) - return JSON.parse(uint8ArrayToString(res)) - } catch (err: any) { - await randomDelay() - log.error(err) - throw errCode(new Error(`Key '${name}' does not exist.`), codes.ERR_KEY_NOT_FOUND) - } - } - - /** - * Remove an existing key. - * - * @param {string} name - The local key name; must already exist. - * @returns {Promise} - */ - async removeKey (name: string) { - if (!validateKeyName(name) || name === 'self') { - await randomDelay() - throw errCode(new Error(`Invalid key name '${name}'`), codes.ERR_INVALID_KEY_NAME) - } - const dsname = DsName(name) - const keyInfo = await this.findKeyByName(name) - const batch = this.components.datastore.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 {Promise} - */ - async renameKey (oldName: string, newName: string): Promise { - if (!validateKeyName(oldName) || oldName === 'self') { - await randomDelay() - throw errCode(new Error(`Invalid old key name '${oldName}'`), codes.ERR_OLD_KEY_NAME_INVALID) - } - if (!validateKeyName(newName) || newName === 'self') { - await randomDelay() - throw errCode(new Error(`Invalid new key name '${newName}'`), codes.ERR_NEW_KEY_NAME_INVALID) - } - const oldDsname = DsName(oldName) - const newDsname = DsName(newName) - const oldInfoName = DsInfoName(oldName) - const newInfoName = DsInfoName(newName) - - const exists = await this.components.datastore.has(newDsname) - if (exists) { - await randomDelay() - throw errCode(new Error(`Key '${newName}' already exists`), codes.ERR_KEY_ALREADY_EXISTS) - } - - try { - const pem = await this.components.datastore.get(oldDsname) - const res = await this.components.datastore.get(oldInfoName) - - const keyInfo = JSON.parse(uint8ArrayToString(res)) - keyInfo.name = newName - const batch = this.components.datastore.batch() - batch.put(newDsname, pem) - batch.put(newInfoName, uint8ArrayFromString(JSON.stringify(keyInfo))) - batch.delete(oldDsname) - batch.delete(oldInfoName) - await batch.commit() - return keyInfo - } catch (err: any) { - await randomDelay() - throw err - } - } - - /** - * Export an existing key as a PEM encrypted PKCS #8 string - */ - async exportKey (name: string, password: string) { - if (!validateKeyName(name)) { - await randomDelay() - throw errCode(new Error(`Invalid key name '${name}'`), codes.ERR_INVALID_KEY_NAME) - } - if (password == null) { - await randomDelay() - throw errCode(new Error('Password is required'), codes.ERR_PASSWORD_REQUIRED) - } - - const dsname = DsName(name) - try { - const res = await this.components.datastore.get(dsname) - const pem = uint8ArrayToString(res) - const cached = privates.get(this) - - if (cached == null) { - throw errCode(new Error('dek missing'), codes.ERR_INVALID_PARAMETERS) - } - - const dek = cached.dek - const privateKey = await importKey(pem, dek) - return await privateKey.export(password) - } catch (err: any) { - await randomDelay() - throw err - } - } - - /** - * Export an existing key as a PeerId - */ - async exportPeerId (name: string) { - const password = 'temporary-password' - const pem = await this.exportKey(name, password) - const privateKey = await importKey(pem, password) - - return await peerIdFromKeys(privateKey.public.bytes, privateKey.bytes) - } - - /** - * 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 {Promise} - */ - async importKey (name: string, pem: string, password: string): Promise { - if (!validateKeyName(name) || name === 'self') { - await randomDelay() - throw errCode(new Error(`Invalid key name '${name}'`), codes.ERR_INVALID_KEY_NAME) - } - if (pem == null) { - await randomDelay() - throw errCode(new Error('PEM encoded key is required'), codes.ERR_PEM_REQUIRED) - } - const dsname = DsName(name) - const exists = await this.components.datastore.has(dsname) - if (exists) { - await randomDelay() - throw errCode(new Error(`Key '${name}' already exists`), codes.ERR_KEY_ALREADY_EXISTS) - } - - let privateKey - try { - privateKey = await importKey(pem, password) - } catch (err: any) { - await randomDelay() - throw errCode(new Error('Cannot read the key, most likely the password is wrong'), codes.ERR_CANNOT_READ_KEY) - } - - let kid - try { - kid = await privateKey.id() - const cached = privates.get(this) - - if (cached == null) { - throw errCode(new Error('dek missing'), codes.ERR_INVALID_PARAMETERS) - } - - const dek = cached.dek - pem = await privateKey.export(dek) - } catch (err: any) { - await randomDelay() - throw err - } - - const keyInfo = { - name: name, - id: kid - } - const batch = this.components.datastore.batch() - batch.put(dsname, uint8ArrayFromString(pem)) - batch.put(DsInfoName(name), uint8ArrayFromString(JSON.stringify(keyInfo))) - await batch.commit() - - return keyInfo - } - - /** - * Import a peer key - */ - async importPeer (name: string, peer: PeerId): Promise { - try { - if (!validateKeyName(name)) { - throw errCode(new Error(`Invalid key name '${name}'`), codes.ERR_INVALID_KEY_NAME) - } - if (peer == null) { - throw errCode(new Error('PeerId is required'), codes.ERR_MISSING_PRIVATE_KEY) - } - if (peer.privateKey == null) { - throw errCode(new Error('PeerId.privKey is required'), codes.ERR_MISSING_PRIVATE_KEY) - } - - const privateKey = await unmarshalPrivateKey(peer.privateKey) - - const dsname = DsName(name) - const exists = await this.components.datastore.has(dsname) - if (exists) { - await randomDelay() - throw errCode(new Error(`Key '${name}' already exists`), codes.ERR_KEY_ALREADY_EXISTS) - } - - const cached = privates.get(this) - - if (cached == null) { - throw errCode(new Error('dek missing'), codes.ERR_INVALID_PARAMETERS) - } - - const dek = cached.dek - const pem = await privateKey.export(dek) - const keyInfo: KeyInfo = { - name: name, - id: peer.toString() - } - const batch = this.components.datastore.batch() - batch.put(dsname, uint8ArrayFromString(pem)) - batch.put(DsInfoName(name), uint8ArrayFromString(JSON.stringify(keyInfo))) - await batch.commit() - return keyInfo - } catch (err: any) { - await randomDelay() - throw err - } - } - - /** - * Gets the private key as PEM encoded PKCS #8 string - */ - async getPrivateKey (name: string): Promise { - if (!validateKeyName(name)) { - await randomDelay() - throw errCode(new Error(`Invalid key name '${name}'`), codes.ERR_INVALID_KEY_NAME) - } - - try { - const dsname = DsName(name) - const res = await this.components.datastore.get(dsname) - return uint8ArrayToString(res) - } catch (err: any) { - await randomDelay() - log.error(err) - throw errCode(new Error(`Key '${name}' does not exist.`), codes.ERR_KEY_NOT_FOUND) - } - } - - /** - * Rotate keychain password and re-encrypt all associated keys - */ - async rotateKeychainPass (oldPass: string, newPass: string) { - if (typeof oldPass !== 'string') { - await randomDelay() - throw errCode(new Error(`Invalid old pass type '${typeof oldPass}'`), codes.ERR_INVALID_OLD_PASS_TYPE) - } - if (typeof newPass !== 'string') { - await randomDelay() - throw errCode(new Error(`Invalid new pass type '${typeof newPass}'`), codes.ERR_INVALID_NEW_PASS_TYPE) - } - if (newPass.length < 20) { - await randomDelay() - throw errCode(new Error(`Invalid pass length ${newPass.length}`), codes.ERR_INVALID_PASS_LENGTH) - } - log('recreating keychain') - const cached = privates.get(this) - - if (cached == null) { - throw errCode(new Error('dek missing'), codes.ERR_INVALID_PARAMETERS) - } - - const oldDek = cached.dek - this.init.pass = newPass - const newDek = newPass != null && this.init.dek?.salt != null - ? pbkdf2( - newPass, - this.init.dek.salt, - this.init.dek?.iterationCount, - this.init.dek?.keyLength, - this.init.dek?.hash) - : '' - privates.set(this, { dek: newDek }) - const keys = await this.listKeys() - for (const key of keys) { - const res = await this.components.datastore.get(DsName(key.name)) - const pem = uint8ArrayToString(res) - const privateKey = await importKey(pem, oldDek) - const password = newDek.toString() - const keyAsPEM = await privateKey.export(password) - - // Update stored key - const batch = this.components.datastore.batch() - const keyInfo = { - name: key.name, - id: key.id - } - batch.put(DsName(key.name), uint8ArrayFromString(keyAsPEM)) - batch.put(DsInfoName(key.name), uint8ArrayFromString(JSON.stringify(keyInfo))) - await batch.commit() - } - log('keychain reconstructed') - } -} diff --git a/src/keychain/util.ts b/src/keychain/util.ts deleted file mode 100644 index 7e01542649..0000000000 --- a/src/keychain/util.ts +++ /dev/null @@ -1,82 +0,0 @@ -import 'node-forge/lib/x509.js' -// @ts-expect-error types are missing -import forge from 'node-forge/lib/forge.js' - -const pki = forge.pki - -/** - * Gets a self-signed X.509 certificate for the key. - * - * The output Uint8Array contains the PKCS #7 message in DER. - * - * TODO: move to libp2p-crypto package - */ -export const certificateForKey = (key: any, privateKey: forge.pki.rsa.PrivateKey) => { - const publicKey = pki.rsa.setPublicKey(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) // eslint-disable-line @typescript-eslint/restrict-plus-operands - 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 - */ -export async function findAsync (array: T[], asyncCompare: (val: T) => Promise) { - const promises = array.map(asyncCompare) - const results = await Promise.all(promises) - const index = results.findIndex(result => result) - return array[index] -} diff --git a/src/libp2p.ts b/src/libp2p.ts index 00cdb23647..819f6c3e53 100644 --- a/src/libp2p.ts +++ b/src/libp2p.ts @@ -12,7 +12,7 @@ import { DefaultConnectionManager } from './connection-manager/index.js' import { AutoDialler } from './connection-manager/auto-dialler.js' import { Circuit } from './circuit/transport.js' import { Relay } from './circuit/index.js' -import { KeyChain } from './keychain/index.js' +import { DefaultKeyChain } from '@libp2p/keychain' import { DefaultTransportManager } from './transport-manager.js' import { DefaultUpgrader } from './upgrader.js' import { DefaultRegistrar } from './registrar.js' @@ -50,6 +50,8 @@ import { PeerSet } from '@libp2p/peer-collections' import { DefaultDialer } from './connection-manager/dialer/index.js' import { peerIdFromString } from '@libp2p/peer-id' import type { Datastore } from 'interface-datastore' +import type { KeyChain } from '@libp2p/interface-keychain' +import mergeOptions from 'merge-options' const log = logger('libp2p') @@ -160,8 +162,8 @@ export class Libp2pNode extends EventEmitter implements Libp2p { })) // Create keychain - const keychainOpts = KeyChain.generateOptions() - this.keychain = this.configureComponent(new KeyChain(this.components, { + const keychainOpts = DefaultKeyChain.generateOptions() + this.keychain = this.configureComponent(new DefaultKeyChain(this.components, { ...keychainOpts, ...init.keychain })) @@ -271,6 +273,13 @@ export class Libp2pNode extends EventEmitter implements Libp2p { log('libp2p is starting') + const keys = await this.keychain.listKeys() + + if (keys.find(key => key.name === 'self') == null) { + log('importing self key into keychain') + await this.keychain.importPeer('self', this.components.peerId) + } + try { await Promise.all( this.services.map(async service => { @@ -516,13 +525,9 @@ export async function createLibp2pNode (options: Libp2pOptions): Promise { - const passPhrase = 'this is not a secure phrase' - const aliceKeyName = 'cms-interop-alice' - let ks: KeyChain - - before(async () => { - const datastore = new MemoryDatastore() - ks = new KeyChain({ - peerId: await createEd25519PeerId(), - datastore - }, { pass: passPhrase }) - }) - - const plainData = uint8ArrayFromString('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(uint8ArrayFromString(example.replace(/\s/g, ''), 'base64')) - expect(plain).to.exist() - expect(uint8ArrayToString(plain)).to.equal(uint8ArrayToString(plainData)) - }) -}) diff --git a/test/keychain/keychain.spec.ts b/test/keychain/keychain.spec.ts deleted file mode 100644 index 287f0cd44f..0000000000 --- a/test/keychain/keychain.spec.ts +++ /dev/null @@ -1,645 +0,0 @@ -/* eslint max-nested-callbacks: ["error", 8] */ -/* eslint-env mocha */ - -import { expect } from 'aegir/chai' -import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' -import { toString as uint8ArrayToString } from 'uint8arrays/to-string' -import { createNode } from '../utils/creators/peer.js' -import { Key } from 'interface-datastore/key' -import { MemoryDatastore } from 'datastore-core/memory' -import { KeyChain, KeyChainInit, KeyInfo } from '../../src/keychain/index.js' -import { pbkdf2 } from '@libp2p/crypto' -import type { Datastore } from 'interface-datastore' -import type { PeerId } from '@libp2p/interface-peer-id' -import { createEd25519PeerId, createFromPrivKey } from '@libp2p/peer-id-factory' -import { unmarshalPrivateKey } from '@libp2p/crypto/keys' - -describe('keychain', () => { - const passPhrase = 'this is not a secure phrase' - const rsaKeyName = 'tajné jméno' - const renamedRsaKeyName = 'ชื่อลับ' - let rsaKeyInfo: KeyInfo - let emptyKeystore: KeyChain - let ks: KeyChain - let datastore1: Datastore, datastore2: Datastore - - before(async () => { - datastore1 = new MemoryDatastore() - datastore2 = new MemoryDatastore() - - ks = new KeyChain({ - peerId: await createEd25519PeerId(), - datastore: datastore2 - }, { pass: passPhrase }) - emptyKeystore = new KeyChain({ - peerId: await createEd25519PeerId(), - datastore: datastore1 - }, { pass: passPhrase }) - - await datastore1.open() - await datastore2.open() - }) - - after(async () => { - await datastore2.close() - await datastore2.close() - }) - - it('can start without a password', async () => { - await expect(async function () { - return new KeyChain({ - peerId: await createEd25519PeerId(), - datastore: datastore2 - }, {}) - }()).to.eventually.be.ok() - }) - - it('needs a NIST SP 800-132 non-weak pass phrase', async () => { - await expect(async function () { - return new KeyChain({ - peerId: await createEd25519PeerId(), - datastore: datastore2 - }, { pass: '< 20 character' }) - }()).to.eventually.be.rejected() - }) - - it('has default options', () => { - expect(KeyChain.options).to.exist() - }) - - it('supports supported hashing alorithms', async () => { - const ok = new KeyChain({ - peerId: await createEd25519PeerId(), - datastore: datastore2 - }, { pass: passPhrase, dek: { hash: 'sha2-256', salt: 'salt-salt-salt-salt', iterationCount: 1000, keyLength: 14 } }) - expect(ok).to.exist() - }) - - it('does not support unsupported hashing alorithms', async () => { - await expect(async function () { - return new KeyChain({ - peerId: await createEd25519PeerId(), - datastore: datastore2 - }, { pass: passPhrase, dek: { hash: 'my-hash', salt: 'salt-salt-salt-salt', iterationCount: 1000, keyLength: 14 } }) - }()).to.eventually.be.rejected() - }) - - it('can list keys without a password', async () => { - const keychain = new KeyChain({ - peerId: await createEd25519PeerId(), - datastore: datastore2 - }, {}) - - expect(await keychain.listKeys()).to.have.lengthOf(0) - }) - - it('can find a key without a password', async () => { - const keychain = new KeyChain({ - peerId: await createEd25519PeerId(), - datastore: datastore2 - }, {}) - const keychainWithPassword = new KeyChain({ - peerId: await createEd25519PeerId(), - datastore: datastore2 - }, { pass: `hello-${Date.now()}-${Date.now()}` }) - const name = `key-${Math.random()}` - - const { id } = await keychainWithPassword.createKey(name, 'Ed25519') - - await expect(keychain.findKeyById(id)).to.eventually.be.ok() - }) - - it('can remove a key without a password', async () => { - const keychainWithoutPassword = new KeyChain({ - peerId: await createEd25519PeerId(), - datastore: datastore2 - }, {}) - const keychainWithPassword = new KeyChain({ - peerId: await createEd25519PeerId(), - datastore: datastore2 - }, { pass: `hello-${Date.now()}-${Date.now()}` }) - const name = `key-${Math.random()}` - - expect(await keychainWithPassword.createKey(name, 'Ed25519')).to.have.property('name', name) - expect(await keychainWithoutPassword.findKeyByName(name)).to.have.property('name', name) - await keychainWithoutPassword.removeKey(name) - await expect(keychainWithoutPassword.findKeyByName(name)).to.be.rejectedWith(/does not exist/) - }) - - it('requires a name to create a password', async () => { - const keychain = new KeyChain({ - peerId: await createEd25519PeerId(), - datastore: datastore2 - }, {}) - - // @ts-expect-error invalid parameters - await expect(keychain.createKey(undefined, 'derp')).to.eventually.be.rejected() - }) - - it('can generate options', async () => { - const options = KeyChain.generateOptions() - options.pass = passPhrase - const chain = new KeyChain({ - peerId: await createEd25519PeerId(), - datastore: 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').catch(err => err), - ks.removeKey('').catch(err => err), - ks.removeKey(' ').catch(err => err), - // @ts-expect-error invalid parameters - ks.removeKey(null).catch(err => err), - // @ts-expect-error invalid parameters - ks.removeKey(undefined).catch(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 () => { - // @ts-expect-error invalid parameters - await expect(ks.getPrivateKey(undefined)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') - }) - - it('throws if a private key cant be found', async () => { - await expect(ks.getPrivateKey('not real')).to.eventually.be.rejected.with.property('code', 'ERR_KEY_NOT_FOUND') - }) - - it('does not overwrite existing key', async () => { - await expect(ks.createKey(rsaKeyName, 'RSA', 2048)).to.eventually.be.rejected.with.property('code', 'ERR_KEY_ALREADY_EXISTS') - }) - - it('cannot create the "self" key', async () => { - await expect(ks.createKey('self', 'RSA', 2048)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') - }) - - it('should validate name is string', async () => { - // @ts-expect-error invalid parameters - await expect(ks.createKey(5, 'rsa', 2048)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') - }) - - it('should validate type is string', async () => { - // @ts-expect-error invalid parameters - await expect(ks.createKey(`TEST-${Date.now()}`, null, 2048)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_TYPE') - }) - - it('should validate size is integer', async () => { - // @ts-expect-error invalid parameters - await expect(ks.createKey(`TEST-${Date.now()}`, 'RSA', 'string')).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_SIZE') - }) - - describe('implements NIST SP 800-131A', () => { - it('disallows RSA length < 2048', async () => { - await expect(ks.createKey('bad-nist-rsa', 'RSA', 1024)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_SIZE') - }) - }) - }) - - describe('Ed25519 keys', () => { - const keyName = 'my custom key' - it('can be an Ed25519 key', async () => { - const keyInfo = await ks.createKey(keyName, 'Ed25519') - expect(keyInfo).to.exist() - expect(keyInfo).to.have.property('name', keyName) - expect(keyInfo).to.have.property('id') - }) - - it('does not overwrite existing key', async () => { - await expect(ks.createKey(keyName, 'Ed25519')).to.eventually.be.rejected.with.property('code', 'ERR_KEY_ALREADY_EXISTS') - }) - - it('can export/import a key', async () => { - const keyName = 'a new key' - const password = 'my sneaky password' - const keyInfo = await ks.createKey(keyName, 'Ed25519') - const exportedKey = await ks.exportKey(keyName, password) - // remove it so we can import it - await ks.removeKey(keyName) - const importedKey = await ks.importKey(keyName, exportedKey, password) - expect(importedKey.id).to.eql(keyInfo.id) - }) - - it('cannot create the "self" key', async () => { - await expect(ks.createKey('self', 'Ed25519')).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') - }) - }) - - 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 = uint8ArrayFromString('This is a message from Alice to Bob') - let cms: Uint8Array - - it('service is available', () => { - expect(ks).to.have.property('cms') - }) - - it('requires a key', async () => { - await expect(ks.cms.encrypt('no-key', plainData)).to.eventually.be.rejected.with.property('code', 'ERR_KEY_NOT_FOUND') - }) - - it('requires plain data as a Uint8Array', async () => { - // @ts-expect-error invalid parameters - await expect(ks.cms.encrypt(rsaKeyName, 'plain data')).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_PARAMETERS') - }) - - it('encrypts', async () => { - cms = await ks.cms.encrypt(rsaKeyName, plainData) - expect(cms).to.exist() - expect(cms).to.be.instanceOf(Uint8Array) - }) - - it('is a PKCS #7 message', async () => { - // @ts-expect-error invalid parameters - await expect(ks.cms.decrypt('not CMS')).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_PARAMETERS') - }) - - it('is a PKCS #7 binary message', async () => { - await expect(ks.cms.decrypt(plainData)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_CMS') - }) - - it('cannot be read without the key', async () => { - await expect(emptyKeystore.cms.decrypt(cms)).to.eventually.be.rejected.with.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(uint8ArrayToString(plain)).to.equal(uint8ArrayToString(plainData)) - }) - }) - - describe('exported key', () => { - let pemKey: string - - it('requires the password', async () => { - // @ts-expect-error invalid parameters - await expect(ks.exportKey(rsaKeyName)).to.eventually.be.rejected.with.property('code', 'ERR_PASSWORD_REQUIRED') - }) - - it('requires the key name', async () => { - // @ts-expect-error invalid parameters - await expect(ks.exportKey(undefined, 'password')).to.eventually.be.rejected.with.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 () => { - // @ts-expect-error invalid parameters - await expect(ks.importKey('imported-key', undefined, 'password')).to.eventually.be.rejected.with.property('code', 'ERR_PEM_REQUIRED') - }) - - it('cannot be imported as an existing key name', async () => { - await expect(ks.importKey(rsaKeyName, pemKey, 'password')).to.eventually.be.rejected.with.property('code', 'ERR_KEY_ALREADY_EXISTS') - }) - - it('cannot be imported with the wrong password', async () => { - await expect(ks.importKey('a-new-name-for-import', pemKey, 'not the password')).to.eventually.be.rejected.with.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: PeerId - - before(async function () { - const encoded = uint8ArrayFromString(alicePrivKey, 'base64pad') - const privateKey = await unmarshalPrivateKey(encoded) - alice = await createFromPrivKey(privateKey) - }) - - 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.toString()) - }) - - it('private key can be exported', async () => { - const alice2 = await ks.exportPeerId('alice') - - expect(alice.equals(alice2)).to.be.true() - expect(alice2).to.have.property('privateKey').that.is.ok() - expect(alice2).to.have.property('publicKey').that.is.ok() - }) - - it('private key import requires a valid name', async () => { - // @ts-expect-error invalid parameters - await expect(ks.importPeer(undefined, alice)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') - }) - - it('private key import requires the peer', async () => { - // @ts-expect-error invalid parameters - await expect(ks.importPeer('alice')).to.eventually.be.rejected.with.property('code', 'ERR_MISSING_PRIVATE_KEY') - }) - - it('key id exists', async () => { - const key = await ks.findKeyById(alice.toString()) - expect(key).to.exist() - expect(key).to.have.property('name', 'alice') - expect(key).to.have.property('id', alice.toString()) - }) - - 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.toString()) - }) - - it('can create Ed25519 peer id', async () => { - const name = 'ed-key' - await ks.createKey(name, 'Ed25519') - const peer = await ks.exportPeerId(name) - - expect(peer).to.have.property('type', 'Ed25519') - expect(peer).to.have.property('privateKey').that.is.ok() - expect(peer).to.have.property('publicKey').that.is.ok() - }) - - it('can create RSA peer id', async () => { - const name = 'rsa-key' - await ks.createKey(name, 'RSA', 2048) - const peer = await ks.exportPeerId(name) - - expect(peer).to.have.property('type', 'RSA') - expect(peer).to.have.property('privateKey').that.is.ok() - expect(peer).to.have.property('publicKey').that.is.ok() - }) - - it('can create secp256k1 peer id', async () => { - const name = 'secp256k1-key' - await ks.createKey(name, 'secp256k1') - const peer = await ks.exportPeerId(name) - - expect(peer).to.have.property('type', 'secp256k1') - expect(peer).to.have.property('privateKey').that.is.ok() - expect(peer).to.have.property('publicKey').that.is.ok() - }) - }) - - describe('rename', () => { - it('requires an existing key name', async () => { - await expect(ks.renameKey('not-there', renamedRsaKeyName)).to.eventually.be.rejected.with.property('code', 'ERR_NOT_FOUND') - }) - - it('requires a valid new key name', async () => { - await expect(ks.renameKey(rsaKeyName, '..\not-valid')).to.eventually.be.rejected.with.property('code', 'ERR_NEW_KEY_NAME_INVALID') - }) - - it('does not overwrite existing key', async () => { - await expect(ks.renameKey(rsaKeyName, rsaKeyName)).to.eventually.be.rejected.with.property('code', 'ERR_KEY_ALREADY_EXISTS') - }) - - it('cannot create the "self" key', async () => { - await expect(ks.renameKey(rsaKeyName, 'self')).to.eventually.be.rejected.with.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 - await expect(ks.findKeyByName(rsaKeyName)).to.eventually.be.rejected() - }) - - 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 () => { - // @ts-expect-error invalid parameters - await expect(ks.findKeyByName(undefined)).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') - }) - }) - - describe('key removal', () => { - it('cannot remove the "self" key', async () => { - await expect(ks.removeKey('self')).to.eventually.be.rejected.with.property('code', 'ERR_INVALID_KEY_NAME') - }) - - it('cannot remove an unknown key', async () => { - await expect(ks.removeKey('not-there')).to.eventually.be.rejected.with.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) - }) - }) - - describe('rotate keychain passphrase', () => { - let oldPass: string - let kc: KeyChain - let options: KeyChainInit - let ds: Datastore - before(async () => { - ds = new MemoryDatastore() - oldPass = `hello-${Date.now()}-${Date.now()}` - options = { - pass: oldPass, - dek: { - salt: '3Nd/Ya4ENB3bcByNKptb4IR', - iterationCount: 10000, - keyLength: 64, - hash: 'sha2-512' - } - } - kc = new KeyChain({ - peerId: await createEd25519PeerId(), - datastore: ds - }, options) - await ds.open() - }) - - it('should validate newPass is a string', async () => { - // @ts-expect-error invalid parameters - await expect(kc.rotateKeychainPass(oldPass, 1234567890)).to.eventually.be.rejected() - }) - - it('should validate oldPass is a string', async () => { - // @ts-expect-error invalid parameters - await expect(kc.rotateKeychainPass(1234, 'newInsecurePassword1')).to.eventually.be.rejected() - }) - - it('should validate newPass is at least 20 characters', async () => { - try { - await kc.rotateKeychainPass(oldPass, 'not20Chars') - } catch (err: any) { - expect(err).to.exist() - } - }) - - it('can rotate keychain passphrase', async () => { - await kc.createKey('keyCreatedWithOldPassword', 'RSA', 2048) - await kc.rotateKeychainPass(oldPass, 'newInsecurePassphrase') - - // Get Key PEM from datastore - const dsname = new Key('/pkcs8/' + 'keyCreatedWithOldPassword') - const res = await ds.get(dsname) - const pem = uint8ArrayToString(res) - - const oldDek = options.pass != null - ? pbkdf2( - options.pass, - options.dek?.salt ?? 'salt', - options.dek?.iterationCount ?? 0, - options.dek?.keyLength ?? 0, - options.dek?.hash ?? 'sha2-256' - ) - : '' - - const newDek = pbkdf2( - 'newInsecurePassphrase', - options.dek?.salt ?? 'salt', - options.dek?.iterationCount ?? 0, - options.dek?.keyLength ?? 0, - options.dek?.hash ?? 'sha2-256' - ) - - // Dek with old password should not work: - await expect(kc.importKey('keyWhosePassChanged', pem, oldDek)) - .to.eventually.be.rejected() - // Dek with new password should work: - await expect(kc.importKey('keyWhosePasswordChanged', pem, newDek)) - .to.eventually.have.property('name', 'keyWhosePasswordChanged') - }).timeout(10000) - }) -}) - -describe('libp2p.keychain', () => { - it.skip('needs a passphrase to be used, otherwise throws an error', async () => { - const libp2p = await createNode({ - started: false - }) - - await expect(libp2p.keychain.createKey('keyName', 'RSA', 2048)).to.be.rejected() - }) - - it('can be used when a passphrase is provided', async () => { - const libp2p = await createNode({ - started: false, - config: { - datastore: new MemoryDatastore(), - keychain: { - pass: '12345678901234567890' - } - } - }) - - const kInfo = await libp2p.keychain.createKey('keyName', 'Ed25519') - expect(kInfo).to.exist() - }) - - it('does not require a keychain passphrase', async () => { - const libp2p = await createNode({ - started: false, - config: { - datastore: new MemoryDatastore() - } - }) - - const kInfo = await libp2p.keychain.createKey('keyName', 'Ed25519') - expect(kInfo).to.exist() - }) - - it('can reload keys', async () => { - const datastore = new MemoryDatastore() - const libp2p = await createNode({ - started: false, - config: { - datastore, - keychain: { - pass: '12345678901234567890' - } - } - }) - - const kInfo = await libp2p.keychain.createKey('keyName', 'Ed25519') - expect(kInfo).to.exist() - - const libp2p2 = await createNode({ - started: false, - config: { - datastore, - keychain: { - pass: '12345678901234567890' - } - } - }) - - const key = await libp2p2.keychain.findKeyByName('keyName') - - expect(key).to.exist() - }) -}) diff --git a/test/keychain/peerid.spec.ts b/test/keychain/peerid.spec.ts deleted file mode 100644 index 49d5da1a6d..0000000000 --- a/test/keychain/peerid.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* eslint-env mocha */ - -import { expect } from 'aegir/chai' -import { base58btc } from 'multiformats/bases/base58' -import { supportedKeys, unmarshalPrivateKey, unmarshalPublicKey } from '@libp2p/crypto/keys' -import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' -import type { PeerId } from '@libp2p/interface-peer-id' -import { createFromPrivKey } from '@libp2p/peer-id-factory' - -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: PeerId - let publicKeyDer: Uint8Array // a buffer - - before(async () => { - const encoded = uint8ArrayFromString(sample.privKey, 'base64pad') - peer = await createFromPrivKey(await unmarshalPrivateKey(encoded)) - }) - - it('decoded public key', async () => { - if (peer.publicKey == null) { - throw new Error('PublicKey missing from PeerId') - } - - if (peer.privateKey == null) { - throw new Error('PrivateKey missing from PeerId') - } - - // get protobuf version of the public key - const publicKeyProtobuf = peer.publicKey - const publicKey = unmarshalPublicKey(publicKeyProtobuf) - publicKeyDer = publicKey.marshal() - - // get protobuf version of the private key - const privateKeyProtobuf = peer.privateKey - const key = await unmarshalPrivateKey(privateKeyProtobuf) - expect(key).to.exist() - }) - - it('encoded public key with DER', async () => { - const rsa = await supportedKeys.rsa.unmarshalRsaPublicKey(publicKeyDer) - const keyId = await rsa.hash() - const kids = base58btc.encode(keyId).substring(1) - expect(kids).to.equal(peer.toString()) - }) - - 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 supportedKeys.rsa.RsaPublicKey(jwk) - const keyId = await rsa.hash() - const kids = base58btc.encode(keyId).substring(1) - expect(kids).to.equal(peer.toString()) - }) - - it('decoded private key', async () => { - if (peer.privateKey == null) { - throw new Error('PrivateKey missing from PeerId') - } - - // get protobuf version of the private key - const privateKeyProtobuf = peer.privateKey - const key = await unmarshalPrivateKey(privateKeyProtobuf) - expect(key).to.exist() - }) -})