From 1b79b08be75b49af1ad3317289404e282fe8bc04 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Mon, 16 Jan 2023 09:01:37 +0000 Subject: [PATCH 1/2] fix: allow reading PeerId from keychain libp2p has a secure keychain where all items are stored in the datastore in an encrypted format, including the PeerId of the current node. If no PeerId is passed into the factory function, a new PeerId is created for the current node. Instead, if the factory function is passed a DataStore, it should try to read the PeerId from the DataStore and only create a new PeerId if reading the `self` key fails. --- src/libp2p.ts | 25 +++++++++ test/core/peer-id.spec.ts | 106 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 test/core/peer-id.spec.ts diff --git a/src/libp2p.ts b/src/libp2p.ts index a35316a293..00cdb23647 100644 --- a/src/libp2p.ts +++ b/src/libp2p.ts @@ -49,6 +49,7 @@ import { DummyPubSub } from './pubsub/dummy-pubsub.js' import { PeerSet } from '@libp2p/peer-collections' import { DefaultDialer } from './connection-manager/dialer/index.js' import { peerIdFromString } from '@libp2p/peer-id' +import type { Datastore } from 'interface-datastore' const log = logger('libp2p') @@ -510,6 +511,30 @@ export class Libp2pNode extends EventEmitter implements Libp2p { */ 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 + // @ts-expect-error missing the peer id property + const keyChain = new KeyChain({ + datastore + }, { + ...KeyChain.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() } diff --git a/test/core/peer-id.spec.ts b/test/core/peer-id.spec.ts new file mode 100644 index 0000000000..08ac58ec3c --- /dev/null +++ b/test/core/peer-id.spec.ts @@ -0,0 +1,106 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import { webSockets } from '@libp2p/websockets' +import { plaintext } from '../../src/insecure/index.js' +import { createLibp2p, Libp2p } from '../../src/index.js' +import { MemoryDatastore } from 'datastore-core' + +describe('peer-id', () => { + let libp2p: Libp2p + + afterEach(async () => { + if (libp2p != null) { + await libp2p.stop() + } + }) + + it('it should create a PeerId if none is passed', async () => { + libp2p = await createLibp2p({ + transports: [ + webSockets() + ], + connectionEncryption: [ + plaintext() + ] + }) + + expect(libp2p.peerId).to.be.ok() + }) + + it('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('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()) + }) +}) From ceb337401c5a23b437e5f057863a183c5f589149 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Mon, 16 Jan 2023 16:44:39 +0000 Subject: [PATCH 2/2] chore: add test for key decryption failure --- test/core/peer-id.spec.ts | 45 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/test/core/peer-id.spec.ts b/test/core/peer-id.spec.ts index 08ac58ec3c..0219d9ffc8 100644 --- a/test/core/peer-id.spec.ts +++ b/test/core/peer-id.spec.ts @@ -15,7 +15,7 @@ describe('peer-id', () => { } }) - it('it should create a PeerId if none is passed', async () => { + it('should create a PeerId if none is passed', async () => { libp2p = await createLibp2p({ transports: [ webSockets() @@ -28,7 +28,7 @@ describe('peer-id', () => { expect(libp2p.peerId).to.be.ok() }) - it('it should retrieve the PeerId from the datastore', async () => { + it('should retrieve the PeerId from the datastore', async () => { const datastore = new MemoryDatastore() libp2p = await createLibp2p({ @@ -62,7 +62,7 @@ describe('peer-id', () => { expect(libp2p.peerId.toString()).to.equal(peerId.toString()) }) - it('it should retrieve the PeerId from the datastore with a keychain password', async () => { + 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', @@ -103,4 +103,43 @@ describe('peer-id', () => { // 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') + }) })