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.
This commit is contained in:
achingbrain 2022-04-14 08:06:17 +01:00
parent 1b9bab68ed
commit 82330ac4e6
8 changed files with 56 additions and 91 deletions

View File

@ -181,36 +181,6 @@ Required keys in the `options` object:
## Libp2p Instance Methods ## 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 ### start
Starts the libp2p node. Starts the libp2p node.

View File

@ -494,8 +494,6 @@ const node = await createLibp2p({
datastore: dsInstant, datastore: dsInstant,
} }
}) })
await node.loadKeychain()
``` ```
#### Configuring Dialing #### Configuring Dialing

View File

@ -20,7 +20,7 @@ import type { ConnectionManager, Registrar, StreamHandler } from '@libp2p/interf
import type { Metrics, MetricsInit } from '@libp2p/interfaces/metrics' import type { Metrics, MetricsInit } from '@libp2p/interfaces/metrics'
import type { PeerInfo } from '@libp2p/interfaces/peer-info' import type { PeerInfo } from '@libp2p/interfaces/peer-info'
import type { DialerInit } from '@libp2p/interfaces/dialer' import type { DialerInit } from '@libp2p/interfaces/dialer'
import type { KeyChain } from './keychain/index.js' import type { KeyChain } from '@libp2p/interfaces/keychain'
export interface PersistentPeerStoreOptions { export interface PersistentPeerStoreOptions {
threshold?: number threshold?: number
@ -158,12 +158,6 @@ export interface Libp2p extends Startable, EventEmitter<Libp2pEvents> {
pubsub?: PubSub pubsub?: PubSub
dht?: DualDHT dht?: DualDHT
/**
* Load keychain keys from the datastore.
* Imports the private key as 'self', if needed.
*/
loadKeychain: () => Promise<void>
/** /**
* Get a deduplicated list of peer advertising multiaddrs by concatenating * Get a deduplicated list of peer advertising multiaddrs by concatenating
* the listen addresses used by transports with any configured * the listen addresses used by transports with any configured

View File

@ -8,7 +8,7 @@ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string' import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import { codes } from '../errors.js' import { codes } from '../errors.js'
import { logger } from '@libp2p/logger' import { logger } from '@libp2p/logger'
import type { KeyChain } from './index.js' import type { DefaultKeyChain } from './index.js'
const log = logger('libp2p:keychain:cms') const log = logger('libp2p:keychain:cms')
@ -24,12 +24,12 @@ const privates = new WeakMap<object, { dek: string }>()
* See RFC 5652 for all the details. * See RFC 5652 for all the details.
*/ */
export class CMS { export class CMS {
private readonly keychain: KeyChain private readonly keychain: DefaultKeyChain
/** /**
* Creates a new instance with a keychain * Creates a new instance with a keychain
*/ */
constructor (keychain: KeyChain, dek: string) { constructor (keychain: DefaultKeyChain, dek: string) {
if (keychain == null) { if (keychain == null) {
throw errCode(new Error('keychain is required'), codes.ERR_KEYCHAIN_REQUIRED) throw errCode(new Error('keychain is required'), codes.ERR_KEYCHAIN_REQUIRED)
} }

View File

@ -14,6 +14,8 @@ import { generateKeyPair, importKey, unmarshalPrivateKey } from '@libp2p/crypto/
import type { PeerId } from '@libp2p/interfaces/peer-id' import type { PeerId } from '@libp2p/interfaces/peer-id'
import type { Components } from '@libp2p/interfaces/components' import type { Components } from '@libp2p/interfaces/components'
import { pbkdf2, randomBytes } from '@libp2p/crypto' import { pbkdf2, randomBytes } from '@libp2p/crypto'
import type { KeyChain } from '@libp2p/interfaces/keychain'
import type { Startable } from '@libp2p/interfaces'
const log = logger('libp2p:keychain') const log = logger('libp2p:keychain')
@ -111,9 +113,10 @@ function DsInfoName (name: string) {
* - '/pkcs8/*key-name*', contains the PKCS #8 for the key * - '/pkcs8/*key-name*', contains the PKCS #8 for the key
* *
*/ */
export class KeyChain { export class DefaultKeyChain implements KeyChain, Startable {
private readonly components: Components private readonly components: Components
private init: KeyChainInit private init: KeyChainInit
private started: boolean
/** /**
* Creates a new instance of a key chain * Creates a new instance of a key chain
@ -146,6 +149,27 @@ export class KeyChain {
: '' : ''
privates.set(this, { dek }) 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
} }
/** /**

View File

@ -12,7 +12,7 @@ import { AutoDialler } from './connection-manager/auto-dialler.js'
import { Circuit } from './circuit/transport.js' import { Circuit } from './circuit/transport.js'
import { Relay } from './circuit/index.js' import { Relay } from './circuit/index.js'
import { DefaultDialer } from './dialer/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 { DefaultMetrics } from './metrics/index.js'
import { DefaultTransportManager } from './transport-manager.js' import { DefaultTransportManager } from './transport-manager.js'
import { DefaultUpgrader } from './upgrader.js' import { DefaultUpgrader } from './upgrader.js'
@ -44,6 +44,7 @@ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import errCode from 'err-code' import errCode from 'err-code'
import { unmarshalPublicKey } from '@libp2p/crypto/keys' import { unmarshalPublicKey } from '@libp2p/crypto/keys'
import type { Metrics } from '@libp2p/interfaces/metrics' import type { Metrics } from '@libp2p/interfaces/metrics'
import type { KeyChain } from '@libp2p/interfaces/keychain'
const log = logger('libp2p') const log = logger('libp2p')
@ -140,8 +141,8 @@ export class Libp2pNode extends EventEmitter<Libp2pEvents> implements Libp2p {
})) }))
// Create keychain // Create keychain
const keychainOpts = KeyChain.generateOptions() const keychainOpts = DefaultKeyChain.generateOptions()
this.keychain = this.configureComponent(new KeyChain(this.components, { this.keychain = this.configureComponent(new DefaultKeyChain(this.components, {
...keychainOpts, ...keychainOpts,
...init.keychain ...init.keychain
})) }))
@ -352,22 +353,6 @@ export class Libp2pNode extends EventEmitter<Libp2pEvents> implements Libp2p {
log('libp2p has stopped') 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 () { isStarted () {
return this.started return this.started
} }

View File

@ -5,17 +5,17 @@ import { expect } from 'aegir/chai'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string' import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import { MemoryDatastore } from 'datastore-core/memory' 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' import { Components } from '@libp2p/interfaces/components'
describe('cms interop', () => { describe('cms interop', () => {
const passPhrase = 'this is not a secure phrase' const passPhrase = 'this is not a secure phrase'
const aliceKeyName = 'cms-interop-alice' const aliceKeyName = 'cms-interop-alice'
let ks: KeyChain let ks: DefaultKeyChain
before(() => { before(() => {
const datastore = new MemoryDatastore() 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') const plainData = uint8ArrayFromString('This is a message from Alice to Bob')

View File

@ -7,7 +7,7 @@ import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import { createNode } from '../utils/creators/peer.js' import { createNode } from '../utils/creators/peer.js'
import { Key } from 'interface-datastore/key' import { Key } from 'interface-datastore/key'
import { MemoryDatastore } from 'datastore-core/memory' 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 { pbkdf2 } from '@libp2p/crypto'
import { Components } from '@libp2p/interfaces/components' import { Components } from '@libp2p/interfaces/components'
import type { Datastore } from 'interface-datastore' import type { Datastore } from 'interface-datastore'
@ -20,16 +20,16 @@ describe('keychain', () => {
const rsaKeyName = 'tajné jméno' const rsaKeyName = 'tajné jméno'
const renamedRsaKeyName = 'ชื่อลับ' const renamedRsaKeyName = 'ชื่อลับ'
let rsaKeyInfo: KeyInfo let rsaKeyInfo: KeyInfo
let emptyKeystore: KeyChain let emptyKeystore: DefaultKeyChain
let ks: KeyChain let ks: DefaultKeyChain
let datastore1: Datastore, datastore2: Datastore let datastore1: Datastore, datastore2: Datastore
before(async () => { before(async () => {
datastore1 = new MemoryDatastore() datastore1 = new MemoryDatastore()
datastore2 = new MemoryDatastore() datastore2 = new MemoryDatastore()
ks = new KeyChain(new Components({ datastore: datastore2 }), { pass: passPhrase }) ks = new DefaultKeyChain(new Components({ datastore: datastore2 }), { pass: passPhrase })
emptyKeystore = new KeyChain(new Components({ datastore: datastore1 }), { pass: passPhrase }) emptyKeystore = new DefaultKeyChain(new Components({ datastore: datastore1 }), { pass: passPhrase })
await datastore1.open() await datastore1.open()
await datastore2.open() await datastore2.open()
@ -41,35 +41,35 @@ describe('keychain', () => {
}) })
it('can start without a password', () => { 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', () => { 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', () => { it('has default options', () => {
expect(KeyChain.options).to.exist() expect(DefaultKeyChain.options).to.exist()
}) })
it('supports supported hashing alorithms', () => { 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() expect(ok).to.exist()
}) })
it('does not support unsupported hashing alorithms', () => { 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 () => { 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) expect(await keychain.listKeys()).to.have.lengthOf(0)
}) })
it('can find a key without a password', async () => { it('can find a key without a password', async () => {
const keychain = new KeyChain(new Components({ datastore: datastore2 }), {}) const keychain = new DefaultKeyChain(new Components({ datastore: datastore2 }), {})
const keychainWithPassword = new KeyChain(new Components({ datastore: datastore2 }), { pass: `hello-${Date.now()}-${Date.now()}` }) const keychainWithPassword = new DefaultKeyChain(new Components({ datastore: datastore2 }), { pass: `hello-${Date.now()}-${Date.now()}` })
const name = `key-${Math.random()}` const name = `key-${Math.random()}`
const { id } = await keychainWithPassword.createKey(name, 'Ed25519') const { id } = await keychainWithPassword.createKey(name, 'Ed25519')
@ -78,8 +78,8 @@ describe('keychain', () => {
}) })
it('can remove a key without a password', async () => { it('can remove a key without a password', async () => {
const keychainWithoutPassword = new KeyChain(new Components({ datastore: datastore2 }), {}) const keychainWithoutPassword = new DefaultKeyChain(new Components({ datastore: datastore2 }), {})
const keychainWithPassword = new KeyChain(new Components({ datastore: datastore2 }), { pass: `hello-${Date.now()}-${Date.now()}` }) const keychainWithPassword = new DefaultKeyChain(new Components({ datastore: datastore2 }), { pass: `hello-${Date.now()}-${Date.now()}` })
const name = `key-${Math.random()}` const name = `key-${Math.random()}`
expect(await keychainWithPassword.createKey(name, 'Ed25519')).to.have.property('name', name) 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 () => { 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 // @ts-expect-error invalid parameters
await expect(keychain.createKey(undefined, 'derp')).to.be.rejected() await expect(keychain.createKey(undefined, 'derp')).to.be.rejected()
}) })
it('can generate options', () => { it('can generate options', () => {
const options = KeyChain.generateOptions() const options = DefaultKeyChain.generateOptions()
options.pass = passPhrase 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() expect(chain).to.exist()
}) })
@ -418,7 +418,7 @@ describe('keychain', () => {
describe('rotate keychain passphrase', () => { describe('rotate keychain passphrase', () => {
let oldPass: string let oldPass: string
let kc: KeyChain let kc: DefaultKeyChain
let options: KeyChainInit let options: KeyChainInit
let ds: Datastore let ds: Datastore
before(async () => { before(async () => {
@ -433,7 +433,7 @@ describe('keychain', () => {
hash: 'sha2-512' hash: 'sha2-512'
} }
} }
kc = new KeyChain(new Components({ datastore: ds }), options) kc = new DefaultKeyChain(new Components({ datastore: ds }), options)
await ds.open() await ds.open()
}) })
@ -512,8 +512,6 @@ describe('libp2p.keychain', () => {
} }
}) })
await libp2p.loadKeychain()
const kInfo = await libp2p.keychain.createKey('keyName', 'Ed25519') const kInfo = await libp2p.keychain.createKey('keyName', 'Ed25519')
expect(kInfo).to.exist() expect(kInfo).to.exist()
}) })
@ -526,8 +524,6 @@ describe('libp2p.keychain', () => {
} }
}) })
await libp2p.loadKeychain()
const kInfo = await libp2p.keychain.createKey('keyName', 'Ed25519') const kInfo = await libp2p.keychain.createKey('keyName', 'Ed25519')
expect(kInfo).to.exist() expect(kInfo).to.exist()
}) })
@ -543,7 +539,6 @@ describe('libp2p.keychain', () => {
} }
} }
}) })
await libp2p.loadKeychain()
const kInfo = await libp2p.keychain.createKey('keyName', 'Ed25519') const kInfo = await libp2p.keychain.createKey('keyName', 'Ed25519')
expect(kInfo).to.exist() expect(kInfo).to.exist()
@ -558,7 +553,6 @@ describe('libp2p.keychain', () => {
} }
}) })
await libp2p2.loadKeychain()
const key = await libp2p2.keychain.findKeyByName('keyName') const key = await libp2p2.keychain.findKeyByName('keyName')
expect(key).to.exist() expect(key).to.exist()