|
@@ -7,7 +7,7 @@ import { isArrayEqual } from './utils';
|
7
|
7
|
// We use a ringbuffer of keys so we can change them and still decode packets that were
|
8
|
8
|
// encrypted with an old key. We use a size of 16 which corresponds to the four bits
|
9
|
9
|
// in the frame trailer.
|
10
|
|
-const keyRingSize = 16;
|
|
10
|
+const KEYRING_SIZE = 16;
|
11
|
11
|
|
12
|
12
|
// We copy the first bytes of the VP8 payload unencrypted.
|
13
|
13
|
// For keyframes this is 10 bytes, for non-keyframes (delta) 3. See
|
|
@@ -19,7 +19,7 @@ const keyRingSize = 16;
|
19
|
19
|
//
|
20
|
20
|
// For audio (where frame.type is not set) we do not encrypt the opus TOC byte:
|
21
|
21
|
// https://tools.ietf.org/html/rfc6716#section-3.1
|
22
|
|
-const unencryptedBytes = {
|
|
22
|
+const UNENCRYPTED_BYTES = {
|
23
|
23
|
key: 10,
|
24
|
24
|
delta: 3,
|
25
|
25
|
undefined: 1 // frame.type is not set on audio
|
|
@@ -27,11 +27,16 @@ const unencryptedBytes = {
|
27
|
27
|
|
28
|
28
|
// Use truncated SHA-256 hashes, 80 bіts for video, 32 bits for audio.
|
29
|
29
|
// This follows the same principles as DTLS-SRTP.
|
30
|
|
-const authenticationTagOptions = {
|
|
30
|
+const AUTHENTICATIONTAG_OPTIONS = {
|
31
|
31
|
name: 'HMAC',
|
32
|
32
|
hash: 'SHA-256'
|
33
|
33
|
};
|
34
|
|
-const digestLength = {
|
|
34
|
+const ENCRYPTION_ALGORITHM = 'AES-CTR';
|
|
35
|
+
|
|
36
|
+// https://developer.mozilla.org/en-US/docs/Web/API/AesCtrParams
|
|
37
|
+const CTR_LENGTH = 64;
|
|
38
|
+
|
|
39
|
+const DIGEST_LENGTH = {
|
35
|
40
|
key: 10,
|
36
|
41
|
delta: 10,
|
37
|
42
|
undefined: 4 // frame.type is not set on audio
|
|
@@ -39,7 +44,7 @@ const digestLength = {
|
39
|
44
|
|
40
|
45
|
// Maximum number of forward ratchets to attempt when the authentication
|
41
|
46
|
// tag on a remote packet does not match the current key.
|
42
|
|
-const ratchetWindow = 8;
|
|
47
|
+const RATCHET_WINDOW_SIZE = 8;
|
43
|
48
|
|
44
|
49
|
/**
|
45
|
50
|
* Per-participant context holding the cryptographic keys and
|
|
@@ -51,7 +56,7 @@ export class Context {
|
51
|
56
|
*/
|
52
|
57
|
constructor(id) {
|
53
|
58
|
// An array (ring) of keys that we use for sending and receiving.
|
54
|
|
- this._cryptoKeyRing = new Array(keyRingSize);
|
|
59
|
+ this._cryptoKeyRing = new Array(KEYRING_SIZE);
|
55
|
60
|
|
56
|
61
|
// A pointer to the currently used key.
|
57
|
62
|
this._currentKeyIndex = -1;
|
|
@@ -123,7 +128,7 @@ export class Context {
|
123
|
128
|
this._sendCount++;
|
124
|
129
|
|
125
|
130
|
// Thіs is not encrypted and contains the VP8 payload descriptor or the Opus TOC byte.
|
126
|
|
- const frameHeader = new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type]);
|
|
131
|
+ const frameHeader = new Uint8Array(encodedFrame.data, 0, UNENCRYPTED_BYTES[encodedFrame.type]);
|
127
|
132
|
|
128
|
133
|
// Construct frame trailer. Similar to the frame header described in
|
129
|
134
|
// https://tools.ietf.org/html/draft-omara-sframe-00#section-4.2
|
|
@@ -162,28 +167,28 @@ export class Context {
|
162
|
167
|
}
|
163
|
168
|
|
164
|
169
|
return crypto.subtle.encrypt({
|
165
|
|
- name: 'AES-CTR',
|
|
170
|
+ name: ENCRYPTION_ALGORITHM,
|
166
|
171
|
counter,
|
167
|
|
- length: 64
|
|
172
|
+ length: CTR_LENGTH
|
168
|
173
|
}, this._cryptoKeyRing[keyIndex].encryptionKey, new Uint8Array(encodedFrame.data,
|
169
|
|
- unencryptedBytes[encodedFrame.type]))
|
|
174
|
+ UNENCRYPTED_BYTES[encodedFrame.type]))
|
170
|
175
|
.then(cipherText => {
|
171
|
176
|
const newData = new ArrayBuffer(frameHeader.byteLength + cipherText.byteLength
|
172
|
|
- + digestLength[encodedFrame.type] + frameTrailer.byteLength);
|
|
177
|
+ + DIGEST_LENGTH[encodedFrame.type] + frameTrailer.byteLength);
|
173
|
178
|
const newUint8 = new Uint8Array(newData);
|
174
|
179
|
|
175
|
180
|
newUint8.set(frameHeader); // copy first bytes.
|
176
|
|
- newUint8.set(new Uint8Array(cipherText), unencryptedBytes[encodedFrame.type]); // add ciphertext.
|
|
181
|
+ newUint8.set(new Uint8Array(cipherText), UNENCRYPTED_BYTES[encodedFrame.type]); // add ciphertext.
|
177
|
182
|
// Leave some space for the authentication tag. This is filled with 0s initially, similar to
|
178
|
183
|
// STUN message-integrity described in https://tools.ietf.org/html/rfc5389#section-15.4
|
179
|
184
|
newUint8.set(frameTrailer, frameHeader.byteLength + cipherText.byteLength
|
180
|
|
- + digestLength[encodedFrame.type]); // append trailer.
|
|
185
|
+ + DIGEST_LENGTH[encodedFrame.type]); // append trailer.
|
181
|
186
|
|
182
|
|
- return crypto.subtle.sign(authenticationTagOptions, this._cryptoKeyRing[keyIndex].authenticationKey,
|
|
187
|
+ return crypto.subtle.sign(AUTHENTICATIONTAG_OPTIONS, this._cryptoKeyRing[keyIndex].authenticationKey,
|
183
|
188
|
new Uint8Array(newData)).then(authTag => {
|
184
|
189
|
// Set the truncated authentication tag.
|
185
|
|
- newUint8.set(new Uint8Array(authTag, 0, digestLength[encodedFrame.type]),
|
186
|
|
- unencryptedBytes[encodedFrame.type] + cipherText.byteLength);
|
|
190
|
+ newUint8.set(new Uint8Array(authTag, 0, DIGEST_LENGTH[encodedFrame.type]),
|
|
191
|
+ UNENCRYPTED_BYTES[encodedFrame.type] + cipherText.byteLength);
|
187
|
192
|
encodedFrame.data = newData;
|
188
|
193
|
|
189
|
194
|
return controller.enqueue(encodedFrame);
|
|
@@ -215,18 +220,18 @@ export class Context {
|
215
|
220
|
|
216
|
221
|
if (this._cryptoKeyRing[keyIndex]) {
|
217
|
222
|
const counterLength = 1 + ((data[encodedFrame.data.byteLength - 1] >> 4) & 0x7);
|
218
|
|
- const frameHeader = new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type]);
|
|
223
|
+ const frameHeader = new Uint8Array(encodedFrame.data, 0, UNENCRYPTED_BYTES[encodedFrame.type]);
|
219
|
224
|
|
220
|
225
|
// Extract the truncated authentication tag.
|
221
|
|
- const authTagOffset = encodedFrame.data.byteLength - (digestLength[encodedFrame.type]
|
|
226
|
+ const authTagOffset = encodedFrame.data.byteLength - (DIGEST_LENGTH[encodedFrame.type]
|
222
|
227
|
+ counterLength + 1);
|
223
|
228
|
const authTag = encodedFrame.data.slice(authTagOffset, authTagOffset
|
224
|
|
- + digestLength[encodedFrame.type]);
|
|
229
|
+ + DIGEST_LENGTH[encodedFrame.type]);
|
225
|
230
|
|
226
|
231
|
// Set authentication tag bytes to 0.
|
227
|
|
- const zeros = new Uint8Array(digestLength[encodedFrame.type]);
|
|
232
|
+ const zeros = new Uint8Array(DIGEST_LENGTH[encodedFrame.type]);
|
228
|
233
|
|
229
|
|
- data.set(zeros, encodedFrame.data.byteLength - (digestLength[encodedFrame.type] + counterLength + 1));
|
|
234
|
+ data.set(zeros, encodedFrame.data.byteLength - (DIGEST_LENGTH[encodedFrame.type] + counterLength + 1));
|
230
|
235
|
|
231
|
236
|
// Do truncated hash comparison. If the hash does not match we might have to advance the
|
232
|
237
|
// ratchet a limited number of times. See (even though the description there is odd)
|
|
@@ -235,12 +240,12 @@ export class Context {
|
235
|
240
|
let valid = false;
|
236
|
241
|
let newKeys = null;
|
237
|
242
|
|
238
|
|
- for (let distance = 0; distance < ratchetWindow; distance++) {
|
239
|
|
- const calculatedTag = await crypto.subtle.sign(authenticationTagOptions,
|
|
243
|
+ for (let distance = 0; distance < RATCHET_WINDOW_SIZE; distance++) {
|
|
244
|
+ const calculatedTag = await crypto.subtle.sign(AUTHENTICATIONTAG_OPTIONS,
|
240
|
245
|
authenticationKey, encodedFrame.data);
|
241
|
246
|
|
242
|
247
|
if (isArrayEqual(new Uint8Array(authTag),
|
243
|
|
- new Uint8Array(calculatedTag.slice(0, digestLength[encodedFrame.type])))) {
|
|
248
|
+ new Uint8Array(calculatedTag.slice(0, DIGEST_LENGTH[encodedFrame.type])))) {
|
244
|
249
|
valid = true;
|
245
|
250
|
if (distance > 0) {
|
246
|
251
|
this._setKeys(newKeys);
|
|
@@ -279,19 +284,19 @@ export class Context {
|
279
|
284
|
}
|
280
|
285
|
|
281
|
286
|
return crypto.subtle.decrypt({
|
282
|
|
- name: 'AES-CTR',
|
|
287
|
+ name: ENCRYPTION_ALGORITHM,
|
283
|
288
|
counter,
|
284
|
|
- length: 64
|
|
289
|
+ length: CTR_LENGTH
|
285
|
290
|
}, this._cryptoKeyRing[keyIndex].encryptionKey, new Uint8Array(encodedFrame.data,
|
286
|
|
- unencryptedBytes[encodedFrame.type],
|
287
|
|
- encodedFrame.data.byteLength - (unencryptedBytes[encodedFrame.type]
|
288
|
|
- + digestLength[encodedFrame.type] + counterLength + 1))
|
|
291
|
+ UNENCRYPTED_BYTES[encodedFrame.type],
|
|
292
|
+ encodedFrame.data.byteLength - (UNENCRYPTED_BYTES[encodedFrame.type]
|
|
293
|
+ + DIGEST_LENGTH[encodedFrame.type] + counterLength + 1))
|
289
|
294
|
).then(plainText => {
|
290
|
|
- const newData = new ArrayBuffer(unencryptedBytes[encodedFrame.type] + plainText.byteLength);
|
|
295
|
+ const newData = new ArrayBuffer(UNENCRYPTED_BYTES[encodedFrame.type] + plainText.byteLength);
|
291
|
296
|
const newUint8 = new Uint8Array(newData);
|
292
|
297
|
|
293
|
298
|
newUint8.set(frameHeader);
|
294
|
|
- newUint8.set(new Uint8Array(plainText), unencryptedBytes[encodedFrame.type]);
|
|
299
|
+ newUint8.set(new Uint8Array(plainText), UNENCRYPTED_BYTES[encodedFrame.type]);
|
295
|
300
|
encodedFrame.data = newData;
|
296
|
301
|
|
297
|
302
|
return controller.enqueue(encodedFrame);
|