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
### 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.

View File

@ -494,8 +494,6 @@ const node = await createLibp2p({
datastore: dsInstant,
}
})
await node.loadKeychain()
```
#### 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 { 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<Libp2pEvents> {
pubsub?: PubSub
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
* 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 { 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<object, { dek: string }>()
* 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)
}

View File

@ -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
}
/**

View File

@ -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<Libp2pEvents> 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<Libp2pEvents> 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
}

View File

@ -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')

View File

@ -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()