Skip to content

Commit

Permalink
fix: allow reading PeerId from keychain (#1552)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
achingbrain committed Jan 17, 2023
1 parent b6fde93 commit 0831cd9
Show file tree
Hide file tree
Showing 2 changed files with 170 additions and 0 deletions.
25 changes: 25 additions & 0 deletions src/libp2p.ts
Expand Up @@ -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')

Expand Down Expand Up @@ -510,6 +511,30 @@ export class Libp2pNode extends EventEmitter<Libp2pEvents> implements Libp2p {
*/
export async function createLibp2pNode (options: Libp2pOptions): Promise<Libp2pNode> {
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()
}

Expand Down
145 changes: 145 additions & 0 deletions test/core/peer-id.spec.ts
@@ -0,0 +1,145 @@
/* 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('should create a PeerId if none is passed', async () => {
libp2p = await createLibp2p({
transports: [
webSockets()
],
connectionEncryption: [
plaintext()
]
})

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

0 comments on commit 0831cd9

Please sign in to comment.