node-rtc-connection 1.0.19 → 2.0.4
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 +94 -85
- package/dist/index.cjs +20 -5606
- package/dist/index.mjs +25 -5598
- package/dist/types/crypto/der.d.ts +107 -0
- package/dist/types/crypto/x509.d.ts +56 -0
- package/dist/types/datachannel/RTCDataChannel.d.ts +179 -0
- package/dist/types/dtls/RTCCertificate.d.ts +163 -0
- package/dist/types/dtls/cipher.d.ts +81 -0
- package/dist/types/dtls/connection.d.ts +81 -0
- package/dist/types/dtls/prf.d.ts +29 -0
- package/dist/types/dtls/protocol.d.ts +127 -0
- package/dist/types/foundation/ByteBufferQueue.d.ts +71 -0
- package/dist/types/foundation/RTCError.d.ts +152 -0
- package/dist/types/ice/RTCIceCandidate.d.ts +161 -0
- package/dist/types/ice/ice-agent.d.ts +154 -0
- package/dist/types/ice/stun-message.d.ts +92 -0
- package/dist/types/index.d.ts +29 -0
- package/dist/types/peerconnection/RTCPeerConnection.d.ts +74 -0
- package/dist/types/sctp/association.d.ts +77 -0
- package/dist/types/sctp/chunks.d.ts +200 -0
- package/dist/types/sctp/crc32c.d.ts +24 -0
- package/dist/types/sctp/datachannel-manager.d.ts +51 -0
- package/dist/types/sctp/dcep.d.ts +56 -0
- package/dist/types/sdp/RTCSessionDescription.d.ts +73 -0
- package/dist/types/sdp/sdp-utils.d.ts +103 -0
- package/dist/types/stun/stun-client.d.ts +119 -0
- package/dist/types/transport-stack.d.ts +68 -0
- package/package.json +26 -21
- package/src/crypto/der.ts +205 -0
- package/src/crypto/x509.ts +146 -0
- package/src/datachannel/RTCDataChannel.ts +388 -0
- package/src/dtls/RTCCertificate.ts +396 -0
- package/src/dtls/cipher.ts +198 -0
- package/src/dtls/connection.ts +974 -0
- package/src/dtls/prf.ts +62 -0
- package/src/dtls/protocol.ts +204 -0
- package/src/foundation/{ByteBufferQueue.js → ByteBufferQueue.ts} +74 -72
- package/src/foundation/{RTCError.js → RTCError.ts} +110 -60
- package/src/ice/{RTCIceCandidate.js → RTCIceCandidate.ts} +140 -92
- package/src/ice/ice-agent.ts +609 -0
- package/src/ice/stun-message.ts +260 -0
- package/src/index.ts +72 -0
- package/src/peerconnection/RTCPeerConnection.ts +430 -0
- package/src/sctp/association.ts +523 -0
- package/src/sctp/chunks.ts +350 -0
- package/src/sctp/crc32c.ts +57 -0
- package/src/sctp/datachannel-manager.ts +187 -0
- package/src/sctp/dcep.ts +94 -0
- package/src/sdp/{RTCSessionDescription.js → RTCSessionDescription.ts} +42 -29
- package/src/sdp/sdp-utils.ts +229 -0
- package/src/stun/{stun-client.js → stun-client.ts} +346 -187
- package/src/transport-stack.ts +165 -0
- package/dist/index.cjs.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/src/datachannel/RTCDataChannel.js +0 -354
- package/src/dtls/RTCCertificate.js +0 -310
- package/src/dtls/RTCDtlsTransport.js +0 -247
- package/src/ice/RTCIceTransport.js +0 -1018
- package/src/index.d.ts +0 -400
- package/src/index.js +0 -92
- package/src/network/network-transport.js +0 -478
- package/src/peerconnection/RTCPeerConnection.js +0 -875
- package/src/sctp/RTCSctpTransport.js +0 -253
- package/src/sdp/sdp-utils.js +0 -224
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file RTCCertificate.ts
|
|
3
|
+
* @description DTLS certificate implementation for WebRTC.
|
|
4
|
+
* @module dtls/RTCCertificate
|
|
5
|
+
*
|
|
6
|
+
* Implements the W3C RTCCertificate interface
|
|
7
|
+
* (https://www.w3.org/TR/webrtc/#rtccertificate-interface). Certificate and key
|
|
8
|
+
* generation are handled by src/crypto/x509.ts.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as crypto from 'crypto';
|
|
12
|
+
import * as x509 from '../crypto/x509';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* RTCDtlsFingerprint - DTLS certificate fingerprint
|
|
16
|
+
*/
|
|
17
|
+
export interface RTCDtlsFingerprint {
|
|
18
|
+
/** Hash algorithm (e.g., 'sha-256') */
|
|
19
|
+
algorithm: string;
|
|
20
|
+
/** Fingerprint value (colon-separated hex) */
|
|
21
|
+
value: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Options accepted by {@link generateSelfSignedCertificate}. */
|
|
25
|
+
interface GenerateCertificateOptions {
|
|
26
|
+
/** Common name for the certificate */
|
|
27
|
+
name?: string;
|
|
28
|
+
/** Days until expiration */
|
|
29
|
+
days?: number;
|
|
30
|
+
/** Hash algorithm */
|
|
31
|
+
hash?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Internal certificate data held by an {@link RTCCertificate}. */
|
|
35
|
+
interface CertData {
|
|
36
|
+
certDer: Buffer | null;
|
|
37
|
+
privateKey: crypto.KeyObject | string;
|
|
38
|
+
publicKey: crypto.KeyObject | string;
|
|
39
|
+
expires: number;
|
|
40
|
+
hash?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Options accepted by {@link RTCCertificate.generateCertificate}. */
|
|
44
|
+
interface RTCGenerateCertificateOptions {
|
|
45
|
+
/** Common name for the certificate */
|
|
46
|
+
name?: string;
|
|
47
|
+
/** Expiration time in ms (default: 30 days from now) */
|
|
48
|
+
expires?: number;
|
|
49
|
+
/** Days until expiration */
|
|
50
|
+
days?: number;
|
|
51
|
+
/** Hash algorithm */
|
|
52
|
+
hash?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Key parameters accepted by {@link RTCCertificate.isSupportedKeyParams}. */
|
|
56
|
+
interface RTCCertificateKeyParams {
|
|
57
|
+
type: string;
|
|
58
|
+
rsaModulusLength?: number;
|
|
59
|
+
namedCurve?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** PEM serialization produced by {@link RTCCertificate.toPEM}. */
|
|
63
|
+
interface RTCCertificatePEM {
|
|
64
|
+
pemPrivateKey: string;
|
|
65
|
+
pemCertificate: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generate a self-signed X.509 certificate for DTLS.
|
|
70
|
+
*
|
|
71
|
+
* Unlike a bare key pair, a real certificate is required for browser interop:
|
|
72
|
+
* the DTLS handshake transmits the certificate and the peer validates it
|
|
73
|
+
* against the SDP a=fingerprint (a hash over the DER certificate, RFC 8122).
|
|
74
|
+
*
|
|
75
|
+
* @param options - Certificate generation options
|
|
76
|
+
* @returns Certificate object with DER cert, keys and expiry
|
|
77
|
+
* @private
|
|
78
|
+
*/
|
|
79
|
+
function generateSelfSignedCertificate(
|
|
80
|
+
options: GenerateCertificateOptions = {}
|
|
81
|
+
): CertData {
|
|
82
|
+
const { name, days = 30 } = options;
|
|
83
|
+
|
|
84
|
+
const { certDer, privateKey, publicKey, notAfter } = x509.generateSelfSigned({
|
|
85
|
+
commonName: name,
|
|
86
|
+
days,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
certDer, // Buffer, DER-encoded X.509 certificate
|
|
91
|
+
privateKey, // crypto.KeyObject (EC P-256)
|
|
92
|
+
publicKey, // crypto.KeyObject
|
|
93
|
+
expires: notAfter.getTime(),
|
|
94
|
+
hash: 'sha256',
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Calculate the certificate fingerprint per RFC 8122: a hash over the
|
|
100
|
+
* DER-encoded certificate, uppercase hex, colon-separated.
|
|
101
|
+
* @param certDer - DER-encoded X.509 certificate
|
|
102
|
+
* @param algorithm - SDP hash name (e.g. 'sha-256')
|
|
103
|
+
* @returns Fingerprint (colon-separated hex)
|
|
104
|
+
* @private
|
|
105
|
+
*/
|
|
106
|
+
function calculateFingerprint(
|
|
107
|
+
certDer: Buffer,
|
|
108
|
+
algorithm: string = 'sha-256'
|
|
109
|
+
): string {
|
|
110
|
+
return x509.fingerprint(certDer, algorithm);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @class RTCCertificate
|
|
115
|
+
* @description Represents a certificate used for DTLS in WebRTC.
|
|
116
|
+
* The certificate includes a key pair and expiration time.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* // Generate a certificate
|
|
120
|
+
* const cert = await RTCCertificate.generateCertificate();
|
|
121
|
+
* console.log('Expires:', new Date(cert.expires));
|
|
122
|
+
* console.log('Fingerprints:', cert.getFingerprints());
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* // Generate with custom expiration
|
|
126
|
+
* const cert = await RTCCertificate.generateCertificate({
|
|
127
|
+
* name: 'my-peer',
|
|
128
|
+
* expires: Date.now() + (90 * 24 * 60 * 60 * 1000) // 90 days
|
|
129
|
+
* });
|
|
130
|
+
*/
|
|
131
|
+
class RTCCertificate {
|
|
132
|
+
#certDer: Buffer | null;
|
|
133
|
+
#privateKey: crypto.KeyObject | string;
|
|
134
|
+
#publicKey: crypto.KeyObject | string;
|
|
135
|
+
#expires: number;
|
|
136
|
+
#fingerprints: RTCDtlsFingerprint[] | null;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Create an RTCCertificate instance.
|
|
140
|
+
* Use generateCertificate() static method instead of calling directly.
|
|
141
|
+
* @param certData - Internal certificate data
|
|
142
|
+
* @private
|
|
143
|
+
*/
|
|
144
|
+
constructor(certData: CertData) {
|
|
145
|
+
// Store certificate data
|
|
146
|
+
this.#certDer = certData.certDer || null; // Buffer, DER X.509 cert
|
|
147
|
+
this.#privateKey = certData.privateKey; // crypto.KeyObject or PEM string
|
|
148
|
+
this.#publicKey = certData.publicKey;
|
|
149
|
+
this.#expires = certData.expires;
|
|
150
|
+
|
|
151
|
+
// Cache fingerprints
|
|
152
|
+
this.#fingerprints = null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get the DER-encoded X.509 certificate.
|
|
157
|
+
* Used by the DTLS handshake to transmit the local certificate.
|
|
158
|
+
* @internal
|
|
159
|
+
*/
|
|
160
|
+
getCertificateDer(): Buffer | null {
|
|
161
|
+
return this.#certDer;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get the expiration time.
|
|
166
|
+
* @returns Expiration time in milliseconds since epoch (DOMTimeStamp)
|
|
167
|
+
*/
|
|
168
|
+
get expires(): number {
|
|
169
|
+
return this.#expires;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get the certificate fingerprints.
|
|
174
|
+
* Returns an array of fingerprints for the certificate chain.
|
|
175
|
+
* For self-signed certificates, this returns a single fingerprint.
|
|
176
|
+
*
|
|
177
|
+
* @returns Array of fingerprint objects
|
|
178
|
+
*/
|
|
179
|
+
getFingerprints(): RTCDtlsFingerprint[] {
|
|
180
|
+
if (!this.#certDer) {
|
|
181
|
+
throw new Error('Certificate has no DER encoding; cannot compute fingerprint');
|
|
182
|
+
}
|
|
183
|
+
if (!this.#fingerprints) {
|
|
184
|
+
// Fingerprint is computed over the DER certificate (RFC 8122).
|
|
185
|
+
const certDer = this.#certDer;
|
|
186
|
+
const algorithms = ['sha-256', 'sha-384', 'sha-512'];
|
|
187
|
+
this.#fingerprints = algorithms.map(algorithm => ({
|
|
188
|
+
algorithm,
|
|
189
|
+
value: calculateFingerprint(certDer, algorithm),
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return this.#fingerprints.map(fp => ({ ...fp }));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get the private key as a Node crypto KeyObject (for the DTLS handshake).
|
|
198
|
+
* @internal
|
|
199
|
+
*/
|
|
200
|
+
getPrivateKeyObject(): crypto.KeyObject {
|
|
201
|
+
return this.#toKeyObject(this.#privateKey, 'private');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Coerce a stored key (KeyObject or PEM string) into a KeyObject.
|
|
206
|
+
* @private
|
|
207
|
+
*/
|
|
208
|
+
#toKeyObject(
|
|
209
|
+
key: crypto.KeyObject | string,
|
|
210
|
+
kind: 'private' | 'public'
|
|
211
|
+
): crypto.KeyObject {
|
|
212
|
+
if (key && typeof key === 'object' && key.type) {
|
|
213
|
+
return key; // already a KeyObject
|
|
214
|
+
}
|
|
215
|
+
// At this point the key is a PEM string (or an object lacking a key type).
|
|
216
|
+
return kind === 'private'
|
|
217
|
+
? crypto.createPrivateKey(key as string)
|
|
218
|
+
: crypto.createPublicKey(key as string);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get the private key in PEM format.
|
|
223
|
+
* @returns PEM-encoded private key
|
|
224
|
+
* @internal
|
|
225
|
+
*/
|
|
226
|
+
getPrivateKey(): string {
|
|
227
|
+
const obj = this.#toKeyObject(this.#privateKey, 'private');
|
|
228
|
+
return obj.export({ type: 'pkcs8', format: 'pem' }) as string;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get the public key in PEM format.
|
|
233
|
+
* @returns PEM-encoded public key
|
|
234
|
+
* @internal
|
|
235
|
+
*/
|
|
236
|
+
getPublicKey(): string {
|
|
237
|
+
const obj = this.#toKeyObject(this.#publicKey, 'public');
|
|
238
|
+
return obj.export({ type: 'spki', format: 'pem' }) as string;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Convert to PEM format (for serialization/storage).
|
|
243
|
+
* The certificate is exported as a PEM-wrapped DER X.509 certificate.
|
|
244
|
+
* @returns Object with pemPrivateKey and pemCertificate
|
|
245
|
+
*/
|
|
246
|
+
toPEM(): RTCCertificatePEM {
|
|
247
|
+
const pemCertificate = this.#certDer
|
|
248
|
+
? `-----BEGIN CERTIFICATE-----\n${this.#certDer
|
|
249
|
+
.toString('base64')
|
|
250
|
+
.match(/.{1,64}/g)!
|
|
251
|
+
.join('\n')}\n-----END CERTIFICATE-----\n`
|
|
252
|
+
: this.getPublicKey();
|
|
253
|
+
return {
|
|
254
|
+
pemPrivateKey: this.getPrivateKey(),
|
|
255
|
+
pemCertificate,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Check if the certificate has expired.
|
|
261
|
+
* @returns True if expired, false otherwise
|
|
262
|
+
*/
|
|
263
|
+
isExpired(): boolean {
|
|
264
|
+
return Date.now() > this.#expires;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Generate a new RTCCertificate asynchronously.
|
|
269
|
+
*
|
|
270
|
+
* @param options - Generation options
|
|
271
|
+
* @returns Promise resolving to generated certificate
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* const cert = await RTCCertificate.generateCertificate({
|
|
275
|
+
* name: 'my-app',
|
|
276
|
+
* expires: Date.now() + (90 * 24 * 60 * 60 * 1000) // 90 days
|
|
277
|
+
* });
|
|
278
|
+
*/
|
|
279
|
+
static async generateCertificate(
|
|
280
|
+
options: RTCGenerateCertificateOptions = {}
|
|
281
|
+
): Promise<RTCCertificate> {
|
|
282
|
+
return new Promise((resolve, reject) => {
|
|
283
|
+
try {
|
|
284
|
+
// Calculate expiration
|
|
285
|
+
let expires: number;
|
|
286
|
+
if (options.expires) {
|
|
287
|
+
expires = options.expires;
|
|
288
|
+
} else {
|
|
289
|
+
const days = options.days || 30;
|
|
290
|
+
expires = Date.now() + (days * 24 * 60 * 60 * 1000);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Generate certificate in next tick to avoid blocking
|
|
294
|
+
setImmediate(() => {
|
|
295
|
+
try {
|
|
296
|
+
const certData = generateSelfSignedCertificate({
|
|
297
|
+
name: options.name || 'webrtc',
|
|
298
|
+
days: Math.ceil((expires - Date.now()) / (24 * 60 * 60 * 1000)),
|
|
299
|
+
hash: options.hash || 'sha256'
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
certData.expires = expires;
|
|
303
|
+
const certificate = new RTCCertificate(certData);
|
|
304
|
+
resolve(certificate);
|
|
305
|
+
} catch (err) {
|
|
306
|
+
reject(err);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
} catch (err) {
|
|
310
|
+
reject(err);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Create a certificate from PEM strings.
|
|
317
|
+
*
|
|
318
|
+
* @param pemPrivateKey - PEM-encoded private key
|
|
319
|
+
* @param pemCertificate - PEM-encoded certificate (or public key)
|
|
320
|
+
* @param expires - Expiration time in ms (default: 30 days from now)
|
|
321
|
+
* @returns Certificate instance
|
|
322
|
+
*
|
|
323
|
+
* @example
|
|
324
|
+
* const cert = RTCCertificate.fromPEM(
|
|
325
|
+
* privateKeyPEM,
|
|
326
|
+
* publicKeyPEM,
|
|
327
|
+
* Date.now() + (30 * 24 * 60 * 60 * 1000)
|
|
328
|
+
* );
|
|
329
|
+
*/
|
|
330
|
+
static fromPEM(
|
|
331
|
+
pemPrivateKey: string,
|
|
332
|
+
pemCertificate: string,
|
|
333
|
+
expires?: number
|
|
334
|
+
): RTCCertificate {
|
|
335
|
+
if (typeof pemPrivateKey !== 'string' || pemPrivateKey.length === 0) {
|
|
336
|
+
throw new TypeError('pemPrivateKey must be a non-empty string');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (typeof pemCertificate !== 'string' || pemCertificate.length === 0) {
|
|
340
|
+
throw new TypeError('pemCertificate must be a non-empty string');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Default expiration to 30 days if not provided
|
|
344
|
+
const expirationTime = expires || (Date.now() + (30 * 24 * 60 * 60 * 1000));
|
|
345
|
+
|
|
346
|
+
// If a PEM CERTIFICATE block was provided, recover the DER so fingerprints
|
|
347
|
+
// (computed over the DER cert) round-trip correctly.
|
|
348
|
+
let certDer: Buffer | null = null;
|
|
349
|
+
let publicKey: crypto.KeyObject | string = pemCertificate;
|
|
350
|
+
const certMatch = pemCertificate.match(
|
|
351
|
+
/-----BEGIN CERTIFICATE-----([\s\S]+?)-----END CERTIFICATE-----/
|
|
352
|
+
);
|
|
353
|
+
if (certMatch) {
|
|
354
|
+
certDer = Buffer.from(certMatch[1]!.replace(/\s/g, ''), 'base64');
|
|
355
|
+
publicKey = crypto.createPublicKey(crypto.createPrivateKey(pemPrivateKey));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return new RTCCertificate({
|
|
359
|
+
certDer,
|
|
360
|
+
privateKey: pemPrivateKey,
|
|
361
|
+
publicKey,
|
|
362
|
+
expires: expirationTime,
|
|
363
|
+
hash: 'sha256'
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Check if key parameters are supported.
|
|
369
|
+
* Currently supports RSA with 1024-4096 bits and ECDSA.
|
|
370
|
+
*
|
|
371
|
+
* @param keyParams - Key parameters
|
|
372
|
+
* @returns True if supported, false otherwise
|
|
373
|
+
*/
|
|
374
|
+
static isSupportedKeyParams(keyParams: RTCCertificateKeyParams): boolean {
|
|
375
|
+
if (!keyParams || typeof keyParams !== 'object') {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (keyParams.type === 'RSA') {
|
|
380
|
+
const modulusLength = keyParams.rsaModulusLength || 2048;
|
|
381
|
+
// Support 1024 to 4096 bits
|
|
382
|
+
return modulusLength >= 1024 && modulusLength <= 4096;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (keyParams.type === 'ECDSA') {
|
|
386
|
+
// Support common ECDSA curves
|
|
387
|
+
const curve = keyParams.namedCurve;
|
|
388
|
+
return ['P-256', 'P-384', 'P-521'].includes(curve as string);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export default RTCCertificate;
|
|
396
|
+
export { RTCCertificate };
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file cipher.ts
|
|
3
|
+
* @description AEAD record protection for DTLS 1.2 with AES-128-GCM.
|
|
4
|
+
* @module dtls/cipher
|
|
5
|
+
*
|
|
6
|
+
* Implements key derivation and the GCM record encrypt/decrypt for the suite
|
|
7
|
+
* TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (RFC 5288 / RFC 6347).
|
|
8
|
+
*
|
|
9
|
+
* Key block layout for AEAD (no MAC keys):
|
|
10
|
+
* client_write_key[16] | server_write_key[16] |
|
|
11
|
+
* client_write_IV[4] | server_write_IV[4] (implicit salt)
|
|
12
|
+
*
|
|
13
|
+
* GCM nonce = write_IV (4) || explicit_nonce (8)
|
|
14
|
+
* Record = explicit_nonce (8) || ciphertext || tag (16)
|
|
15
|
+
* AAD (DTLS) = seq_num (8 = epoch||seq) || type (1) || version (2) || plaintext_len (2)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
import * as crypto from 'crypto';
|
|
21
|
+
import { prf } from './prf';
|
|
22
|
+
|
|
23
|
+
const KEY_LEN = 16;
|
|
24
|
+
const FIXED_IV_LEN = 4;
|
|
25
|
+
const RECORD_IV_LEN = 8;
|
|
26
|
+
const TAG_LEN = 16;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Per-direction keys/IVs produced by {@link deriveKeys}.
|
|
30
|
+
*/
|
|
31
|
+
export interface DerivedKeys {
|
|
32
|
+
clientKey: Buffer;
|
|
33
|
+
serverKey: Buffer;
|
|
34
|
+
clientIV: Buffer;
|
|
35
|
+
serverIV: Buffer;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Derive the master secret from the pre-master secret (RFC 5246 §8.1).
|
|
40
|
+
* @param {Buffer} preMasterSecret
|
|
41
|
+
* @param {Buffer} clientRandom - 32 bytes
|
|
42
|
+
* @param {Buffer} serverRandom - 32 bytes
|
|
43
|
+
* @returns {Buffer} 48-byte master secret
|
|
44
|
+
*/
|
|
45
|
+
export function deriveMasterSecret(
|
|
46
|
+
preMasterSecret: Buffer,
|
|
47
|
+
clientRandom: Buffer,
|
|
48
|
+
serverRandom: Buffer
|
|
49
|
+
): Buffer {
|
|
50
|
+
return prf(
|
|
51
|
+
preMasterSecret,
|
|
52
|
+
'master secret',
|
|
53
|
+
Buffer.concat([clientRandom, serverRandom]),
|
|
54
|
+
48
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Derive the extended master secret (RFC 7627) using the handshake hash.
|
|
60
|
+
* @param {Buffer} preMasterSecret
|
|
61
|
+
* @param {Buffer} sessionHash - hash of handshake messages through CKE
|
|
62
|
+
* @returns {Buffer} 48-byte master secret
|
|
63
|
+
*/
|
|
64
|
+
export function deriveExtendedMasterSecret(
|
|
65
|
+
preMasterSecret: Buffer,
|
|
66
|
+
sessionHash: Buffer
|
|
67
|
+
): Buffer {
|
|
68
|
+
return prf(preMasterSecret, 'extended master secret', sessionHash, 48);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Expand the key block and split it into per-direction keys/IVs.
|
|
73
|
+
* @param {Buffer} masterSecret
|
|
74
|
+
* @param {Buffer} clientRandom
|
|
75
|
+
* @param {Buffer} serverRandom
|
|
76
|
+
* @returns {DerivedKeys}
|
|
77
|
+
*/
|
|
78
|
+
export function deriveKeys(
|
|
79
|
+
masterSecret: Buffer,
|
|
80
|
+
clientRandom: Buffer,
|
|
81
|
+
serverRandom: Buffer
|
|
82
|
+
): DerivedKeys {
|
|
83
|
+
// Note the order: key_expansion uses server_random || client_random.
|
|
84
|
+
const seed = Buffer.concat([serverRandom, clientRandom]);
|
|
85
|
+
const need = 2 * KEY_LEN + 2 * FIXED_IV_LEN;
|
|
86
|
+
const block = prf(masterSecret, 'key expansion', seed, need);
|
|
87
|
+
|
|
88
|
+
let o = 0;
|
|
89
|
+
const clientKey = block.slice(o, (o += KEY_LEN));
|
|
90
|
+
const serverKey = block.slice(o, (o += KEY_LEN));
|
|
91
|
+
const clientIV = block.slice(o, (o += FIXED_IV_LEN));
|
|
92
|
+
const serverIV = block.slice(o, (o += FIXED_IV_LEN));
|
|
93
|
+
return { clientKey, serverKey, clientIV, serverIV };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Build the DTLS GCM additional authenticated data.
|
|
98
|
+
* @param {number} epoch
|
|
99
|
+
* @param {number} seq - 48-bit record sequence
|
|
100
|
+
* @param {number} type - content type
|
|
101
|
+
* @param {number} version - record version (0xFEFD)
|
|
102
|
+
* @param {number} plaintextLen
|
|
103
|
+
* @returns {Buffer}
|
|
104
|
+
*/
|
|
105
|
+
function buildAAD(
|
|
106
|
+
epoch: number,
|
|
107
|
+
seq: number,
|
|
108
|
+
type: number,
|
|
109
|
+
version: number,
|
|
110
|
+
plaintextLen: number
|
|
111
|
+
): Buffer {
|
|
112
|
+
const aad = Buffer.alloc(13);
|
|
113
|
+
aad.writeUInt16BE(epoch, 0);
|
|
114
|
+
aad.writeUIntBE(seq, 2, 6);
|
|
115
|
+
aad.writeUInt8(type, 8);
|
|
116
|
+
aad.writeUInt16BE(version, 9);
|
|
117
|
+
aad.writeUInt16BE(plaintextLen, 11);
|
|
118
|
+
return aad;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @class GcmCipher
|
|
123
|
+
* @description Holds the key/IV for one direction and does record AEAD.
|
|
124
|
+
*/
|
|
125
|
+
export class GcmCipher {
|
|
126
|
+
#key: Buffer;
|
|
127
|
+
#fixedIv: Buffer;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {Buffer} key - 16-byte AES key
|
|
131
|
+
* @param {Buffer} fixedIv - 4-byte implicit salt
|
|
132
|
+
*/
|
|
133
|
+
constructor(key: Buffer, fixedIv: Buffer) {
|
|
134
|
+
this.#key = key;
|
|
135
|
+
this.#fixedIv = fixedIv;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Encrypt a record fragment.
|
|
140
|
+
* @param {number} epoch
|
|
141
|
+
* @param {number} seq
|
|
142
|
+
* @param {number} type
|
|
143
|
+
* @param {number} version
|
|
144
|
+
* @param {Buffer} plaintext
|
|
145
|
+
* @returns {Buffer} explicit_nonce || ciphertext || tag
|
|
146
|
+
*/
|
|
147
|
+
encrypt(
|
|
148
|
+
epoch: number,
|
|
149
|
+
seq: number,
|
|
150
|
+
type: number,
|
|
151
|
+
version: number,
|
|
152
|
+
plaintext: Buffer
|
|
153
|
+
): Buffer {
|
|
154
|
+
// Explicit nonce: the 64-bit (epoch||seq) record number, unique per record.
|
|
155
|
+
const explicitNonce = Buffer.alloc(RECORD_IV_LEN);
|
|
156
|
+
explicitNonce.writeUInt16BE(epoch, 0);
|
|
157
|
+
explicitNonce.writeUIntBE(seq, 2, 6);
|
|
158
|
+
|
|
159
|
+
const nonce = Buffer.concat([this.#fixedIv, explicitNonce]);
|
|
160
|
+
const aad = buildAAD(epoch, seq, type, version, plaintext.length);
|
|
161
|
+
|
|
162
|
+
const cipher = crypto.createCipheriv('aes-128-gcm', this.#key, nonce);
|
|
163
|
+
cipher.setAAD(aad);
|
|
164
|
+
const ct = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
165
|
+
const tag = cipher.getAuthTag();
|
|
166
|
+
return Buffer.concat([explicitNonce, ct, tag]);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Decrypt a record fragment.
|
|
171
|
+
* @param {number} epoch
|
|
172
|
+
* @param {number} seq
|
|
173
|
+
* @param {number} type
|
|
174
|
+
* @param {number} version
|
|
175
|
+
* @param {Buffer} record - explicit_nonce || ciphertext || tag
|
|
176
|
+
* @returns {Buffer} plaintext
|
|
177
|
+
* @throws on authentication failure
|
|
178
|
+
*/
|
|
179
|
+
decrypt(
|
|
180
|
+
epoch: number,
|
|
181
|
+
seq: number,
|
|
182
|
+
type: number,
|
|
183
|
+
version: number,
|
|
184
|
+
record: Buffer
|
|
185
|
+
): Buffer {
|
|
186
|
+
const explicitNonce = record.slice(0, RECORD_IV_LEN);
|
|
187
|
+
const tag = record.slice(record.length - TAG_LEN);
|
|
188
|
+
const ct = record.slice(RECORD_IV_LEN, record.length - TAG_LEN);
|
|
189
|
+
|
|
190
|
+
const nonce = Buffer.concat([this.#fixedIv, explicitNonce]);
|
|
191
|
+
const aad = buildAAD(epoch, seq, type, version, ct.length);
|
|
192
|
+
|
|
193
|
+
const decipher = crypto.createDecipheriv('aes-128-gcm', this.#key, nonce);
|
|
194
|
+
decipher.setAAD(aad);
|
|
195
|
+
decipher.setAuthTag(tag);
|
|
196
|
+
return Buffer.concat([decipher.update(ct), decipher.final()]);
|
|
197
|
+
}
|
|
198
|
+
}
|