@libp2p/webrtc 5.2.9 → 5.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -0
- package/dist/index.min.js +153 -18
- package/dist/src/constants.d.ts +20 -0
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +20 -0
- package/dist/src/constants.js.map +1 -1
- package/dist/src/index.d.ts +54 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +54 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/private-to-private/util.d.ts +2 -2
- package/dist/src/private-to-private/util.d.ts.map +1 -1
- package/dist/src/private-to-public/listener.d.ts +9 -4
- package/dist/src/private-to-public/listener.d.ts.map +1 -1
- package/dist/src/private-to-public/listener.js +7 -18
- package/dist/src/private-to-public/listener.js.map +1 -1
- package/dist/src/private-to-public/transport.d.ts +67 -5
- package/dist/src/private-to-public/transport.d.ts.map +1 -1
- package/dist/src/private-to-public/transport.js +181 -2
- package/dist/src/private-to-public/transport.js.map +1 -1
- package/dist/src/private-to-public/utils/pem.d.ts +6 -0
- package/dist/src/private-to-public/utils/pem.d.ts.map +1 -0
- package/dist/src/private-to-public/utils/pem.js +15 -0
- package/dist/src/private-to-public/utils/pem.js.map +1 -0
- package/package.json +12 -9
- package/src/constants.ts +25 -0
- package/src/index.ts +54 -0
- package/src/private-to-private/util.ts +2 -2
- package/src/private-to-public/listener.ts +17 -26
- package/src/private-to-public/transport.ts +281 -7
- package/src/private-to-public/utils/pem.ts +18 -0
@@ -1,20 +1,28 @@
|
|
1
|
-
import {
|
1
|
+
import { generateKeyPair, privateKeyToCryptoKeyPair } from '@libp2p/crypto/keys'
|
2
|
+
import { InvalidParametersError, NotFoundError, NotStartedError, TypedEventEmitter, serviceCapabilities, transportSymbol } from '@libp2p/interface'
|
2
3
|
import { peerIdFromString } from '@libp2p/peer-id'
|
3
4
|
import { WebRTCDirect } from '@multiformats/multiaddr-matcher'
|
5
|
+
import { BasicConstraintsExtension, X509Certificate, X509CertificateGenerator } from '@peculiar/x509'
|
6
|
+
import { Key } from 'interface-datastore'
|
7
|
+
import { base64url } from 'multiformats/bases/base64'
|
8
|
+
import { sha256 } from 'multiformats/hashes/sha2'
|
4
9
|
import { raceSignal } from 'race-signal'
|
10
|
+
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
|
11
|
+
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
|
12
|
+
import { DEFAULT_CERTIFICATE_DATASTORE_KEY, DEFAULT_CERTIFICATE_LIFESPAN, DEFAULT_CERTIFICATE_PRIVATE_KEY_NAME, DEFAULT_CERTIFICATE_RENEWAL_THRESHOLD } from '../constants.js'
|
5
13
|
import { genUfrag } from '../util.js'
|
6
14
|
import { WebRTCDirectListener } from './listener.js'
|
7
15
|
import { connect } from './utils/connect.js'
|
8
16
|
import { createDialerRTCPeerConnection } from './utils/get-rtcpeerconnection.js'
|
17
|
+
import { formatAsPem } from './utils/pem.js'
|
9
18
|
import type { DataChannelOptions, TransportCertificate } from '../index.js'
|
10
19
|
import type { WebRTCDialEvents } from '../private-to-private/transport.js'
|
11
|
-
import type { CreateListenerOptions, Transport, Listener, ComponentLogger, Logger, Connection, CounterGroup, Metrics, PeerId, DialTransportOptions, PrivateKey, Upgrader } from '@libp2p/interface'
|
20
|
+
import type { CreateListenerOptions, Transport, Listener, ComponentLogger, Logger, Connection, CounterGroup, Metrics, PeerId, DialTransportOptions, PrivateKey, Upgrader, Startable, TypedEventTarget } from '@libp2p/interface'
|
12
21
|
import type { TransportManager } from '@libp2p/interface-internal'
|
22
|
+
import type { Keychain } from '@libp2p/keychain'
|
13
23
|
import type { Multiaddr } from '@multiformats/multiaddr'
|
24
|
+
import type { Datastore } from 'interface-datastore'
|
14
25
|
|
15
|
-
/**
|
16
|
-
* The peer for this transport
|
17
|
-
*/
|
18
26
|
export interface WebRTCDirectTransportComponents {
|
19
27
|
peerId: PeerId
|
20
28
|
privateKey: PrivateKey
|
@@ -22,6 +30,8 @@ export interface WebRTCDirectTransportComponents {
|
|
22
30
|
logger: ComponentLogger
|
23
31
|
transportManager: TransportManager
|
24
32
|
upgrader: Upgrader
|
33
|
+
keychain?: Keychain
|
34
|
+
datastore: Datastore
|
25
35
|
}
|
26
36
|
|
27
37
|
export interface WebRTCMetrics {
|
@@ -29,26 +39,90 @@ export interface WebRTCMetrics {
|
|
29
39
|
}
|
30
40
|
|
31
41
|
export interface WebRTCTransportDirectInit {
|
42
|
+
/**
|
43
|
+
* The default configuration used by all created RTCPeerConnections
|
44
|
+
*/
|
32
45
|
rtcConfiguration?: RTCConfiguration | (() => RTCConfiguration | Promise<RTCConfiguration>)
|
46
|
+
|
47
|
+
/**
|
48
|
+
* The default configuration used by all created RTCDataChannels
|
49
|
+
*/
|
33
50
|
dataChannel?: DataChannelOptions
|
51
|
+
|
52
|
+
/**
|
53
|
+
* @deprecated use `certificate` instead - this option will be removed in a future release
|
54
|
+
*/
|
34
55
|
certificates?: TransportCertificate[]
|
35
56
|
|
57
|
+
/**
|
58
|
+
* Use an existing TLS certificate to secure incoming connections or supply
|
59
|
+
* settings to generate one.
|
60
|
+
*
|
61
|
+
* This must be an ECDSA certificate using the P-256 curve.
|
62
|
+
*
|
63
|
+
* From our testing we find that P-256 elliptic curve is supported by Pion,
|
64
|
+
* webrtc-rs, as well as Chromium (P-228 and P-384 was not supported in
|
65
|
+
* Chromium).
|
66
|
+
*/
|
67
|
+
certificate?: TransportCertificate
|
68
|
+
|
36
69
|
/**
|
37
70
|
* @deprecated this setting is ignored and will be removed in a future release
|
38
71
|
*/
|
39
72
|
useLibjuice?: boolean
|
73
|
+
|
74
|
+
/**
|
75
|
+
* The key the certificate is stored in the datastore under
|
76
|
+
*
|
77
|
+
* @default '/libp2p/webrtc-direct/certificate'
|
78
|
+
*/
|
79
|
+
certificateDatastoreKey?: string
|
80
|
+
|
81
|
+
/**
|
82
|
+
* The name the certificate private key is stored in the keychain with
|
83
|
+
*
|
84
|
+
* @default 'webrtc-direct-certificate-private-key'
|
85
|
+
*/
|
86
|
+
certificateKeychainName?: string
|
87
|
+
|
88
|
+
/**
|
89
|
+
* Number of ms a certificate should be valid for (defaults to 14 days)
|
90
|
+
*
|
91
|
+
* @default 2_592_000_000
|
92
|
+
*/
|
93
|
+
certificateLifespan?: number
|
94
|
+
|
95
|
+
/**
|
96
|
+
* Certificates will be renewed this many ms before expiry (defaults to 1 day)
|
97
|
+
*
|
98
|
+
* @default 86_400_000
|
99
|
+
*/
|
100
|
+
certificateRenewalThreshold?: number
|
101
|
+
}
|
102
|
+
|
103
|
+
export interface WebRTCDirectTransportCertificateEvents {
|
104
|
+
'certificate:renew': CustomEvent<TransportCertificate>
|
40
105
|
}
|
41
106
|
|
42
|
-
export class WebRTCDirectTransport implements Transport {
|
107
|
+
export class WebRTCDirectTransport implements Transport, Startable {
|
43
108
|
private readonly log: Logger
|
44
109
|
private readonly metrics?: WebRTCMetrics
|
45
110
|
private readonly components: WebRTCDirectTransportComponents
|
46
111
|
private readonly init: WebRTCTransportDirectInit
|
112
|
+
private certificate?: TransportCertificate
|
113
|
+
private privateKey?: PrivateKey
|
114
|
+
private readonly emitter: TypedEventTarget<WebRTCDirectTransportCertificateEvents>
|
115
|
+
private renewCertificateTask?: ReturnType<typeof setTimeout>
|
47
116
|
|
48
117
|
constructor (components: WebRTCDirectTransportComponents, init: WebRTCTransportDirectInit = {}) {
|
49
118
|
this.log = components.logger.forComponent('libp2p:webrtc-direct')
|
50
119
|
this.components = components
|
51
120
|
this.init = init
|
121
|
+
this.emitter = new TypedEventEmitter()
|
122
|
+
|
123
|
+
if (init.certificateLifespan != null && init.certificateRenewalThreshold != null && init.certificateRenewalThreshold >= init.certificateLifespan) {
|
124
|
+
throw new InvalidParametersError('Certificate renewal threshold must be less than certificate lifespan')
|
125
|
+
}
|
52
126
|
|
53
127
|
if (components.metrics != null) {
|
54
128
|
this.metrics = {
|
@@ -68,6 +142,18 @@ export class WebRTCDirectTransport implements Transport {
|
|
68
142
|
'@libp2p/transport'
|
69
143
|
]
|
70
144
|
|
145
|
+
async start (): Promise<void> {
|
146
|
+
this.certificate = await this.getCertificate()
|
147
|
+
}
|
148
|
+
|
149
|
+
async stop (): Promise<void> {
|
150
|
+
if (this.renewCertificateTask != null) {
|
151
|
+
clearTimeout(this.renewCertificateTask)
|
152
|
+
}
|
153
|
+
|
154
|
+
this.certificate = undefined
|
155
|
+
}
|
156
|
+
|
71
157
|
/**
|
72
158
|
* Dial a given multiaddr
|
73
159
|
*/
|
@@ -81,9 +167,15 @@ export class WebRTCDirectTransport implements Transport {
|
|
81
167
|
* Create transport listeners no supported by browsers
|
82
168
|
*/
|
83
169
|
createListener (options: CreateListenerOptions): Listener {
|
170
|
+
if (this.certificate == null) {
|
171
|
+
throw new NotStartedError()
|
172
|
+
}
|
173
|
+
|
84
174
|
return new WebRTCDirectListener(this.components, {
|
85
175
|
...this.init,
|
86
|
-
...options
|
176
|
+
...options,
|
177
|
+
certificate: this.certificate,
|
178
|
+
emitter: this.emitter
|
87
179
|
})
|
88
180
|
}
|
89
181
|
|
@@ -139,4 +231,186 @@ export class WebRTCDirectTransport implements Transport {
|
|
139
231
|
throw err
|
140
232
|
}
|
141
233
|
}
|
234
|
+
|
235
|
+
private async getCertificate (forceRenew?: boolean): Promise<TransportCertificate> {
|
236
|
+
if (isTransportCertificate(this.init.certificate)) {
|
237
|
+
this.log('using provided TLS certificate')
|
238
|
+
return this.init.certificate
|
239
|
+
}
|
240
|
+
|
241
|
+
const privateKey = await this.loadOrCreatePrivateKey()
|
242
|
+
const { pem, certhash } = await this.loadOrCreateCertificate(privateKey, forceRenew)
|
243
|
+
|
244
|
+
return {
|
245
|
+
privateKey: await formatAsPem(privateKey),
|
246
|
+
pem,
|
247
|
+
certhash
|
248
|
+
}
|
249
|
+
}
|
250
|
+
|
251
|
+
private async loadOrCreatePrivateKey (): Promise<PrivateKey> {
|
252
|
+
if (this.privateKey != null) {
|
253
|
+
return this.privateKey
|
254
|
+
}
|
255
|
+
|
256
|
+
const keychainName = this.init.certificateKeychainName ?? DEFAULT_CERTIFICATE_PRIVATE_KEY_NAME
|
257
|
+
const keychain = this.getKeychain()
|
258
|
+
|
259
|
+
try {
|
260
|
+
if (keychain == null) {
|
261
|
+
this.log('no keychain configured - not checking for stored private key')
|
262
|
+
throw new NotFoundError()
|
263
|
+
}
|
264
|
+
|
265
|
+
this.log.trace('checking for stored private key')
|
266
|
+
this.privateKey = await keychain.exportKey(keychainName)
|
267
|
+
} catch (err: any) {
|
268
|
+
if (err.name !== 'NotFoundError') {
|
269
|
+
throw err
|
270
|
+
}
|
271
|
+
|
272
|
+
this.log.trace('generating private key')
|
273
|
+
this.privateKey = await generateKeyPair('ECDSA', 'P-256')
|
274
|
+
|
275
|
+
if (keychain != null) {
|
276
|
+
this.log.trace('storing private key')
|
277
|
+
await keychain.importKey(keychainName, this.privateKey)
|
278
|
+
} else {
|
279
|
+
this.log('no keychain configured - not storing private key')
|
280
|
+
}
|
281
|
+
}
|
282
|
+
|
283
|
+
return this.privateKey
|
284
|
+
}
|
285
|
+
|
286
|
+
private async loadOrCreateCertificate (privateKey: PrivateKey, forceRenew?: boolean): Promise<{ pem: string, certhash: string }> {
|
287
|
+
if (this.certificate != null && forceRenew !== true) {
|
288
|
+
return this.certificate
|
289
|
+
}
|
290
|
+
|
291
|
+
let cert: X509Certificate
|
292
|
+
const dsKey = new Key(this.init.certificateDatastoreKey ?? DEFAULT_CERTIFICATE_DATASTORE_KEY)
|
293
|
+
const keyPair = await privateKeyToCryptoKeyPair(privateKey)
|
294
|
+
|
295
|
+
try {
|
296
|
+
if (forceRenew === true) {
|
297
|
+
this.log.trace('forcing renewal of TLS certificate')
|
298
|
+
throw new NotFoundError()
|
299
|
+
}
|
300
|
+
|
301
|
+
this.log.trace('checking for stored TLS certificate')
|
302
|
+
cert = await this.loadCertificate(dsKey, keyPair)
|
303
|
+
} catch (err: any) {
|
304
|
+
if (err.name !== 'NotFoundError') {
|
305
|
+
throw err
|
306
|
+
}
|
307
|
+
|
308
|
+
this.log.trace('generating new TLS certificate')
|
309
|
+
cert = await this.createCertificate(dsKey, keyPair)
|
310
|
+
}
|
311
|
+
|
312
|
+
// set timeout to renew certificate
|
313
|
+
let renewTime = (cert.notAfter.getTime() - (this.init.certificateRenewalThreshold ?? DEFAULT_CERTIFICATE_RENEWAL_THRESHOLD)) - Date.now()
|
314
|
+
|
315
|
+
if (renewTime < 0) {
|
316
|
+
renewTime = 100
|
317
|
+
}
|
318
|
+
|
319
|
+
this.log('will renew TLS certificate after %d ms', renewTime)
|
320
|
+
|
321
|
+
this.renewCertificateTask = setTimeout(() => {
|
322
|
+
this.log('renewing TLS certificate')
|
323
|
+
this.getCertificate(true)
|
324
|
+
.then(cert => {
|
325
|
+
this.certificate = cert
|
326
|
+
this.emitter.safeDispatchEvent('certificate:renew', {
|
327
|
+
detail: cert
|
328
|
+
})
|
329
|
+
})
|
330
|
+
.catch(err => {
|
331
|
+
this.log.error('could not renew certificate - %e', err)
|
332
|
+
})
|
333
|
+
}, renewTime)
|
334
|
+
|
335
|
+
return {
|
336
|
+
pem: cert.toString('pem'),
|
337
|
+
certhash: base64url.encode((await sha256.digest(new Uint8Array(cert.rawData))).bytes)
|
338
|
+
}
|
339
|
+
}
|
340
|
+
|
341
|
+
async loadCertificate (dsKey: Key, keyPair: CryptoKeyPair): Promise<X509Certificate> {
|
342
|
+
const buf = await this.components.datastore.get(dsKey)
|
343
|
+
const cert = new X509Certificate(buf)
|
344
|
+
|
345
|
+
// check expiry date
|
346
|
+
const expiryTime = cert.notAfter.getTime() - (this.init.certificateRenewalThreshold ?? DEFAULT_CERTIFICATE_RENEWAL_THRESHOLD)
|
347
|
+
|
348
|
+
if (Date.now() > expiryTime) {
|
349
|
+
this.log('stored TLS certificate has expired')
|
350
|
+
// act as if no certificate was present
|
351
|
+
throw new NotFoundError()
|
352
|
+
}
|
353
|
+
|
354
|
+
this.log('loaded certificate, expires in %d ms', expiryTime)
|
355
|
+
|
356
|
+
// check public keys match
|
357
|
+
const exportedCertKey = await cert.publicKey.export(crypto)
|
358
|
+
const rawCertKey = await crypto.subtle.exportKey('raw', exportedCertKey)
|
359
|
+
const rawKeyPairKey = await crypto.subtle.exportKey('raw', keyPair.publicKey)
|
360
|
+
|
361
|
+
if (!uint8ArrayEquals(
|
362
|
+
new Uint8Array(rawCertKey, 0, rawCertKey.byteLength),
|
363
|
+
new Uint8Array(rawKeyPairKey, 0, rawKeyPairKey.byteLength)
|
364
|
+
)) {
|
365
|
+
this.log('stored TLS certificate public key did not match public key from private key')
|
366
|
+
throw new NotFoundError()
|
367
|
+
}
|
368
|
+
|
369
|
+
this.log('loaded certificate, expiry time is %o', expiryTime)
|
370
|
+
|
371
|
+
return cert
|
372
|
+
}
|
373
|
+
|
374
|
+
async createCertificate (dsKey: Key, keyPair: CryptoKeyPair): Promise<X509Certificate> {
|
375
|
+
const notBefore = new Date()
|
376
|
+
const notAfter = new Date(Date.now() + (this.init.certificateLifespan ?? DEFAULT_CERTIFICATE_LIFESPAN))
|
377
|
+
|
378
|
+
// have to set ms to 0 to work around https://github.com/PeculiarVentures/x509/issues/73
|
379
|
+
notBefore.setMilliseconds(0)
|
380
|
+
notAfter.setMilliseconds(0)
|
381
|
+
|
382
|
+
const cert = await X509CertificateGenerator.createSelfSigned({
|
383
|
+
serialNumber: (BigInt(Math.random().toString().replace('.', '')) * 100000n).toString(16),
|
384
|
+
name: 'CN=example.com, C=US, L=CA, O=example, ST=CA',
|
385
|
+
notBefore,
|
386
|
+
notAfter,
|
387
|
+
keys: keyPair,
|
388
|
+
extensions: [
|
389
|
+
new BasicConstraintsExtension(false, undefined, true)
|
390
|
+
]
|
391
|
+
}, crypto)
|
392
|
+
|
393
|
+
if (this.getKeychain() != null) {
|
394
|
+
this.log.trace('storing TLS certificate')
|
395
|
+
await this.components.datastore.put(dsKey, uint8ArrayFromString(cert.toString('pem')))
|
396
|
+
} else {
|
397
|
+
this.log('no keychain is configured so not storing TLS certificate since the private key will not be reused')
|
398
|
+
}
|
399
|
+
|
400
|
+
return cert
|
401
|
+
}
|
402
|
+
|
403
|
+
private getKeychain (): Keychain | undefined {
|
404
|
+
try {
|
405
|
+
return this.components.keychain
|
406
|
+
} catch {}
|
407
|
+
}
|
408
|
+
}
|
409
|
+
|
410
|
+
function isTransportCertificate (obj?: any): obj is TransportCertificate {
|
411
|
+
if (obj == null) {
|
412
|
+
return false
|
413
|
+
}
|
414
|
+
|
415
|
+
return typeof obj.privateKey === 'string' && typeof obj.pem === 'string' && typeof obj.certhash === 'string'
|
142
416
|
}
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import { privateKeyToCryptoKeyPair } from '@libp2p/crypto/keys'
|
2
|
+
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
|
3
|
+
import type { PrivateKey } from '@libp2p/interface'
|
4
|
+
|
5
|
+
export function toBuffer (uint8Array: Uint8Array): Buffer {
|
6
|
+
return Buffer.from(uint8Array.buffer, uint8Array.byteOffset, uint8Array.byteLength)
|
7
|
+
}
|
8
|
+
|
9
|
+
export async function formatAsPem (privateKey: PrivateKey): Promise<string> {
|
10
|
+
const keyPair = await privateKeyToCryptoKeyPair(privateKey)
|
11
|
+
const exported = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey)
|
12
|
+
|
13
|
+
return [
|
14
|
+
'-----BEGIN PRIVATE KEY-----',
|
15
|
+
...uint8ArrayToString(new Uint8Array(exported), 'base64pad').split(/(.{64})/).filter(Boolean),
|
16
|
+
'-----END PRIVATE KEY-----'
|
17
|
+
].join('\n')
|
18
|
+
}
|