From 6779b952a0051727d40ad956d27d9b405711ce42 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Fri, 27 Jan 2023 18:29:28 +0100 Subject: [PATCH] fix: use @libp2p/keychain module instead of bundling source code The @libp2p/keychain module has the keychain code split out from this module so use that instead of bundling the code here. It's also dropped it's dependency on `node-forge` so it's now only in `@libp2p/crypto`. --- package.json | 6 +- src/index.ts | 2 +- src/keychain/README.md | 55 --- src/keychain/cms.ts | 150 ------- src/keychain/doc/private-key.png | Bin 25518 -> 0 bytes src/keychain/doc/private-key.xml | 1 - src/keychain/index.ts | 629 ----------------------------- src/keychain/util.ts | 82 ---- src/libp2p.ts | 23 +- test/keychain/cms-interop.spec.ts | 67 ---- test/keychain/keychain.spec.ts | 645 ------------------------------ test/keychain/peerid.spec.ts | 76 ---- 12 files changed, 17 insertions(+), 1719 deletions(-) delete mode 100644 src/keychain/README.md delete mode 100644 src/keychain/cms.ts delete mode 100644 src/keychain/doc/private-key.png delete mode 100644 src/keychain/doc/private-key.xml delete mode 100644 src/keychain/index.ts delete mode 100644 src/keychain/util.ts delete mode 100644 test/keychain/cms-interop.spec.ts delete mode 100644 test/keychain/keychain.spec.ts delete mode 100644 test/keychain/peerid.spec.ts 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 4c85dc610c883942212ff3b419cad3cae3c24769..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25518 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 NO7iM*Wil3F{{{GIyypM_ 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() - }) -})