@agentdance/node-webrtc-srtp 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.
@@ -0,0 +1,174 @@
1
+ import { SrtpContext, SrtcpContext, ProtectionProfile } from './types.js';
2
+ import { aes128cmKeystream, computeSrtpIv, computeSrtcpIv } from './cipher.js';
3
+ import { computeSrtpAuthTag, computeSrtcpAuthTag } from './auth.js';
4
+ import { gcmSrtpUnprotect } from './gcm.js';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Helpers
8
+ // ---------------------------------------------------------------------------
9
+
10
+ function tagLength(profile: ProtectionProfile): 10 | 4 {
11
+ return profile === ProtectionProfile.AES_128_CM_HMAC_SHA1_32 ? 4 : 10;
12
+ }
13
+
14
+ function timingSafeEqual(a: Buffer, b: Buffer): boolean {
15
+ if (a.length !== b.length) return false;
16
+ let result = 0;
17
+ for (let i = 0; i < a.length; i++) {
18
+ result |= a[i]! ^ b[i]!;
19
+ }
20
+ return result === 0;
21
+ }
22
+
23
+ /** Parse the minimum fixed RTP header fields. */
24
+ function parseRtpHeader(pkt: Buffer): { seq: number; ssrc: number; headerLen: number } {
25
+ if (pkt.length < 12) return { seq: 0, ssrc: 0, headerLen: 0 };
26
+ const cc = pkt[0]! & 0x0f;
27
+ const x = (pkt[0]! & 0x10) !== 0;
28
+ let headerLen = 12 + cc * 4;
29
+ if (x && pkt.length >= headerLen + 4) {
30
+ const extLen = pkt.readUInt16BE(headerLen + 2);
31
+ headerLen += 4 + extLen * 4;
32
+ }
33
+ return {
34
+ seq: pkt.readUInt16BE(2),
35
+ ssrc: pkt.readUInt32BE(8),
36
+ headerLen,
37
+ };
38
+ }
39
+
40
+ /**
41
+ * RFC 3711 §3.3.1 – estimate the full 48-bit packet index from a received
42
+ * 16-bit sequence number and the current context state.
43
+ */
44
+ function estimateSrtpIndex(ctx: SrtpContext, seq: number): bigint {
45
+ if (ctx.lastSeq === -1) {
46
+ // No previous packet; accept as-is with the current ROC.
47
+ return BigInt(ctx.rolloverCounter) << 16n | BigInt(seq);
48
+ }
49
+
50
+ const v = BigInt(ctx.rolloverCounter);
51
+ const diff = seq - ctx.lastSeq;
52
+
53
+ if (diff > 0x8000) {
54
+ // seq is much higher than lastSeq → previous ROC (seq wrapped backwards)
55
+ return (v === 0n ? 0n : v - 1n) << 16n | BigInt(seq);
56
+ } else if (diff < -0x8000) {
57
+ // seq is much lower than lastSeq → ROC has incremented (forward wrap)
58
+ return (v + 1n) << 16n | BigInt(seq);
59
+ }
60
+ return v << 16n | BigInt(seq);
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // SRTP unprotect (RFC 3711 §3.1)
65
+ // ---------------------------------------------------------------------------
66
+
67
+ /**
68
+ * Authenticate and decrypt an SRTP packet.
69
+ *
70
+ * @returns Plaintext RTP packet, or `null` if auth fails / replay detected.
71
+ */
72
+ export function srtpUnprotect(ctx: SrtpContext, srtpPacket: Buffer): Buffer | null {
73
+ if (
74
+ ctx.profile === ProtectionProfile.AES_128_GCM ||
75
+ ctx.profile === ProtectionProfile.AES_256_GCM
76
+ ) {
77
+ return gcmSrtpUnprotect(ctx, srtpPacket);
78
+ }
79
+
80
+ const tl = tagLength(ctx.profile);
81
+
82
+ if (srtpPacket.length < 12 + tl) return null;
83
+
84
+ const { seq, ssrc, headerLen } = parseRtpHeader(srtpPacket);
85
+ if (headerLen === 0) return null;
86
+ if (srtpPacket.length < headerLen + tl) return null;
87
+
88
+ // 1. Estimate packet index (handles ROC)
89
+ const index = estimateSrtpIndex(ctx, seq);
90
+
91
+ // 2. Replay check (before expensive crypto)
92
+ if (!ctx.replayWindow.check(index)) return null;
93
+
94
+ // 3. Split the packet
95
+ const header = srtpPacket.subarray(0, headerLen);
96
+ const encryptedPayload = srtpPacket.subarray(headerLen, srtpPacket.length - tl);
97
+ const receivedTag = srtpPacket.subarray(srtpPacket.length - tl);
98
+
99
+ // 4. Verify auth tag
100
+ const roc = Number(index >> 16n) >>> 0;
101
+ const expectedTag = computeSrtpAuthTag(ctx.sessionAuthKey, header, encryptedPayload, roc, tl);
102
+ if (!timingSafeEqual(expectedTag, receivedTag)) return null;
103
+
104
+ // 5. Decrypt payload (AES-128-CM is symmetric: XOR with keystream)
105
+ const iv = computeSrtpIv(ctx.sessionSaltKey, ssrc, index);
106
+ const keystream = aes128cmKeystream(ctx.sessionEncKey, iv, encryptedPayload.length);
107
+ const payload = Buffer.allocUnsafe(encryptedPayload.length);
108
+ for (let i = 0; i < encryptedPayload.length; i++) {
109
+ payload[i] = encryptedPayload[i]! ^ keystream[i]!;
110
+ }
111
+
112
+ // 6. Update state
113
+ ctx.replayWindow.update(index);
114
+ ctx.index = index;
115
+ ctx.rolloverCounter = roc;
116
+ ctx.lastSeq = seq;
117
+
118
+ return Buffer.concat([header, payload]);
119
+ }
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // SRTCP unprotect (RFC 3711 §3.4)
123
+ // ---------------------------------------------------------------------------
124
+
125
+ /**
126
+ * Authenticate and decrypt an SRTCP packet.
127
+ *
128
+ * @returns Plaintext RTCP packet, or `null` if auth fails / replay detected.
129
+ */
130
+ export function srtcpUnprotect(ctx: SrtcpContext, srtcpPacket: Buffer): Buffer | null {
131
+ const tl = tagLength(ctx.profile);
132
+
133
+ // Minimum: 8 bytes RTCP header + 4 bytes E|index + tl bytes tag
134
+ if (srtcpPacket.length < 8 + 4 + tl) return null;
135
+
136
+ // 1. Extract E || SRTCP_index (4 bytes before the auth tag)
137
+ const eSrtcpIndexOffset = srtcpPacket.length - tl - 4;
138
+ const eSrtcpIndex = srtcpPacket.readUInt32BE(eSrtcpIndexOffset);
139
+ const encrypted = (eSrtcpIndex & 0x80000000) !== 0;
140
+ const index = eSrtcpIndex & 0x7fffffff;
141
+
142
+ // 2. Replay check
143
+ if (!ctx.replayWindow.check(BigInt(index))) return null;
144
+
145
+ // 3. Auth tag verification
146
+ // Auth input = packet bytes (everything except the tag itself)
147
+ const packetForAuth = srtcpPacket.subarray(0, eSrtcpIndexOffset); // header + encrypted body
148
+ const receivedTag = srtcpPacket.subarray(srtcpPacket.length - tl);
149
+ const expectedTag = computeSrtcpAuthTag(ctx.sessionAuthKey, packetForAuth, eSrtcpIndex, tl);
150
+ if (!timingSafeEqual(expectedTag, receivedTag)) return null;
151
+
152
+ // 4. Decrypt
153
+ const header = srtcpPacket.subarray(0, 8);
154
+ const encryptedRest = srtcpPacket.subarray(8, eSrtcpIndexOffset);
155
+
156
+ let decryptedRest: Buffer;
157
+ if (encrypted) {
158
+ const ssrc = srtcpPacket.readUInt32BE(4);
159
+ const iv = computeSrtcpIv(ctx.sessionSaltKey, ssrc, index);
160
+ const keystream = aes128cmKeystream(ctx.sessionEncKey, iv, encryptedRest.length);
161
+ decryptedRest = Buffer.allocUnsafe(encryptedRest.length);
162
+ for (let i = 0; i < encryptedRest.length; i++) {
163
+ decryptedRest[i] = encryptedRest[i]! ^ keystream[i]!;
164
+ }
165
+ } else {
166
+ decryptedRest = encryptedRest;
167
+ }
168
+
169
+ // 5. Update state
170
+ ctx.replayWindow.update(BigInt(index));
171
+ ctx.index = index;
172
+
173
+ return Buffer.concat([header, decryptedRest]);
174
+ }