@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/dist/certificate.d.ts +31 -0
- package/dist/certificate.d.ts.map +1 -0
- package/dist/certificate.js +199 -0
- package/dist/certificate.js.map +1 -0
- package/dist/crypto.d.ts +89 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +193 -0
- package/dist/crypto.js.map +1 -0
- package/dist/handshake.d.ts +113 -0
- package/dist/handshake.d.ts.map +1 -0
- package/dist/handshake.js +420 -0
- package/dist/handshake.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/record.d.ts +19 -0
- package/dist/record.d.ts.map +1 -0
- package/dist/record.js +108 -0
- package/dist/record.js.map +1 -0
- package/dist/state.d.ts +44 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +10 -0
- package/dist/state.js.map +1 -0
- package/dist/transport.d.ts +87 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +559 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +44 -0
- package/dist/types.js.map +1 -0
- package/package.json +56 -0
- package/src/certificate.ts +253 -0
- package/src/crypto.ts +279 -0
- package/src/handshake.ts +544 -0
- package/src/index.ts +72 -0
- package/src/record.ts +127 -0
- package/src/state.ts +59 -0
- package/src/transport.ts +692 -0
- package/src/types.ts +57 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AAEtC,oBAAY,SAAS;IACnB,GAAG,QAAQ;IACX,UAAU,eAAe;IACzB,SAAS,cAAc;IACvB,MAAM,WAAW;IACjB,MAAM,WAAW;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAE/B,QAAQ,EAAE,MAAM,EAAE,CAAC;IAEnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,cAAc,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC;IAClC,aAAa,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC;IAEjC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAE9B,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAE7B,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IAEvB,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB"}
|
package/dist/state.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// DTLS 1.2 state machine types and security parameters
|
|
2
|
+
export var DtlsState;
|
|
3
|
+
(function (DtlsState) {
|
|
4
|
+
DtlsState["New"] = "new";
|
|
5
|
+
DtlsState["Connecting"] = "connecting";
|
|
6
|
+
DtlsState["Connected"] = "connected";
|
|
7
|
+
DtlsState["Closed"] = "closed";
|
|
8
|
+
DtlsState["Failed"] = "failed";
|
|
9
|
+
})(DtlsState || (DtlsState = {}));
|
|
10
|
+
//# sourceMappingURL=state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.js","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAIvD,MAAM,CAAN,IAAY,SAMX;AAND,WAAY,SAAS;IACnB,wBAAW,CAAA;IACX,sCAAyB,CAAA;IACzB,oCAAuB,CAAA;IACvB,8BAAiB,CAAA;IACjB,8BAAiB,CAAA;AACnB,CAAC,EANW,SAAS,KAAT,SAAS,QAMpB"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { type DtlsCertificate } from './certificate.js';
|
|
3
|
+
import { DtlsState } from './state.js';
|
|
4
|
+
export { DtlsState };
|
|
5
|
+
export type { DtlsCertificate };
|
|
6
|
+
export interface SrtpKeyingMaterial {
|
|
7
|
+
clientKey: Buffer;
|
|
8
|
+
clientSalt: Buffer;
|
|
9
|
+
serverKey: Buffer;
|
|
10
|
+
serverSalt: Buffer;
|
|
11
|
+
profile: number;
|
|
12
|
+
}
|
|
13
|
+
export interface DtlsTransportOptions {
|
|
14
|
+
role: 'client' | 'server';
|
|
15
|
+
remoteFingerprint?: {
|
|
16
|
+
algorithm: string;
|
|
17
|
+
value: string;
|
|
18
|
+
};
|
|
19
|
+
certificate?: DtlsCertificate;
|
|
20
|
+
mtu?: number;
|
|
21
|
+
}
|
|
22
|
+
export declare interface DtlsTransport {
|
|
23
|
+
on(event: 'connected', listener: (srtpKeys: SrtpKeyingMaterial) => void): this;
|
|
24
|
+
on(event: 'data', listener: (data: Buffer) => void): this;
|
|
25
|
+
on(event: 'error', listener: (err: Error) => void): this;
|
|
26
|
+
on(event: 'close', listener: () => void): this;
|
|
27
|
+
}
|
|
28
|
+
export declare class DtlsTransport extends EventEmitter {
|
|
29
|
+
readonly localCertificate: DtlsCertificate;
|
|
30
|
+
private _state;
|
|
31
|
+
private readonly role;
|
|
32
|
+
private readonly remoteFingerprint;
|
|
33
|
+
private readonly _mtu;
|
|
34
|
+
private sendCb;
|
|
35
|
+
private _startResolve;
|
|
36
|
+
private _startReject;
|
|
37
|
+
private ctx;
|
|
38
|
+
private cipherState;
|
|
39
|
+
private writeEpoch;
|
|
40
|
+
private writeSeq;
|
|
41
|
+
private srtpKeys;
|
|
42
|
+
private readonly _cookieSecret;
|
|
43
|
+
constructor(options: DtlsTransportOptions);
|
|
44
|
+
getState(): DtlsState;
|
|
45
|
+
getLocalFingerprint(): {
|
|
46
|
+
algorithm: 'sha-256';
|
|
47
|
+
value: string;
|
|
48
|
+
};
|
|
49
|
+
setSendCallback(cb: (data: Buffer) => void): void;
|
|
50
|
+
start(): Promise<SrtpKeyingMaterial>;
|
|
51
|
+
handleIncoming(data: Buffer): void;
|
|
52
|
+
private static readonly MAX_RECORD_PLAINTEXT;
|
|
53
|
+
send(data: Buffer): void;
|
|
54
|
+
close(): void;
|
|
55
|
+
private _processRecord;
|
|
56
|
+
private _processHandshakeRecord;
|
|
57
|
+
private _processHandshakeMessage;
|
|
58
|
+
private _processAsClient;
|
|
59
|
+
private _processAsServer;
|
|
60
|
+
private _sendClientHello;
|
|
61
|
+
private _sendClientKeyExchange;
|
|
62
|
+
/**
|
|
63
|
+
* Transmit HelloVerifyRequest WITHOUT adding it to the transcript.
|
|
64
|
+
* RFC 6347 §4.2.1: HVR is excluded from the handshake hash.
|
|
65
|
+
*/
|
|
66
|
+
private _transmitHelloVerifyRequest;
|
|
67
|
+
private _sendServerFlight;
|
|
68
|
+
private _deriveKeys;
|
|
69
|
+
private _sendChangeCipherSpec;
|
|
70
|
+
private _processChangeCipherSpec;
|
|
71
|
+
private _computeFinished;
|
|
72
|
+
private _computePeerFinished;
|
|
73
|
+
private _sendFinished;
|
|
74
|
+
private _processServerFinished;
|
|
75
|
+
private _processClientFinished;
|
|
76
|
+
private _processApplicationData;
|
|
77
|
+
private _processAlert;
|
|
78
|
+
private _encryptRecord;
|
|
79
|
+
private _decryptRecord;
|
|
80
|
+
private _buildHandshakeMessage;
|
|
81
|
+
private _sendHandshakeBody;
|
|
82
|
+
private _sendHandshake;
|
|
83
|
+
private _generateCookie;
|
|
84
|
+
private _transmit;
|
|
85
|
+
private _fail;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
// DTLS 1.2 Transport (RFC 6347)
|
|
2
|
+
// Implements client and server handshake state machines.
|
|
3
|
+
import { EventEmitter } from 'node:events';
|
|
4
|
+
import * as crypto from 'node:crypto';
|
|
5
|
+
import { ContentType, DTLS_VERSION_1_2, } from './types.js';
|
|
6
|
+
import { encodeRecord, decodeRecords, makeRecord } from './record.js';
|
|
7
|
+
import { HandshakeType, CipherSuites, ExtensionType, NamedCurve, SrtpProtectionProfile, encodeHandshakeMessage, decodeHandshakeMessage, encodeClientHello, decodeClientHello, encodeServerHello, decodeServerHello, encodeHelloVerifyRequest, decodeHelloVerifyRequest, encodeCertificate, decodeCertificate, encodeServerKeyExchange, decodeServerKeyExchange, encodeClientKeyExchange, decodeClientKeyExchange, buildUseSrtpExtension, buildSupportedGroupsExtension, buildSignatureAlgorithmsExtension, parseSrtpProfiles, } from './handshake.js';
|
|
8
|
+
import { prf, computeMasterSecret, expandKeyMaterial, exportKeyingMaterial, aesgcmEncrypt, aesgcmDecrypt, generateEcdhKeyPair, computeEcdhPreMasterSecret, encodeEcPublicKey, ecdsaSign, ecdsaVerify, hmacSha256, } from './crypto.js';
|
|
9
|
+
import { generateSelfSignedCertificate, verifyFingerprint, extractPublicKeyFromCert, } from './certificate.js';
|
|
10
|
+
import { DtlsState } from './state.js';
|
|
11
|
+
export { DtlsState };
|
|
12
|
+
// Signature algorithm identifiers (RFC 5246 Section 7.4.1.4.1)
|
|
13
|
+
const SIG_HASH_SHA256 = 4;
|
|
14
|
+
const SIG_ALG_ECDSA = 3;
|
|
15
|
+
const NAMED_CURVE_P256 = 23;
|
|
16
|
+
export class DtlsTransport extends EventEmitter {
|
|
17
|
+
localCertificate;
|
|
18
|
+
_state = DtlsState.New;
|
|
19
|
+
role;
|
|
20
|
+
remoteFingerprint;
|
|
21
|
+
_mtu;
|
|
22
|
+
sendCb;
|
|
23
|
+
_startResolve;
|
|
24
|
+
_startReject;
|
|
25
|
+
ctx = {
|
|
26
|
+
messages: [],
|
|
27
|
+
sendMessageSeq: 0,
|
|
28
|
+
recvMessageSeq: 0,
|
|
29
|
+
};
|
|
30
|
+
cipherState;
|
|
31
|
+
writeEpoch = 0;
|
|
32
|
+
writeSeq = 0n;
|
|
33
|
+
srtpKeys;
|
|
34
|
+
_cookieSecret = crypto.randomBytes(32);
|
|
35
|
+
constructor(options) {
|
|
36
|
+
super();
|
|
37
|
+
this.role = options.role;
|
|
38
|
+
this.remoteFingerprint = options.remoteFingerprint;
|
|
39
|
+
this._mtu = options.mtu ?? 1200;
|
|
40
|
+
this.localCertificate = options.certificate ?? generateSelfSignedCertificate();
|
|
41
|
+
}
|
|
42
|
+
getState() {
|
|
43
|
+
return this._state;
|
|
44
|
+
}
|
|
45
|
+
getLocalFingerprint() {
|
|
46
|
+
return this.localCertificate.fingerprint;
|
|
47
|
+
}
|
|
48
|
+
setSendCallback(cb) {
|
|
49
|
+
this.sendCb = cb;
|
|
50
|
+
}
|
|
51
|
+
async start() {
|
|
52
|
+
if (this._state !== DtlsState.New) {
|
|
53
|
+
throw new Error('DtlsTransport already started');
|
|
54
|
+
}
|
|
55
|
+
this._state = DtlsState.Connecting;
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
this._startResolve = resolve;
|
|
58
|
+
this._startReject = reject;
|
|
59
|
+
if (this.role === 'client') {
|
|
60
|
+
this._sendClientHello(Buffer.alloc(0)).catch((e) => this._fail(e instanceof Error ? e : new Error(String(e))));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
handleIncoming(data) {
|
|
65
|
+
if (this._state === DtlsState.Closed || this._state === DtlsState.Failed)
|
|
66
|
+
return;
|
|
67
|
+
try {
|
|
68
|
+
const records = decodeRecords(data);
|
|
69
|
+
for (const record of records) {
|
|
70
|
+
this._processRecord(record);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
this._fail(e instanceof Error ? e : new Error(String(e)));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Maximum plaintext payload per DTLS record (RFC 6347 §4.1.1 limits to 2^14-1)
|
|
78
|
+
static MAX_RECORD_PLAINTEXT = 16383;
|
|
79
|
+
send(data) {
|
|
80
|
+
if (this._state !== DtlsState.Connected || !this.cipherState) {
|
|
81
|
+
throw new Error('DTLS not connected');
|
|
82
|
+
}
|
|
83
|
+
// Fragment large payloads into individual DTLS records
|
|
84
|
+
const maxLen = DtlsTransport.MAX_RECORD_PLAINTEXT;
|
|
85
|
+
if (data.length <= maxLen) {
|
|
86
|
+
this._transmit(this._encryptRecord(ContentType.ApplicationData, data));
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
let offset = 0;
|
|
90
|
+
while (offset < data.length) {
|
|
91
|
+
const end = Math.min(offset + maxLen, data.length);
|
|
92
|
+
this._transmit(this._encryptRecord(ContentType.ApplicationData, data.subarray(offset, end)));
|
|
93
|
+
offset = end;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
close() {
|
|
98
|
+
if (this._state === DtlsState.Closed)
|
|
99
|
+
return;
|
|
100
|
+
try {
|
|
101
|
+
const alert = Buffer.from([0x01, 0x00]);
|
|
102
|
+
this._transmit(encodeRecord(makeRecord(ContentType.Alert, this.writeEpoch, this.writeSeq++, alert)));
|
|
103
|
+
}
|
|
104
|
+
catch { /* ignore */ }
|
|
105
|
+
this._state = DtlsState.Closed;
|
|
106
|
+
this.emit('close');
|
|
107
|
+
}
|
|
108
|
+
// ── Record dispatch ──────────────────────────────────────────────────────────
|
|
109
|
+
_processRecord(record) {
|
|
110
|
+
switch (record.contentType) {
|
|
111
|
+
case ContentType.Handshake:
|
|
112
|
+
this._processHandshakeRecord(record);
|
|
113
|
+
break;
|
|
114
|
+
case ContentType.ChangeCipherSpec:
|
|
115
|
+
this._processChangeCipherSpec();
|
|
116
|
+
break;
|
|
117
|
+
case ContentType.ApplicationData:
|
|
118
|
+
this._processApplicationData(record);
|
|
119
|
+
break;
|
|
120
|
+
case ContentType.Alert:
|
|
121
|
+
this._processAlert(record);
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
_processHandshakeRecord(record) {
|
|
126
|
+
let fragment;
|
|
127
|
+
try {
|
|
128
|
+
fragment =
|
|
129
|
+
record.epoch > 0 && this.cipherState
|
|
130
|
+
? this._decryptRecord(record)
|
|
131
|
+
: record.fragment;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return; // ignore undecryptable records
|
|
135
|
+
}
|
|
136
|
+
let off = 0;
|
|
137
|
+
while (off < fragment.length) {
|
|
138
|
+
if (fragment.length - off < 12)
|
|
139
|
+
break;
|
|
140
|
+
const preFragLen = (fragment[off + 9] << 16) |
|
|
141
|
+
(fragment[off + 10] << 8) |
|
|
142
|
+
fragment[off + 11];
|
|
143
|
+
if (off + 12 + preFragLen > fragment.length)
|
|
144
|
+
break;
|
|
145
|
+
const msg = decodeHandshakeMessage(fragment.subarray(off));
|
|
146
|
+
const msgSize = 12 + msg.fragmentLength;
|
|
147
|
+
// Add to transcript BEFORE processing (so peer's Finished is included)
|
|
148
|
+
this.ctx.messages.push(Buffer.from(fragment.subarray(off, off + msgSize)));
|
|
149
|
+
off += msgSize;
|
|
150
|
+
this._processHandshakeMessage(msg);
|
|
151
|
+
if (this._state === DtlsState.Failed)
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
_processHandshakeMessage(msg) {
|
|
156
|
+
if (this.role === 'client') {
|
|
157
|
+
this._processAsClient(msg);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
this._processAsServer(msg);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// ── Client state machine ─────────────────────────────────────────────────────
|
|
164
|
+
_processAsClient(msg) {
|
|
165
|
+
switch (msg.msgType) {
|
|
166
|
+
case HandshakeType.HelloVerifyRequest: {
|
|
167
|
+
const hvr = decodeHelloVerifyRequest(msg.body);
|
|
168
|
+
// RFC 6347 §4.2.1: reset transcript/seqs before retrying
|
|
169
|
+
this.ctx.messages = [];
|
|
170
|
+
this.ctx.sendMessageSeq = 0;
|
|
171
|
+
this.ctx.recvMessageSeq = 0;
|
|
172
|
+
this._sendClientHello(hvr.cookie).catch((e) => this._fail(e instanceof Error ? e : new Error(String(e))));
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
case HandshakeType.ServerHello: {
|
|
176
|
+
this.ctx.selectedCipherSuite = decodeServerHello(msg.body).cipherSuite;
|
|
177
|
+
this.ctx.serverRandom = Buffer.from(msg.body.subarray(2, 34));
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
case HandshakeType.Certificate: {
|
|
181
|
+
const certs = decodeCertificate(msg.body);
|
|
182
|
+
const first = certs[0];
|
|
183
|
+
if (!first) {
|
|
184
|
+
this._fail(new Error('No certificate'));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
this.ctx.peerCertDer = first;
|
|
188
|
+
if (this.remoteFingerprint && !verifyFingerprint(first, this.remoteFingerprint)) {
|
|
189
|
+
this._fail(new Error('Certificate fingerprint mismatch'));
|
|
190
|
+
}
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
case HandshakeType.ServerKeyExchange: {
|
|
194
|
+
const ske = decodeServerKeyExchange(msg.body);
|
|
195
|
+
if (this.ctx.peerCertDer) {
|
|
196
|
+
const peerPk = extractPublicKeyFromCert(this.ctx.peerCertDer);
|
|
197
|
+
const toVerify = Buffer.concat([
|
|
198
|
+
this.ctx.clientRandom ?? Buffer.alloc(32),
|
|
199
|
+
this.ctx.serverRandom ?? Buffer.alloc(32),
|
|
200
|
+
Buffer.from([ske.curveType]),
|
|
201
|
+
Buffer.from([(ske.namedCurve >> 8) & 0xff, ske.namedCurve & 0xff]),
|
|
202
|
+
Buffer.from([ske.publicKey.length]),
|
|
203
|
+
ske.publicKey,
|
|
204
|
+
]);
|
|
205
|
+
if (!ecdsaVerify(peerPk, toVerify, ske.signature)) {
|
|
206
|
+
this._fail(new Error('ServerKeyExchange signature verification failed'));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
this.ctx.peerEcPublicKeyBytes = ske.publicKey;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
case HandshakeType.ServerHelloDone:
|
|
214
|
+
this._sendClientKeyExchange().catch((e) => this._fail(e instanceof Error ? e : new Error(String(e))));
|
|
215
|
+
break;
|
|
216
|
+
case HandshakeType.Finished:
|
|
217
|
+
this._processServerFinished(msg.body);
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// ── Server state machine ─────────────────────────────────────────────────────
|
|
222
|
+
_processAsServer(msg) {
|
|
223
|
+
console.log(`[DTLS server] received msgType=${msg.msgType} seq=${msg.messageSeq}`);
|
|
224
|
+
switch (msg.msgType) {
|
|
225
|
+
case HandshakeType.ClientHello: {
|
|
226
|
+
const ch = decodeClientHello(msg.body);
|
|
227
|
+
if (ch.cookie.length === 0) {
|
|
228
|
+
// First ClientHello: send HVR, reset transcript
|
|
229
|
+
const cookie = this._generateCookie(ch.random);
|
|
230
|
+
this.ctx.cookie = cookie;
|
|
231
|
+
this.ctx.messages = [];
|
|
232
|
+
this.ctx.sendMessageSeq = 0;
|
|
233
|
+
this.ctx.recvMessageSeq = 0;
|
|
234
|
+
this._transmitHelloVerifyRequest(cookie);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
// Second ClientHello with cookie
|
|
238
|
+
if (this.ctx.cookie && !ch.cookie.equals(this.ctx.cookie)) {
|
|
239
|
+
this._fail(new Error('Invalid DTLS cookie'));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
this.ctx.clientRandom = Buffer.from(ch.random);
|
|
243
|
+
this._sendServerFlight(ch).catch((e) => this._fail(e instanceof Error ? e : new Error(String(e))));
|
|
244
|
+
}
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
case HandshakeType.ClientKeyExchange: {
|
|
248
|
+
const cke = decodeClientKeyExchange(msg.body);
|
|
249
|
+
this.ctx.peerEcPublicKeyBytes = cke.publicKey;
|
|
250
|
+
if (this.ctx.ecdhPrivateKey && cke.publicKey) {
|
|
251
|
+
this.ctx.preMasterSecret = computeEcdhPreMasterSecret(this.ctx.ecdhPrivateKey, cke.publicKey);
|
|
252
|
+
}
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
case HandshakeType.Finished:
|
|
256
|
+
this._processClientFinished(msg.body);
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// ── Client sends ─────────────────────────────────────────────────────────────
|
|
261
|
+
async _sendClientHello(cookie) {
|
|
262
|
+
const ecdh = generateEcdhKeyPair();
|
|
263
|
+
this.ctx.ecdhPrivateKey = ecdh.privateKey;
|
|
264
|
+
this.ctx.ecdhPublicKey = ecdh.publicKey;
|
|
265
|
+
const clientRandom = crypto.randomBytes(32);
|
|
266
|
+
this.ctx.clientRandom = clientRandom;
|
|
267
|
+
const hello = {
|
|
268
|
+
clientVersion: DTLS_VERSION_1_2,
|
|
269
|
+
random: clientRandom,
|
|
270
|
+
sessionId: Buffer.alloc(0),
|
|
271
|
+
cookie,
|
|
272
|
+
cipherSuites: [
|
|
273
|
+
CipherSuites.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
|
274
|
+
CipherSuites.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
|
275
|
+
],
|
|
276
|
+
compressionMethods: [0],
|
|
277
|
+
extensions: [
|
|
278
|
+
{ type: ExtensionType.UseSrtp, data: buildUseSrtpExtension([SrtpProtectionProfile.SRTP_AES128_CM_SHA1_80, SrtpProtectionProfile.SRTP_AES128_CM_SHA1_32]) },
|
|
279
|
+
{ type: ExtensionType.SupportedGroups, data: buildSupportedGroupsExtension([NamedCurve.secp256r1]) },
|
|
280
|
+
{ type: ExtensionType.SignatureAlgorithms, data: buildSignatureAlgorithmsExtension([{ hash: SIG_HASH_SHA256, sig: SIG_ALG_ECDSA }]) },
|
|
281
|
+
],
|
|
282
|
+
};
|
|
283
|
+
const body = encodeClientHello(hello);
|
|
284
|
+
const seq = this.ctx.sendMessageSeq++;
|
|
285
|
+
this._sendHandshake({ msgType: HandshakeType.ClientHello, length: body.length, messageSeq: seq, fragmentOffset: 0, fragmentLength: body.length, body });
|
|
286
|
+
}
|
|
287
|
+
async _sendClientKeyExchange() {
|
|
288
|
+
if (!this.ctx.ecdhPrivateKey || !this.ctx.ecdhPublicKey || !this.ctx.peerEcPublicKeyBytes) {
|
|
289
|
+
this._fail(new Error('Missing ECDH keys for ClientKeyExchange'));
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
this.ctx.preMasterSecret = computeEcdhPreMasterSecret(this.ctx.ecdhPrivateKey, this.ctx.peerEcPublicKeyBytes);
|
|
293
|
+
const myPkBytes = encodeEcPublicKey(this.ctx.ecdhPublicKey);
|
|
294
|
+
this._sendHandshakeBody(HandshakeType.ClientKeyExchange, encodeClientKeyExchange({ publicKey: myPkBytes }));
|
|
295
|
+
this._deriveKeys();
|
|
296
|
+
this._sendChangeCipherSpec();
|
|
297
|
+
this._sendFinished();
|
|
298
|
+
}
|
|
299
|
+
// ── Server sends ─────────────────────────────────────────────────────────────
|
|
300
|
+
/**
|
|
301
|
+
* Transmit HelloVerifyRequest WITHOUT adding it to the transcript.
|
|
302
|
+
* RFC 6347 §4.2.1: HVR is excluded from the handshake hash.
|
|
303
|
+
*/
|
|
304
|
+
_transmitHelloVerifyRequest(cookie) {
|
|
305
|
+
const hvr = encodeHelloVerifyRequest({ serverVersion: DTLS_VERSION_1_2, cookie });
|
|
306
|
+
const seq = this.ctx.sendMessageSeq++;
|
|
307
|
+
const hsBuf = encodeHandshakeMessage({
|
|
308
|
+
msgType: HandshakeType.HelloVerifyRequest,
|
|
309
|
+
length: hvr.length,
|
|
310
|
+
messageSeq: seq,
|
|
311
|
+
fragmentOffset: 0,
|
|
312
|
+
fragmentLength: hvr.length,
|
|
313
|
+
body: hvr,
|
|
314
|
+
});
|
|
315
|
+
this._transmit(encodeRecord(makeRecord(ContentType.Handshake, this.writeEpoch, this.writeSeq++, hsBuf)));
|
|
316
|
+
// NOT added to this.ctx.messages
|
|
317
|
+
}
|
|
318
|
+
async _sendServerFlight(clientHello) {
|
|
319
|
+
const serverRandom = crypto.randomBytes(32);
|
|
320
|
+
this.ctx.serverRandom = serverRandom;
|
|
321
|
+
const supported = [
|
|
322
|
+
CipherSuites.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
|
323
|
+
CipherSuites.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
|
324
|
+
];
|
|
325
|
+
const cipherSuite = clientHello.cipherSuites.find((cs) => supported.includes(cs)) ??
|
|
326
|
+
CipherSuites.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256;
|
|
327
|
+
this.ctx.selectedCipherSuite = cipherSuite;
|
|
328
|
+
const serverHelloExts = [];
|
|
329
|
+
const srtpExt = clientHello.extensions.find((e) => e.type === ExtensionType.UseSrtp);
|
|
330
|
+
if (srtpExt) {
|
|
331
|
+
const profiles = parseSrtpProfiles(srtpExt.data);
|
|
332
|
+
const profile = profiles.find((p) => p === SrtpProtectionProfile.SRTP_AES128_CM_SHA1_80) ??
|
|
333
|
+
profiles[0] ??
|
|
334
|
+
SrtpProtectionProfile.SRTP_AES128_CM_SHA1_80;
|
|
335
|
+
serverHelloExts.push({ type: ExtensionType.UseSrtp, data: buildUseSrtpExtension([profile]) });
|
|
336
|
+
}
|
|
337
|
+
const sh = {
|
|
338
|
+
serverVersion: DTLS_VERSION_1_2,
|
|
339
|
+
random: serverRandom,
|
|
340
|
+
sessionId: Buffer.alloc(0),
|
|
341
|
+
cipherSuite,
|
|
342
|
+
compressionMethod: 0,
|
|
343
|
+
extensions: serverHelloExts,
|
|
344
|
+
};
|
|
345
|
+
this._sendHandshakeBody(HandshakeType.ServerHello, encodeServerHello(sh));
|
|
346
|
+
this._sendHandshakeBody(HandshakeType.Certificate, encodeCertificate(this.localCertificate.cert));
|
|
347
|
+
const ecdhPair = generateEcdhKeyPair();
|
|
348
|
+
this.ctx.ecdhPrivateKey = ecdhPair.privateKey;
|
|
349
|
+
this.ctx.ecdhPublicKey = ecdhPair.publicKey;
|
|
350
|
+
const serverEcPk = encodeEcPublicKey(ecdhPair.publicKey);
|
|
351
|
+
const clientRandom = this.ctx.clientRandom ?? Buffer.alloc(32);
|
|
352
|
+
const toSign = Buffer.concat([
|
|
353
|
+
clientRandom, serverRandom,
|
|
354
|
+
Buffer.from([3]),
|
|
355
|
+
Buffer.from([(NAMED_CURVE_P256 >> 8) & 0xff, NAMED_CURVE_P256 & 0xff]),
|
|
356
|
+
Buffer.from([serverEcPk.length]),
|
|
357
|
+
serverEcPk,
|
|
358
|
+
]);
|
|
359
|
+
const sig = ecdsaSign(this.localCertificate.privateKey, toSign);
|
|
360
|
+
const ske = {
|
|
361
|
+
curveType: 3,
|
|
362
|
+
namedCurve: NAMED_CURVE_P256,
|
|
363
|
+
publicKey: serverEcPk,
|
|
364
|
+
signatureAlgorithm: { hash: SIG_HASH_SHA256, signature: SIG_ALG_ECDSA },
|
|
365
|
+
signature: sig,
|
|
366
|
+
};
|
|
367
|
+
this._sendHandshakeBody(HandshakeType.ServerKeyExchange, encodeServerKeyExchange(ske));
|
|
368
|
+
this._sendHandshakeBody(HandshakeType.ServerHelloDone, Buffer.alloc(0));
|
|
369
|
+
}
|
|
370
|
+
// ── Key derivation ───────────────────────────────────────────────────────────
|
|
371
|
+
_deriveKeys() {
|
|
372
|
+
const cr = this.ctx.clientRandom ?? Buffer.alloc(32);
|
|
373
|
+
const sr = this.ctx.serverRandom ?? Buffer.alloc(32);
|
|
374
|
+
if (!this.ctx.preMasterSecret)
|
|
375
|
+
throw new Error('No pre-master secret');
|
|
376
|
+
const ms = computeMasterSecret(this.ctx.preMasterSecret, cr, sr);
|
|
377
|
+
const kb = expandKeyMaterial(ms, cr, sr, 16, 4);
|
|
378
|
+
this.cipherState = {
|
|
379
|
+
writeKey: this.role === 'client' ? kb.clientWriteKey : kb.serverWriteKey,
|
|
380
|
+
writeIv: this.role === 'client' ? kb.clientWriteIv : kb.serverWriteIv,
|
|
381
|
+
readKey: this.role === 'client' ? kb.serverWriteKey : kb.clientWriteKey,
|
|
382
|
+
readIv: this.role === 'client' ? kb.serverWriteIv : kb.clientWriteIv,
|
|
383
|
+
writeEpoch: 1, writeSeq: 0n,
|
|
384
|
+
readEpoch: 1, readSeq: 0n,
|
|
385
|
+
};
|
|
386
|
+
const srtpMat = exportKeyingMaterial(ms, cr, sr, 'EXTRACTOR-dtls_srtp', 60);
|
|
387
|
+
this.srtpKeys = {
|
|
388
|
+
clientKey: Buffer.from(srtpMat.subarray(0, 16)),
|
|
389
|
+
serverKey: Buffer.from(srtpMat.subarray(16, 32)),
|
|
390
|
+
clientSalt: Buffer.from(srtpMat.subarray(32, 46)),
|
|
391
|
+
serverSalt: Buffer.from(srtpMat.subarray(46, 60)),
|
|
392
|
+
profile: SrtpProtectionProfile.SRTP_AES128_CM_SHA1_80,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
// ── ChangeCipherSpec / Finished ──────────────────────────────────────────────
|
|
396
|
+
_sendChangeCipherSpec() {
|
|
397
|
+
this._transmit(encodeRecord(makeRecord(ContentType.ChangeCipherSpec, this.writeEpoch, this.writeSeq++, Buffer.from([1]))));
|
|
398
|
+
this.writeEpoch = 1;
|
|
399
|
+
this.writeSeq = 0n;
|
|
400
|
+
}
|
|
401
|
+
_processChangeCipherSpec() {
|
|
402
|
+
// When server receives client's CCS, derive keys if not done yet
|
|
403
|
+
if (!this.cipherState && this.ctx.preMasterSecret) {
|
|
404
|
+
this._deriveKeys();
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
_computeFinished(role) {
|
|
408
|
+
const cr = this.ctx.clientRandom ?? Buffer.alloc(32);
|
|
409
|
+
const sr = this.ctx.serverRandom ?? Buffer.alloc(32);
|
|
410
|
+
if (!this.ctx.preMasterSecret)
|
|
411
|
+
throw new Error('No pre-master secret');
|
|
412
|
+
const ms = computeMasterSecret(this.ctx.preMasterSecret, cr, sr);
|
|
413
|
+
const hash = crypto.createHash('sha256').update(Buffer.concat(this.ctx.messages)).digest();
|
|
414
|
+
return prf(ms, role === 'client' ? 'client finished' : 'server finished', hash, 12);
|
|
415
|
+
}
|
|
416
|
+
_computePeerFinished(peerRole) {
|
|
417
|
+
const cr = this.ctx.clientRandom ?? Buffer.alloc(32);
|
|
418
|
+
const sr = this.ctx.serverRandom ?? Buffer.alloc(32);
|
|
419
|
+
if (!this.ctx.preMasterSecret)
|
|
420
|
+
throw new Error('No pre-master secret');
|
|
421
|
+
const ms = computeMasterSecret(this.ctx.preMasterSecret, cr, sr);
|
|
422
|
+
// Exclude the peer's Finished message (the last one added) from the transcript
|
|
423
|
+
const msgs = this.ctx.messages.slice(0, -1);
|
|
424
|
+
const hash = crypto.createHash('sha256').update(Buffer.concat(msgs)).digest();
|
|
425
|
+
return prf(ms, peerRole === 'client' ? 'client finished' : 'server finished', hash, 12);
|
|
426
|
+
}
|
|
427
|
+
_sendFinished() {
|
|
428
|
+
const verifyData = this._computeFinished(this.role);
|
|
429
|
+
const hsBuf = this._buildHandshakeMessage(HandshakeType.Finished, verifyData);
|
|
430
|
+
this._transmit(this._encryptRecord(ContentType.Handshake, hsBuf));
|
|
431
|
+
// Add our own Finished to transcript AFTER computing verify_data
|
|
432
|
+
this.ctx.messages.push(hsBuf);
|
|
433
|
+
}
|
|
434
|
+
_processServerFinished(body) {
|
|
435
|
+
const expected = this._computePeerFinished('server');
|
|
436
|
+
if (!body.equals(expected)) {
|
|
437
|
+
this._fail(new Error('Server Finished verification failed'));
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
this._state = DtlsState.Connected;
|
|
441
|
+
if (this.srtpKeys) {
|
|
442
|
+
this._startResolve?.(this.srtpKeys);
|
|
443
|
+
this.emit('connected', this.srtpKeys);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
_processClientFinished(body) {
|
|
447
|
+
// Derive keys if CCS was not received first
|
|
448
|
+
if (!this.cipherState && this.ctx.preMasterSecret) {
|
|
449
|
+
this._deriveKeys();
|
|
450
|
+
}
|
|
451
|
+
const expected = this._computePeerFinished('client');
|
|
452
|
+
if (!body.equals(expected)) {
|
|
453
|
+
this._fail(new Error('Client Finished verification failed'));
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
this._sendChangeCipherSpec();
|
|
457
|
+
this._sendFinished();
|
|
458
|
+
this._state = DtlsState.Connected;
|
|
459
|
+
if (this.srtpKeys) {
|
|
460
|
+
this._startResolve?.(this.srtpKeys);
|
|
461
|
+
this.emit('connected', this.srtpKeys);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
// ── Application data / Alerts ────────────────────────────────────────────────
|
|
465
|
+
_processApplicationData(record) {
|
|
466
|
+
if (!this.cipherState)
|
|
467
|
+
return;
|
|
468
|
+
try {
|
|
469
|
+
const decrypted = this._decryptRecord(record);
|
|
470
|
+
this.emit('data', decrypted);
|
|
471
|
+
}
|
|
472
|
+
catch (e) {
|
|
473
|
+
this.emit('error', e instanceof Error ? e : new Error(String(e)));
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
_processAlert(record) {
|
|
477
|
+
if (record.fragment.length >= 2 && record.fragment[1] === 0) {
|
|
478
|
+
this._state = DtlsState.Closed;
|
|
479
|
+
this.emit('close');
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
// ── AES-128-GCM ──────────────────────────────────────────────────────────────
|
|
483
|
+
_encryptRecord(contentType, plaintext) {
|
|
484
|
+
if (!this.cipherState)
|
|
485
|
+
throw new Error('No cipher state');
|
|
486
|
+
const epoch = this.cipherState.writeEpoch;
|
|
487
|
+
const seq = this.cipherState.writeSeq++;
|
|
488
|
+
const explicit = seqBuf8(seq);
|
|
489
|
+
const nonce = Buffer.concat([this.cipherState.writeIv, explicit]);
|
|
490
|
+
const aad = buildAad(epoch, seq, contentType, plaintext.length);
|
|
491
|
+
const { ciphertext, tag } = aesgcmEncrypt(this.cipherState.writeKey, nonce, plaintext, aad);
|
|
492
|
+
return encodeRecord(makeRecord(contentType, epoch, seq, Buffer.concat([explicit, ciphertext, tag])));
|
|
493
|
+
}
|
|
494
|
+
_decryptRecord(record) {
|
|
495
|
+
if (!this.cipherState)
|
|
496
|
+
throw new Error('No cipher state');
|
|
497
|
+
const f = record.fragment;
|
|
498
|
+
if (f.length < 24)
|
|
499
|
+
throw new Error('Encrypted record too short');
|
|
500
|
+
const explicit = f.subarray(0, 8);
|
|
501
|
+
const ciphertext = f.subarray(8, f.length - 16);
|
|
502
|
+
const tag = f.subarray(f.length - 16);
|
|
503
|
+
const nonce = Buffer.concat([this.cipherState.readIv, explicit]);
|
|
504
|
+
const aad = buildAad(record.epoch, record.sequenceNumber, record.contentType, ciphertext.length);
|
|
505
|
+
return aesgcmDecrypt(this.cipherState.readKey, nonce, ciphertext, tag, aad);
|
|
506
|
+
}
|
|
507
|
+
// ── Handshake helpers ────────────────────────────────────────────────────────
|
|
508
|
+
_buildHandshakeMessage(msgType, body) {
|
|
509
|
+
return encodeHandshakeMessage({
|
|
510
|
+
msgType, length: body.length,
|
|
511
|
+
messageSeq: this.ctx.sendMessageSeq++,
|
|
512
|
+
fragmentOffset: 0, fragmentLength: body.length, body,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
_sendHandshakeBody(msgType, body) {
|
|
516
|
+
const hsBuf = this._buildHandshakeMessage(msgType, body);
|
|
517
|
+
this.ctx.messages.push(hsBuf);
|
|
518
|
+
this._transmit(encodeRecord(makeRecord(ContentType.Handshake, this.writeEpoch, this.writeSeq++, hsBuf)));
|
|
519
|
+
}
|
|
520
|
+
_sendHandshake(msg) {
|
|
521
|
+
const hsBuf = encodeHandshakeMessage(msg);
|
|
522
|
+
this.ctx.messages.push(hsBuf);
|
|
523
|
+
this._transmit(encodeRecord(makeRecord(ContentType.Handshake, this.writeEpoch, this.writeSeq++, hsBuf)));
|
|
524
|
+
}
|
|
525
|
+
// ── Cookie ───────────────────────────────────────────────────────────────────
|
|
526
|
+
_generateCookie(clientRandom) {
|
|
527
|
+
return hmacSha256(this._cookieSecret, clientRandom).subarray(0, 20);
|
|
528
|
+
}
|
|
529
|
+
// ── Transmit / Fail ──────────────────────────────────────────────────────────
|
|
530
|
+
_transmit(data) {
|
|
531
|
+
this.sendCb?.(data);
|
|
532
|
+
}
|
|
533
|
+
_fail(err) {
|
|
534
|
+
if (this._state === DtlsState.Failed || this._state === DtlsState.Closed)
|
|
535
|
+
return;
|
|
536
|
+
this._state = DtlsState.Failed;
|
|
537
|
+
this._startReject?.(err);
|
|
538
|
+
this.emit('error', err);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
// ── Utilities ────────────────────────────────────────────────────────────────
|
|
542
|
+
function seqBuf8(seq) {
|
|
543
|
+
const b = Buffer.allocUnsafe(8);
|
|
544
|
+
b.writeUInt32BE(Number((seq >> 32n) & 0xffffffffn), 0);
|
|
545
|
+
b.writeUInt32BE(Number(seq & 0xffffffffn), 4);
|
|
546
|
+
return b;
|
|
547
|
+
}
|
|
548
|
+
function buildAad(epoch, seq, ct, len) {
|
|
549
|
+
const aad = Buffer.allocUnsafe(13);
|
|
550
|
+
aad.writeUInt16BE(epoch, 0);
|
|
551
|
+
aad.writeUInt16BE(Number((seq >> 32n) & 0xffffn), 2);
|
|
552
|
+
aad.writeUInt32BE(Number(seq & 0xffffffffn), 4);
|
|
553
|
+
aad.writeUInt8(ct, 8);
|
|
554
|
+
aad.writeUInt8(DTLS_VERSION_1_2.major, 9);
|
|
555
|
+
aad.writeUInt8(DTLS_VERSION_1_2.minor, 10);
|
|
556
|
+
aad.writeUInt16BE(len, 11);
|
|
557
|
+
return aad;
|
|
558
|
+
}
|
|
559
|
+
//# sourceMappingURL=transport.js.map
|