Skip to content

Commit 0831cd9

Browse files
authored
fix: allow reading PeerId from keychain (#1552)
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.
1 parent b6fde93 commit 0831cd9

File tree

2 files changed

+170
-0
lines changed

2 files changed

+170
-0
lines changed

src/libp2p.ts

+25
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import { DummyPubSub } from './pubsub/dummy-pubsub.js'
4949
import { PeerSet } from '@libp2p/peer-collections'
5050
import { DefaultDialer } from './connection-manager/dialer/index.js'
5151
import { peerIdFromString } from '@libp2p/peer-id'
52+
import type { Datastore } from 'interface-datastore'
5253

5354
const log = logger('libp2p')
5455

@@ -510,6 +511,30 @@ export class Libp2pNode extends EventEmitter<Libp2pEvents> implements Libp2p {
510511
*/
511512
export async function createLibp2pNode (options: Libp2pOptions): Promise<Libp2pNode> {
512513
if (options.peerId == null) {
514+
const datastore = options.datastore as Datastore | undefined
515+
516+
if (datastore != null) {
517+
try {
518+
// try load the peer id from the keychain
519+
// @ts-expect-error missing the peer id property
520+
const keyChain = new KeyChain({
521+
datastore
522+
}, {
523+
...KeyChain.generateOptions(),
524+
...(options.keychain ?? {})
525+
})
526+
527+
options.peerId = await keyChain.exportPeerId('self')
528+
} catch (err: any) {
529+
if (err.code !== 'ERR_NOT_FOUND') {
530+
throw err
531+
}
532+
}
533+
}
534+
}
535+
536+
if (options.peerId == null) {
537+
// no peer id in the keychain, create a new peer id
513538
options.peerId = await createEd25519PeerId()
514539
}
515540

test/core/peer-id.spec.ts

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/* eslint-env mocha */
2+
3+
import { expect } from 'aegir/chai'
4+
import { webSockets } from '@libp2p/websockets'
5+
import { plaintext } from '../../src/insecure/index.js'
6+
import { createLibp2p, Libp2p } from '../../src/index.js'
7+
import { MemoryDatastore } from 'datastore-core'
8+
9+
describe('peer-id', () => {
10+
let libp2p: Libp2p
11+
12+
afterEach(async () => {
13+
if (libp2p != null) {
14+
await libp2p.stop()
15+
}
16+
})
17+
18+
it('should create a PeerId if none is passed', async () => {
19+
libp2p = await createLibp2p({
20+
transports: [
21+
webSockets()
22+
],
23+
connectionEncryption: [
24+
plaintext()
25+
]
26+
})
27+
28+
expect(libp2p.peerId).to.be.ok()
29+
})
30+
31+
it('should retrieve the PeerId from the datastore', async () => {
32+
const datastore = new MemoryDatastore()
33+
34+
libp2p = await createLibp2p({
35+
datastore,
36+
transports: [
37+
webSockets()
38+
],
39+
connectionEncryption: [
40+
plaintext()
41+
]
42+
})
43+
44+
// this PeerId was created by default
45+
const peerId = libp2p.peerId
46+
47+
await libp2p.stop()
48+
49+
// create a new node from the same datastore
50+
libp2p = await createLibp2p({
51+
datastore,
52+
transports: [
53+
webSockets()
54+
],
55+
connectionEncryption: [
56+
plaintext()
57+
]
58+
})
59+
60+
// the new node should have read the PeerId from the datastore
61+
// instead of creating a new one
62+
expect(libp2p.peerId.toString()).to.equal(peerId.toString())
63+
})
64+
65+
it('should retrieve the PeerId from the datastore with a keychain password', async () => {
66+
const datastore = new MemoryDatastore()
67+
const keychain = {
68+
pass: 'very-long-password-must-be-over-twenty-characters-long',
69+
dek: {
70+
salt: 'CpjNIxMqAZ+aJg+ezLfuzG4a'
71+
}
72+
}
73+
74+
libp2p = await createLibp2p({
75+
datastore,
76+
keychain,
77+
transports: [
78+
webSockets()
79+
],
80+
connectionEncryption: [
81+
plaintext()
82+
]
83+
})
84+
85+
// this PeerId was created by default
86+
const peerId = libp2p.peerId
87+
88+
await libp2p.stop()
89+
90+
// create a new node from the same datastore
91+
libp2p = await createLibp2p({
92+
datastore,
93+
keychain,
94+
transports: [
95+
webSockets()
96+
],
97+
connectionEncryption: [
98+
plaintext()
99+
]
100+
})
101+
102+
// the new node should have read the PeerId from the datastore
103+
// instead of creating a new one
104+
expect(libp2p.peerId.toString()).to.equal(peerId.toString())
105+
})
106+
107+
it('should fail to start if retrieving the PeerId from the datastore fails', async () => {
108+
const datastore = new MemoryDatastore()
109+
const keychain = {
110+
pass: 'very-long-password-must-be-over-twenty-characters-long',
111+
dek: {
112+
salt: 'CpjNIxMqAZ+aJg+ezLfuzG4a'
113+
}
114+
}
115+
116+
libp2p = await createLibp2p({
117+
datastore,
118+
keychain,
119+
transports: [
120+
webSockets()
121+
],
122+
connectionEncryption: [
123+
plaintext()
124+
]
125+
})
126+
await libp2p.stop()
127+
128+
// creating a new node from the same datastore but with the wrong keychain config should fail
129+
await expect(createLibp2p({
130+
datastore,
131+
keychain: {
132+
pass: 'different-very-long-password-must-be-over-twenty-characters-long',
133+
dek: {
134+
salt: 'different-CpjNIxMqAZ+aJg+ezLfuzG4a'
135+
}
136+
},
137+
transports: [
138+
webSockets()
139+
],
140+
connectionEncryption: [
141+
plaintext()
142+
]
143+
})).to.eventually.rejectedWith('Invalid PEM formatted message')
144+
})
145+
})

0 commit comments

Comments
 (0)