diff --git a/doc/migrations/v0.46-v1.0.0.md b/doc/migrations/v0.46-v1.0.0.md index a8b22de3e5..b4bee48972 100644 --- a/doc/migrations/v0.46-v1.0.0.md +++ b/doc/migrations/v0.46-v1.0.0.md @@ -6,6 +6,8 @@ A migration guide for refactoring your application code from libp2p `v0.46` to ` - [New features](#new-features) - [Breaking changes](#breaking-changes) +- [KeyChain](#keychain) +- [Metrics](#metrics) ## New features @@ -13,4 +15,48 @@ A migration guide for refactoring your application code from libp2p `v0.46` to ` ## Breaking changes -... +```ts +import { autoNATService } from 'libp2p/autonat' +``` + +**After** + +```ts +import { autoNATService } from '@libp2p/autonat' +``` + +## 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 +``` + +## 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/doc/migrations/v0.46-v1.0.md b/doc/migrations/v0.46-v1.0.md index 184b266cf0..420048289a 100644 --- a/doc/migrations/v0.46-v1.0.md +++ b/doc/migrations/v0.46-v1.0.md @@ -32,6 +32,38 @@ __Describe__ --> +### 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 { DefaultKeyChain } from '@libp2p/keychain' +import type { KeyChain } from '@libp2p/interface/keychain' + +const libp2p = await createLibp2p({ + ... + services: { + keychain: (components) => new DefaultKeyChain(components, { + ...DefaultKeyChain.generateOptions() + }) + } +}) + +const keychain: KeyChain = libp2p.services.keychain +``` + ## Module Updates With this release you should update the following libp2p modules if you are relying on them: 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/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..d1b392ef5a 100644 --- a/packages/libp2p/package.json +++ b/packages/libp2p/package.json @@ -124,7 +124,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", diff --git a/packages/libp2p/src/errors.ts b/packages/libp2p/src/errors.ts index a2d2ffb6c6..a895c1b5b2 100644 --- a/packages/libp2p/src/errors.ts +++ b/packages/libp2p/src/errors.ts @@ -48,7 +48,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..05cb801e2b 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 @@ -130,13 +125,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 +207,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() @@ -408,29 +389,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/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') - }) })