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.
Files changed (64) hide show
  1. package/README.md +94 -85
  2. package/dist/index.cjs +20 -5606
  3. package/dist/index.mjs +25 -5598
  4. package/dist/types/crypto/der.d.ts +107 -0
  5. package/dist/types/crypto/x509.d.ts +56 -0
  6. package/dist/types/datachannel/RTCDataChannel.d.ts +179 -0
  7. package/dist/types/dtls/RTCCertificate.d.ts +163 -0
  8. package/dist/types/dtls/cipher.d.ts +81 -0
  9. package/dist/types/dtls/connection.d.ts +81 -0
  10. package/dist/types/dtls/prf.d.ts +29 -0
  11. package/dist/types/dtls/protocol.d.ts +127 -0
  12. package/dist/types/foundation/ByteBufferQueue.d.ts +71 -0
  13. package/dist/types/foundation/RTCError.d.ts +152 -0
  14. package/dist/types/ice/RTCIceCandidate.d.ts +161 -0
  15. package/dist/types/ice/ice-agent.d.ts +154 -0
  16. package/dist/types/ice/stun-message.d.ts +92 -0
  17. package/dist/types/index.d.ts +29 -0
  18. package/dist/types/peerconnection/RTCPeerConnection.d.ts +74 -0
  19. package/dist/types/sctp/association.d.ts +77 -0
  20. package/dist/types/sctp/chunks.d.ts +200 -0
  21. package/dist/types/sctp/crc32c.d.ts +24 -0
  22. package/dist/types/sctp/datachannel-manager.d.ts +51 -0
  23. package/dist/types/sctp/dcep.d.ts +56 -0
  24. package/dist/types/sdp/RTCSessionDescription.d.ts +73 -0
  25. package/dist/types/sdp/sdp-utils.d.ts +103 -0
  26. package/dist/types/stun/stun-client.d.ts +119 -0
  27. package/dist/types/transport-stack.d.ts +68 -0
  28. package/package.json +26 -21
  29. package/src/crypto/der.ts +205 -0
  30. package/src/crypto/x509.ts +146 -0
  31. package/src/datachannel/RTCDataChannel.ts +388 -0
  32. package/src/dtls/RTCCertificate.ts +396 -0
  33. package/src/dtls/cipher.ts +198 -0
  34. package/src/dtls/connection.ts +974 -0
  35. package/src/dtls/prf.ts +62 -0
  36. package/src/dtls/protocol.ts +204 -0
  37. package/src/foundation/{ByteBufferQueue.js → ByteBufferQueue.ts} +74 -72
  38. package/src/foundation/{RTCError.js → RTCError.ts} +110 -60
  39. package/src/ice/{RTCIceCandidate.js → RTCIceCandidate.ts} +140 -92
  40. package/src/ice/ice-agent.ts +609 -0
  41. package/src/ice/stun-message.ts +260 -0
  42. package/src/index.ts +72 -0
  43. package/src/peerconnection/RTCPeerConnection.ts +430 -0
  44. package/src/sctp/association.ts +523 -0
  45. package/src/sctp/chunks.ts +350 -0
  46. package/src/sctp/crc32c.ts +57 -0
  47. package/src/sctp/datachannel-manager.ts +187 -0
  48. package/src/sctp/dcep.ts +94 -0
  49. package/src/sdp/{RTCSessionDescription.js → RTCSessionDescription.ts} +42 -29
  50. package/src/sdp/sdp-utils.ts +229 -0
  51. package/src/stun/{stun-client.js → stun-client.ts} +346 -187
  52. package/src/transport-stack.ts +165 -0
  53. package/dist/index.cjs.map +0 -1
  54. package/dist/index.mjs.map +0 -1
  55. package/src/datachannel/RTCDataChannel.js +0 -354
  56. package/src/dtls/RTCCertificate.js +0 -310
  57. package/src/dtls/RTCDtlsTransport.js +0 -247
  58. package/src/ice/RTCIceTransport.js +0 -1018
  59. package/src/index.d.ts +0 -400
  60. package/src/index.js +0 -92
  61. package/src/network/network-transport.js +0 -478
  62. package/src/peerconnection/RTCPeerConnection.js +0 -875
  63. package/src/sctp/RTCSctpTransport.js +0 -253
  64. 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
+ }