@agentdance/node-webrtc-dtls 1.0.0

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/src/crypto.ts ADDED
@@ -0,0 +1,279 @@
1
+ // DTLS 1.2 Cryptographic primitives
2
+ // RFC 5246 (TLS 1.2) PRF with SHA-256, AES-GCM, ECDH
3
+
4
+ import * as crypto from 'node:crypto';
5
+
6
+ // ─── HMAC / PRF ───────────────────────────────────────────────────────────────
7
+
8
+ /**
9
+ * HMAC-SHA256
10
+ */
11
+ export function hmacSha256(key: Buffer, data: Buffer): Buffer {
12
+ return crypto.createHmac('sha256', key).update(data).digest() as Buffer;
13
+ }
14
+
15
+ /**
16
+ * HMAC-SHA384
17
+ */
18
+ export function hmacSha384(key: Buffer, data: Buffer): Buffer {
19
+ return crypto.createHmac('sha384', key).update(data).digest() as Buffer;
20
+ }
21
+
22
+ /**
23
+ * P_hash function (RFC 5246 Section 5)
24
+ * P_hash(secret, seed) = HMAC(secret, A(1) + seed) + HMAC(secret, A(2) + seed) + ...
25
+ * where A(0) = seed, A(i) = HMAC(secret, A(i-1))
26
+ */
27
+ function pHash(
28
+ hmacFn: (key: Buffer, data: Buffer) => Buffer,
29
+ secret: Buffer,
30
+ seed: Buffer,
31
+ length: number,
32
+ ): Buffer {
33
+ const output = Buffer.allocUnsafe(length);
34
+ let written = 0;
35
+
36
+ // A(1)
37
+ let a = hmacFn(secret, seed);
38
+
39
+ while (written < length) {
40
+ const chunk = hmacFn(secret, Buffer.concat([a, seed]));
41
+ const toCopy = Math.min(chunk.length, length - written);
42
+ chunk.copy(output, written, 0, toCopy);
43
+ written += toCopy;
44
+ // A(i+1) = HMAC(secret, A(i))
45
+ a = hmacFn(secret, a);
46
+ }
47
+
48
+ return output;
49
+ }
50
+
51
+ /**
52
+ * DTLS 1.2 PRF (SHA-256 based, RFC 5246)
53
+ * PRF(secret, label, seed) = P_SHA256(secret, label + seed)
54
+ */
55
+ export function prf(secret: Buffer, label: string, seed: Buffer, length: number): Buffer {
56
+ const labelBuf = Buffer.from(label, 'ascii');
57
+ const combined = Buffer.concat([labelBuf, seed]);
58
+ return pHash(hmacSha256, secret, combined, length);
59
+ }
60
+
61
+ /**
62
+ * PRF with SHA-384 (for AES-256 cipher suites)
63
+ */
64
+ export function prfSha384(secret: Buffer, label: string, seed: Buffer, length: number): Buffer {
65
+ const labelBuf = Buffer.from(label, 'ascii');
66
+ const combined = Buffer.concat([labelBuf, seed]);
67
+ return pHash(hmacSha384, secret, combined, length);
68
+ }
69
+
70
+ // ─── Key derivation ───────────────────────────────────────────────────────────
71
+
72
+ /**
73
+ * Compute master secret from pre-master secret.
74
+ * master_secret = PRF(pre_master_secret, "master secret", ClientRandom + ServerRandom, 48)
75
+ */
76
+ export function computeMasterSecret(
77
+ preMasterSecret: Buffer,
78
+ clientRandom: Buffer,
79
+ serverRandom: Buffer,
80
+ ): Buffer {
81
+ const seed = Buffer.concat([clientRandom, serverRandom]);
82
+ return prf(preMasterSecret, 'master secret', seed, 48);
83
+ }
84
+
85
+ export interface KeyBlock {
86
+ clientWriteKey: Buffer;
87
+ serverWriteKey: Buffer;
88
+ clientWriteIv: Buffer;
89
+ serverWriteIv: Buffer;
90
+ }
91
+
92
+ /**
93
+ * Expand key material from master secret (RFC 5246 Section 6.3).
94
+ * For AES-128-GCM: key_length=16, iv_length=4 (implicit part).
95
+ * key_block = PRF(master_secret, "key expansion", ServerRandom + ClientRandom, ...)
96
+ */
97
+ export function expandKeyMaterial(
98
+ masterSecret: Buffer,
99
+ clientRandom: Buffer,
100
+ serverRandom: Buffer,
101
+ keyLength: number = 16,
102
+ ivLength: number = 4,
103
+ ): KeyBlock {
104
+ // NOTE: seed is ServerRandom + ClientRandom (reversed from master secret)
105
+ const seed = Buffer.concat([serverRandom, clientRandom]);
106
+ const totalLength = 2 * keyLength + 2 * ivLength;
107
+ const keyBlock = prf(masterSecret, 'key expansion', seed, totalLength);
108
+
109
+ let off = 0;
110
+ const clientWriteKey = Buffer.from(keyBlock.subarray(off, off + keyLength));
111
+ off += keyLength;
112
+ const serverWriteKey = Buffer.from(keyBlock.subarray(off, off + keyLength));
113
+ off += keyLength;
114
+ const clientWriteIv = Buffer.from(keyBlock.subarray(off, off + ivLength));
115
+ off += ivLength;
116
+ const serverWriteIv = Buffer.from(keyBlock.subarray(off, off + ivLength));
117
+
118
+ return { clientWriteKey, serverWriteKey, clientWriteIv, serverWriteIv };
119
+ }
120
+
121
+ /**
122
+ * Export keying material (RFC 5705 / RFC 5764 Section 4.2).
123
+ * Used to derive SRTP master keys from DTLS.
124
+ * EKM = PRF(master_secret, label, ClientRandom + ServerRandom, length)
125
+ */
126
+ export function exportKeyingMaterial(
127
+ masterSecret: Buffer,
128
+ clientRandom: Buffer,
129
+ serverRandom: Buffer,
130
+ label: string,
131
+ length: number,
132
+ ): Buffer {
133
+ const seed = Buffer.concat([clientRandom, serverRandom]);
134
+ return prf(masterSecret, label, seed, length);
135
+ }
136
+
137
+ // ─── AES-GCM ──────────────────────────────────────────────────────────────────
138
+
139
+ export interface AesGcmResult {
140
+ ciphertext: Buffer;
141
+ tag: Buffer;
142
+ }
143
+
144
+ /**
145
+ * AES-128-GCM encrypt.
146
+ * Returns ciphertext + 16-byte authentication tag.
147
+ */
148
+ export function aesgcmEncrypt(
149
+ key: Buffer,
150
+ iv: Buffer,
151
+ plaintext: Buffer,
152
+ aad: Buffer,
153
+ ): AesGcmResult {
154
+ const cipher = crypto.createCipheriv('aes-128-gcm', key, iv);
155
+ cipher.setAAD(aad);
156
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
157
+ const tag = cipher.getAuthTag();
158
+ return { ciphertext, tag };
159
+ }
160
+
161
+ /**
162
+ * AES-128-GCM decrypt.
163
+ */
164
+ export function aesgcmDecrypt(
165
+ key: Buffer,
166
+ iv: Buffer,
167
+ ciphertext: Buffer,
168
+ tag: Buffer,
169
+ aad: Buffer,
170
+ ): Buffer {
171
+ const decipher = crypto.createDecipheriv('aes-128-gcm', key, iv);
172
+ decipher.setAAD(aad);
173
+ decipher.setAuthTag(tag);
174
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
175
+ }
176
+
177
+ // ─── ECDH ─────────────────────────────────────────────────────────────────────
178
+
179
+ export interface EcdhKeyPair {
180
+ privateKey: crypto.KeyObject;
181
+ publicKey: crypto.KeyObject;
182
+ }
183
+
184
+ /**
185
+ * Generate an ephemeral ECDH key pair on P-256 (secp256r1).
186
+ */
187
+ export function generateEcdhKeyPair(): EcdhKeyPair {
188
+ const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
189
+ namedCurve: 'P-256',
190
+ });
191
+ return { privateKey, publicKey };
192
+ }
193
+
194
+ /**
195
+ * Compute ECDH pre-master secret from our private key and peer's public key bytes.
196
+ * peerPublicKeyBytes: uncompressed EC point (0x04 + x + y, 65 bytes for P-256)
197
+ */
198
+ export function computeEcdhPreMasterSecret(
199
+ privateKey: crypto.KeyObject,
200
+ peerPublicKeyBytes: Buffer,
201
+ ): Buffer {
202
+ const peerPublicKey = decodeEcPublicKey(peerPublicKeyBytes);
203
+ return crypto.diffieHellman({ privateKey, publicKey: peerPublicKey }) as Buffer;
204
+ }
205
+
206
+ /**
207
+ * Encode EC public key to uncompressed point format: 0x04 + x (32 bytes) + y (32 bytes)
208
+ */
209
+ export function encodeEcPublicKey(publicKey: crypto.KeyObject): Buffer {
210
+ // Export as raw uncompressed point via JWK
211
+ const jwk = publicKey.export({ format: 'jwk' }) as crypto.JsonWebKey;
212
+ if (!jwk.x || !jwk.y) throw new Error('Not an EC public key');
213
+ const x = Buffer.from(jwk.x, 'base64url');
214
+ const y = Buffer.from(jwk.y, 'base64url');
215
+
216
+ // Pad to 32 bytes for P-256
217
+ const coordSize = 32;
218
+ const xPadded = Buffer.alloc(coordSize);
219
+ const yPadded = Buffer.alloc(coordSize);
220
+ x.copy(xPadded, coordSize - x.length);
221
+ y.copy(yPadded, coordSize - y.length);
222
+
223
+ const out = Buffer.allocUnsafe(1 + coordSize * 2);
224
+ out[0] = 0x04; // uncompressed
225
+ xPadded.copy(out, 1);
226
+ yPadded.copy(out, 1 + coordSize);
227
+ return out;
228
+ }
229
+
230
+ /**
231
+ * Decode uncompressed EC point to a KeyObject (P-256).
232
+ */
233
+ export function decodeEcPublicKey(bytes: Buffer): crypto.KeyObject {
234
+ if (bytes[0] !== 0x04) {
235
+ throw new Error('Only uncompressed EC points supported (0x04 prefix)');
236
+ }
237
+ const coordSize = (bytes.length - 1) / 2;
238
+ const x = bytes.subarray(1, 1 + coordSize);
239
+ const y = bytes.subarray(1 + coordSize);
240
+
241
+ const jwk: crypto.JsonWebKey = {
242
+ kty: 'EC',
243
+ crv: 'P-256',
244
+ x: x.toString('base64url'),
245
+ y: y.toString('base64url'),
246
+ };
247
+
248
+ return crypto.createPublicKey({ key: jwk, format: 'jwk' });
249
+ }
250
+
251
+ /**
252
+ * Compute SHA-256 hash of data.
253
+ */
254
+ export function sha256(data: Buffer): Buffer {
255
+ return crypto.createHash('sha256').update(data).digest() as Buffer;
256
+ }
257
+
258
+ /**
259
+ * Sign data with ECDSA-SHA256 private key.
260
+ * Returns DER-encoded signature.
261
+ */
262
+ export function ecdsaSign(privateKey: crypto.KeyObject, data: Buffer): Buffer {
263
+ return crypto.sign('sha256', data, privateKey) as Buffer;
264
+ }
265
+
266
+ /**
267
+ * Verify ECDSA-SHA256 signature.
268
+ */
269
+ export function ecdsaVerify(
270
+ publicKey: crypto.KeyObject,
271
+ data: Buffer,
272
+ signature: Buffer,
273
+ ): boolean {
274
+ try {
275
+ return crypto.verify('sha256', data, publicKey, signature);
276
+ } catch {
277
+ return false;
278
+ }
279
+ }