Skip to content

Commit 52e9056

Browse files
committed
Adding AES-256 and AES-256-CTR encryption modes
1 parent 1950906 commit 52e9056

File tree

15 files changed

+315
-35
lines changed

15 files changed

+315
-35
lines changed

src/controller/base-stream-controller.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import { ErrorDetails, ErrorTypes } from '../errors';
77
import { ChunkMetadata } from '../types/transmuxer';
88
import { appendUint8Array } from '../utils/mp4-tools';
99
import { alignStream } from '../utils/discontinuities';
10+
import {
11+
isFullSegmentEncryption,
12+
getAesModeFromFullSegmentMethod,
13+
} from '../utils/encryption-methods-util';
1014
import {
1115
findFragmentByPDT,
1216
findFragmentByPTS,
@@ -484,7 +488,7 @@ export default class BaseStreamController
484488
payload.byteLength > 0 &&
485489
decryptData?.key &&
486490
decryptData.iv &&
487-
decryptData.method === 'AES-128'
491+
isFullSegmentEncryption(decryptData.method)
488492
) {
489493
const startTime = self.performance.now();
490494
// decrypt init segment data
@@ -493,6 +497,7 @@ export default class BaseStreamController
493497
new Uint8Array(payload),
494498
decryptData.key.buffer,
495499
decryptData.iv.buffer,
500+
getAesModeFromFullSegmentMethod(decryptData.method),
496501
)
497502
.catch((err) => {
498503
hls.trigger(Events.ERROR, {

src/controller/subtitle-stream-controller.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import { PlaylistLevelType } from '../types/loader';
99
import { Level } from '../types/level';
1010
import { subtitleOptionsIdentical } from '../utils/media-option-attributes';
1111
import { ErrorDetails, ErrorTypes } from '../errors';
12+
import {
13+
isFullSegmentEncryption,
14+
getAesModeFromFullSegmentMethod,
15+
} from '../utils/encryption-methods-util';
1216
import type { NetworkComponentAPI } from '../types/component-api';
1317
import type Hls from '../hls';
1418
import type { FragmentTracker } from './fragment-tracker';
@@ -359,7 +363,7 @@ export class SubtitleStreamController
359363
payload.byteLength > 0 &&
360364
decryptData?.key &&
361365
decryptData.iv &&
362-
decryptData.method === 'AES-128'
366+
isFullSegmentEncryption(decryptData.method)
363367
) {
364368
const startTime = performance.now();
365369
// decrypt the subtitles
@@ -368,6 +372,7 @@ export class SubtitleStreamController
368372
new Uint8Array(payload),
369373
decryptData.key.buffer,
370374
decryptData.iv.buffer,
375+
getAesModeFromFullSegmentMethod(decryptData.method),
371376
)
372377
.catch((err) => {
373378
hls.trigger(Events.ERROR, {

src/crypt/aes-crypto.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,32 @@
1+
import { DecrypterAesMode } from './decrypter-aes-mode';
2+
13
export default class AESCrypto {
24
private subtle: SubtleCrypto;
35
private aesIV: Uint8Array;
6+
private aesMode: DecrypterAesMode;
47

5-
constructor(subtle: SubtleCrypto, iv: Uint8Array) {
8+
constructor(subtle: SubtleCrypto, iv: Uint8Array, aesMode: DecrypterAesMode) {
69
this.subtle = subtle;
710
this.aesIV = iv;
11+
this.aesMode = aesMode;
812
}
913

1014
decrypt(data: ArrayBuffer, key: CryptoKey) {
11-
return this.subtle.decrypt({ name: 'AES-CBC', iv: this.aesIV }, key, data);
15+
switch (this.aesMode) {
16+
case DecrypterAesMode.cbc:
17+
return this.subtle.decrypt(
18+
{ name: 'AES-CBC', iv: this.aesIV },
19+
key,
20+
data,
21+
);
22+
case DecrypterAesMode.ctr:
23+
return this.subtle.decrypt(
24+
{ name: 'AES-CTR', counter: this.aesIV, length: 64 }, //64 : NIST SP800-38A standard suggests that the counter should occupy half of the counter block
25+
key,
26+
data,
27+
);
28+
default:
29+
throw new Error(`[AESCrypto] invalid aes mode ${this.aesMode}`);
30+
}
1231
}
1332
}

src/crypt/decrypter-aes-mode.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const enum DecrypterAesMode {
2+
cbc = 0,
3+
ctr = 1,
4+
}

src/crypt/decrypter.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import AESDecryptor, { removePadding } from './aes-decryptor';
44
import { logger } from '../utils/logger';
55
import { appendUint8Array } from '../utils/mp4-tools';
66
import { sliceUint8 } from '../utils/typed-array';
7+
import { DecrypterAesMode } from './decrypter-aes-mode';
78
import type { HlsConfig } from '../config';
89

910
const CHUNK_SIZE = 16; // 16 bytes, 128 bits
@@ -82,10 +83,11 @@ export default class Decrypter {
8283
data: Uint8Array | ArrayBuffer,
8384
key: ArrayBuffer,
8485
iv: ArrayBuffer,
86+
aesMode: DecrypterAesMode,
8587
): Promise<ArrayBuffer> {
8688
if (this.useSoftware) {
8789
return new Promise((resolve, reject) => {
88-
this.softwareDecrypt(new Uint8Array(data), key, iv);
90+
this.softwareDecrypt(new Uint8Array(data), key, iv, aesMode);
8991
const decryptResult = this.flush();
9092
if (decryptResult) {
9193
resolve(decryptResult.buffer);
@@ -94,7 +96,7 @@ export default class Decrypter {
9496
}
9597
});
9698
}
97-
return this.webCryptoDecrypt(new Uint8Array(data), key, iv);
99+
return this.webCryptoDecrypt(new Uint8Array(data), key, iv, aesMode);
98100
}
99101

100102
// Software decryption is progressive. Progressive decryption may not return a result on each call. Any cached
@@ -103,8 +105,13 @@ export default class Decrypter {
103105
data: Uint8Array,
104106
key: ArrayBuffer,
105107
iv: ArrayBuffer,
108+
aesMode: DecrypterAesMode,
106109
): ArrayBuffer | null {
107110
const { currentIV, currentResult, remainderData } = this;
111+
if (aesMode !== DecrypterAesMode.cbc || key.byteLength !== 16) {
112+
logger.warn('SoftwareDecrypt: can only handle AES-128-CBC');
113+
return null;
114+
}
108115
this.logOnce('JS AES decrypt');
109116
// The output is staggered during progressive parsing - the current result is cached, and emitted on the next call
110117
// This is done in order to strip PKCS7 padding, which is found at the end of each segment. We only know we've reached
@@ -147,36 +154,37 @@ export default class Decrypter {
147154
data: Uint8Array,
148155
key: ArrayBuffer,
149156
iv: ArrayBuffer,
157+
aesMode: DecrypterAesMode,
150158
): Promise<ArrayBuffer> {
151159
const subtle = this.subtle;
152160
if (this.key !== key || !this.fastAesKey) {
153161
this.key = key;
154-
this.fastAesKey = new FastAESKey(subtle, key);
162+
this.fastAesKey = new FastAESKey(subtle, key, aesMode);
155163
}
156164
return this.fastAesKey
157165
.expandKey()
158-
.then((aesKey) => {
166+
.then((aesKey: CryptoKey) => {
159167
// decrypt using web crypto
160168
if (!subtle) {
161169
return Promise.reject(new Error('web crypto not initialized'));
162170
}
163171
this.logOnce('WebCrypto AES decrypt');
164-
const crypto = new AESCrypto(subtle, new Uint8Array(iv));
172+
const crypto = new AESCrypto(subtle, new Uint8Array(iv), aesMode);
165173
return crypto.decrypt(data.buffer, aesKey);
166174
})
167175
.catch((err) => {
168176
logger.warn(
169177
`[decrypter]: WebCrypto Error, disable WebCrypto API, ${err.name}: ${err.message}`,
170178
);
171179

172-
return this.onWebCryptoError(data, key, iv);
180+
return this.onWebCryptoError(data, key, iv, aesMode);
173181
});
174182
}
175183

176-
private onWebCryptoError(data, key, iv): ArrayBuffer | never {
184+
private onWebCryptoError(data, key, iv, aesMode): ArrayBuffer | never {
177185
this.useSoftware = true;
178186
this.logEnabled = true;
179-
this.softwareDecrypt(data, key, iv);
187+
this.softwareDecrypt(data, key, iv, aesMode);
180188
const decryptResult = this.flush();
181189
if (decryptResult) {
182190
return decryptResult.buffer;

src/crypt/fast-aes-key.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,35 @@
1+
import { DecrypterAesMode } from './decrypter-aes-mode';
2+
13
export default class FastAESKey {
24
private subtle: any;
35
private key: ArrayBuffer;
6+
private aesMode: DecrypterAesMode;
47

5-
constructor(subtle, key) {
8+
constructor(subtle, key, aesMode: DecrypterAesMode) {
69
this.subtle = subtle;
710
this.key = key;
11+
this.aesMode = aesMode;
812
}
913

1014
expandKey() {
11-
return this.subtle.importKey('raw', this.key, { name: 'AES-CBC' }, false, [
12-
'encrypt',
13-
'decrypt',
14-
]);
15+
const subtleAlgoName = getSubtleAlgoName(this.aesMode);
16+
return this.subtle.importKey(
17+
'raw',
18+
this.key,
19+
{ name: subtleAlgoName },
20+
false,
21+
['encrypt', 'decrypt'],
22+
);
23+
}
24+
}
25+
26+
function getSubtleAlgoName(aesMode: DecrypterAesMode) {
27+
switch (aesMode) {
28+
case DecrypterAesMode.cbc:
29+
return 'AES-CBC';
30+
case DecrypterAesMode.ctr:
31+
return 'AES-CTR';
32+
default:
33+
throw new Error(`[FastAESKey] invalid aes mode ${aesMode}`);
1534
}
1635
}

src/demux/sample-aes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import { HlsConfig } from '../config';
66
import Decrypter from '../crypt/decrypter';
7+
import { DecrypterAesMode } from '../crypt/decrypter-aes-mode';
78
import { HlsEventEmitter } from '../events';
89
import type {
910
AudioSample,
@@ -30,6 +31,7 @@ class SampleAesDecrypter {
3031
encryptedData,
3132
this.keyData.key.buffer,
3233
this.keyData.iv.buffer,
34+
DecrypterAesMode.cbc,
3335
);
3436
}
3537

src/demux/transmuxer.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import { AC3Demuxer } from './audio/ac3-demuxer';
1010
import MP4Remuxer from '../remux/mp4-remuxer';
1111
import PassThroughRemuxer from '../remux/passthrough-remuxer';
1212
import { logger } from '../utils/logger';
13+
import {
14+
isFullSegmentEncryption,
15+
getAesModeFromFullSegmentMethod,
16+
} from '../utils/encryption-methods-util';
1317
import type { Demuxer, DemuxerResult, KeyData } from '../types/demuxer';
1418
import type { Remuxer } from '../types/remuxer';
1519
import type { TransmuxerResult, ChunkMetadata } from '../types/transmuxer';
@@ -114,8 +118,10 @@ export default class Transmuxer {
114118
} = transmuxConfig;
115119

116120
const keyData = getEncryptionType(uintData, decryptdata);
117-
if (keyData && keyData.method === 'AES-128') {
121+
if (keyData && isFullSegmentEncryption(keyData.method)) {
118122
const decrypter = this.getDecrypter();
123+
const aesMode = getAesModeFromFullSegmentMethod(keyData.method);
124+
119125
// Software decryption is synchronous; webCrypto is not
120126
if (decrypter.isSync()) {
121127
// Software decryption is progressive. Progressive decryption may not return a result on each call. Any cached
@@ -124,6 +130,7 @@ export default class Transmuxer {
124130
uintData,
125131
keyData.key.buffer,
126132
keyData.iv.buffer,
133+
aesMode,
127134
);
128135
// For Low-Latency HLS Parts, decrypt in place, since part parsing is expected on push progress
129136
const loadingParts = chunkMeta.part > -1;
@@ -137,7 +144,12 @@ export default class Transmuxer {
137144
uintData = new Uint8Array(decryptedData);
138145
} else {
139146
this.decryptionPromise = decrypter
140-
.webCryptoDecrypt(uintData, keyData.key.buffer, keyData.iv.buffer)
147+
.webCryptoDecrypt(
148+
uintData,
149+
keyData.key.buffer,
150+
keyData.iv.buffer,
151+
aesMode,
152+
)
141153
.then((decryptedData): TransmuxerResult => {
142154
// Calling push here is important; if flush() is called while this is still resolving, this ensures that
143155
// the decrypted data has been transmuxed

src/loader/fragment-loader.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,8 +336,11 @@ function createLoaderContext(
336336
if (Number.isFinite(start) && Number.isFinite(end)) {
337337
let byteRangeStart = start;
338338
let byteRangeEnd = end;
339-
if (frag.sn === 'initSegment' && frag.decryptdata?.method === 'AES-128') {
340-
// MAP segment encrypted with method 'AES-128', when served with HTTP Range,
339+
if (
340+
frag.sn === 'initSegment' &&
341+
isMethodFullSegmentAesCbc(frag.decryptdata?.method)
342+
) {
343+
// MAP segment encrypted with method 'AES-128' or 'AES-256' (cbc), when served with HTTP Range,
341344
// has the unencrypted size specified in the range.
342345
// Ref: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-08#section-6.3.6
343346
const fragmentLen = end - start;
@@ -372,6 +375,10 @@ function createGapLoadError(frag: Fragment, part?: Part): LoadError {
372375
return new LoadError(errorData);
373376
}
374377

378+
function isMethodFullSegmentAesCbc(method) {
379+
return method === 'AES-128' || method === 'AES-256';
380+
}
381+
375382
export class LoadError extends Error {
376383
public readonly data: FragLoadFailResult;
377384
constructor(data: FragLoadFailResult) {

src/loader/key-loader.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,8 @@ export default class KeyLoader implements ComponentAPI {
194194
}
195195
return this.loadKeyEME(keyInfo, frag);
196196
case 'AES-128':
197+
case 'AES-256':
198+
case 'AES-256-CTR':
197199
return this.loadKeyHTTP(keyInfo, frag);
198200
default:
199201
return Promise.reject(

0 commit comments

Comments
 (0)