From 82330ac4e67d3b82e7a7e1e4c12b5853411c7ac8 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Thu, 14 Apr 2022 08:06:17 +0100 Subject: [PATCH] fix: load keychain on startup Renames KeyChain to DefaultKeyChain and makes it implement the Startable interface. Loads the current peer id into the keychain on startup instead of the user having to do it manually. --- doc/API.md | 30 -------------------- doc/CONFIGURATION.md | 2 -- src/index.ts | 8 +----- src/keychain/cms.ts | 6 ++-- src/keychain/index.ts | 26 ++++++++++++++++- src/libp2p.ts | 23 +++------------- test/keychain/cms-interop.spec.ts | 6 ++-- test/keychain/keychain.spec.ts | 46 ++++++++++++++----------------- 8 files changed, 56 insertions(+), 91 deletions(-) diff --git a/doc/API.md b/doc/API.md index 908fff23..14fddbce 100644 --- a/doc/API.md +++ b/doc/API.md @@ -181,36 +181,6 @@ Required keys in the `options` object: ## Libp2p Instance Methods -### loadKeychain - -Load keychain keys from the datastore, importing the private key as 'self', if needed. - -`libp2p.loadKeychain()` - -#### Returns - -| Type | Description | -|------|-------------| -| `Promise` | Promise resolves when the keychain is ready | - -#### Example - -```js -import { createLibp2p } from 'libp2p' - -// ... - -const libp2p = await createLibp2p({ - // ... - keychain: { - pass: '0123456789pass1234567890' - } -}) - -// load keychain -await libp2p.loadKeychain() -``` - ### start Starts the libp2p node. diff --git a/doc/CONFIGURATION.md b/doc/CONFIGURATION.md index df5e689a..fb550561 100644 --- a/doc/CONFIGURATION.md +++ b/doc/CONFIGURATION.md @@ -494,8 +494,6 @@ const node = await createLibp2p({ datastore: dsInstant, } }) - -await node.loadKeychain() ``` #### Configuring Dialing diff --git a/src/index.ts b/src/index.ts index d64bb224..e76b7939 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,7 +20,7 @@ import type { ConnectionManager, Registrar, StreamHandler } from '@libp2p/interf import type { Metrics, MetricsInit } from '@libp2p/interfaces/metrics' import type { PeerInfo } from '@libp2p/interfaces/peer-info' import type { DialerInit } from '@libp2p/interfaces/dialer' -import type { KeyChain } from './keychain/index.js' +import type { KeyChain } from '@libp2p/interfaces/keychain' export interface PersistentPeerStoreOptions { threshold?: number @@ -158,12 +158,6 @@ export interface Libp2p extends Startable, EventEmitter { pubsub?: PubSub dht?: DualDHT - /** - * Load keychain keys from the datastore. - * Imports the private key as 'self', if needed. - */ - loadKeychain: () => Promise - /** * Get a deduplicated list of peer advertising multiaddrs by concatenating * the listen addresses used by transports with any configured diff --git a/src/keychain/cms.ts b/src/keychain/cms.ts index 8a26c338..51da640b 100644 --- a/src/keychain/cms.ts +++ b/src/keychain/cms.ts @@ -8,7 +8,7 @@ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' import { codes } from '../errors.js' import { logger } from '@libp2p/logger' -import type { KeyChain } from './index.js' +import type { DefaultKeyChain } from './index.js' const log = logger('libp2p:keychain:cms') @@ -24,12 +24,12 @@ const privates = new WeakMap() * See RFC 5652 for all the details. */ export class CMS { - private readonly keychain: KeyChain + private readonly keychain: DefaultKeyChain /** * Creates a new instance with a keychain */ - constructor (keychain: KeyChain, dek: string) { + constructor (keychain: DefaultKeyChain, dek: string) { if (keychain == null) { throw errCode(new Error('keychain is required'), codes.ERR_KEYCHAIN_REQUIRED) } diff --git a/src/keychain/index.ts b/src/keychain/index.ts index b2b99126..8c58819a 100644 --- a/src/keychain/index.ts +++ b/src/keychain/index.ts @@ -14,6 +14,8 @@ import { generateKeyPair, importKey, unmarshalPrivateKey } from '@libp2p/crypto/ import type { PeerId } from '@libp2p/interfaces/peer-id' import type { Components } from '@libp2p/interfaces/components' import { pbkdf2, randomBytes } from '@libp2p/crypto' +import type { KeyChain } from '@libp2p/interfaces/keychain' +import type { Startable } from '@libp2p/interfaces' const log = logger('libp2p:keychain') @@ -111,9 +113,10 @@ function DsInfoName (name: string) { * - '/pkcs8/*key-name*', contains the PKCS #8 for the key * */ -export class KeyChain { +export class DefaultKeyChain implements KeyChain, Startable { private readonly components: Components private init: KeyChainInit + private started: boolean /** * Creates a new instance of a key chain @@ -146,6 +149,27 @@ export class KeyChain { : '' privates.set(this, { dek }) + this.started = false + } + + isStarted () { + return this.started + } + + async start () { + // Load keychain keys from the datastore. + // Imports the private key as 'self', if needed. + try { + await this.findKeyByName('self') + } catch (err: any) { + await this.importPeer('self', this.components.getPeerId()) + } + + this.started = true + } + + async stop () { + this.started = false } /** diff --git a/src/libp2p.ts b/src/libp2p.ts index 62f412bf..ade4602e 100644 --- a/src/libp2p.ts +++ b/src/libp2p.ts @@ -12,7 +12,7 @@ import { AutoDialler } from './connection-manager/auto-dialler.js' import { Circuit } from './circuit/transport.js' import { Relay } from './circuit/index.js' import { DefaultDialer } from './dialer/index.js' -import { KeyChain } from './keychain/index.js' +import { DefaultKeyChain } from './keychain/index.js' import { DefaultMetrics } from './metrics/index.js' import { DefaultTransportManager } from './transport-manager.js' import { DefaultUpgrader } from './upgrader.js' @@ -44,6 +44,7 @@ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import errCode from 'err-code' import { unmarshalPublicKey } from '@libp2p/crypto/keys' import type { Metrics } from '@libp2p/interfaces/metrics' +import type { KeyChain } from '@libp2p/interfaces/keychain' const log = logger('libp2p') @@ -140,8 +141,8 @@ export class Libp2pNode extends EventEmitter implements Libp2p { })) // Create keychain - const keychainOpts = KeyChain.generateOptions() - this.keychain = this.configureComponent(new KeyChain(this.components, { + const keychainOpts = DefaultKeyChain.generateOptions() + this.keychain = this.configureComponent(new DefaultKeyChain(this.components, { ...keychainOpts, ...init.keychain })) @@ -352,22 +353,6 @@ export class Libp2pNode extends EventEmitter implements Libp2p { log('libp2p has stopped') } - /** - * Load keychain keys from the datastore. - * Imports the private key as 'self', if needed. - */ - async loadKeychain () { - if (this.keychain == null) { - return - } - - try { - await this.keychain.findKeyByName('self') - } catch (err: any) { - await this.keychain.importPeer('self', this.peerId) - } - } - isStarted () { return this.started } diff --git a/test/keychain/cms-interop.spec.ts b/test/keychain/cms-interop.spec.ts index 5a3494c7..716cd960 100644 --- a/test/keychain/cms-interop.spec.ts +++ b/test/keychain/cms-interop.spec.ts @@ -5,17 +5,17 @@ import { expect } from 'aegir/chai' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' import { MemoryDatastore } from 'datastore-core/memory' -import { KeyChain } from '../../src/keychain/index.js' +import { DefaultKeyChain } from '../../src/keychain/index.js' import { Components } from '@libp2p/interfaces/components' describe('cms interop', () => { const passPhrase = 'this is not a secure phrase' const aliceKeyName = 'cms-interop-alice' - let ks: KeyChain + let ks: DefaultKeyChain before(() => { const datastore = new MemoryDatastore() - ks = new KeyChain(new Components({ datastore }), { pass: passPhrase }) + ks = new DefaultKeyChain(new Components({ datastore }), { pass: passPhrase }) }) const plainData = uint8ArrayFromString('This is a message from Alice to Bob') diff --git a/test/keychain/keychain.spec.ts b/test/keychain/keychain.spec.ts index fc7feb79..5fefa065 100644 --- a/test/keychain/keychain.spec.ts +++ b/test/keychain/keychain.spec.ts @@ -7,7 +7,7 @@ import { toString as uint8ArrayToString } from 'uint8arrays/to-string' import { createNode } from '../utils/creators/peer.js' import { Key } from 'interface-datastore/key' import { MemoryDatastore } from 'datastore-core/memory' -import { KeyChain, KeyChainInit, KeyInfo } from '../../src/keychain/index.js' +import { DefaultKeyChain, KeyChainInit, KeyInfo } from '../../src/keychain/index.js' import { pbkdf2 } from '@libp2p/crypto' import { Components } from '@libp2p/interfaces/components' import type { Datastore } from 'interface-datastore' @@ -20,16 +20,16 @@ describe('keychain', () => { const rsaKeyName = 'tajné jméno' const renamedRsaKeyName = 'ชื่อลับ' let rsaKeyInfo: KeyInfo - let emptyKeystore: KeyChain - let ks: KeyChain + let emptyKeystore: DefaultKeyChain + let ks: DefaultKeyChain let datastore1: Datastore, datastore2: Datastore before(async () => { datastore1 = new MemoryDatastore() datastore2 = new MemoryDatastore() - ks = new KeyChain(new Components({ datastore: datastore2 }), { pass: passPhrase }) - emptyKeystore = new KeyChain(new Components({ datastore: datastore1 }), { pass: passPhrase }) + ks = new DefaultKeyChain(new Components({ datastore: datastore2 }), { pass: passPhrase }) + emptyKeystore = new DefaultKeyChain(new Components({ datastore: datastore1 }), { pass: passPhrase }) await datastore1.open() await datastore2.open() @@ -41,35 +41,35 @@ describe('keychain', () => { }) it('can start without a password', () => { - expect(() => new KeyChain(new Components({ datastore: datastore2 }), {})).to.not.throw() + expect(() => new DefaultKeyChain(new Components({ datastore: datastore2 }), {})).to.not.throw() }) it('needs a NIST SP 800-132 non-weak pass phrase', () => { - expect(() => new KeyChain(new Components({ datastore: datastore2 }), { pass: '< 20 character' })).to.throw() + expect(() => new DefaultKeyChain(new Components({ datastore: datastore2 }), { pass: '< 20 character' })).to.throw() }) it('has default options', () => { - expect(KeyChain.options).to.exist() + expect(DefaultKeyChain.options).to.exist() }) it('supports supported hashing alorithms', () => { - const ok = new KeyChain(new Components({ datastore: datastore2 }), { pass: passPhrase, dek: { hash: 'sha2-256', salt: 'salt-salt-salt-salt', iterationCount: 1000, keyLength: 14 } }) + const ok = new DefaultKeyChain(new Components({ datastore: datastore2 }), { pass: passPhrase, dek: { hash: 'sha2-256', salt: 'salt-salt-salt-salt', iterationCount: 1000, keyLength: 14 } }) expect(ok).to.exist() }) it('does not support unsupported hashing alorithms', () => { - expect(() => new KeyChain(new Components({ datastore: datastore2 }), { pass: passPhrase, dek: { hash: 'my-hash', salt: 'salt-salt-salt-salt', iterationCount: 1000, keyLength: 14 } })).to.throw() + expect(() => new DefaultKeyChain(new Components({ datastore: datastore2 }), { pass: passPhrase, dek: { hash: 'my-hash', salt: 'salt-salt-salt-salt', iterationCount: 1000, keyLength: 14 } })).to.throw() }) it('can list keys without a password', async () => { - const keychain = new KeyChain(new Components({ datastore: datastore2 }), {}) + const keychain = new DefaultKeyChain(new Components({ datastore: datastore2 }), {}) expect(await keychain.listKeys()).to.have.lengthOf(0) }) it('can find a key without a password', async () => { - const keychain = new KeyChain(new Components({ datastore: datastore2 }), {}) - const keychainWithPassword = new KeyChain(new Components({ datastore: datastore2 }), { pass: `hello-${Date.now()}-${Date.now()}` }) + const keychain = new DefaultKeyChain(new Components({ datastore: datastore2 }), {}) + const keychainWithPassword = new DefaultKeyChain(new Components({ datastore: datastore2 }), { pass: `hello-${Date.now()}-${Date.now()}` }) const name = `key-${Math.random()}` const { id } = await keychainWithPassword.createKey(name, 'Ed25519') @@ -78,8 +78,8 @@ describe('keychain', () => { }) it('can remove a key without a password', async () => { - const keychainWithoutPassword = new KeyChain(new Components({ datastore: datastore2 }), {}) - const keychainWithPassword = new KeyChain(new Components({ datastore: datastore2 }), { pass: `hello-${Date.now()}-${Date.now()}` }) + const keychainWithoutPassword = new DefaultKeyChain(new Components({ datastore: datastore2 }), {}) + const keychainWithPassword = new DefaultKeyChain(new Components({ datastore: datastore2 }), { pass: `hello-${Date.now()}-${Date.now()}` }) const name = `key-${Math.random()}` expect(await keychainWithPassword.createKey(name, 'Ed25519')).to.have.property('name', name) @@ -89,16 +89,16 @@ describe('keychain', () => { }) it('requires a name to create a password', async () => { - const keychain = new KeyChain(new Components({ datastore: datastore2 }), {}) + const keychain = new DefaultKeyChain(new Components({ datastore: datastore2 }), {}) // @ts-expect-error invalid parameters await expect(keychain.createKey(undefined, 'derp')).to.be.rejected() }) it('can generate options', () => { - const options = KeyChain.generateOptions() + const options = DefaultKeyChain.generateOptions() options.pass = passPhrase - const chain = new KeyChain(new Components({ datastore: datastore2 }), options) + const chain = new DefaultKeyChain(new Components({ datastore: datastore2 }), options) expect(chain).to.exist() }) @@ -418,7 +418,7 @@ describe('keychain', () => { describe('rotate keychain passphrase', () => { let oldPass: string - let kc: KeyChain + let kc: DefaultKeyChain let options: KeyChainInit let ds: Datastore before(async () => { @@ -433,7 +433,7 @@ describe('keychain', () => { hash: 'sha2-512' } } - kc = new KeyChain(new Components({ datastore: ds }), options) + kc = new DefaultKeyChain(new Components({ datastore: ds }), options) await ds.open() }) @@ -512,8 +512,6 @@ describe('libp2p.keychain', () => { } }) - await libp2p.loadKeychain() - const kInfo = await libp2p.keychain.createKey('keyName', 'Ed25519') expect(kInfo).to.exist() }) @@ -526,8 +524,6 @@ describe('libp2p.keychain', () => { } }) - await libp2p.loadKeychain() - const kInfo = await libp2p.keychain.createKey('keyName', 'Ed25519') expect(kInfo).to.exist() }) @@ -543,7 +539,6 @@ describe('libp2p.keychain', () => { } } }) - await libp2p.loadKeychain() const kInfo = await libp2p.keychain.createKey('keyName', 'Ed25519') expect(kInfo).to.exist() @@ -558,7 +553,6 @@ describe('libp2p.keychain', () => { } }) - await libp2p2.loadKeychain() const key = await libp2p2.keychain.findKeyByName('keyName') expect(key).to.exist()