diff --git a/doc/CONFIGURATION.md b/doc/CONFIGURATION.md index 2173ac59be..6f82e087bd 100644 --- a/doc/CONFIGURATION.md +++ b/doc/CONFIGURATION.md @@ -930,7 +930,7 @@ For more information see https://docs.libp2p.io/concepts/nat/autonat/#what-is-au ```ts import { createLibp2p } from 'libp2p' -import { autoNATService } from 'libp2p/autonat' +import { autoNATService } from '@libp2p/autonat' const node = await createLibp2p({ services: { diff --git a/doc/migrations/v0.46-v1.0.0.md b/doc/migrations/v0.46-v1.0.0.md new file mode 100644 index 0000000000..13ee735733 --- /dev/null +++ b/doc/migrations/v0.46-v1.0.0.md @@ -0,0 +1,96 @@ + +# Migrating to libp2p@1.0.0 + +A migration guide for refactoring your application code from libp2p `v0.46` to `v1.0.0`. + +## Table of Contents + +- [AutoNAT](#autonat) +- [Fetch](#fetch) +- [KeyChain](#keychain) +- [Pnet](#pnet) +- [Metrics](#metrics) + +## AutoNAT + +The AutoNAT service is now published in its own package. + +**Before** + +```ts +import { autoNATService } from 'libp2p/autonat' +``` + +**After** + +```ts +import { autoNATService } from '@libp2p/autonat' +``` + +## Fetch + +The Fetch service is now publisehd as it's own package. + +**Before** + +```ts +import { autoNATService } from 'libp2p/fetch' +``` + +**After** + +```ts +import { autoNATService } from '@libp2p/fetch' +``` + +## KeyChain + +The KeyChain object is no longer included on Libp2p and must be instantiated explicitly if desired. + +**Before** + +```ts +import type { KeyChain } from '@libp2p/interface/keychain' + +const libp2p = await createLibp2p(...) + +const keychain: KeyChain = libp2p.keychain +``` + +**After** + +```ts +import { keychain, type Keychain } from '@libp2p/keychain' + +const libp2p = await createLibp2p({ + ... + services: { + keychain: keychain() + } +}) + +const keychain: Keychain = libp2p.services.keychain +``` + +## Pnet + +The pnet module is now published in its own package. + +**Before** + +```ts +import { preSharedKey, generateKey } from 'libp2p/pnet' +``` + +**After** + +```ts +import { preSharedKey, generateKey } from '@libp2p/pnet' +``` + +## Metrics + +The following metrics were renamed: + +`libp2p_dialler_pending_dials` => `libp2p_dial_queue_pending_dials` +`libp2p_dialler_in_progress_dials` => `libp2p_dial_queue_in_progress_dials` diff --git a/packages/interface-compliance-tests/src/mocks/peer-discovery.ts b/packages/interface-compliance-tests/src/mocks/peer-discovery.ts index 69bd2afc29..7fa6abedc1 100644 --- a/packages/interface-compliance-tests/src/mocks/peer-discovery.ts +++ b/packages/interface-compliance-tests/src/mocks/peer-discovery.ts @@ -49,8 +49,7 @@ export class MockDiscovery extends TypedEventEmitter implem this.safeDispatchEvent('peer', { detail: { id: peerId, - multiaddrs: [multiaddr('/ip4/127.0.0.1/tcp/8000')], - protocols: [] + multiaddrs: [multiaddr('/ip4/127.0.0.1/tcp/8000')] } }) }, this.options.discoveryDelay ?? 1000) diff --git a/packages/interface-compliance-tests/src/pubsub/api.ts b/packages/interface-compliance-tests/src/pubsub/api.ts index a879c46473..4c81f4f595 100644 --- a/packages/interface-compliance-tests/src/pubsub/api.ts +++ b/packages/interface-compliance-tests/src/pubsub/api.ts @@ -48,7 +48,6 @@ export default (common: TestSetup): void => { await start(...Object.values(components)) - expect(pubsub.isStarted()).to.equal(true) expect(components.registrar.register).to.have.property('callCount', 1) }) @@ -62,7 +61,6 @@ export default (common: TestSetup): void => { await start(...Object.values(components)) await stop(...Object.values(components)) - expect(pubsub.isStarted()).to.equal(false) expect(components.registrar.unregister).to.have.property('callCount', 1) }) diff --git a/packages/interface/src/errors.ts b/packages/interface/src/errors.ts index a5911adb76..b7fb770c07 100644 --- a/packages/interface/src/errors.ts +++ b/packages/interface/src/errors.ts @@ -65,3 +65,10 @@ export class InvalidCryptoTransmissionError extends Error { static readonly code = 'ERR_INVALID_CRYPTO_TRANSMISSION' } + +// Error codes + +export const ERR_TIMEOUT = 'ERR_TIMEOUT' +export const ERR_INVALID_MESSAGE = 'ERR_INVALID_MESSAGE' +export const ERR_INVALID_PARAMETERS = 'ERR_INVALID_PARAMETERS' +export const ERR_KEY_ALREADY_EXISTS = 'ERR_KEY_ALREADY_EXISTS' diff --git a/packages/interface/src/index.ts b/packages/interface/src/index.ts index 5b1463e4c2..c1f3b95a2c 100644 --- a/packages/interface/src/index.ts +++ b/packages/interface/src/index.ts @@ -17,7 +17,6 @@ import type { Connection, NewStreamOptions, Stream } from './connection/index.js' import type { ContentRouting } from './content-routing/index.js' import type { TypedEventTarget } from './events.js' -import type { KeyChain } from './keychain/index.js' import type { Metrics } from './metrics/index.js' import type { PeerId } from './peer-id/index.js' import type { PeerInfo } from './peer-info/index.js' @@ -377,20 +376,6 @@ export interface Libp2p extends Startable, Ty */ contentRouting: ContentRouting - /** - * The keychain contains the keys used by the current node, and can create new - * keys, export them, import them, etc. - * - * @example - * - * ```js - * const keyInfo = await libp2p.keychain.createKey('new key') - * console.info(keyInfo) - * // { id: '...', name: 'new key' } - * ``` - */ - keychain: KeyChain - /** * The metrics subsystem allows recording values to assess the health/performance * of the running node. diff --git a/packages/interface/src/keychain/index.ts b/packages/interface/src/keychain/index.ts deleted file mode 100644 index 0812ff1cd8..0000000000 --- a/packages/interface/src/keychain/index.ts +++ /dev/null @@ -1,167 +0,0 @@ -/** - * @packageDocumentation - * - * The libp2p keychain provides an API to store keys in a datastore in - * an encrypted format. - * - * @example - * - * ```typescript - * import { createLibp2p } from 'libp2p' - * import { FsDatastore } from 'datastore-fs' - * - * const node = await createLibp2p({ - * datastore: new FsDatastore('/path/to/dir') - * }) - * - * const info = await node.keychain.createKey('my-new-key', 'Ed25519') - * - * console.info(info) // { id: '...', name: 'my-new-key' } - * ``` - */ - -import type { KeyType } from '../keys/index.js' -import type { PeerId } from '../peer-id/index.js' -import type { Multibase } from 'multiformats/bases/interface' - -export interface KeyInfo { - /** - * The universally unique key id - */ - id: string - - /** - * The local key name - */ - name: string -} - -export interface KeyChain { - /** - * Export an existing key as a PEM encrypted PKCS #8 string. - * - * @example - * - * ```js - * await libp2p.keychain.createKey('keyTest', 'RSA', 4096) - * const pemKey = await libp2p.keychain.exportKey('keyTest', 'password123') - * ``` - */ - exportKey(name: string, password: string): Promise> - - /** - * Import a new key from a PEM encoded PKCS #8 string. - * - * @example - * - * ```js - * await libp2p.keychain.createKey('keyTest', 'RSA', 4096) - * const pemKey = await libp2p.keychain.exportKey('keyTest', 'password123') - * const keyInfo = await libp2p.keychain.importKey('keyTestImport', pemKey, 'password123') - * ``` - */ - importKey(name: string, pem: string, password: string): Promise - - /** - * Import a new key from a PeerId with a private key component - * - * @example - * - * ```js - * const keyInfo = await libp2p.keychain.importPeer('keyTestImport', peerIdFromString('12D3Foo...')) - * ``` - */ - importPeer(name: string, peerId: PeerId): Promise - - /** - * Export an existing key as a PeerId - * - * @example - * - * ```js - * const peerId = await libp2p.keychain.exportPeerId('key-name') - * ``` - */ - exportPeerId(name: string): Promise - - /** - * Create a key in the keychain. - * - * @example - * - * ```js - * const keyInfo = await libp2p.keychain.createKey('keyTest', 'RSA', 4096) - * ``` - */ - createKey(name: string, type: KeyType, size?: number): Promise - - /** - * List all the keys. - * - * @example - * - * ```js - * const keyInfos = await libp2p.keychain.listKeys() - * ``` - */ - listKeys(): Promise - - /** - * Removes a key from the keychain. - * - * @example - * - * ```js - * await libp2p.keychain.createKey('keyTest', 'RSA', 4096) - * const keyInfo = await libp2p.keychain.removeKey('keyTest') - * ``` - */ - removeKey(name: string): Promise - - /** - * Rename a key in the keychain. - * - * @example - * - * ```js - * await libp2p.keychain.createKey('keyTest', 'RSA', 4096) - * const keyInfo = await libp2p.keychain.renameKey('keyTest', 'keyNewNtest') - * ``` - */ - renameKey(oldName: string, newName: string): Promise - - /** - * Find a key by it's id. - * - * @example - * - * ```js - * const keyInfo = await libp2p.keychain.createKey('keyTest', 'RSA', 4096) - * const keyInfo2 = await libp2p.keychain.findKeyById(keyInfo.id) - * ``` - */ - findKeyById(id: string): Promise - - /** - * Find a key by it's name. - * - * @example - * - * ```js - * const keyInfo = await libp2p.keychain.createKey('keyTest', 'RSA', 4096) - * const keyInfo2 = await libp2p.keychain.findKeyByName('keyTest') - * ``` - */ - findKeyByName(name: string): Promise - - /** - * Rotate keychain password and re-encrypt all associated keys - * - * @example - * - * ```js - * await libp2p.keychain.rotateKeychainPass('oldPassword', 'newPassword') - * ``` - */ - rotateKeychainPass(oldPass: string, newPass: string): Promise -} diff --git a/packages/interface/src/peer-info/index.ts b/packages/interface/src/peer-info/index.ts index 25dbd9f208..5b7c8cce9d 100644 --- a/packages/interface/src/peer-info/index.ts +++ b/packages/interface/src/peer-info/index.ts @@ -1,8 +1,20 @@ import type { PeerId } from '../peer-id/index.js' import type { Multiaddr } from '@multiformats/multiaddr' +/** + * A `PeerInfo` is a lightweight object that represents a remote peer, it can be + * obtained from peer discovery mechanisms, HTTP RPC endpoints, etc. + * + * @see https://docs.libp2p.io/concepts/fundamentals/peers/#peer-info + */ export interface PeerInfo { + /** + * The identifier of the remote peer + */ id: PeerId + + /** + * The multiaddrs a peer is listening on + */ multiaddrs: Multiaddr[] - protocols: string[] } diff --git a/packages/interface/src/startable.ts b/packages/interface/src/startable.ts index 8393abc489..7923b1d557 100644 --- a/packages/interface/src/startable.ts +++ b/packages/interface/src/startable.ts @@ -2,8 +2,6 @@ * Implemented by components that have a lifecycle */ export interface Startable { - isStarted(): boolean - /** * If implemented, this method will be invoked before the start method. * diff --git a/packages/interface/src/topology/index.ts b/packages/interface/src/topology/index.ts index 25c321201b..76bb64c1ee 100644 --- a/packages/interface/src/topology/index.ts +++ b/packages/interface/src/topology/index.ts @@ -2,9 +2,6 @@ import type { Connection } from '../connection/index.js' import type { PeerId } from '../peer-id/index.js' export interface Topology { - min?: number - max?: number - /** * If true, invoke `onConnect` for this topology on transient (e.g. short-lived * and/or data-limited) connections. (default: false) diff --git a/packages/kad-dht/src/content-routing/index.ts b/packages/kad-dht/src/content-routing/index.ts index 2ca2033cd4..569e7942f7 100644 --- a/packages/kad-dht/src/content-routing/index.ts +++ b/packages/kad-dht/src/content-routing/index.ts @@ -64,8 +64,7 @@ export class ContentRouting { const msg = new Message(MESSAGE_TYPE.ADD_PROVIDER, key.multihash.bytes, 0) msg.providerPeers = [{ id: this.components.peerId, - multiaddrs, - protocols: [] + multiaddrs }] let sent = 0 @@ -140,8 +139,7 @@ export class ContentRouting { providers.push({ id: peerId, - multiaddrs: peer.addresses.map(({ multiaddr }) => multiaddr), - protocols: peer.protocols + multiaddrs: peer.addresses.map(({ multiaddr }) => multiaddr) }) } catch (err: any) { if (err.code !== 'ERR_NOT_FOUND') { diff --git a/packages/kad-dht/src/kad-dht.ts b/packages/kad-dht/src/kad-dht.ts index fb5be07aa9..6efd51cb09 100644 --- a/packages/kad-dht/src/kad-dht.ts +++ b/packages/kad-dht/src/kad-dht.ts @@ -218,7 +218,7 @@ export class DefaultKadDHT extends TypedEventEmitter implem } async onPeerConnect (peerData: PeerInfo): Promise { - this.log('peer %p connected with protocols', peerData.id, peerData.protocols) + this.log('peer %p connected', peerData.id) if (this.lan) { peerData = removePublicAddresses(peerData) diff --git a/packages/kad-dht/src/message/index.ts b/packages/kad-dht/src/message/index.ts index 585f83c108..ef24ee4c40 100644 --- a/packages/kad-dht/src/message/index.ts +++ b/packages/kad-dht/src/message/index.ts @@ -104,7 +104,6 @@ function fromPbPeer (peer: PBMessage.Peer): PeerInfo { return { id: peerIdFromBytes(peer.id), - multiaddrs: (peer.addrs ?? []).map((a) => multiaddr(a)), - protocols: [] + multiaddrs: (peer.addrs ?? []).map((a) => multiaddr(a)) } } diff --git a/packages/kad-dht/src/peer-routing/index.ts b/packages/kad-dht/src/peer-routing/index.ts index b356f86263..e2b2b304b5 100644 --- a/packages/kad-dht/src/peer-routing/index.ts +++ b/packages/kad-dht/src/peer-routing/index.ts @@ -85,8 +85,7 @@ export class PeerRouting { return { id: peerData.id, - multiaddrs: peerData.addresses.map((address) => address.multiaddr), - protocols: [] + multiaddrs: peerData.addresses.map((address) => address.multiaddr) } } @@ -226,8 +225,7 @@ export class PeerRouting { from: this.components.peerId, peer: { id: peerId, - multiaddrs: peer.addresses.map(({ multiaddr }) => multiaddr), - protocols: peer.protocols + multiaddrs: peer.addresses.map(({ multiaddr }) => multiaddr) } }, options) } catch (err: any) { @@ -296,8 +294,7 @@ export class PeerRouting { output.push({ id: peerId, - multiaddrs: peer.addresses.map(({ multiaddr }) => multiaddr), - protocols: peer.protocols + multiaddrs: peer.addresses.map(({ multiaddr }) => multiaddr) }) } catch (err: any) { if (err.code !== 'ERR_NOT_FOUND') { diff --git a/packages/kad-dht/src/rpc/handlers/find-node.ts b/packages/kad-dht/src/rpc/handlers/find-node.ts index 979ed1919a..4763ba5376 100644 --- a/packages/kad-dht/src/rpc/handlers/find-node.ts +++ b/packages/kad-dht/src/rpc/handlers/find-node.ts @@ -48,8 +48,7 @@ export class FindNodeHandler implements DHTMessageHandler { if (uint8ArrayEquals(this.components.peerId.toBytes(), msg.key)) { closer = [{ id: this.components.peerId, - multiaddrs: this.components.addressManager.getAddresses().map(ma => ma.decapsulateCode(protocols('p2p').code)), - protocols: [] + multiaddrs: this.components.addressManager.getAddresses().map(ma => ma.decapsulateCode(protocols('p2p').code)) }] } else { closer = await this.peerRouting.getCloserPeersOffline(msg.key, peerId) diff --git a/packages/kad-dht/src/rpc/handlers/get-providers.ts b/packages/kad-dht/src/rpc/handlers/get-providers.ts index 8e0c8563bb..18778887a6 100644 --- a/packages/kad-dht/src/rpc/handlers/get-providers.ts +++ b/packages/kad-dht/src/rpc/handlers/get-providers.ts @@ -86,8 +86,7 @@ export class GetProvidersHandler implements DHTMessageHandler { const peerAfterFilter = addrFilter({ id: peerId, - multiaddrs: peer.addresses.map(({ multiaddr }) => multiaddr), - protocols: peer.protocols + multiaddrs: peer.addresses.map(({ multiaddr }) => multiaddr) }) if (peerAfterFilter.multiaddrs.length > 0) { diff --git a/packages/kad-dht/test/kad-utils.spec.ts b/packages/kad-dht/test/kad-utils.spec.ts index 2d99cc4ebf..4f27ae4a27 100644 --- a/packages/kad-dht/test/kad-utils.spec.ts +++ b/packages/kad-dht/test/kad-utils.spec.ts @@ -73,7 +73,7 @@ describe('kad utils', () => { multiaddr('/dns4/localhost/tcp/4001') ] - const peerInfo = utils.removePrivateAddresses({ id, multiaddrs, protocols: [] }) + const peerInfo = utils.removePrivateAddresses({ id, multiaddrs }) expect(peerInfo.multiaddrs.map((ma) => ma.toString())) .to.eql(['/dns4/example.com/tcp/4001', '/ip4/1.1.1.1/tcp/4001']) }) @@ -90,7 +90,7 @@ describe('kad utils', () => { multiaddr('/dns4/localhost/tcp/4001') ] - const peerInfo = utils.removePublicAddresses({ id, multiaddrs, protocols: [] }) + const peerInfo = utils.removePublicAddresses({ id, multiaddrs }) expect(peerInfo.multiaddrs.map((ma) => ma.toString())) .to.eql(['/ip4/192.168.0.1/tcp/4001', '/dns4/localhost/tcp/4001']) }) diff --git a/packages/kad-dht/test/query-self.spec.ts b/packages/kad-dht/test/query-self.spec.ts index d43313d29e..2c4904276b 100644 --- a/packages/kad-dht/test/query-self.spec.ts +++ b/packages/kad-dht/test/query-self.spec.ts @@ -78,8 +78,7 @@ describe('Query Self', () => { from: remotePeer, peer: { id: remotePeer, - multiaddrs: [], - protocols: [] + multiaddrs: [] } }) }()) @@ -110,8 +109,7 @@ describe('Query Self', () => { from: remotePeer, peer: { id: remotePeer, - multiaddrs: [], - protocols: [] + multiaddrs: [] } }) }()) diff --git a/packages/kad-dht/test/query.spec.ts b/packages/kad-dht/test/query.spec.ts index e1fd7460af..f7e661803c 100644 --- a/packages/kad-dht/test/query.spec.ts +++ b/packages/kad-dht/test/query.spec.ts @@ -534,8 +534,7 @@ describe('QueryManager', () => { messageType: MESSAGE_TYPE.GET_VALUE, closer: [{ id: peers[2], - multiaddrs: [], - protocols: [] + multiaddrs: [] }] }) } diff --git a/packages/kad-dht/test/rpc/handlers/add-provider.spec.ts b/packages/kad-dht/test/rpc/handlers/add-provider.spec.ts index ad9633856d..652060570c 100644 --- a/packages/kad-dht/test/rpc/handlers/add-provider.spec.ts +++ b/packages/kad-dht/test/rpc/handlers/add-provider.spec.ts @@ -68,12 +68,10 @@ describe('rpc - handlers - AddProvider', () => { msg.providerPeers = [{ id: peerIds[0], - multiaddrs: [ma1], - protocols: [] + multiaddrs: [ma1] }, { id: peerIds[1], - multiaddrs: [ma2], - protocols: [] + multiaddrs: [ma2] }] await handler.handle(peerIds[0], msg) diff --git a/packages/kad-dht/test/rpc/handlers/find-node.spec.ts b/packages/kad-dht/test/rpc/handlers/find-node.spec.ts index 5eb13982bf..e197e3770a 100644 --- a/packages/kad-dht/test/rpc/handlers/find-node.spec.ts +++ b/packages/kad-dht/test/rpc/handlers/find-node.spec.ts @@ -71,8 +71,7 @@ describe('rpc - handlers - FindNode', () => { multiaddr('/ip4/127.0.0.1/tcp/4002'), multiaddr('/ip4/192.168.1.5/tcp/4002'), multiaddr('/ip4/221.4.67.0/tcp/4002') - ], - protocols: [] + ] }]) const response = await handler.handle(sourcePeer, msg) @@ -109,8 +108,7 @@ describe('rpc - handlers - FindNode', () => { multiaddr('/ip4/127.0.0.1/tcp/4002'), multiaddr('/ip4/192.168.1.5/tcp/4002'), multiaddr('/ip4/221.4.67.0/tcp/4002') - ], - protocols: [] + ] }]) handler = new FindNodeHandler({ @@ -146,8 +144,7 @@ describe('rpc - handlers - FindNode', () => { multiaddr('/ip4/127.0.0.1/tcp/4002'), multiaddr('/ip4/192.168.1.5/tcp/4002'), multiaddr('/ip4/221.4.67.0/tcp/4002') - ], - protocols: [] + ] }]) const response = await handler.handle(sourcePeer, msg) diff --git a/packages/kad-dht/test/rpc/handlers/get-providers.spec.ts b/packages/kad-dht/test/rpc/handlers/get-providers.spec.ts index 18a3706f64..97688407d4 100644 --- a/packages/kad-dht/test/rpc/handlers/get-providers.spec.ts +++ b/packages/kad-dht/test/rpc/handlers/get-providers.spec.ts @@ -73,8 +73,7 @@ describe('rpc - handlers - GetProviders', () => { multiaddr('/ip4/127.0.0.1/tcp/4002'), multiaddr('/ip4/192.168.2.6/tcp/4002'), multiaddr('/ip4/21.31.57.23/tcp/4002') - ], - protocols: [] + ] }] const provider: PeerInfo[] = [{ @@ -83,8 +82,7 @@ describe('rpc - handlers - GetProviders', () => { multiaddr('/ip4/127.0.0.1/tcp/4002'), multiaddr('/ip4/192.168.1.5/tcp/4002'), multiaddr('/ip4/135.4.67.0/tcp/4002') - ], - protocols: [] + ] }] providers.getProviders.withArgs(v.cid).resolves([providerPeer]) diff --git a/packages/kad-dht/test/rpc/handlers/get-value.spec.ts b/packages/kad-dht/test/rpc/handlers/get-value.spec.ts index ad79dda7c6..61bf6a35aa 100644 --- a/packages/kad-dht/test/rpc/handlers/get-value.spec.ts +++ b/packages/kad-dht/test/rpc/handlers/get-value.spec.ts @@ -94,8 +94,7 @@ describe('rpc - handlers - GetValue', () => { peerRouting.getCloserPeersOffline.withArgs(key, sourcePeer) .resolves([{ id: closerPeer, - multiaddrs: [], - protocols: [] + multiaddrs: [] }]) const msg = new Message(T, key, 0) diff --git a/packages/kad-dht/test/utils/test-dht.ts b/packages/kad-dht/test/utils/test-dht.ts index bfddebdaad..6d81567526 100644 --- a/packages/kad-dht/test/utils/test-dht.ts +++ b/packages/kad-dht/test/utils/test-dht.ts @@ -94,8 +94,7 @@ export class TestDHT { } components.peerStore.merge(peerData.id, { - multiaddrs: peerData.multiaddrs, - protocols: peerData.protocols + multiaddrs: peerData.multiaddrs }) .catch(err => { log.error(err) }) }) diff --git a/packages/keychain/src/index.ts b/packages/keychain/src/index.ts index 8507c4c7bf..848481bddc 100644 --- a/packages/keychain/src/index.ts +++ b/packages/keychain/src/index.ts @@ -50,25 +50,11 @@ * A key benefit is that now the key chain can be used in browser with the [js-datastore-level](https://github.com/ipfs/js-datastore-level) implementation. */ -/* eslint max-nested-callbacks: ["error", 5] */ - -import { pbkdf2, randomBytes } from '@libp2p/crypto' -import { generateKeyPair, importKey, unmarshalPrivateKey } from '@libp2p/crypto/keys' -import { CodeError } from '@libp2p/interface/errors' -import { logger } from '@libp2p/logger' -import { peerIdFromKeys } from '@libp2p/peer-id' -import { Key } from 'interface-datastore/key' -import mergeOptions from 'merge-options' -import sanitize from 'sanitize-filename' -import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' -import { toString as uint8ArrayToString } from 'uint8arrays/to-string' -import { codes } from './errors.js' -import type { KeyChain, KeyInfo } from '@libp2p/interface/keychain' +import { DefaultKeychain } from './keychain.js' import type { KeyType } from '@libp2p/interface/keys' import type { PeerId } from '@libp2p/interface/peer-id' import type { Datastore } from 'interface-datastore' - -const log = logger('libp2p:keychain') +import type { Multibase } from 'multiformats/bases/interface.js' export interface DEKConfig { hash: string @@ -77,556 +63,159 @@ export interface DEKConfig { keyLength: number } -export interface KeyChainInit { +export interface KeychainInit { pass?: string dek?: DEKConfig } -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): boolean { - 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 (): Promise { - 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): Key { - return new Key(keyPrefix + name) -} - -/** - * Converts a key name into a datastore info name - */ -function DsInfoName (name: string): Key { - return new Key(infoPrefix + name) -} - -export interface KeyChainComponents { +export interface KeychainComponents { 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 DefaultKeyChain implements KeyChain { - private readonly components: KeyChainComponents - private readonly init: KeyChainInit - +export interface KeyInfo { /** - * Creates a new instance of a key chain + * The universally unique key id */ - 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 }) - } + id: string /** - * Generates the options for a keychain. A random salt is produced. - * - * @returns {object} + * The local key name */ - 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 - } + name: string +} +export interface Keychain { /** - * Gets an object that can encrypt/decrypt protected data. - * The default options for a keychain. + * Export an existing key as a PEM encrypted PKCS #8 string. * - * @returns {object} - */ - static get options (): typeof defaultOptions { - return defaultOptions - } - - /** - * Create a new key. + * @example * - * @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 + * ```js + * await libp2p.keychain.createKey('keyTest', 'RSA', 4096) + * const pemKey = await libp2p.keychain.exportKey('keyTest', 'password123') + * ``` */ - async createKey (name: string, type: KeyType, size = 2048): Promise { - if (!validateKeyName(name) || name === 'self') { - await randomDelay() - throw new CodeError('Invalid key name', codes.ERR_INVALID_KEY_NAME) - } - - if (typeof type !== 'string') { - await randomDelay() - throw new CodeError('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 new CodeError('Key name already exists', codes.ERR_KEY_ALREADY_EXISTS) - } - - switch (type.toLowerCase()) { - case 'rsa': - if (!Number.isSafeInteger(size) || size < 2048) { - await randomDelay() - throw new CodeError('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 new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) - } - - const dek = cached.dek - const pem = await keypair.export(dek) - keyInfo = { - 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 - } + exportKey(name: string, password: string): Promise> /** - * List all the keys. + * Import a new key from a PEM encoded PKCS #8 string. * - * @returns {Promise} - */ - async listKeys (): Promise { - 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 + * @example + * + * ```js + * await libp2p.keychain.createKey('keyTest', 'RSA', 4096) + * const pemKey = await libp2p.keychain.exportKey('keyTest', 'password123') + * const keyInfo = await libp2p.keychain.importKey('keyTestImport', pemKey, 'password123') + * ``` */ - async findKeyById (id: string): Promise { - try { - const keys = await this.listKeys() - const key = keys.find((k) => k.id === id) - - if (key == null) { - throw new CodeError(`Key with id '${id}' does not exist.`, codes.ERR_KEY_NOT_FOUND) - } - - return key - } catch (err: any) { - await randomDelay() - throw err - } - } + importKey(name: string, pem: string, password: string): Promise /** - * Find a key by it's name. + * Import a new key from a PeerId with a private key component + * + * @example * - * @param {string} name - The local key name. - * @returns {Promise} + * ```js + * const keyInfo = await libp2p.keychain.importPeer('keyTestImport', peerIdFromString('12D3Foo...')) + * ``` */ - async findKeyByName (name: string): Promise { - if (!validateKeyName(name)) { - await randomDelay() - throw new CodeError(`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 new CodeError(`Key '${name}' does not exist.`, codes.ERR_KEY_NOT_FOUND) - } - } + importPeer(name: string, peerId: PeerId): Promise /** - * Remove an existing key. + * Export an existing key as a PeerId + * + * @example * - * @param {string} name - The local key name; must already exist. - * @returns {Promise} + * ```js + * const peerId = await libp2p.keychain.exportPeerId('key-name') + * ``` */ - async removeKey (name: string): Promise { - if (!validateKeyName(name) || name === 'self') { - await randomDelay() - throw new CodeError(`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 - } + exportPeerId(name: string): Promise /** - * Rename a key + * Create a key in the keychain. + * + * @example * - * @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} + * ```js + * const keyInfo = await libp2p.keychain.createKey('keyTest', 'RSA', 4096) + * ``` */ - async renameKey (oldName: string, newName: string): Promise { - if (!validateKeyName(oldName) || oldName === 'self') { - await randomDelay() - throw new CodeError(`Invalid old key name '${oldName}'`, codes.ERR_OLD_KEY_NAME_INVALID) - } - if (!validateKeyName(newName) || newName === 'self') { - await randomDelay() - throw new CodeError(`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 new CodeError(`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 - } - } + createKey(name: string, type: KeyType, size?: number): Promise /** - * Export an existing key as a PEM encrypted PKCS #8 string + * List all the keys. + * + * @example + * + * ```js + * const keyInfos = await libp2p.keychain.listKeys() + * ``` */ - async exportKey (name: string, password: string): Promise { - if (!validateKeyName(name)) { - await randomDelay() - throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) - } - if (password == null) { - await randomDelay() - throw new CodeError('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 new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) - } - - const dek = cached.dek - const privateKey = await importKey(pem, dek) - const keyString = await privateKey.export(password) - - return keyString - } catch (err: any) { - await randomDelay() - throw err - } - } + listKeys(): Promise /** - * Export an existing key as a PeerId + * Removes a key from the keychain. + * + * @example + * + * ```js + * await libp2p.keychain.createKey('keyTest', 'RSA', 4096) + * const keyInfo = await libp2p.keychain.removeKey('keyTest') + * ``` */ - async exportPeerId (name: string): Promise { - const password = 'temporary-password' - const pem = await this.exportKey(name, password) - const privateKey = await importKey(pem, password) - - return peerIdFromKeys(privateKey.public.bytes, privateKey.bytes) - } + removeKey(name: string): Promise /** - * Import a new key from a PEM encoded PKCS #8 string + * Rename a key in the keychain. * - * @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} + * @example + * + * ```js + * await libp2p.keychain.createKey('keyTest', 'RSA', 4096) + * const keyInfo = await libp2p.keychain.renameKey('keyTest', 'keyNewNtest') + * ``` */ - async importKey (name: string, pem: string, password: string): Promise { - if (!validateKeyName(name) || name === 'self') { - await randomDelay() - throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) - } - if (pem == null) { - await randomDelay() - throw new CodeError('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 new CodeError(`Key '${name}' already exists`, codes.ERR_KEY_ALREADY_EXISTS) - } - - let privateKey - try { - privateKey = await importKey(pem, password) - } catch (err: any) { - await randomDelay() - throw new CodeError('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 new CodeError('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, - 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 - } + renameKey(oldName: string, newName: string): Promise /** - * Import a peer key + * Find a key by it's id. + * + * @example + * + * ```js + * const keyInfo = await libp2p.keychain.createKey('keyTest', 'RSA', 4096) + * const keyInfo2 = await libp2p.keychain.findKeyById(keyInfo.id) + * ``` */ - async importPeer (name: string, peer: PeerId): Promise { - try { - if (!validateKeyName(name)) { - throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) - } - if (peer == null) { - throw new CodeError('PeerId is required', codes.ERR_MISSING_PRIVATE_KEY) - } - if (peer.privateKey == null) { - throw new CodeError('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 new CodeError(`Key '${name}' already exists`, codes.ERR_KEY_ALREADY_EXISTS) - } - - const cached = privates.get(this) - - if (cached == null) { - throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) - } - - const dek = cached.dek - const pem = await privateKey.export(dek) - const keyInfo: KeyInfo = { - 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 - } - } + findKeyById(id: string): Promise /** - * Gets the private key as PEM encoded PKCS #8 string + * Find a key by it's name. + * + * @example + * + * ```js + * const keyInfo = await libp2p.keychain.createKey('keyTest', 'RSA', 4096) + * const keyInfo2 = await libp2p.keychain.findKeyByName('keyTest') + * ``` */ - async getPrivateKey (name: string): Promise { - if (!validateKeyName(name)) { - await randomDelay() - throw new CodeError(`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 new CodeError(`Key '${name}' does not exist.`, codes.ERR_KEY_NOT_FOUND) - } - } + findKeyByName(name: string): Promise /** * Rotate keychain password and re-encrypt all associated keys + * + * @example + * + * ```js + * await libp2p.keychain.rotateKeychainPass('oldPassword', 'newPassword') + * ``` */ - async rotateKeychainPass (oldPass: string, newPass: string): Promise { - if (typeof oldPass !== 'string') { - await randomDelay() - throw new CodeError(`Invalid old pass type '${typeof oldPass}'`, codes.ERR_INVALID_OLD_PASS_TYPE) - } - if (typeof newPass !== 'string') { - await randomDelay() - throw new CodeError(`Invalid new pass type '${typeof newPass}'`, codes.ERR_INVALID_NEW_PASS_TYPE) - } - if (newPass.length < 20) { - await randomDelay() - throw new CodeError(`Invalid pass length ${newPass.length}`, codes.ERR_INVALID_PASS_LENGTH) - } - log('recreating keychain') - const cached = privates.get(this) - - if (cached == null) { - throw new CodeError('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) + rotateKeychainPass(oldPass: string, newPass: string): Promise +} - // 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') +export function keychain (init: KeychainInit = {}): (components: KeychainComponents) => Keychain { + return (components: KeychainComponents) => { + return new DefaultKeychain(components, init) } } diff --git a/packages/keychain/src/keychain.ts b/packages/keychain/src/keychain.ts new file mode 100644 index 0000000000..1c64c06596 --- /dev/null +++ b/packages/keychain/src/keychain.ts @@ -0,0 +1,563 @@ +/* eslint max-nested-callbacks: ["error", 5] */ + +import { pbkdf2, randomBytes } from '@libp2p/crypto' +import { generateKeyPair, importKey, unmarshalPrivateKey } from '@libp2p/crypto/keys' +import { CodeError } from '@libp2p/interface/errors' +import { logger } from '@libp2p/logger' +import { peerIdFromKeys } from '@libp2p/peer-id' +import { Key } from 'interface-datastore/key' +import mergeOptions from 'merge-options' +import sanitize from 'sanitize-filename' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { codes } from './errors.js' +import type { KeychainComponents, KeychainInit, Keychain, KeyInfo } from './index.js' +import type { KeyType } from '@libp2p/interface/keys' +import type { PeerId } from '@libp2p/interface/peer-id' + +const log = logger('libp2p:keychain') + +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): boolean { + 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 (): Promise { + 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): Key { + return new Key(keyPrefix + name) +} + +/** + * Converts a key name into a datastore info name + */ +function DsInfoName (name: string): Key { + return new Key(infoPrefix + name) +} + +/** + * Manages the lifecycle of a key. Keys are encrypted at rest using PKCS #8. + * + * A key in the store has two entries + * - '/info/*key-name*', contains the KeyInfo for the key + * - '/pkcs8/*key-name*', contains the PKCS #8 for the key + * + */ +export class DefaultKeychain implements Keychain { + private readonly components: KeychainComponents + private readonly init: KeychainInit + + /** + * 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 }) + } + + /** + * 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 (): typeof defaultOptions { + 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: KeyType, size = 2048): Promise { + if (!validateKeyName(name) || name === 'self') { + await randomDelay() + throw new CodeError('Invalid key name', codes.ERR_INVALID_KEY_NAME) + } + + if (typeof type !== 'string') { + await randomDelay() + throw new CodeError('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 new CodeError('Key name already exists', codes.ERR_KEY_ALREADY_EXISTS) + } + + switch (type.toLowerCase()) { + case 'rsa': + if (!Number.isSafeInteger(size) || size < 2048) { + await randomDelay() + throw new CodeError('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 new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) + } + + const dek = cached.dek + const pem = await keypair.export(dek) + keyInfo = { + 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 (): Promise { + 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() + const key = keys.find((k) => k.id === id) + + if (key == null) { + throw new CodeError(`Key with id '${id}' does not exist.`, codes.ERR_KEY_NOT_FOUND) + } + + return key + } 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 new CodeError(`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 new CodeError(`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): Promise { + if (!validateKeyName(name) || name === 'self') { + await randomDelay() + throw new CodeError(`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 new CodeError(`Invalid old key name '${oldName}'`, codes.ERR_OLD_KEY_NAME_INVALID) + } + if (!validateKeyName(newName) || newName === 'self') { + await randomDelay() + throw new CodeError(`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 new CodeError(`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): Promise { + if (!validateKeyName(name)) { + await randomDelay() + throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) + } + if (password == null) { + await randomDelay() + throw new CodeError('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 new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) + } + + const dek = cached.dek + const privateKey = await importKey(pem, dek) + const keyString = await privateKey.export(password) + + return keyString + } catch (err: any) { + await randomDelay() + throw err + } + } + + /** + * Export an existing key as a PeerId + */ + async exportPeerId (name: string): Promise { + const password = 'temporary-password' + const pem = await this.exportKey(name, password) + const privateKey = await importKey(pem, password) + + return 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 new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) + } + if (pem == null) { + await randomDelay() + throw new CodeError('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 new CodeError(`Key '${name}' already exists`, codes.ERR_KEY_ALREADY_EXISTS) + } + + let privateKey + try { + privateKey = await importKey(pem, password) + } catch (err: any) { + await randomDelay() + throw new CodeError('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 new CodeError('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, + 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 new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME) + } + if (peer == null) { + throw new CodeError('PeerId is required', codes.ERR_MISSING_PRIVATE_KEY) + } + if (peer.privateKey == null) { + throw new CodeError('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 new CodeError(`Key '${name}' already exists`, codes.ERR_KEY_ALREADY_EXISTS) + } + + const cached = privates.get(this) + + if (cached == null) { + throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS) + } + + const dek = cached.dek + const pem = await privateKey.export(dek) + const keyInfo: KeyInfo = { + 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 new CodeError(`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 new CodeError(`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): Promise { + if (typeof oldPass !== 'string') { + await randomDelay() + throw new CodeError(`Invalid old pass type '${typeof oldPass}'`, codes.ERR_INVALID_OLD_PASS_TYPE) + } + if (typeof newPass !== 'string') { + await randomDelay() + throw new CodeError(`Invalid new pass type '${typeof newPass}'`, codes.ERR_INVALID_NEW_PASS_TYPE) + } + if (newPass.length < 20) { + await randomDelay() + throw new CodeError(`Invalid pass length ${newPass.length}`, codes.ERR_INVALID_PASS_LENGTH) + } + log('recreating keychain') + const cached = privates.get(this) + + if (cached == null) { + throw new CodeError('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/packages/keychain/test/keychain.spec.ts b/packages/keychain/test/keychain.spec.ts index 70a70bff53..89c1128485 100644 --- a/packages/keychain/test/keychain.spec.ts +++ b/packages/keychain/test/keychain.spec.ts @@ -9,8 +9,8 @@ import { MemoryDatastore } from 'datastore-core/memory' import { Key } from 'interface-datastore/key' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' -import { DefaultKeyChain, type KeyChainInit } from '../src/index.js' -import type { KeyChain, KeyInfo } from '@libp2p/interface/keychain' +import { DefaultKeychain } from '../src/keychain.js' +import type { KeychainInit, Keychain, KeyInfo } from '../src/index.js' import type { PeerId } from '@libp2p/interface/peer-id' import type { Datastore } from 'interface-datastore' @@ -19,20 +19,20 @@ describe('keychain', () => { const rsaKeyName = 'tajné jméno' const renamedRsaKeyName = 'ชื่อลับ' let rsaKeyInfo: KeyInfo - let ks: DefaultKeyChain + let ks: DefaultKeychain let datastore2: Datastore before(async () => { datastore2 = new MemoryDatastore() - ks = new DefaultKeyChain({ + ks = new DefaultKeychain({ datastore: datastore2 }, { pass: passPhrase }) }) it('can start without a password', async () => { await expect(async function () { - return new DefaultKeyChain({ + return new DefaultKeychain({ datastore: datastore2 }, {}) }()).to.eventually.be.ok() @@ -40,18 +40,18 @@ describe('keychain', () => { it('needs a NIST SP 800-132 non-weak pass phrase', async () => { await expect(async function () { - return new DefaultKeyChain({ + return new DefaultKeychain({ datastore: datastore2 }, { pass: '< 20 character' }) }()).to.eventually.be.rejected() }) it('has default options', () => { - expect(DefaultKeyChain.options).to.exist() + expect(DefaultKeychain.options).to.exist() }) it('supports supported hashing alorithms', async () => { - const ok = new DefaultKeyChain({ + const ok = new DefaultKeychain({ datastore: datastore2 }, { pass: passPhrase, dek: { hash: 'sha2-256', salt: 'salt-salt-salt-salt', iterationCount: 1000, keyLength: 14 } }) expect(ok).to.exist() @@ -59,14 +59,14 @@ describe('keychain', () => { it('does not support unsupported hashing alorithms', async () => { await expect(async function () { - return new DefaultKeyChain({ + return new DefaultKeychain({ 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 DefaultKeyChain({ + const keychain = new DefaultKeychain({ datastore: datastore2 }, {}) @@ -74,10 +74,10 @@ describe('keychain', () => { }) it('can find a key without a password', async () => { - const keychain = new DefaultKeyChain({ + const keychain = new DefaultKeychain({ datastore: datastore2 }, {}) - const keychainWithPassword = new DefaultKeyChain({ + const keychainWithPassword = new DefaultKeychain({ datastore: datastore2 }, { pass: `hello-${Date.now()}-${Date.now()}` }) const name = `key-${Math.random()}` @@ -88,10 +88,10 @@ describe('keychain', () => { }) it('can remove a key without a password', async () => { - const keychainWithoutPassword = new DefaultKeyChain({ + const keychainWithoutPassword = new DefaultKeychain({ datastore: datastore2 }, {}) - const keychainWithPassword = new DefaultKeyChain({ + const keychainWithPassword = new DefaultKeychain({ datastore: datastore2 }, { pass: `hello-${Date.now()}-${Date.now()}` }) const name = `key-${Math.random()}` @@ -103,7 +103,7 @@ describe('keychain', () => { }) it('requires a name to create a password', async () => { - const keychain = new DefaultKeyChain({ + const keychain = new DefaultKeychain({ datastore: datastore2 }, {}) @@ -112,9 +112,9 @@ describe('keychain', () => { }) it('can generate options', async () => { - const options = DefaultKeyChain.generateOptions() + const options = DefaultKeychain.generateOptions() options.pass = passPhrase - const chain = new DefaultKeyChain({ + const chain = new DefaultKeychain({ datastore: datastore2 }, options) expect(chain).to.exist() @@ -431,8 +431,8 @@ describe('keychain', () => { describe('rotate keychain passphrase', () => { let oldPass: string - let kc: KeyChain - let options: KeyChainInit + let kc: Keychain + let options: KeychainInit let ds: Datastore before(async () => { ds = new MemoryDatastore() @@ -446,7 +446,7 @@ describe('keychain', () => { hash: 'sha2-512' } } - kc = new DefaultKeyChain({ + kc = new DefaultKeychain({ datastore: ds }, options) }) @@ -509,7 +509,7 @@ describe('keychain', () => { describe('libp2p.keychain', () => { it('needs a passphrase to be used, otherwise throws an error', async () => { expect(() => { - return new DefaultKeyChain({ + return new DefaultKeychain({ datastore: new MemoryDatastore() }, { pass: '' @@ -518,7 +518,7 @@ describe('libp2p.keychain', () => { }) it('can be used when a passphrase is provided', async () => { - const keychain = new DefaultKeyChain({ + const keychain = new DefaultKeychain({ datastore: new MemoryDatastore() }, { pass: '12345678901234567890' @@ -530,7 +530,7 @@ describe('libp2p.keychain', () => { it('can reload keys', async () => { const datastore = new MemoryDatastore() - const keychain = new DefaultKeyChain({ + const keychain = new DefaultKeychain({ datastore }, { pass: '12345678901234567890' @@ -539,7 +539,7 @@ describe('libp2p.keychain', () => { const kInfo = await keychain.createKey('keyName', 'Ed25519') expect(kInfo).to.exist() - const keychain2 = new DefaultKeyChain({ + const keychain2 = new DefaultKeychain({ datastore }, { pass: '12345678901234567890' diff --git a/packages/libp2p/package.json b/packages/libp2p/package.json index d2c6a5e1a7..95a04fc341 100644 --- a/packages/libp2p/package.json +++ b/packages/libp2p/package.json @@ -48,10 +48,6 @@ "types": "./dist/src/index.d.ts", "import": "./dist/src/index.js" }, - "./autonat": { - "types": "./dist/src/autonat/index.d.ts", - "import": "./dist/src/autonat/index.js" - }, "./circuit-relay": { "types": "./dist/src/circuit-relay/index.d.ts", "import": "./dist/src/circuit-relay/index.js" @@ -76,10 +72,6 @@ "types": "./dist/src/ping/index.d.ts", "import": "./dist/src/ping/index.js" }, - "./pnet": { - "types": "./dist/src/pnet/index.d.ts", - "import": "./dist/src/pnet/index.js" - }, "./upnp-nat": { "types": "./dist/src/upnp-nat/index.d.ts", "import": "./dist/src/upnp-nat/index.js" @@ -104,7 +96,6 @@ "prepublishOnly": "node scripts/update-version.js && npm run build", "build": "aegir build", "generate": "run-s generate:proto:*", - "generate:proto:autonat": "protons ./src/autonat/pb/index.proto", "generate:proto:circuit-relay": "protons ./src/circuit-relay/pb/index.proto", "generate:proto:dcutr": "protons ./src/dcutr/pb/message.proto", "generate:proto:fetch": "protons ./src/fetch/pb/proto.proto", @@ -124,7 +115,6 @@ "@libp2p/crypto": "^2.0.7", "@libp2p/interface": "^0.1.5", "@libp2p/interface-internal": "^0.1.8", - "@libp2p/keychain": "^3.0.7", "@libp2p/logger": "^3.0.5", "@libp2p/multistream-select": "^4.0.5", "@libp2p/peer-collections": "^4.0.7", @@ -149,7 +139,6 @@ "it-map": "^3.0.3", "it-merge": "^3.0.0", "it-pair": "^2.0.6", - "it-parallel": "^3.0.0", "it-pipe": "^3.0.1", "it-protobuf-stream": "^1.0.0", "it-stream-types": "^2.0.1", @@ -163,8 +152,7 @@ "rate-limiter-flexible": "^3.0.0", "uint8arraylist": "^2.4.3", "uint8arrays": "^4.0.6", - "wherearewe": "^2.0.1", - "xsalsa20": "^1.1.0" + "wherearewe": "^2.0.1" }, "devDependencies": { "@chainsafe/libp2p-gossipsub": "^10.0.0", @@ -181,7 +169,6 @@ "@libp2p/mplex": "^9.0.10", "@libp2p/tcp": "^8.0.11", "@libp2p/websockets": "^7.0.11", - "@types/xsalsa20": "^1.1.0", "aegir": "^41.0.2", "execa": "^8.0.1", "go-libp2p": "^1.1.1", diff --git a/packages/libp2p/src/connection-manager/dial-queue.ts b/packages/libp2p/src/connection-manager/dial-queue.ts index 280e677c02..a32c38102f 100644 --- a/packages/libp2p/src/connection-manager/dial-queue.ts +++ b/packages/libp2p/src/connection-manager/dial-queue.ts @@ -1,4 +1,4 @@ -import { AbortError, CodeError } from '@libp2p/interface/errors' +import { AbortError, CodeError, ERR_TIMEOUT } from '@libp2p/interface/errors' import { setMaxListeners } from '@libp2p/interface/events' import { logger } from '@libp2p/logger' import { PeerMap } from '@libp2p/peer-collections' @@ -103,8 +103,8 @@ export class DialQueue { setMaxListeners(Infinity, this.shutDownController.signal) - this.pendingDialCount = components.metrics?.registerMetric('libp2p_dialler_pending_dials') - this.inProgressDialCount = components.metrics?.registerMetric('libp2p_dialler_in_progress_dials') + this.pendingDialCount = components.metrics?.registerMetric('libp2p_dial_queue_pending_dials') + this.inProgressDialCount = components.metrics?.registerMetric('libp2p_dial_queue_in_progress_dials') this.pendingDials = [] for (const [key, value] of Object.entries(init.resolvers ?? {})) { @@ -269,7 +269,7 @@ export class DialQueue { // Error is a timeout if (signal.aborted) { - const error = new CodeError(err.message, codes.ERR_TIMEOUT) + const error = new CodeError(err.message, ERR_TIMEOUT) throw error } diff --git a/packages/libp2p/src/content-routing/index.ts b/packages/libp2p/src/content-routing/index.ts index f8a17a3cdd..a96c2bd9ab 100644 --- a/packages/libp2p/src/content-routing/index.ts +++ b/packages/libp2p/src/content-routing/index.ts @@ -1,7 +1,7 @@ import { CodeError } from '@libp2p/interface/errors' import merge from 'it-merge' import { pipe } from 'it-pipe' -import { messages, codes } from '../errors.js' +import { codes, messages } from '../errors.js' import { storeAddresses, uniquePeers, diff --git a/packages/libp2p/src/errors.ts b/packages/libp2p/src/errors.ts index a2d2ffb6c6..0b84215aa9 100644 --- a/packages/libp2p/src/errors.ts +++ b/packages/libp2p/src/errors.ts @@ -37,7 +37,6 @@ export enum codes { ERR_INVALID_PEER = 'ERR_INVALID_PEER', ERR_MUXER_UNAVAILABLE = 'ERR_MUXER_UNAVAILABLE', ERR_NOT_FOUND = 'ERR_NOT_FOUND', - ERR_TIMEOUT = 'ERR_TIMEOUT', ERR_TRANSPORT_UNAVAILABLE = 'ERR_TRANSPORT_UNAVAILABLE', ERR_TRANSPORT_DIAL_FAILED = 'ERR_TRANSPORT_DIAL_FAILED', ERR_UNSUPPORTED_PROTOCOL = 'ERR_UNSUPPORTED_PROTOCOL', @@ -48,7 +47,6 @@ export enum codes { ERR_NO_ROUTERS_AVAILABLE = 'ERR_NO_ROUTERS_AVAILABLE', ERR_CONNECTION_NOT_MULTIPLEXED = 'ERR_CONNECTION_NOT_MULTIPLEXED', ERR_NO_DIAL_TOKENS = 'ERR_NO_DIAL_TOKENS', - ERR_KEYCHAIN_REQUIRED = 'ERR_KEYCHAIN_REQUIRED', ERR_INVALID_CMS = 'ERR_INVALID_CMS', ERR_MISSING_KEYS = 'ERR_MISSING_KEYS', ERR_NO_KEY = 'ERR_NO_KEY', diff --git a/packages/libp2p/src/index.ts b/packages/libp2p/src/index.ts index 8fa5882135..b83ee40512 100644 --- a/packages/libp2p/src/index.ts +++ b/packages/libp2p/src/index.ts @@ -30,7 +30,6 @@ import type { PeerId } from '@libp2p/interface/peer-id' import type { PeerRouting } from '@libp2p/interface/peer-routing' import type { StreamMuxerFactory } from '@libp2p/interface/stream-muxer' import type { Transport } from '@libp2p/interface/transport' -import type { KeyChainInit } from '@libp2p/keychain' import type { PersistentPeerStoreInit } from '@libp2p/peer-store' import type { Datastore } from 'interface-datastore' @@ -79,11 +78,6 @@ export interface Libp2pInit */ peerStore: PersistentPeerStoreInit - /** - * keychain configuration - */ - keychain: KeyChainInit - /** * An array that must include at least 1 compliant transport */ diff --git a/packages/libp2p/src/libp2p.ts b/packages/libp2p/src/libp2p.ts index 5708aa59e3..2aaf92caf5 100644 --- a/packages/libp2p/src/libp2p.ts +++ b/packages/libp2p/src/libp2p.ts @@ -4,7 +4,6 @@ import { CodeError } from '@libp2p/interface/errors' import { TypedEventEmitter, CustomEvent, setMaxListeners } from '@libp2p/interface/events' import { peerDiscovery } from '@libp2p/interface/peer-discovery' import { type PeerRouting, peerRouting } from '@libp2p/interface/peer-routing' -import { DefaultKeyChain } from '@libp2p/keychain' import { logger } from '@libp2p/logger' import { PeerSet } from '@libp2p/peer-collections' import { peerIdFromString } from '@libp2p/peer-id' @@ -12,7 +11,6 @@ import { createEd25519PeerId } from '@libp2p/peer-id-factory' import { PersistentPeerStore } from '@libp2p/peer-store' import { isMultiaddr, type Multiaddr } from '@multiformats/multiaddr' import { MemoryDatastore } from 'datastore-core/memory' -import mergeOptions from 'merge-options' import { concat as uint8ArrayConcat } from 'uint8arrays/concat' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import { DefaultAddressManager } from './address-manager/index.js' @@ -30,14 +28,12 @@ import type { Components } from './components.js' import type { Libp2p, Libp2pInit, Libp2pOptions } from './index.js' import type { Libp2pEvents, PendingDial, ServiceMap, AbortOptions } from '@libp2p/interface' import type { Connection, NewStreamOptions, Stream } from '@libp2p/interface/connection' -import type { KeyChain } from '@libp2p/interface/keychain' import type { Metrics } from '@libp2p/interface/metrics' import type { PeerId } from '@libp2p/interface/peer-id' import type { PeerInfo } from '@libp2p/interface/peer-info' import type { PeerStore } from '@libp2p/interface/peer-store' import type { Topology } from '@libp2p/interface/topology' import type { StreamHandler, StreamHandlerOptions } from '@libp2p/interface-internal/registrar' -import type { Datastore } from 'interface-datastore' const log = logger('libp2p') @@ -46,7 +42,6 @@ export class Libp2pNode> extends public peerStore: PeerStore public contentRouting: ContentRouting public peerRouting: PeerRouting - public keychain: KeyChain public metrics?: Metrics public services: T @@ -98,8 +93,7 @@ export class Libp2pNode> extends if (evt.detail.previous == null) { const peerInfo: PeerInfo = { id: evt.detail.peer.id, - multiaddrs: evt.detail.peer.addresses.map(a => a.multiaddr), - protocols: evt.detail.peer.protocols + multiaddrs: evt.detail.peer.addresses.map(a => a.multiaddr) } components.events.safeDispatchEvent('peer:discovery', { detail: peerInfo }) @@ -130,13 +124,6 @@ export class Libp2pNode> extends // Addresses {listen, announce, noAnnounce} this.configureComponent('addressManager', new DefaultAddressManager(this.components, init.addresses)) - // Create keychain - const keychainOpts = DefaultKeyChain.generateOptions() - this.keychain = this.configureComponent('keyChain', new DefaultKeyChain(this.components, { - ...keychainOpts, - ...init.keychain - })) - // Peer routers const peerRouters: PeerRouting[] = (init.peerRouters ?? []).map((fn, index) => this.configureComponent(`peer-router-${index}`, fn(this.components))) this.peerRouting = this.components.peerRouting = this.configureComponent('peerRouting', new DefaultPeerRouting(this.components, { @@ -219,13 +206,6 @@ export class Libp2pNode> extends 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 this.components.beforeStart?.() await this.components.start() @@ -396,8 +376,7 @@ export class Libp2pNode> extends } void this.components.peerStore.merge(peer.id, { - multiaddrs: peer.multiaddrs, - protocols: peer.protocols + multiaddrs: peer.multiaddrs }) .catch(err => { log.error(err) }) } @@ -408,29 +387,7 @@ export class Libp2pNode> extends * libp2p interface and is useful for testing and debugging. */ export async function createLibp2pNode > (options: Libp2pOptions): Promise> { - if (options.peerId == null) { - const datastore = options.datastore as Datastore | undefined - - if (datastore != null) { - try { - // try load the peer id from the keychain - const keyChain = new DefaultKeyChain({ - datastore - }, mergeOptions(DefaultKeyChain.generateOptions(), options.keychain)) - - options.peerId = await keyChain.exportPeerId('self') - } catch (err: any) { - if (err.code !== 'ERR_NOT_FOUND') { - throw err - } - } - } - } - - if (options.peerId == null) { - // no peer id in the keychain, create a new peer id - options.peerId = await createEd25519PeerId() - } + options.peerId ??= await createEd25519PeerId() return new Libp2pNode(validateConfig(options)) } diff --git a/packages/libp2p/src/ping/index.ts b/packages/libp2p/src/ping/index.ts index ea8d23db5e..ea87229d9a 100644 --- a/packages/libp2p/src/ping/index.ts +++ b/packages/libp2p/src/ping/index.ts @@ -1,5 +1,5 @@ import { randomBytes } from '@libp2p/crypto' -import { CodeError } from '@libp2p/interface/errors' +import { CodeError, ERR_TIMEOUT } from '@libp2p/interface/errors' import { logger } from '@libp2p/logger' import first from 'it-first' import { pipe } from 'it-pipe' @@ -118,7 +118,7 @@ class DefaultPingService implements Startable, PingService { }) onAbort = () => { - stream?.abort(new CodeError('ping timeout', codes.ERR_TIMEOUT)) + stream?.abort(new CodeError('ping timeout', ERR_TIMEOUT)) } // make stream abortable diff --git a/packages/libp2p/src/pnet/README.md b/packages/libp2p/src/pnet/README.md deleted file mode 100644 index 94d03c0362..0000000000 --- a/packages/libp2p/src/pnet/README.md +++ /dev/null @@ -1,68 +0,0 @@ -js-libp2p-pnet -================== - -> Connection protection management for libp2p leveraging PSK encryption via XSalsa20. - -**Note**: git history prior to merging into js-libp2p can be found in the original repository, https://github.com/libp2p/js-libp2p-pnet. - -## Table of Contents - -- [Usage](#usage) -- [Private Shared Keys](#private-shared-keys) -- [PSK Generation](#psk-generation) - - [From a module using libp2p](#from-a-module-using-libp2p) - - [Programmatically](#programmatically) - -## Usage - -```js -import { createLibp2p } from 'libp2p' -import { preSharedKey, generateKey } from 'libp2p/pnet' - -// Create a Uint8Array and write the swarm key to it -const swarmKey = new Uint8Array(95) -generateKey(swarmKey) - -const node = await createLibp2p({ - // ...other options - connectionProtector: preSharedKey({ - psk: swarmKey - }) -}) -``` - -## Private Shared Keys - -Private Shared Keys are expected to be in the following format: - -``` -/key/swarm/psk/1.0.0/ -/base16/ -dffb7e3135399a8b1612b2aaca1c36a3a8ac2cd0cca51ceeb2ced87d308cac6d -``` - -## PSK Generation - -A utility method has been created to generate a key for your private network. You can -use one of the methods below to generate your key. - -#### From a module using libp2p - -If you have a module locally that depends on libp2p, you can run the following from -that project, assuming the node_modules are installed. - -```console -node -e "import('libp2p/pnet').then(({ generateKey }) => generateKey(process.stdout))" > swarm.key -``` - -#### Programmatically - -```js -import fs from 'fs' -import { generateKey } from 'libp2p/pnet' - -const swarmKey = new Uint8Array(95) -generateKey(swarmKey) - -fs.writeFileSync('swarm.key', swarmKey) -``` diff --git a/packages/libp2p/src/upgrader.ts b/packages/libp2p/src/upgrader.ts index 4000cde7a8..2b168def22 100644 --- a/packages/libp2p/src/upgrader.ts +++ b/packages/libp2p/src/upgrader.ts @@ -1,4 +1,4 @@ -import { CodeError } from '@libp2p/interface/errors' +import { CodeError, ERR_TIMEOUT } from '@libp2p/interface/errors' import { setMaxListeners } from '@libp2p/interface/events' import { logger } from '@libp2p/logger' import * as mss from '@libp2p/multistream-select' @@ -165,7 +165,7 @@ export class DefaultUpgrader implements Upgrader { const signal = AbortSignal.timeout(this.inboundUpgradeTimeout) const onAbort = (): void => { - maConn.abort(new CodeError('inbound upgrade timeout', codes.ERR_TIMEOUT)) + maConn.abort(new CodeError('inbound upgrade timeout', ERR_TIMEOUT)) } signal.addEventListener('abort', onAbort, { once: true }) diff --git a/packages/libp2p/test/circuit-relay/utils.ts b/packages/libp2p/test/circuit-relay/utils.ts index aa3cde6e15..b530b8d241 100644 --- a/packages/libp2p/test/circuit-relay/utils.ts +++ b/packages/libp2p/test/circuit-relay/utils.ts @@ -159,8 +159,7 @@ export class MockContentRouting implements ContentRouting { providers.push({ id: this.peerId, - multiaddrs: this.addressManager.getAddresses(), - protocols: [] + multiaddrs: this.addressManager.getAddresses() }) MockContentRouting.providers.set(cid.toString(), providers) diff --git a/packages/libp2p/test/configuration/protocol-prefix.node.ts b/packages/libp2p/test/configuration/protocol-prefix.node.ts index db4ecea61c..fd6fb53d56 100644 --- a/packages/libp2p/test/configuration/protocol-prefix.node.ts +++ b/packages/libp2p/test/configuration/protocol-prefix.node.ts @@ -1,8 +1,8 @@ /* eslint-env mocha */ +import { type FetchService, fetchService } from '@libp2p/fetch' import { expect } from 'aegir/chai' import { pEvent } from 'p-event' -import { type FetchService, fetchService } from '../../src/fetch/index.js' import { identifyService } from '../../src/identify/index.js' import { createLibp2p } from '../../src/index.js' import { type PingService, pingService } from '../../src/ping/index.js' diff --git a/packages/libp2p/test/connection-manager/auto-dial.spec.ts b/packages/libp2p/test/connection-manager/auto-dial.spec.ts index 9e45ae8c7b..9019890e61 100644 --- a/packages/libp2p/test/connection-manager/auto-dial.spec.ts +++ b/packages/libp2p/test/connection-manager/auto-dial.spec.ts @@ -22,7 +22,7 @@ import type { PeerStore, Peer } from '@libp2p/interface/peer-store' import type { ConnectionManager } from '@libp2p/interface-internal/connection-manager' describe('auto-dial', () => { - let autoDialler: AutoDial + let autoDialer: AutoDial let events: TypedEventTarget let peerStore: PeerStore let peerId: PeerId @@ -38,8 +38,8 @@ describe('auto-dial', () => { }) afterEach(() => { - if (autoDialler != null) { - autoDialler.stop() + if (autoDialer != null) { + autoDialer.stop() } }) @@ -73,7 +73,7 @@ describe('auto-dial', () => { getDialQueue: Sinon.stub().returns([]) }) - autoDialler = new AutoDial({ + autoDialer = new AutoDial({ peerStore, connectionManager, events @@ -81,8 +81,8 @@ describe('auto-dial', () => { minConnections: 10, autoDialInterval: 10000 }) - autoDialler.start() - void autoDialler.autoDial() + autoDialer.start() + void autoDialer.autoDial() await pWaitFor(() => { return connectionManager.openConnection.callCount === 1 @@ -127,15 +127,15 @@ describe('auto-dial', () => { getDialQueue: Sinon.stub().returns([]) }) - autoDialler = new AutoDial({ + autoDialer = new AutoDial({ peerStore, connectionManager, events }, { minConnections: 10 }) - autoDialler.start() - await autoDialler.autoDial() + autoDialer.start() + await autoDialer.autoDial() await pWaitFor(() => connectionManager.openConnection.callCount === 1) await delay(1000) @@ -181,15 +181,15 @@ describe('auto-dial', () => { }]) }) - autoDialler = new AutoDial({ + autoDialer = new AutoDial({ peerStore, connectionManager, events }, { minConnections: 10 }) - autoDialler.start() - await autoDialler.autoDial() + autoDialer.start() + await autoDialer.autoDial() await pWaitFor(() => connectionManager.openConnection.callCount === 1) await delay(1000) @@ -207,7 +207,7 @@ describe('auto-dial', () => { getDialQueue: Sinon.stub().returns([]) }) - autoDialler = new AutoDial({ + autoDialer = new AutoDial({ peerStore, connectionManager, events @@ -215,12 +215,12 @@ describe('auto-dial', () => { minConnections: 10, autoDialInterval: 10000 }) - autoDialler.start() + autoDialer.start() // call autodial twice await Promise.all([ - autoDialler.autoDial(), - autoDialler.autoDial() + autoDialer.autoDial(), + autoDialer.autoDial() ]) // should only have queried peer store once @@ -258,7 +258,7 @@ describe('auto-dial', () => { getDialQueue: Sinon.stub().returns([]) }) - autoDialler = new AutoDial({ + autoDialer = new AutoDial({ peerStore, connectionManager, events @@ -266,9 +266,9 @@ describe('auto-dial', () => { minConnections: 10, autoDialPeerRetryThreshold: 2000 }) - autoDialler.start() + autoDialer.start() - void autoDialler.autoDial() + void autoDialer.autoDial() await pWaitFor(() => { return connectionManager.openConnection.callCount === 1 @@ -282,7 +282,7 @@ describe('auto-dial', () => { await delay(2000) // autodial again - void autoDialler.autoDial() + void autoDialer.autoDial() await pWaitFor(() => { return connectionManager.openConnection.callCount === 3 diff --git a/packages/libp2p/test/connection-manager/direct.node.ts b/packages/libp2p/test/connection-manager/direct.node.ts index 72f478d332..a345a5c8d1 100644 --- a/packages/libp2p/test/connection-manager/direct.node.ts +++ b/packages/libp2p/test/connection-manager/direct.node.ts @@ -5,7 +5,7 @@ import os from 'node:os' import path from 'node:path' import { yamux } from '@chainsafe/libp2p-yamux' import { type Connection, type ConnectionProtector, isConnection } from '@libp2p/interface/connection' -import { AbortError } from '@libp2p/interface/errors' +import { AbortError, ERR_TIMEOUT } from '@libp2p/interface/errors' import { TypedEventEmitter } from '@libp2p/interface/events' import { start, stop } from '@libp2p/interface/startable' import { mockConnection, mockConnectionGater, mockDuplex, mockMultiaddrConnection, mockUpgrader } from '@libp2p/interface-compliance-tests/mocks' @@ -32,14 +32,11 @@ import { DefaultConnectionManager } from '../../src/connection-manager/index.js' import { codes as ErrorCodes } from '../../src/errors.js' import { plaintext } from '../../src/insecure/index.js' import { createLibp2pNode, type Libp2pNode } from '../../src/libp2p.js' -import { preSharedKey } from '../../src/pnet/index.js' import { DefaultTransportManager } from '../../src/transport-manager.js' -import swarmKey from '../fixtures/swarm.key.js' import type { PeerId } from '@libp2p/interface/peer-id' import type { TransportManager } from '@libp2p/interface-internal/transport-manager' import type { Multiaddr } from '@multiformats/multiaddr' -const swarmKeyBuffer = uint8ArrayFromString(swarmKey) const listenAddr = multiaddr('/ip4/127.0.0.1/tcp/0') const unsupportedAddr = multiaddr('/ip4/127.0.0.1/tcp/9999/ws/p2p/QmckxVrJw1Yo8LqvmDJNUmdAsKtSbiKWmrXJFyKmUraBoN') @@ -218,7 +215,7 @@ describe('dialing (direct, TCP)', () => { await expect(dialer.dial(remoteAddr)) .to.eventually.be.rejectedWith(Error) - .and.to.have.property('code', ErrorCodes.ERR_TIMEOUT) + .and.to.have.property('code', ERR_TIMEOUT) }) it('should dial to the max concurrency', async () => { @@ -496,9 +493,11 @@ describe('libp2p.dialer (direct, TCP)', () => { }) it('should use the protectors when provided for connecting', async () => { - const protector: ConnectionProtector = preSharedKey({ - psk: swarmKeyBuffer - })() + const protector: ConnectionProtector = { + async protect (connection) { + return connection + } + } libp2p = await createLibp2pNode({ peerId, @@ -517,8 +516,6 @@ describe('libp2p.dialer (direct, TCP)', () => { const protectorProtectSpy = Sinon.spy(protector, 'protect') - remoteLibp2p.components.connectionProtector = preSharedKey({ psk: swarmKeyBuffer })() - await libp2p.start() const connection = await libp2p.dial(remoteAddr) diff --git a/packages/libp2p/test/connection-manager/direct.spec.ts b/packages/libp2p/test/connection-manager/direct.spec.ts index 305ae4e524..7f9d9d1ab4 100644 --- a/packages/libp2p/test/connection-manager/direct.spec.ts +++ b/packages/libp2p/test/connection-manager/direct.spec.ts @@ -1,7 +1,7 @@ /* eslint-env mocha */ import { yamux } from '@chainsafe/libp2p-yamux' -import { AbortError } from '@libp2p/interface/errors' +import { AbortError, ERR_TIMEOUT } from '@libp2p/interface/errors' import { TypedEventEmitter } from '@libp2p/interface/events' import { mockConnectionGater, mockDuplex, mockMultiaddrConnection, mockUpgrader, mockConnection } from '@libp2p/interface-compliance-tests/mocks' import { mplex } from '@libp2p/mplex' @@ -167,7 +167,7 @@ describe('dialing (direct, WebSockets)', () => { await expect(connectionManager.openConnection(remoteAddr)) .to.eventually.be.rejected() - .and.to.have.property('code', ErrorCodes.ERR_TIMEOUT) + .and.to.have.property('code', ERR_TIMEOUT) }) it('should throw when a peer advertises more than the allowed number of addresses', async () => { diff --git a/packages/libp2p/test/content-routing/content-routing.node.ts b/packages/libp2p/test/content-routing/content-routing.node.ts index 66967667ea..64f95d5616 100644 --- a/packages/libp2p/test/content-routing/content-routing.node.ts +++ b/packages/libp2p/test/content-routing/content-routing.node.ts @@ -251,8 +251,7 @@ describe('content-routing', () => { id: providerPeerId, multiaddrs: [ multiaddr('/ip4/123.123.123.123/tcp/49320') - ], - protocols: [] + ] } if (node.services.dht == null) { diff --git a/packages/libp2p/test/core/peer-id.spec.ts b/packages/libp2p/test/core/peer-id.spec.ts index 663610c6a4..220a9fbe16 100644 --- a/packages/libp2p/test/core/peer-id.spec.ts +++ b/packages/libp2p/test/core/peer-id.spec.ts @@ -2,7 +2,6 @@ import { webSockets } from '@libp2p/websockets' import { expect } from 'aegir/chai' -import { MemoryDatastore } from 'datastore-core' import { createLibp2p, type Libp2p } from '../../src/index.js' import { plaintext } from '../../src/insecure/index.js' @@ -27,119 +26,4 @@ describe('peer-id', () => { expect(libp2p.peerId).to.be.ok() }) - - it('should retrieve the PeerId from the datastore', async () => { - const datastore = new MemoryDatastore() - - libp2p = await createLibp2p({ - datastore, - transports: [ - webSockets() - ], - connectionEncryption: [ - plaintext() - ] - }) - - // this PeerId was created by default - const peerId = libp2p.peerId - - await libp2p.stop() - - // create a new node from the same datastore - libp2p = await createLibp2p({ - datastore, - transports: [ - webSockets() - ], - connectionEncryption: [ - plaintext() - ] - }) - - // the new node should have read the PeerId from the datastore - // instead of creating a new one - expect(libp2p.peerId.toString()).to.equal(peerId.toString()) - }) - - it('should retrieve the PeerId from the datastore with a keychain password', async () => { - const datastore = new MemoryDatastore() - const keychain = { - pass: 'very-long-password-must-be-over-twenty-characters-long', - dek: { - salt: 'CpjNIxMqAZ+aJg+ezLfuzG4a' - } - } - - libp2p = await createLibp2p({ - datastore, - keychain, - transports: [ - webSockets() - ], - connectionEncryption: [ - plaintext() - ] - }) - - // this PeerId was created by default - const peerId = libp2p.peerId - - await libp2p.stop() - - // create a new node from the same datastore - libp2p = await createLibp2p({ - datastore, - keychain, - transports: [ - webSockets() - ], - connectionEncryption: [ - plaintext() - ] - }) - - // the new node should have read the PeerId from the datastore - // instead of creating a new one - expect(libp2p.peerId.toString()).to.equal(peerId.toString()) - }) - - it('should fail to start if retrieving the PeerId from the datastore fails', async () => { - const datastore = new MemoryDatastore() - const keychain = { - pass: 'very-long-password-must-be-over-twenty-characters-long', - dek: { - salt: 'CpjNIxMqAZ+aJg+ezLfuzG4a' - } - } - - libp2p = await createLibp2p({ - datastore, - keychain, - transports: [ - webSockets() - ], - connectionEncryption: [ - plaintext() - ] - }) - await libp2p.stop() - - // creating a new node from the same datastore but with the wrong keychain config should fail - await expect(createLibp2p({ - datastore, - keychain: { - pass: 'different-very-long-password-must-be-over-twenty-characters-long', - dek: { - salt: 'different-CpjNIxMqAZ+aJg+ezLfuzG4a' - } - }, - transports: [ - webSockets() - ], - connectionEncryption: [ - plaintext() - ] - })).to.eventually.rejectedWith('Invalid PEM formatted message') - }) }) diff --git a/packages/libp2p/test/core/start.spec.ts b/packages/libp2p/test/core/start.spec.ts deleted file mode 100644 index a8c5b44855..0000000000 --- a/packages/libp2p/test/core/start.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* eslint-env mocha */ - -import { webSockets } from '@libp2p/websockets' -import { expect } from 'aegir/chai' -import { createLibp2p, type Libp2p } from '../../src/index.js' -import { plaintext } from '../../src/insecure/index.js' - -describe('start', () => { - let libp2p: Libp2p - - afterEach(async () => { - if (libp2p != null) { - await libp2p.stop() - } - }) - - it('it should start by default', async () => { - libp2p = await createLibp2p({ - transports: [ - webSockets() - ], - connectionEncryption: [ - plaintext() - ] - }) - - expect(libp2p.isStarted()).to.be.true() - }) - - it('it should allow overriding', async () => { - libp2p = await createLibp2p({ - start: false, - transports: [ - webSockets() - ], - connectionEncryption: [ - plaintext() - ] - }) - - expect(libp2p.isStarted()).to.be.false() - - await libp2p.start() - - expect(libp2p.isStarted()).to.be.true() - }) -}) diff --git a/packages/libp2p/test/fixtures/swarm.key.ts b/packages/libp2p/test/fixtures/swarm.key.ts deleted file mode 100644 index ee524766d1..0000000000 --- a/packages/libp2p/test/fixtures/swarm.key.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default '/key/swarm/psk/1.0.0/\n' + - '/base16/\n' + - '411f0a244cbbc25ecbb2b070d00a1832516ded521eb3ee3aa13189efe2e2b9a2' diff --git a/packages/libp2p/test/identify/service.spec.ts b/packages/libp2p/test/identify/service.spec.ts index 64e7303e2d..5181b8cdfc 100644 --- a/packages/libp2p/test/identify/service.spec.ts +++ b/packages/libp2p/test/identify/service.spec.ts @@ -153,7 +153,6 @@ describe('identify', () => { // Wait for identify to finish await identityServiceIdentifySpy.firstCall.returnValue - sinon.stub(libp2p, 'isStarted').returns(true) // Cause supported protocols to change await libp2p.handle('/echo/2.0.0', () => {}) @@ -251,7 +250,6 @@ describe('identify', () => { // Wait for identify to finish await identityServiceIdentifySpy.firstCall.returnValue - sinon.stub(libp2p, 'isStarted').returns(true) await libp2p.peerStore.merge(libp2p.peerId, { multiaddrs: [multiaddr('/ip4/180.0.0.1/tcp/15001/ws')] diff --git a/packages/libp2p/test/ping/index.spec.ts b/packages/libp2p/test/ping/index.spec.ts index 9b6aef1385..dd6c877f50 100644 --- a/packages/libp2p/test/ping/index.spec.ts +++ b/packages/libp2p/test/ping/index.spec.ts @@ -1,5 +1,6 @@ /* eslint-env mocha */ +import { ERR_TIMEOUT } from '@libp2p/interface/errors' import { TypedEventEmitter } from '@libp2p/interface/events' import { start, stop } from '@libp2p/interface/startable' import { mockRegistrar, mockUpgrader, connectionPair } from '@libp2p/interface-compliance-tests/mocks' @@ -125,7 +126,7 @@ describe('ping', () => { await expect(localPing.ping(remoteComponents.peerId, { signal })) - .to.eventually.be.rejected.with.property('code', 'ERR_TIMEOUT') + .to.eventually.be.rejected.with.property('code', ERR_TIMEOUT) // should have closed stream expect(newStreamSpy).to.have.property('callCount', 1) diff --git a/packages/libp2p/test/transports/transport-manager.spec.ts b/packages/libp2p/test/transports/transport-manager.spec.ts index c71a124a92..075364aea9 100644 --- a/packages/libp2p/test/transports/transport-manager.spec.ts +++ b/packages/libp2p/test/transports/transport-manager.spec.ts @@ -124,7 +124,6 @@ describe('libp2p.transportManager (dial only)', () => { start: false }) - expect(libp2p.isStarted()).to.be.false() await expect(libp2p.start()).to.eventually.be.rejected .with.property('code', ErrorCodes.ERR_NO_VALID_ADDRESSES) }) @@ -147,7 +146,6 @@ describe('libp2p.transportManager (dial only)', () => { start: false }) - expect(libp2p.isStarted()).to.be.false() await expect(libp2p.start()).to.eventually.be.undefined() }) @@ -169,7 +167,6 @@ describe('libp2p.transportManager (dial only)', () => { start: false }) - expect(libp2p.isStarted()).to.be.false() await expect(libp2p.start()).to.eventually.be.undefined() }) }) diff --git a/packages/libp2p/test/upgrading/upgrader.spec.ts b/packages/libp2p/test/upgrading/upgrader.spec.ts index 6f83c08ecd..0c2267d73b 100644 --- a/packages/libp2p/test/upgrading/upgrader.spec.ts +++ b/packages/libp2p/test/upgrading/upgrader.spec.ts @@ -26,10 +26,8 @@ import { type Components, defaultComponents } from '../../src/components.js' import { codes } from '../../src/errors.js' import { createLibp2p } from '../../src/index.js' import { plaintext } from '../../src/insecure/index.js' -import { preSharedKey } from '../../src/pnet/index.js' import { DEFAULT_MAX_OUTBOUND_STREAMS } from '../../src/registrar.js' import { DefaultUpgrader } from '../../src/upgrader.js' -import swarmKey from '../fixtures/swarm.key.js' import type { Libp2p } from '@libp2p/interface' import type { Connection, ConnectionProtector, Stream } from '@libp2p/interface/connection' import type { ConnectionEncrypter, SecuredConnection } from '@libp2p/interface/connection-encrypter' @@ -206,9 +204,12 @@ describe('Upgrader', () => { it('should use a private connection protector when provided', async () => { const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer }) - const protector = preSharedKey({ - psk: uint8ArrayFromString(swarmKey) - })() + const protector: ConnectionProtector = { + async protect (connection) { + return connection + } + } + const protectorProtectSpy = sinon.spy(protector, 'protect') localComponents.connectionProtector = protector @@ -615,6 +616,12 @@ describe('libp2p.upgrader', () => { it('should create an Upgrader', async () => { const deferred = pDefer() + const protector: ConnectionProtector = { + async protect (connection) { + return connection + } + } + libp2p = await createLibp2p({ peerId: peers[0], transports: [ @@ -627,9 +634,7 @@ describe('libp2p.upgrader', () => { connectionEncryption: [ plaintext() ], - connectionProtector: preSharedKey({ - psk: uint8ArrayFromString(swarmKey) - }), + connectionProtector: () => protector, services: { test: (components: any) => { deferred.resolve(components) diff --git a/packages/libp2p/typedoc.json b/packages/libp2p/typedoc.json index 8ed11ea9f3..dde288fe84 100644 --- a/packages/libp2p/typedoc.json +++ b/packages/libp2p/typedoc.json @@ -1,13 +1,11 @@ { "entryPoints": [ "./src/index.ts", - "./src/autonat/index.ts", "./src/circuit-relay/index.ts", "./src/fetch/index.ts", "./src/identify/index.ts", "./src/insecure/index.ts", "./src/ping/index.ts", - "./src/pnet/index.ts", "./src/upnp-nat/index.ts" ] } diff --git a/packages/peer-discovery-bootstrap/src/index.ts b/packages/peer-discovery-bootstrap/src/index.ts index 999b14f5e0..f7e3d22da5 100644 --- a/packages/peer-discovery-bootstrap/src/index.ts +++ b/packages/peer-discovery-bootstrap/src/index.ts @@ -134,8 +134,7 @@ class Bootstrap extends TypedEventEmitter implements PeerDi const peerData: PeerInfo = { id: peerIdFromString(peerIdStr), - multiaddrs: [ma], - protocols: [] + multiaddrs: [ma] } this.list.push(peerData) diff --git a/packages/peer-discovery-mdns/src/index.ts b/packages/peer-discovery-mdns/src/index.ts index ed37439b57..6f78e6f7e2 100644 --- a/packages/peer-discovery-mdns/src/index.ts +++ b/packages/peer-discovery-mdns/src/index.ts @@ -76,186 +76,9 @@ * ``` */ -import { CustomEvent, TypedEventEmitter } from '@libp2p/interface/events' -import { peerDiscovery } from '@libp2p/interface/peer-discovery' -import { logger } from '@libp2p/logger' -import multicastDNS from 'multicast-dns' -import * as query from './query.js' -import { stringGen } from './utils.js' -import type { PeerDiscovery, PeerDiscoveryEvents } from '@libp2p/interface/peer-discovery' -import type { PeerInfo } from '@libp2p/interface/peer-info' -import type { Startable } from '@libp2p/interface/src/startable.js' -import type { AddressManager } from '@libp2p/interface-internal/address-manager' - -const log = logger('libp2p:mdns') - -export interface MulticastDNSInit { - /** - * (true/false) announce our presence through mDNS, default `true` - */ - broadcast?: boolean - - /** - * query interval, default 10 \* 1000 (10 seconds) - */ - interval?: number - - /** - * name of the service announce , default '_p2p._udp.local\` - */ - serviceTag?: string - /** - * Peer name to announce (should not be peeer id), default random string - */ - peerName?: string - - /** - * UDP port to broadcast to - */ - port?: number - - /** - * UDP IP to broadcast to - */ - ip?: string -} - -export interface MulticastDNSComponents { - addressManager: AddressManager -} - -class MulticastDNS extends TypedEventEmitter implements PeerDiscovery, Startable { - public mdns?: multicastDNS.MulticastDNS - - private readonly broadcast: boolean - private readonly interval: number - private readonly serviceTag: string - private readonly peerName: string - private readonly port: number - private readonly ip: string - private _queryInterval: ReturnType | null - private readonly components: MulticastDNSComponents - - constructor (components: MulticastDNSComponents, init: MulticastDNSInit = {}) { - super() - - this.broadcast = init.broadcast !== false - this.interval = init.interval ?? (1e3 * 10) - this.serviceTag = init.serviceTag ?? '_p2p._udp.local' - this.ip = init.ip ?? '224.0.0.251' - this.peerName = init.peerName ?? stringGen(63) - // 63 is dns label limit - if (this.peerName.length >= 64) { - throw new Error('Peer name should be less than 64 chars long') - } - this.port = init.port ?? 5353 - this.components = components - this._queryInterval = null - this._onMdnsQuery = this._onMdnsQuery.bind(this) - this._onMdnsResponse = this._onMdnsResponse.bind(this) - this._onMdnsWarning = this._onMdnsWarning.bind(this) - this._onMdnsError = this._onMdnsError.bind(this) - } - - readonly [peerDiscovery] = this - - readonly [Symbol.toStringTag] = '@libp2p/mdns' - - isStarted (): boolean { - return Boolean(this.mdns) - } - - /** - * Start sending queries to the LAN. - * - * @returns {void} - */ - async start (): Promise { - if (this.mdns != null) { - return - } - - this.mdns = multicastDNS({ port: this.port, ip: this.ip }) - this.mdns.on('query', this._onMdnsQuery) - this.mdns.on('response', this._onMdnsResponse) - this.mdns.on('warning', this._onMdnsWarning) - this.mdns.on('error', this._onMdnsError) - - this._queryInterval = query.queryLAN(this.mdns, this.serviceTag, this.interval) - } - - _onMdnsQuery (event: multicastDNS.QueryPacket): void { - if (this.mdns == null) { - return - } - - log.trace('received incoming mDNS query') - query.gotQuery( - event, - this.mdns, - this.peerName, - this.components.addressManager.getAddresses(), - this.serviceTag, - this.broadcast) - } - - _onMdnsResponse (event: multicastDNS.ResponsePacket): void { - log.trace('received mDNS query response') - - try { - const foundPeer = query.gotResponse(event, this.peerName, this.serviceTag) - - if (foundPeer != null) { - log('discovered peer in mDNS query response %p', foundPeer.id) - - this.dispatchEvent(new CustomEvent('peer', { - detail: foundPeer - })) - } - } catch (err) { - log.error('Error processing peer response', err) - } - } - - _onMdnsWarning (err: Error): void { - log.error('mdns warning', err) - } - - _onMdnsError (err: Error): void { - log.error('mdns error', err) - } - - /** - * Stop sending queries to the LAN. - * - * @returns {Promise} - */ - async stop (): Promise { - if (this.mdns == null) { - return - } - - this.mdns.removeListener('query', this._onMdnsQuery) - this.mdns.removeListener('response', this._onMdnsResponse) - this.mdns.removeListener('warning', this._onMdnsWarning) - this.mdns.removeListener('error', this._onMdnsError) - - if (this._queryInterval != null) { - clearInterval(this._queryInterval) - this._queryInterval = null - } - - await new Promise((resolve) => { - if (this.mdns != null) { - this.mdns.destroy(resolve) - } else { - resolve() - } - }) - - this.mdns = undefined - } -} +import { MulticastDNS } from './mdns.js' +import type { MulticastDNSInit, MulticastDNSComponents } from './mdns.js' +import type { PeerDiscovery } from '@libp2p/interface/peer-discovery' export function mdns (init: MulticastDNSInit = {}): (components: MulticastDNSComponents) => PeerDiscovery { return (components: MulticastDNSComponents) => new MulticastDNS(components, init) diff --git a/packages/peer-discovery-mdns/src/mdns.ts b/packages/peer-discovery-mdns/src/mdns.ts new file mode 100644 index 0000000000..cdcd802a46 --- /dev/null +++ b/packages/peer-discovery-mdns/src/mdns.ts @@ -0,0 +1,158 @@ +import { CustomEvent, EventEmitter } from '@libp2p/interface/events' +import { peerDiscovery } from '@libp2p/interface/peer-discovery' +import { logger } from '@libp2p/logger' +import multicastDNS from 'multicast-dns' +import * as query from './query.js' +import { stringGen } from './utils.js' +import type { PeerDiscovery, PeerDiscoveryEvents } from '@libp2p/interface/peer-discovery' +import type { PeerInfo } from '@libp2p/interface/peer-info' +import type { Startable } from '@libp2p/interface/src/startable.js' +import type { AddressManager } from '@libp2p/interface-internal/address-manager' + +const log = logger('libp2p:mdns') + +export interface MulticastDNSInit { + broadcast?: boolean + interval?: number + serviceTag?: string + peerName?: string + port?: number + ip?: string +} + +export interface MulticastDNSComponents { + addressManager: AddressManager +} + +export class MulticastDNS extends EventEmitter implements PeerDiscovery, Startable { + public mdns?: multicastDNS.MulticastDNS + + private readonly broadcast: boolean + private readonly interval: number + private readonly serviceTag: string + private readonly peerName: string + private readonly port: number + private readonly ip: string + private _queryInterval: ReturnType | null + private readonly components: MulticastDNSComponents + + constructor (components: MulticastDNSComponents, init: MulticastDNSInit = {}) { + super() + + this.broadcast = init.broadcast !== false + this.interval = init.interval ?? (1e3 * 10) + this.serviceTag = init.serviceTag ?? '_p2p._udp.local' + this.ip = init.ip ?? '224.0.0.251' + this.peerName = init.peerName ?? stringGen(63) + // 63 is dns label limit + if (this.peerName.length >= 64) { + throw new Error('Peer name should be less than 64 chars long') + } + this.port = init.port ?? 5353 + this.components = components + this._queryInterval = null + this._onMdnsQuery = this._onMdnsQuery.bind(this) + this._onMdnsResponse = this._onMdnsResponse.bind(this) + this._onMdnsWarning = this._onMdnsWarning.bind(this) + this._onMdnsError = this._onMdnsError.bind(this) + } + + readonly [peerDiscovery] = this + + readonly [Symbol.toStringTag] = '@libp2p/mdns' + + isStarted (): boolean { + return Boolean(this.mdns) + } + + /** + * Start sending queries to the LAN. + * + * @returns {void} + */ + async start (): Promise { + if (this.mdns != null) { + return + } + + this.mdns = multicastDNS({ port: this.port, ip: this.ip }) + this.mdns.on('query', this._onMdnsQuery) + this.mdns.on('response', this._onMdnsResponse) + this.mdns.on('warning', this._onMdnsWarning) + this.mdns.on('error', this._onMdnsError) + + this._queryInterval = query.queryLAN(this.mdns, this.serviceTag, this.interval) + } + + _onMdnsQuery (event: multicastDNS.QueryPacket): void { + if (this.mdns == null) { + return + } + + log.trace('received incoming mDNS query') + query.gotQuery( + event, + this.mdns, + this.peerName, + this.components.addressManager.getAddresses(), + this.serviceTag, + this.broadcast) + } + + _onMdnsResponse (event: multicastDNS.ResponsePacket): void { + log.trace('received mDNS query response') + + try { + const foundPeer = query.gotResponse(event, this.peerName, this.serviceTag) + + if (foundPeer != null) { + log('discovered peer in mDNS query response %p', foundPeer.id) + + this.dispatchEvent(new CustomEvent('peer', { + detail: foundPeer + })) + } + } catch (err) { + log.error('Error processing peer response', err) + } + } + + _onMdnsWarning (err: Error): void { + log.error('mdns warning', err) + } + + _onMdnsError (err: Error): void { + log.error('mdns error', err) + } + + /** + * Stop sending queries to the LAN. + * + * @returns {Promise} + */ + async stop (): Promise { + if (this.mdns == null) { + return + } + + this.mdns.removeListener('query', this._onMdnsQuery) + this.mdns.removeListener('response', this._onMdnsResponse) + this.mdns.removeListener('warning', this._onMdnsWarning) + this.mdns.removeListener('error', this._onMdnsError) + + if (this._queryInterval != null) { + clearInterval(this._queryInterval) + this._queryInterval = null + } + + await new Promise((resolve) => { + if (this.mdns != null) { + this.mdns.destroy(resolve) + } else { + resolve() + } + }) + + this.mdns = undefined + } +} diff --git a/packages/peer-discovery-mdns/src/query.ts b/packages/peer-discovery-mdns/src/query.ts index bb6aa8ba30..45f287a705 100644 --- a/packages/peer-discovery-mdns/src/query.ts +++ b/packages/peer-discovery-mdns/src/query.ts @@ -74,8 +74,7 @@ export function gotResponse (rsp: ResponsePacket, localPeerName: string, service return { id: peerIdFromString(peerId), - multiaddrs: multiaddrs.map(addr => addr.decapsulateCode(protocols('p2p').code)), - protocols: [] + multiaddrs: multiaddrs.map(addr => addr.decapsulateCode(protocols('p2p').code)) } } catch (e) { log.error('failed to parse mdns response', e) diff --git a/packages/peer-discovery-mdns/test/compliance.spec.ts b/packages/peer-discovery-mdns/test/compliance.spec.ts index b24d590d91..d5975b654c 100644 --- a/packages/peer-discovery-mdns/test/compliance.spec.ts +++ b/packages/peer-discovery-mdns/test/compliance.spec.ts @@ -1,16 +1,14 @@ /* eslint-env mocha */ import { CustomEvent } from '@libp2p/interface/events' -import { isStartable } from '@libp2p/interface/startable' import tests from '@libp2p/interface-compliance-tests/peer-discovery' import { createEd25519PeerId } from '@libp2p/peer-id-factory' import { multiaddr } from '@multiformats/multiaddr' import { stubInterface } from 'ts-sinon' -import { mdns } from '../src/index.js' -import type { PeerDiscovery } from '@libp2p/interface/peer-discovery' +import { MulticastDNS } from '../src/mdns.js' import type { AddressManager } from '@libp2p/interface-internal/address-manager' -let discovery: PeerDiscovery +let discovery: MulticastDNS describe('compliance tests', () => { let intervalId: ReturnType @@ -25,18 +23,18 @@ describe('compliance tests', () => { multiaddr(`/ip4/127.0.0.1/tcp/13921/p2p/${peerId1.toString()}`) ]) - discovery = mdns({ + discovery = new MulticastDNS({ + addressManager + }, { broadcast: false, port: 50001 - })({ - addressManager }) // Trigger discovery const maStr = '/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star' intervalId = setInterval(() => { - if (isStartable(discovery) && !discovery.isStarted()) { + if (!discovery.isStarted()) { return } diff --git a/packages/peer-discovery-mdns/test/multicast-dns.spec.ts b/packages/peer-discovery-mdns/test/multicast-dns.spec.ts index 9955002dd7..1ac9b75056 100644 --- a/packages/peer-discovery-mdns/test/multicast-dns.spec.ts +++ b/packages/peer-discovery-mdns/test/multicast-dns.spec.ts @@ -6,7 +6,8 @@ import { multiaddr } from '@multiformats/multiaddr' import { expect } from 'aegir/chai' import pWaitFor from 'p-wait-for' import { stubInterface } from 'ts-sinon' -import { mdns, type MulticastDNSComponents } from './../src/index.js' +import { mdns } from './../src/index.js' +import type { MulticastDNSComponents } from './../src/mdns.js' import type { PeerId } from '@libp2p/interface/peer-id' import type { PeerInfo } from '@libp2p/interface/peer-info' import type { AddressManager } from '@libp2p/interface-internal/address-manager' diff --git a/packages/pnet/LICENSE b/packages/pnet/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/pnet/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/pnet/LICENSE-APACHE b/packages/pnet/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/pnet/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/pnet/LICENSE-MIT b/packages/pnet/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/pnet/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/pnet/README.md b/packages/pnet/README.md new file mode 100644 index 0000000000..3b12436c22 --- /dev/null +++ b/packages/pnet/README.md @@ -0,0 +1,89 @@ +> Connection protection management for libp2p leveraging PSK encryption via XSalsa20. + +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> Implementation of Connection protection management via a shared secret + +# About + +Connection protection management for libp2p leveraging PSK encryption via XSalsa20. + +## Example + +```typescript +import { createLibp2p } from 'libp2p' +import { preSharedKey, generateKey } from '@libp2p/pnet' + +// Create a Uint8Array and write the swarm key to it +const swarmKey = new Uint8Array(95) +generateKey(swarmKey) + +const node = await createLibp2p({ + // ...other options + connectionProtector: preSharedKey({ + psk: swarmKey + }) +}) +``` + +## Private Shared Keys + +Private Shared Keys are expected to be in the following format: + +``` +/key/swarm/psk/1.0.0/ +/base16/ +dffb7e3135399a8b1612b2aaca1c36a3a8ac2cd0cca51ceeb2ced87d308cac6d +``` + +## PSK Generation + +A utility method has been created to generate a key for your private network. You can use one of the methods below to generate your key. + +### From a module using libp2p + +If you have a module locally that depends on libp2p, you can run the following from that project, assuming the node\_modules are installed. + +```console +node -e "import('@libp2p/pnet').then(({ generateKey }) => generateKey(process.stdout))" > swarm.key +``` + +### Programmatically + +```js +import fs from 'fs' +import { generateKey } from '@libp2p/pnet' + +const swarmKey = new Uint8Array(95) +generateKey(swarmKey) + +fs.writeFileSync('swarm.key', swarmKey) +``` + +# Install + +```console +$ npm i @libp2p/pnet +``` + +## Browser ` +``` + +# License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +# Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/pnet/package.json b/packages/pnet/package.json new file mode 100644 index 0000000000..a568b2e111 --- /dev/null +++ b/packages/pnet/package.json @@ -0,0 +1,67 @@ +{ + "name": "@libp2p/pnet", + "version": "1.0.0", + "description": "Implementation of Connection protection management via a shared secret", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/pnet#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "project": true, + "sourceType": "module" + } + }, + "scripts": { + "build": "aegir build", + "test": "aegir test", + "clean": "aegir clean", + "lint": "aegir lint", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "dep-check": "aegir dep-check" + }, + "dependencies": { + "@libp2p/crypto": "^2.0.5", + "@libp2p/interface": "^0.1.3", + "@libp2p/logger": "^3.0.2", + "it-handshake": "^4.1.3", + "it-map": "^3.0.3", + "it-pair": "^2.0.6", + "it-pipe": "^3.0.1", + "it-stream-types": "^2.0.1", + "uint8arrays": "^4.0.6", + "xsalsa20": "^1.1.0" + }, + "devDependencies": { + "@libp2p/interface-compliance-tests": "^4.1.1", + "@libp2p/peer-id-factory": "^3.0.5", + "@multiformats/multiaddr": "^12.1.5", + "@types/xsalsa20": "^1.1.0", + "aegir": "^41.0.2", + "it-all": "^3.0.1" + } +} diff --git a/packages/libp2p/src/pnet/crypto.ts b/packages/pnet/src/crypto.ts similarity index 100% rename from packages/libp2p/src/pnet/crypto.ts rename to packages/pnet/src/crypto.ts diff --git a/packages/libp2p/src/pnet/errors.ts b/packages/pnet/src/errors.ts similarity index 83% rename from packages/libp2p/src/pnet/errors.ts rename to packages/pnet/src/errors.ts index b09f68a2a1..a8288ac045 100644 --- a/packages/libp2p/src/pnet/errors.ts +++ b/packages/pnet/src/errors.ts @@ -3,3 +3,4 @@ export const INVALID_PSK = 'Your private shared key is invalid' export const NO_LOCAL_ID = 'No local private key provided' export const NO_HANDSHAKE_CONNECTION = 'No connection for the handshake provided' export const STREAM_ENDED = 'Stream ended prematurely' +export const ERR_INVALID_PARAMETERS = 'ERR_INVALID_PARAMETERS' diff --git a/packages/libp2p/src/pnet/index.ts b/packages/pnet/src/index.ts similarity index 72% rename from packages/libp2p/src/pnet/index.ts rename to packages/pnet/src/index.ts index 3433710512..c072e47de8 100644 --- a/packages/libp2p/src/pnet/index.ts +++ b/packages/pnet/src/index.ts @@ -7,7 +7,7 @@ * * ```typescript * import { createLibp2p } from 'libp2p' - * import { preSharedKey, generateKey } from 'libp2p/pnet' + * import { preSharedKey, generateKey } from '@libp2p/pnet' * * // Create a Uint8Array and write the swarm key to it * const swarmKey = new Uint8Array(95) @@ -20,6 +20,40 @@ * }) * }) * ``` + * + * ## Private Shared Keys + * + * Private Shared Keys are expected to be in the following format: + * + * ``` + * /key/swarm/psk/1.0.0/ + * /base16/ + * dffb7e3135399a8b1612b2aaca1c36a3a8ac2cd0cca51ceeb2ced87d308cac6d + * ``` + * + * ## PSK Generation + * + * A utility method has been created to generate a key for your private network. You can use one of the methods below to generate your key. + * + * ### From a module using libp2p + * + * If you have a module locally that depends on libp2p, you can run the following from that project, assuming the node_modules are installed. + * + * ```console + * node -e "import('@libp2p/pnet').then(({ generateKey }) => generateKey(process.stdout))" > swarm.key + * ``` + * + * ### Programmatically + * + * ```js + * import fs from 'fs' + * import { generateKey } from '@libp2p/pnet' + * + * const swarmKey = new Uint8Array(95) + * generateKey(swarmKey) + * + * fs.writeFileSync('swarm.key', swarmKey) + * ``` */ import { randomBytes } from '@libp2p/crypto' @@ -29,7 +63,6 @@ import { handshake } from 'it-handshake' import map from 'it-map' import { duplexPair } from 'it-pair/duplex' import { pipe } from 'it-pipe' -import { codes } from '../errors.js' import { createBoxStream, createUnboxStream, @@ -81,7 +114,7 @@ class PreSharedKeyConnectionProtector implements ConnectionProtector { } if (connection == null) { - throw new CodeError(Errors.NO_HANDSHAKE_CONNECTION, codes.ERR_INVALID_PARAMETERS) + throw new CodeError(Errors.NO_HANDSHAKE_CONNECTION, Errors.ERR_INVALID_PARAMETERS) } // Exchange nonces @@ -94,7 +127,7 @@ class PreSharedKeyConnectionProtector implements ConnectionProtector { const result = await shake.reader.next(NONCE_LENGTH) if (result.value == null) { - throw new CodeError(Errors.STREAM_ENDED, codes.ERR_INVALID_PARAMETERS) + throw new CodeError(Errors.STREAM_ENDED, Errors.ERR_INVALID_PARAMETERS) } const remoteNonce = result.value.slice() diff --git a/packages/libp2p/src/pnet/key-generator.ts b/packages/pnet/src/key-generator.ts similarity index 100% rename from packages/libp2p/src/pnet/key-generator.ts rename to packages/pnet/src/key-generator.ts diff --git a/packages/libp2p/test/pnet/index.spec.ts b/packages/pnet/test/index.spec.ts similarity index 96% rename from packages/libp2p/test/pnet/index.spec.ts rename to packages/pnet/test/index.spec.ts index 20812893a0..e713e16d51 100644 --- a/packages/libp2p/test/pnet/index.spec.ts +++ b/packages/pnet/test/index.spec.ts @@ -6,8 +6,8 @@ import { expect } from 'aegir/chai' import all from 'it-all' import { pipe } from 'it-pipe' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' -import { INVALID_PSK } from '../../src/pnet/errors.js' -import { preSharedKey, generateKey } from '../../src/pnet/index.js' +import { INVALID_PSK } from '../src/errors.js' +import { preSharedKey, generateKey } from '../src/index.js' const swarmKeyBuffer = new Uint8Array(95) const wrongSwarmKeyBuffer = new Uint8Array(95) diff --git a/packages/pnet/tsconfig.json b/packages/pnet/tsconfig.json new file mode 100644 index 0000000000..1e17899ddc --- /dev/null +++ b/packages/pnet/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../crypto" + }, + { + "path": "../interface" + }, + { + "path": "../interface-compliance-tests" + }, + { + "path": "../logger" + }, + { + "path": "../peer-id-factory" + } + ] +} diff --git a/packages/pnet/typedoc.json b/packages/pnet/typedoc.json new file mode 100644 index 0000000000..f599dc728d --- /dev/null +++ b/packages/pnet/typedoc.json @@ -0,0 +1,5 @@ +{ + "entryPoints": [ + "./src/index.ts" + ] +} diff --git a/packages/protocol-autonat/LICENSE b/packages/protocol-autonat/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/protocol-autonat/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/protocol-autonat/LICENSE-APACHE b/packages/protocol-autonat/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/protocol-autonat/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/protocol-autonat/LICENSE-MIT b/packages/protocol-autonat/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/protocol-autonat/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/protocol-autonat/README.md b/packages/protocol-autonat/README.md new file mode 100644 index 0000000000..e11bd46ab6 --- /dev/null +++ b/packages/protocol-autonat/README.md @@ -0,0 +1,64 @@ +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> Implementation of Autonat Protocol + +# About + +Use the `autoNATService` function to add support for the [AutoNAT protocol](https://docs.libp2p.io/concepts/nat/autonat/) +to libp2p. + +## Example + +```typescript +import { createLibp2p } from 'libp2p' +import { autoNATService } from '@libp2p/autonat' + +const node = await createLibp2p({ + // ...other options + services: { + autoNAT: autoNATService() + } +}) +``` + +## Table of contents + +- [About](#about) + - [Example](#example) +- [Install](#install) + - [Browser ` +``` + +# API Docs + +- + +# License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +# Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/protocol-autonat/package.json b/packages/protocol-autonat/package.json new file mode 100644 index 0000000000..a7bfd0ab08 --- /dev/null +++ b/packages/protocol-autonat/package.json @@ -0,0 +1,70 @@ +{ + "name": "@libp2p/autonat", + "version": "1.0.0", + "description": "Implementation of Autonat Protocol", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/protocol-autonat#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "build": "aegir build", + "test": "aegir test", + "clean": "aegir clean", + "generate": "protons ./src/pb/index.proto", + "lint": "aegir lint", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "dep-check": "aegir dep-check" + }, + "dependencies": { + "@libp2p/interface": "^0.1.2", + "@libp2p/interface-internal": "^0.1.5", + "@libp2p/peer-id": "^3.0.2", + "@libp2p/peer-id-factory": "^3.0.4", + "@libp2p/logger": "^3.0.2", + "@multiformats/multiaddr": "^12.1.5", + "it-first": "^3.0.1", + "it-length-prefixed": "^9.0.1", + "it-map": "^3.0.3", + "it-parallel": "^3.0.0", + "it-pipe": "^3.0.1", + "private-ip": "^3.0.0", + "protons-runtime": "^5.0.0", + "uint8arraylist": "^2.4.3" + }, + "devDependencies": { + "aegir": "^41.0.2", + "it-all": "^3.0.1", + "it-pushable": "^3.2.0", + "sinon": "^17.0.0", + "sinon-ts": "^1.0.0" + } +} diff --git a/packages/libp2p/src/autonat/constants.ts b/packages/protocol-autonat/src/constants.ts similarity index 100% rename from packages/libp2p/src/autonat/constants.ts rename to packages/protocol-autonat/src/constants.ts diff --git a/packages/libp2p/src/autonat/index.ts b/packages/protocol-autonat/src/index.ts similarity index 98% rename from packages/libp2p/src/autonat/index.ts rename to packages/protocol-autonat/src/index.ts index f958d76621..3ca49d43e6 100644 --- a/packages/libp2p/src/autonat/index.ts +++ b/packages/protocol-autonat/src/index.ts @@ -8,7 +8,7 @@ * * ```typescript * import { createLibp2p } from 'libp2p' - * import { autoNATService } from 'libp2p/autonat' + * import { autoNATService } from '@libp2p/autonat' * * const node = await createLibp2p({ * // ...other options @@ -19,7 +19,7 @@ * ``` */ -import { CodeError } from '@libp2p/interface/errors' +import { CodeError, ERR_TIMEOUT } from '@libp2p/interface/errors' import { setMaxListeners } from '@libp2p/interface/events' import { logger } from '@libp2p/logger' import { peerIdFromBytes } from '@libp2p/peer-id' @@ -31,7 +31,6 @@ import map from 'it-map' import parallel from 'it-parallel' import { pipe } from 'it-pipe' import isPrivateIp from 'private-ip' -import { codes } from '../errors.js' import { MAX_INBOUND_STREAMS, MAX_OUTBOUND_STREAMS, @@ -157,7 +156,7 @@ class DefaultAutoNATService implements Startable { const signal = AbortSignal.timeout(this.timeout) const onAbort = (): void => { - data.stream.abort(new CodeError('handleIncomingAutonatStream timeout', codes.ERR_TIMEOUT)) + data.stream.abort(new CodeError('handleIncomingAutonatStream timeout', ERR_TIMEOUT)) } signal.addEventListener('abort', onAbort, { once: true }) @@ -466,7 +465,7 @@ class DefaultAutoNATService implements Startable { signal }) - onAbort = () => { stream.abort(new CodeError('verifyAddress timeout', codes.ERR_TIMEOUT)) } + onAbort = () => { stream.abort(new CodeError('verifyAddress timeout', ERR_TIMEOUT)) } signal.addEventListener('abort', onAbort, { once: true }) diff --git a/packages/libp2p/src/autonat/pb/index.proto b/packages/protocol-autonat/src/pb/index.proto similarity index 100% rename from packages/libp2p/src/autonat/pb/index.proto rename to packages/protocol-autonat/src/pb/index.proto diff --git a/packages/libp2p/src/autonat/pb/index.ts b/packages/protocol-autonat/src/pb/index.ts similarity index 100% rename from packages/libp2p/src/autonat/pb/index.ts rename to packages/protocol-autonat/src/pb/index.ts diff --git a/packages/libp2p/test/autonat/index.spec.ts b/packages/protocol-autonat/test/index.spec.ts similarity index 96% rename from packages/libp2p/test/autonat/index.spec.ts rename to packages/protocol-autonat/test/index.spec.ts index 250033f7b3..09beda2c67 100644 --- a/packages/libp2p/test/autonat/index.spec.ts +++ b/packages/protocol-autonat/test/index.spec.ts @@ -12,20 +12,17 @@ import { pushable } from 'it-pushable' import sinon from 'sinon' import { stubInterface } from 'sinon-ts' import { Uint8ArrayList } from 'uint8arraylist' -import { PROTOCOL_NAME, PROTOCOL_PREFIX, PROTOCOL_VERSION } from '../../src/autonat/constants.js' -import { autoNATService } from '../../src/autonat/index.js' -import { Message } from '../../src/autonat/pb/index.js' -import { defaultComponents } from '../../src/components.js' -import type { AutoNATServiceInit } from '../../src/autonat/index.js' -import type { Components } from '../../src/components.js' -import type { DefaultConnectionManager } from '../../src/connection-manager/index.js' +import { PROTOCOL_NAME, PROTOCOL_PREFIX, PROTOCOL_VERSION } from '../src/constants.js' +import { autoNATService } from '../src/index.js' +import { Message } from '../src/pb/index.js' +import type { AutoNATComponents, AutoNATServiceInit } from '../src/index.js' import type { Connection, Stream } from '@libp2p/interface/connection' import type { PeerId } from '@libp2p/interface/peer-id' import type { PeerInfo } from '@libp2p/interface/peer-info' import type { PeerRouting } from '@libp2p/interface/peer-routing' -import type { PeerStore } from '@libp2p/interface/peer-store' import type { Transport } from '@libp2p/interface/transport' import type { AddressManager } from '@libp2p/interface-internal/address-manager' +import type { ConnectionManager } from '@libp2p/interface-internal/connection-manager' import type { Registrar } from '@libp2p/interface-internal/registrar' import type { TransportManager } from '@libp2p/interface-internal/transport-manager' import type { Multiaddr } from '@multiformats/multiaddr' @@ -42,13 +39,12 @@ const defaultInit: AutoNATServiceInit = { describe('autonat', () => { let service: any - let components: Components + let components: AutoNATComponents let peerRouting: StubbedInstance let registrar: StubbedInstance let addressManager: StubbedInstance - let connectionManager: StubbedInstance + let connectionManager: StubbedInstance let transportManager: StubbedInstance - let peerStore: StubbedInstance beforeEach(async () => { peerRouting = stubInterface() @@ -56,19 +52,17 @@ describe('autonat', () => { addressManager = stubInterface() addressManager.getAddresses.returns([]) - connectionManager = stubInterface() + connectionManager = stubInterface() transportManager = stubInterface() - peerStore = stubInterface() - components = defaultComponents({ + components = { peerId: await createEd25519PeerId(), peerRouting, registrar, addressManager, connectionManager, - transportManager, - peerStore - }) + transportManager + } service = autoNATService(defaultInit)(components) diff --git a/packages/protocol-autonat/tsconfig.json b/packages/protocol-autonat/tsconfig.json new file mode 100644 index 0000000000..fd38c2c239 --- /dev/null +++ b/packages/protocol-autonat/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface" + }, + { + "path": "../interface-internal" + }, + { + "path": "../peer-id" + }, + { + "path": "../peer-id-factory" + }, + { + "path": "../logger" + } + ] +} diff --git a/packages/protocol-autonat/typedoc.json b/packages/protocol-autonat/typedoc.json new file mode 100644 index 0000000000..f599dc728d --- /dev/null +++ b/packages/protocol-autonat/typedoc.json @@ -0,0 +1,5 @@ +{ + "entryPoints": [ + "./src/index.ts" + ] +} diff --git a/packages/protocol-fetch/LICENSE b/packages/protocol-fetch/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/protocol-fetch/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/protocol-fetch/LICENSE-APACHE b/packages/protocol-fetch/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/protocol-fetch/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/protocol-fetch/LICENSE-MIT b/packages/protocol-fetch/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/protocol-fetch/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/libp2p/src/fetch/README.md b/packages/protocol-fetch/README.md similarity index 98% rename from packages/libp2p/src/fetch/README.md rename to packages/protocol-fetch/README.md index 8d231d2e83..fb38d2f9de 100644 --- a/packages/libp2p/src/fetch/README.md +++ b/packages/protocol-fetch/README.md @@ -16,7 +16,7 @@ The fetch protocol is a simple protocol for requesting a value corresponding to ## Usage -```javascript +```ts import { createLibp2p } from 'libp2p' /** diff --git a/packages/protocol-fetch/package.json b/packages/protocol-fetch/package.json new file mode 100644 index 0000000000..049a8064ac --- /dev/null +++ b/packages/protocol-fetch/package.json @@ -0,0 +1,72 @@ +{ + "name": "@libp2p/fetch", + "version": "1.0.0", + "description": "Implementation of the Fetch Protocol", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/protocol-fetch#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "scripts": { + "build": "aegir build", + "test": "aegir test", + "clean": "aegir clean", + "generate": "protons ./src/pb/index.proto", + "lint": "aegir lint", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "dep-check": "aegir dep-check" + }, + "dependencies": { + "@libp2p/interface": "^0.1.4", + "@libp2p/interface-internal": "^0.1.7", + "@libp2p/logger": "^3.0.4", + "it-first": "^3.0.3", + "it-length-prefixed": "^9.0.3", + "it-pipe": "^3.0.1", + "protons-runtime": "^5.2.0", + "uint8arraylist": "^2.4.3", + "uint8arrays": "^4.0.6" + }, + "devDependencies": { + "@chainsafe/libp2p-noise": "^13.0.2", + "@chainsafe/libp2p-yamux": "^5.0.0", + "@libp2p/interface-compliance-tests": "^4.1.2", + "@libp2p/peer-id-factory": "^3.0.6", + "@libp2p/peer-store": "^9.0.7", + "@libp2p/tcp": "^8.0.10", + "aegir": "^40.0.8", + "datastore-core": "^9.2.3", + "delay": "^6.0.0", + "libp2p": "^0.46.16", + "sinon": "^17.0.1", + "sinon-ts": "^2.0.0" + } +} diff --git a/packages/libp2p/src/fetch/constants.ts b/packages/protocol-fetch/src/constants.ts similarity index 100% rename from packages/libp2p/src/fetch/constants.ts rename to packages/protocol-fetch/src/constants.ts diff --git a/packages/libp2p/src/fetch/index.ts b/packages/protocol-fetch/src/index.ts similarity index 94% rename from packages/libp2p/src/fetch/index.ts rename to packages/protocol-fetch/src/index.ts index 6e345f0d58..fe88e0874a 100644 --- a/packages/libp2p/src/fetch/index.ts +++ b/packages/protocol-fetch/src/index.ts @@ -1,12 +1,11 @@ -import { CodeError } from '@libp2p/interface/errors' import { setMaxListeners } from '@libp2p/interface/events' +import { CodeError, ERR_TIMEOUT, ERR_INVALID_MESSAGE, ERR_INVALID_PARAMETERS, ERR_KEY_ALREADY_EXISTS } from '@libp2p/interface/errors' import { logger } from '@libp2p/logger' import first from 'it-first' import * as lp from 'it-length-prefixed' import { pipe } from 'it-pipe' import { fromString as uint8arrayFromString } from 'uint8arrays/from-string' import { toString as uint8arrayToString } from 'uint8arrays/to-string' -import { codes } from '../errors.js' import { PROTOCOL_NAME, PROTOCOL_VERSION } from './constants.js' import { FetchRequest, FetchResponse } from './pb/proto.js' import type { AbortOptions } from '@libp2p/interface' @@ -155,7 +154,7 @@ class DefaultFetchService implements Startable, FetchService { }) onAbort = () => { - stream?.abort(new CodeError('fetch timeout', codes.ERR_TIMEOUT)) + stream?.abort(new CodeError('fetch timeout', ERR_TIMEOUT)) } // make stream abortable @@ -172,7 +171,7 @@ class DefaultFetchService implements Startable, FetchService { const buf = await first(source) if (buf == null) { - throw new CodeError('No data received', codes.ERR_INVALID_MESSAGE) + throw new CodeError('No data received', ERR_INVALID_MESSAGE) } const response = FetchResponse.decode(buf) @@ -189,11 +188,11 @@ class DefaultFetchService implements Startable, FetchService { case (FetchResponse.StatusCode.ERROR): { log('received status for %s error', key) const errmsg = uint8arrayToString(response.data) - throw new CodeError('Error in fetch protocol response: ' + errmsg, codes.ERR_INVALID_PARAMETERS) + throw new CodeError('Error in fetch protocol response: ' + errmsg, ERR_INVALID_PARAMETERS) } default: { log('received status for %s unknown', key) - throw new CodeError('Unknown response status', codes.ERR_INVALID_MESSAGE) + throw new CodeError('Unknown response status', ERR_INVALID_MESSAGE) } } } @@ -224,7 +223,7 @@ class DefaultFetchService implements Startable, FetchService { const buf = await first(source) if (buf == null) { - throw new CodeError('No data received', codes.ERR_INVALID_MESSAGE) + throw new CodeError('No data received', ERR_INVALID_MESSAGE) } // for await (const buf of source) { @@ -280,7 +279,7 @@ class DefaultFetchService implements Startable, FetchService { */ registerLookupFunction (prefix: string, lookup: LookupFunction): void { if (this.lookupFunctions.has(prefix)) { - throw new CodeError(`Fetch protocol handler for key prefix '${prefix}' already registered`, codes.ERR_KEY_ALREADY_EXISTS) + throw new CodeError(`Fetch protocol handler for key prefix '${prefix}' already registered`, ERR_KEY_ALREADY_EXISTS) } this.lookupFunctions.set(prefix, lookup) diff --git a/packages/libp2p/src/fetch/pb/proto.proto b/packages/protocol-fetch/src/pb/proto.proto similarity index 100% rename from packages/libp2p/src/fetch/pb/proto.proto rename to packages/protocol-fetch/src/pb/proto.proto diff --git a/packages/libp2p/src/fetch/pb/proto.ts b/packages/protocol-fetch/src/pb/proto.ts similarity index 100% rename from packages/libp2p/src/fetch/pb/proto.ts rename to packages/protocol-fetch/src/pb/proto.ts diff --git a/packages/libp2p/test/fetch/fetch.node.ts b/packages/protocol-fetch/test/fetch.node.ts similarity index 93% rename from packages/libp2p/test/fetch/fetch.node.ts rename to packages/protocol-fetch/test/fetch.node.ts index 53420c9a45..247e0b1b78 100644 --- a/packages/libp2p/test/fetch/fetch.node.ts +++ b/packages/protocol-fetch/test/fetch.node.ts @@ -1,16 +1,14 @@ /* eslint-env mocha */ import { yamux } from '@chainsafe/libp2p-yamux' -import { mplex } from '@libp2p/mplex' +import { type FetchService, fetchService } from '../src/index.js' import { tcp } from '@libp2p/tcp' import { expect } from 'aegir/chai' -import { codes } from '../../src/errors.js' -import { type FetchService, fetchService } from '../../src/fetch/index.js' -import { createLibp2p } from '../../src/index.js' -import { plaintext } from '../../src/insecure/index.js' -import { createPeerId } from '../fixtures/creators/peer.js' import type { Libp2p } from '@libp2p/interface' import type { PeerId } from '@libp2p/interface/peer-id' +import { createLibp2p } from 'libp2p' +import { noise } from '@chainsafe/libp2p-noise' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' async function createNode (peerId: PeerId): Promise> { return createLibp2p({ @@ -24,11 +22,10 @@ async function createNode (peerId: PeerId): Promise { } beforeEach(async () => { - const peerIdA = await createPeerId() - const peerIdB = await createPeerId() + const peerIdA = await createEd25519PeerId() + const peerIdB = await createEd25519PeerId() sender = await createNode(peerIdA) receiver = await createNode(peerIdB) diff --git a/packages/libp2p/test/fetch/index.spec.ts b/packages/protocol-fetch/test/index.spec.ts similarity index 93% rename from packages/libp2p/test/fetch/index.spec.ts rename to packages/protocol-fetch/test/index.spec.ts index 7b90a0f7db..fbd03acae8 100644 --- a/packages/libp2p/test/fetch/index.spec.ts +++ b/packages/protocol-fetch/test/index.spec.ts @@ -1,5 +1,7 @@ /* eslint-env mocha */ +import { fetchService, type FetchServiceInit } from '../src/index.js' +import { ERR_TIMEOUT } from '@libp2p/interface/errors' import { TypedEventEmitter } from '@libp2p/interface/events' import { start, stop } from '@libp2p/interface/startable' import { mockRegistrar, mockUpgrader, connectionPair } from '@libp2p/interface-compliance-tests/mocks' @@ -11,9 +13,6 @@ import delay from 'delay' import { pipe } from 'it-pipe' import sinon from 'sinon' import { stubInterface } from 'sinon-ts' -import { defaultComponents, type Components } from '../../src/components.js' -import { DefaultConnectionManager } from '../../src/connection-manager/index.js' -import { fetchService, type FetchServiceInit } from '../../src/fetch/index.js' import type { ConnectionGater } from '@libp2p/interface/connection-gater' import type { TransportManager } from '@libp2p/interface-internal/transport-manager' @@ -135,7 +134,7 @@ describe('fetch', () => { await expect(localFetch.fetch(remoteComponents.peerId, key, { signal })) - .to.eventually.be.rejected.with.property('code', 'ERR_TIMEOUT') + .to.eventually.be.rejected.with.property('code', ERR_TIMEOUT) // should have closed stream expect(newStreamSpy).to.have.property('callCount', 1) diff --git a/packages/protocol-fetch/tsconfig.json b/packages/protocol-fetch/tsconfig.json new file mode 100644 index 0000000000..13a3599639 --- /dev/null +++ b/packages/protocol-fetch/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ] +} diff --git a/packages/protocol-fetch/typedoc.json b/packages/protocol-fetch/typedoc.json new file mode 100644 index 0000000000..f599dc728d --- /dev/null +++ b/packages/protocol-fetch/typedoc.json @@ -0,0 +1,5 @@ +{ + "entryPoints": [ + "./src/index.ts" + ] +}