Skip to content

Commit 7dcc65c

Browse files
author
root
committed
Adding AES-256 and AES-256-CTR encryption modes
1 parent de29d91 commit 7dcc65c

16 files changed

+320
-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,
@@ -486,7 +490,7 @@ export default class BaseStreamController
486490
payload.byteLength > 0 &&
487491
decryptData?.key &&
488492
decryptData.iv &&
489-
decryptData.method === 'AES-128'
493+
isFullSegmentEncryption(decryptData.method)
490494
) {
491495
const startTime = self.performance.now();
492496
// decrypt init segment data
@@ -495,6 +499,7 @@ export default class BaseStreamController
495499
new Uint8Array(payload),
496500
decryptData.key.buffer,
497501
decryptData.iv.buffer,
502+
getAesModeFromFullSegmentMethod(decryptData.method),
498503
)
499504
.catch((err) => {
500505
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';
@@ -360,7 +364,7 @@ export class SubtitleStreamController
360364
payload.byteLength > 0 &&
361365
decryptData?.key &&
362366
decryptData.iv &&
363-
decryptData.method === 'AES-128'
367+
isFullSegmentEncryption(decryptData.method)
364368
) {
365369
const startTime = performance.now();
366370
// decrypt the subtitles
@@ -369,6 +373,7 @@ export class SubtitleStreamController
369373
new Uint8Array(payload),
370374
decryptData.key.buffer,
371375
decryptData.iv.buffer,
376+
getAesModeFromFullSegmentMethod(decryptData.method),
372377
)
373378
.catch((err) => {
374379
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
@@ -81,10 +82,11 @@ export default class Decrypter {
8182
data: Uint8Array | ArrayBuffer,
8283
key: ArrayBuffer,
8384
iv: ArrayBuffer,
85+
aesMode: DecrypterAesMode,
8486
): Promise<ArrayBuffer> {
8587
if (this.useSoftware) {
8688
return new Promise((resolve, reject) => {
87-
this.softwareDecrypt(new Uint8Array(data), key, iv);
89+
this.softwareDecrypt(new Uint8Array(data), key, iv, aesMode);
8890
const decryptResult = this.flush();
8991
if (decryptResult) {
9092
resolve(decryptResult.buffer);
@@ -93,7 +95,7 @@ export default class Decrypter {
9395
}
9496
});
9597
}
96-
return this.webCryptoDecrypt(new Uint8Array(data), key, iv);
98+
return this.webCryptoDecrypt(new Uint8Array(data), key, iv, aesMode);
9799
}
98100

99101
// Software decryption is progressive. Progressive decryption may not return a result on each call. Any cached
@@ -102,8 +104,13 @@ export default class Decrypter {
102104
data: Uint8Array,
103105
key: ArrayBuffer,
104106
iv: ArrayBuffer,
107+
aesMode: DecrypterAesMode,
105108
): ArrayBuffer | null {
106109
const { currentIV, currentResult, remainderData } = this;
110+
if (aesMode !== DecrypterAesMode.cbc || key.byteLength !== 16) {
111+
logger.warn('SoftwareDecrypt: can only handle AES-128-CBC');
112+
return null;
113+
}
107114
this.logOnce('JS AES decrypt');
108115
// The output is staggered during progressive parsing - the current result is cached, and emitted on the next call
109116
// This is done in order to strip PKCS7 padding, which is found at the end of each segment. We only know we've reached
@@ -146,38 +153,39 @@ export default class Decrypter {
146153
data: Uint8Array,
147154
key: ArrayBuffer,
148155
iv: ArrayBuffer,
156+
aesMode: DecrypterAesMode,
149157
): Promise<ArrayBuffer> {
150158
const subtle = this.subtle;
151159
if (this.key !== key || !this.fastAesKey) {
152160
this.key = key;
153-
this.fastAesKey = new FastAESKey(subtle, key);
161+
this.fastAesKey = new FastAESKey(subtle, key, aesMode);
154162
}
155163
return this.fastAesKey
156164
.expandKey()
157-
.then((aesKey) => {
165+
.then((aesKey: CryptoKey) => {
158166
// decrypt using web crypto
159167
if (!subtle) {
160168
return Promise.reject(new Error('web crypto not initialized'));
161169
}
162170
this.logOnce('WebCrypto AES decrypt');
163-
const crypto = new AESCrypto(subtle, new Uint8Array(iv));
171+
const crypto = new AESCrypto(subtle, new Uint8Array(iv), aesMode);
164172
return crypto.decrypt(data.buffer, aesKey);
165173
})
166174
.catch((err) => {
167175
logger.warn(
168176
`[decrypter]: WebCrypto Error, disable WebCrypto API, ${err.name}: ${err.message}`,
169177
);
170178

171-
return this.onWebCryptoError(data, key, iv);
179+
return this.onWebCryptoError(data, key, iv, aesMode);
172180
});
173181
}
174182

175-
private onWebCryptoError(data, key, iv): ArrayBuffer | never {
183+
private onWebCryptoError(data, key, iv, aesMode): ArrayBuffer | never {
176184
const enableSoftwareAES = this.enableSoftwareAES;
177185
if (enableSoftwareAES) {
178186
this.useSoftware = true;
179187
this.logEnabled = true;
180-
this.softwareDecrypt(data, key, iv);
188+
this.softwareDecrypt(data, key, iv, aesMode);
181189
const decryptResult = this.flush();
182190
if (decryptResult) {
183191
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)