@libp2p/webrtc 5.2.9-0b9090aea → 5.2.9-4c64bd06d
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 +16 -0
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +16 -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-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 +6 -18
- package/dist/src/private-to-public/listener.js.map +1 -1
- package/dist/src/private-to-public/transport.d.ts +66 -5
- package/dist/src/private-to-public/transport.d.ts.map +1 -1
- package/dist/src/private-to-public/transport.js +149 -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 +11 -8
- package/src/constants.ts +20 -0
- package/src/index.ts +54 -0
- package/src/private-to-public/listener.ts +16 -26
- package/src/private-to-public/transport.ts +242 -7
- package/src/private-to-public/utils/pem.ts +18 -0
|
@@ -1,20 +1,30 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { generateKeyPair, privateKeyToCryptoKeyPair } from '@libp2p/crypto/keys'
|
|
2
|
+
import { 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 } 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'
|
|
25
|
+
|
|
26
|
+
const ONE_DAY_MS = 86_400_000
|
|
14
27
|
|
|
15
|
-
/**
|
|
16
|
-
* The peer for this transport
|
|
17
|
-
*/
|
|
18
28
|
export interface WebRTCDirectTransportComponents {
|
|
19
29
|
peerId: PeerId
|
|
20
30
|
privateKey: PrivateKey
|
|
@@ -22,6 +32,8 @@ export interface WebRTCDirectTransportComponents {
|
|
|
22
32
|
logger: ComponentLogger
|
|
23
33
|
transportManager: TransportManager
|
|
24
34
|
upgrader: Upgrader
|
|
35
|
+
keychain?: Keychain
|
|
36
|
+
datastore: Datastore
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
export interface WebRTCMetrics {
|
|
@@ -29,26 +41,85 @@ export interface WebRTCMetrics {
|
|
|
29
41
|
}
|
|
30
42
|
|
|
31
43
|
export interface WebRTCTransportDirectInit {
|
|
44
|
+
/**
|
|
45
|
+
* The default configuration used by all created RTCPeerConnections
|
|
46
|
+
*/
|
|
32
47
|
rtcConfiguration?: RTCConfiguration | (() => RTCConfiguration | Promise<RTCConfiguration>)
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The default configuration used by all created RTCDataChannels
|
|
51
|
+
*/
|
|
33
52
|
dataChannel?: DataChannelOptions
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @deprecated use `certificate` instead - this option will be removed in a future release
|
|
56
|
+
*/
|
|
34
57
|
certificates?: TransportCertificate[]
|
|
35
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Use an existing TLS certificate to secure incoming connections or supply
|
|
61
|
+
* settings to generate one.
|
|
62
|
+
*
|
|
63
|
+
* This must be an ECDSA certificate using the P-256 curve.
|
|
64
|
+
*
|
|
65
|
+
* From our testing we find that P-256 elliptic curve is supported by Pion,
|
|
66
|
+
* webrtc-rs, as well as Chromium (P-228 and P-384 was not supported in
|
|
67
|
+
* Chromium).
|
|
68
|
+
*/
|
|
69
|
+
certificate?: TransportCertificate
|
|
70
|
+
|
|
36
71
|
/**
|
|
37
72
|
* @deprecated this setting is ignored and will be removed in a future release
|
|
38
73
|
*/
|
|
39
74
|
useLibjuice?: boolean
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The key the certificate is stored in the datastore under
|
|
78
|
+
*
|
|
79
|
+
* @default '/libp2p/webrtc-direct/certificate'
|
|
80
|
+
*/
|
|
81
|
+
certificateDatastoreKey?: string
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The name the certificate private key is stored in the keychain with
|
|
85
|
+
*
|
|
86
|
+
* @default 'webrtc-direct-certificate-private-key'
|
|
87
|
+
*/
|
|
88
|
+
certificateKeychainName?: string
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Number of days a certificate should be valid for
|
|
92
|
+
*
|
|
93
|
+
* @default 365
|
|
94
|
+
*/
|
|
95
|
+
certificateLifespan?: number
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Certificates will be renewed this many days before their expiry
|
|
99
|
+
*
|
|
100
|
+
* @default 5
|
|
101
|
+
*/
|
|
102
|
+
certificateRenewalThreshold?: number
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface WebRTCDirectTransportCertificateEvents {
|
|
106
|
+
'certificate:renew': CustomEvent<TransportCertificate>
|
|
40
107
|
}
|
|
41
108
|
|
|
42
|
-
export class WebRTCDirectTransport implements Transport {
|
|
109
|
+
export class WebRTCDirectTransport implements Transport, Startable {
|
|
43
110
|
private readonly log: Logger
|
|
44
111
|
private readonly metrics?: WebRTCMetrics
|
|
45
112
|
private readonly components: WebRTCDirectTransportComponents
|
|
46
113
|
private readonly init: WebRTCTransportDirectInit
|
|
114
|
+
private certificate?: TransportCertificate
|
|
115
|
+
private privateKey?: PrivateKey
|
|
116
|
+
private readonly emitter: TypedEventTarget<WebRTCDirectTransportCertificateEvents>
|
|
47
117
|
|
|
48
118
|
constructor (components: WebRTCDirectTransportComponents, init: WebRTCTransportDirectInit = {}) {
|
|
49
119
|
this.log = components.logger.forComponent('libp2p:webrtc-direct')
|
|
50
120
|
this.components = components
|
|
51
121
|
this.init = init
|
|
122
|
+
this.emitter = new TypedEventEmitter()
|
|
52
123
|
|
|
53
124
|
if (components.metrics != null) {
|
|
54
125
|
this.metrics = {
|
|
@@ -68,6 +139,14 @@ export class WebRTCDirectTransport implements Transport {
|
|
|
68
139
|
'@libp2p/transport'
|
|
69
140
|
]
|
|
70
141
|
|
|
142
|
+
async start (): Promise<void> {
|
|
143
|
+
this.certificate = await this.getCertificate()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async stop (): Promise<void> {
|
|
147
|
+
|
|
148
|
+
}
|
|
149
|
+
|
|
71
150
|
/**
|
|
72
151
|
* Dial a given multiaddr
|
|
73
152
|
*/
|
|
@@ -81,9 +160,15 @@ export class WebRTCDirectTransport implements Transport {
|
|
|
81
160
|
* Create transport listeners no supported by browsers
|
|
82
161
|
*/
|
|
83
162
|
createListener (options: CreateListenerOptions): Listener {
|
|
163
|
+
if (this.certificate == null) {
|
|
164
|
+
throw new NotStartedError()
|
|
165
|
+
}
|
|
166
|
+
|
|
84
167
|
return new WebRTCDirectListener(this.components, {
|
|
85
168
|
...this.init,
|
|
86
|
-
...options
|
|
169
|
+
...options,
|
|
170
|
+
certificate: this.certificate,
|
|
171
|
+
emitter: this.emitter
|
|
87
172
|
})
|
|
88
173
|
}
|
|
89
174
|
|
|
@@ -139,4 +224,154 @@ export class WebRTCDirectTransport implements Transport {
|
|
|
139
224
|
throw err
|
|
140
225
|
}
|
|
141
226
|
}
|
|
227
|
+
|
|
228
|
+
private async getCertificate (): Promise<TransportCertificate> {
|
|
229
|
+
if (isTransportCertificate(this.init.certificate)) {
|
|
230
|
+
this.log.trace('using provided TLS certificate')
|
|
231
|
+
return this.init.certificate
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const privateKey = await this.loadOrCreatePrivateKey()
|
|
235
|
+
const { pem, certhash } = await this.loadOrCreateCertificate(privateKey)
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
privateKey: await formatAsPem(privateKey),
|
|
239
|
+
pem,
|
|
240
|
+
certhash
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private async loadOrCreatePrivateKey (): Promise<PrivateKey> {
|
|
245
|
+
if (this.privateKey != null) {
|
|
246
|
+
return this.privateKey
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const keychainName = this.init.certificateKeychainName ?? DEFAULT_CERTIFICATE_PRIVATE_KEY_NAME
|
|
250
|
+
const keychain = this.getKeychain()
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
if (keychain == null) {
|
|
254
|
+
this.log('no keychain configured - not checking for stored private key')
|
|
255
|
+
throw new NotFoundError()
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
this.log.trace('checking for stored private key')
|
|
259
|
+
this.privateKey = await keychain.exportKey(keychainName)
|
|
260
|
+
} catch (err: any) {
|
|
261
|
+
if (err.name !== 'NotFoundError') {
|
|
262
|
+
throw err
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
this.log.trace('generating private key')
|
|
266
|
+
this.privateKey = await generateKeyPair('ECDSA', 'P-256')
|
|
267
|
+
|
|
268
|
+
if (keychain != null) {
|
|
269
|
+
this.log.trace('storing private key')
|
|
270
|
+
await keychain.importKey(keychainName, this.privateKey)
|
|
271
|
+
} else {
|
|
272
|
+
this.log('no keychain configured - not storing private key')
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return this.privateKey
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private async loadOrCreateCertificate (privateKey: PrivateKey): Promise<{ pem: string, certhash: string }> {
|
|
280
|
+
if (this.certificate != null) {
|
|
281
|
+
return this.certificate
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
let cert: X509Certificate
|
|
285
|
+
const dsKey = new Key(this.init.certificateDatastoreKey ?? DEFAULT_CERTIFICATE_DATASTORE_KEY)
|
|
286
|
+
const keyPair = await privateKeyToCryptoKeyPair(privateKey)
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
this.log.trace('checking for stored TLS certificate')
|
|
290
|
+
cert = await this.loadCertificate(dsKey, keyPair)
|
|
291
|
+
} catch (err: any) {
|
|
292
|
+
if (err.name !== 'NotFoundError') {
|
|
293
|
+
throw err
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
this.log('generating TLS certificate using private key')
|
|
297
|
+
cert = await this.createCertificate(dsKey, keyPair)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
pem: cert.toString('pem'),
|
|
302
|
+
certhash: base64url.encode((await sha256.digest(new Uint8Array(cert.rawData))).bytes)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async loadCertificate (dsKey: Key, keyPair: CryptoKeyPair): Promise<X509Certificate> {
|
|
307
|
+
const buf = await this.components.datastore.get(dsKey)
|
|
308
|
+
const cert = new X509Certificate(buf)
|
|
309
|
+
|
|
310
|
+
// check expiry date
|
|
311
|
+
const threshold = Date.now() - ((this.init.certificateLifespan ?? DEFAULT_CERTIFICATE_LIFESPAN) * ONE_DAY_MS)
|
|
312
|
+
|
|
313
|
+
if (cert.notAfter.getTime() < threshold) {
|
|
314
|
+
this.log('stored TLS certificate has expired')
|
|
315
|
+
// act as if no certificate was present
|
|
316
|
+
throw new NotFoundError()
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// check public keys match
|
|
320
|
+
const exportedCertKey = await cert.publicKey.export(crypto)
|
|
321
|
+
const rawCertKey = await crypto.subtle.exportKey('raw', exportedCertKey)
|
|
322
|
+
const rawKeyPairKey = await crypto.subtle.exportKey('raw', keyPair.publicKey)
|
|
323
|
+
|
|
324
|
+
if (!uint8ArrayEquals(
|
|
325
|
+
new Uint8Array(rawCertKey, 0, rawCertKey.byteLength),
|
|
326
|
+
new Uint8Array(rawKeyPairKey, 0, rawKeyPairKey.byteLength)
|
|
327
|
+
)) {
|
|
328
|
+
this.log('stored TLS certificate public key did not match public key from private key')
|
|
329
|
+
throw new NotFoundError()
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return cert
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async createCertificate (dsKey: Key, keyPair: CryptoKeyPair): Promise<X509Certificate> {
|
|
336
|
+
const notBefore = new Date()
|
|
337
|
+
const notAfter = new Date(notBefore.getTime() + ((this.init.certificateLifespan ?? DEFAULT_CERTIFICATE_LIFESPAN) * ONE_DAY_MS))
|
|
338
|
+
|
|
339
|
+
// have to set ms to 0 to work around https://github.com/PeculiarVentures/x509/issues/73
|
|
340
|
+
notBefore.setMilliseconds(0)
|
|
341
|
+
notAfter.setMilliseconds(0)
|
|
342
|
+
|
|
343
|
+
const cert = await X509CertificateGenerator.createSelfSigned({
|
|
344
|
+
serialNumber: (BigInt(Math.random().toString().replace('.', '')) * 100000n).toString(16),
|
|
345
|
+
name: 'CN=example.com, C=US, L=CA, O=example, ST=CA',
|
|
346
|
+
notBefore,
|
|
347
|
+
notAfter,
|
|
348
|
+
keys: keyPair,
|
|
349
|
+
extensions: [
|
|
350
|
+
new BasicConstraintsExtension(false, undefined, true)
|
|
351
|
+
]
|
|
352
|
+
}, crypto)
|
|
353
|
+
|
|
354
|
+
if (this.getKeychain() != null) {
|
|
355
|
+
this.log.trace('storing TLS certificate')
|
|
356
|
+
await this.components.datastore.put(dsKey, uint8ArrayFromString(cert.toString('pem')))
|
|
357
|
+
} else {
|
|
358
|
+
this.log('no keychain is configured so not storing TLS certificate since the private key will not be reused')
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return cert
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private getKeychain (): Keychain | undefined {
|
|
365
|
+
try {
|
|
366
|
+
return this.components.keychain
|
|
367
|
+
} catch {}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function isTransportCertificate (obj?: any): obj is TransportCertificate {
|
|
372
|
+
if (obj == null) {
|
|
373
|
+
return false
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return typeof obj.privateKey === 'string' && typeof obj.pem === 'string' && typeof obj.certhash === 'string'
|
|
142
377
|
}
|
|
@@ -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
|
+
}
|