Skip to content

Commit

Permalink
Adding AES-256 and AES-256-CTR encryption modes
Browse files Browse the repository at this point in the history
  • Loading branch information
root authored and robwalch committed Jan 24, 2024
1 parent d3b59f2 commit 55b4aac
Show file tree
Hide file tree
Showing 16 changed files with 320 additions and 35 deletions.
7 changes: 6 additions & 1 deletion src/controller/base-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import { ErrorDetails, ErrorTypes } from '../errors';
import { ChunkMetadata } from '../types/transmuxer';
import { appendUint8Array } from '../utils/mp4-tools';
import { alignStream } from '../utils/discontinuities';
import {
isFullSegmentEncryption,
getAesModeFromFullSegmentMethod,
} from '../utils/encryption-methods-util';
import {
findFragmentByPDT,
findFragmentByPTS,
Expand Down Expand Up @@ -481,7 +485,7 @@ export default class BaseStreamController
payload.byteLength > 0 &&
decryptData?.key &&
decryptData.iv &&
decryptData.method === 'AES-128'
isFullSegmentEncryption(decryptData.method)
) {
const startTime = self.performance.now();
// decrypt init segment data
Expand All @@ -490,6 +494,7 @@ export default class BaseStreamController
new Uint8Array(payload),
decryptData.key.buffer,
decryptData.iv.buffer,
getAesModeFromFullSegmentMethod(decryptData.method),
)
.catch((err) => {
hls.trigger(Events.ERROR, {
Expand Down
7 changes: 6 additions & 1 deletion src/controller/subtitle-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import { PlaylistLevelType } from '../types/loader';
import { Level } from '../types/level';
import { subtitleOptionsIdentical } from '../utils/media-option-attributes';
import { ErrorDetails, ErrorTypes } from '../errors';
import {
isFullSegmentEncryption,
getAesModeFromFullSegmentMethod,
} from '../utils/encryption-methods-util';
import type { NetworkComponentAPI } from '../types/component-api';
import type Hls from '../hls';
import type { FragmentTracker } from './fragment-tracker';
Expand Down Expand Up @@ -360,7 +364,7 @@ export class SubtitleStreamController
payload.byteLength > 0 &&
decryptData?.key &&
decryptData.iv &&
decryptData.method === 'AES-128'
isFullSegmentEncryption(decryptData.method)
) {
const startTime = performance.now();
// decrypt the subtitles
Expand All @@ -369,6 +373,7 @@ export class SubtitleStreamController
new Uint8Array(payload),
decryptData.key.buffer,
decryptData.iv.buffer,
getAesModeFromFullSegmentMethod(decryptData.method),
)
.catch((err) => {
hls.trigger(Events.ERROR, {
Expand Down
23 changes: 21 additions & 2 deletions src/crypt/aes-crypto.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
import { DecrypterAesMode } from './decrypter-aes-mode';

export default class AESCrypto {
private subtle: SubtleCrypto;
private aesIV: Uint8Array;
private aesMode: DecrypterAesMode;

constructor(subtle: SubtleCrypto, iv: Uint8Array) {
constructor(subtle: SubtleCrypto, iv: Uint8Array, aesMode: DecrypterAesMode) {
this.subtle = subtle;
this.aesIV = iv;
this.aesMode = aesMode;
}

decrypt(data: ArrayBuffer, key: CryptoKey) {
return this.subtle.decrypt({ name: 'AES-CBC', iv: this.aesIV }, key, data);
switch (this.aesMode) {
case DecrypterAesMode.cbc:
return this.subtle.decrypt(
{ name: 'AES-CBC', iv: this.aesIV },
key,
data,
);
case DecrypterAesMode.ctr:
return this.subtle.decrypt(
{ name: 'AES-CTR', counter: this.aesIV, length: 64 }, //64 : NIST SP800-38A standard suggests that the counter should occupy half of the counter block
key,
data,
);
default:
throw new Error(`[AESCrypto] invalid aes mode ${this.aesMode}`);
}
}
}
4 changes: 4 additions & 0 deletions src/crypt/decrypter-aes-mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const enum DecrypterAesMode {
cbc = 0,
ctr = 1,
}
24 changes: 16 additions & 8 deletions src/crypt/decrypter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import AESDecryptor, { removePadding } from './aes-decryptor';
import { logger } from '../utils/logger';
import { appendUint8Array } from '../utils/mp4-tools';
import { sliceUint8 } from '../utils/typed-array';
import { DecrypterAesMode } from './decrypter-aes-mode';
import type { HlsConfig } from '../config';

const CHUNK_SIZE = 16; // 16 bytes, 128 bits
Expand Down Expand Up @@ -81,10 +82,11 @@ export default class Decrypter {
data: Uint8Array | ArrayBuffer,
key: ArrayBuffer,
iv: ArrayBuffer,
aesMode: DecrypterAesMode,
): Promise<ArrayBuffer> {
if (this.useSoftware) {
return new Promise((resolve, reject) => {
this.softwareDecrypt(new Uint8Array(data), key, iv);
this.softwareDecrypt(new Uint8Array(data), key, iv, aesMode);
const decryptResult = this.flush();
if (decryptResult) {
resolve(decryptResult.buffer);
Expand All @@ -93,7 +95,7 @@ export default class Decrypter {
}
});
}
return this.webCryptoDecrypt(new Uint8Array(data), key, iv);
return this.webCryptoDecrypt(new Uint8Array(data), key, iv, aesMode);
}

// Software decryption is progressive. Progressive decryption may not return a result on each call. Any cached
Expand All @@ -102,8 +104,13 @@ export default class Decrypter {
data: Uint8Array,
key: ArrayBuffer,
iv: ArrayBuffer,
aesMode: DecrypterAesMode,
): ArrayBuffer | null {
const { currentIV, currentResult, remainderData } = this;
if (aesMode !== DecrypterAesMode.cbc || key.byteLength !== 16) {
logger.warn('SoftwareDecrypt: can only handle AES-128-CBC');
return null;
}
this.logOnce('JS AES decrypt');
// The output is staggered during progressive parsing - the current result is cached, and emitted on the next call
// This is done in order to strip PKCS7 padding, which is found at the end of each segment. We only know we've reached
Expand Down Expand Up @@ -146,38 +153,39 @@ export default class Decrypter {
data: Uint8Array,
key: ArrayBuffer,
iv: ArrayBuffer,
aesMode: DecrypterAesMode,
): Promise<ArrayBuffer> {
const subtle = this.subtle;
if (this.key !== key || !this.fastAesKey) {
this.key = key;
this.fastAesKey = new FastAESKey(subtle, key);
this.fastAesKey = new FastAESKey(subtle, key, aesMode);
}
return this.fastAesKey
.expandKey()
.then((aesKey) => {
.then((aesKey: CryptoKey) => {
// decrypt using web crypto
if (!subtle) {
return Promise.reject(new Error('web crypto not initialized'));
}
this.logOnce('WebCrypto AES decrypt');
const crypto = new AESCrypto(subtle, new Uint8Array(iv));
const crypto = new AESCrypto(subtle, new Uint8Array(iv), aesMode);
return crypto.decrypt(data.buffer, aesKey);
})
.catch((err) => {
logger.warn(
`[decrypter]: WebCrypto Error, disable WebCrypto API, ${err.name}: ${err.message}`,
);

return this.onWebCryptoError(data, key, iv);
return this.onWebCryptoError(data, key, iv, aesMode);
});
}

private onWebCryptoError(data, key, iv): ArrayBuffer | never {
private onWebCryptoError(data, key, iv, aesMode): ArrayBuffer | never {
const enableSoftwareAES = this.enableSoftwareAES;
if (enableSoftwareAES) {
this.useSoftware = true;
this.logEnabled = true;
this.softwareDecrypt(data, key, iv);
this.softwareDecrypt(data, key, iv, aesMode);
const decryptResult = this.flush();
if (decryptResult) {
return decryptResult.buffer;
Expand Down
29 changes: 24 additions & 5 deletions src/crypt/fast-aes-key.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,35 @@
import { DecrypterAesMode } from './decrypter-aes-mode';

export default class FastAESKey {
private subtle: any;
private key: ArrayBuffer;
private aesMode: DecrypterAesMode;

constructor(subtle, key) {
constructor(subtle, key, aesMode: DecrypterAesMode) {
this.subtle = subtle;
this.key = key;
this.aesMode = aesMode;
}

expandKey() {
return this.subtle.importKey('raw', this.key, { name: 'AES-CBC' }, false, [
'encrypt',
'decrypt',
]);
const subtleAlgoName = getSubtleAlgoName(this.aesMode);
return this.subtle.importKey(
'raw',
this.key,
{ name: subtleAlgoName },
false,
['encrypt', 'decrypt'],
);
}
}

function getSubtleAlgoName(aesMode: DecrypterAesMode) {
switch (aesMode) {
case DecrypterAesMode.cbc:
return 'AES-CBC';
case DecrypterAesMode.ctr:
return 'AES-CTR';
default:
throw new Error(`[FastAESKey] invalid aes mode ${aesMode}`);
}
}
2 changes: 2 additions & 0 deletions src/demux/sample-aes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import { HlsConfig } from '../config';
import Decrypter from '../crypt/decrypter';
import { DecrypterAesMode } from '../crypt/decrypter-aes-mode';
import { HlsEventEmitter } from '../events';
import type {
AudioSample,
Expand All @@ -30,6 +31,7 @@ class SampleAesDecrypter {
encryptedData,
this.keyData.key.buffer,
this.keyData.iv.buffer,
DecrypterAesMode.cbc,
);
}

Expand Down
16 changes: 14 additions & 2 deletions src/demux/transmuxer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import { AC3Demuxer } from './audio/ac3-demuxer';
import MP4Remuxer from '../remux/mp4-remuxer';
import PassThroughRemuxer from '../remux/passthrough-remuxer';
import { logger } from '../utils/logger';
import {
isFullSegmentEncryption,
getAesModeFromFullSegmentMethod,
} from '../utils/encryption-methods-util';
import type { Demuxer, DemuxerResult, KeyData } from '../types/demuxer';
import type { Remuxer } from '../types/remuxer';
import type { TransmuxerResult, ChunkMetadata } from '../types/transmuxer';
Expand Down Expand Up @@ -115,8 +119,10 @@ export default class Transmuxer {
} = transmuxConfig;

const keyData = getEncryptionType(uintData, decryptdata);
if (keyData && keyData.method === 'AES-128') {
if (keyData && isFullSegmentEncryption(keyData.method)) {
const decrypter = this.getDecrypter();
const aesMode = getAesModeFromFullSegmentMethod(keyData.method);

// Software decryption is synchronous; webCrypto is not
if (decrypter.isSync()) {
// Software decryption is progressive. Progressive decryption may not return a result on each call. Any cached
Expand All @@ -125,6 +131,7 @@ export default class Transmuxer {
uintData,
keyData.key.buffer,
keyData.iv.buffer,
aesMode,
);
// For Low-Latency HLS Parts, decrypt in place, since part parsing is expected on push progress
const loadingParts = chunkMeta.part > -1;
Expand All @@ -138,7 +145,12 @@ export default class Transmuxer {
uintData = new Uint8Array(decryptedData);
} else {
this.decryptionPromise = decrypter
.webCryptoDecrypt(uintData, keyData.key.buffer, keyData.iv.buffer)
.webCryptoDecrypt(
uintData,
keyData.key.buffer,
keyData.iv.buffer,
aesMode,
)
.then((decryptedData): TransmuxerResult => {
// Calling push here is important; if flush() is called while this is still resolving, this ensures that
// the decrypted data has been transmuxed
Expand Down
11 changes: 9 additions & 2 deletions src/loader/fragment-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,8 +336,11 @@ function createLoaderContext(
if (Number.isFinite(start) && Number.isFinite(end)) {
let byteRangeStart = start;
let byteRangeEnd = end;
if (frag.sn === 'initSegment' && frag.decryptdata?.method === 'AES-128') {
// MAP segment encrypted with method 'AES-128', when served with HTTP Range,
if (
frag.sn === 'initSegment' &&
isMethodFullSegmentAesCbc(frag.decryptdata?.method)
) {
// MAP segment encrypted with method 'AES-128' or 'AES-256' (cbc), when served with HTTP Range,
// has the unencrypted size specified in the range.
// Ref: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-08#section-6.3.6
const fragmentLen = end - start;
Expand Down Expand Up @@ -372,6 +375,10 @@ function createGapLoadError(frag: Fragment, part?: Part): LoadError {
return new LoadError(errorData);
}

function isMethodFullSegmentAesCbc(method) {
return method === 'AES-128' || method === 'AES-256';
}

export class LoadError extends Error {
public readonly data: FragLoadFailResult;
constructor(data: FragLoadFailResult) {
Expand Down
2 changes: 2 additions & 0 deletions src/loader/key-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ export default class KeyLoader implements ComponentAPI {
}
return this.loadKeyEME(keyInfo, frag);
case 'AES-128':
case 'AES-256':
case 'AES-256-CTR':
return this.loadKeyHTTP(keyInfo, frag);
default:
return Promise.reject(
Expand Down
19 changes: 10 additions & 9 deletions src/loader/level-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
changeEndianness,
convertDataUriToArrayBytes,
} from '../utils/keysystem-util';
import { isFullSegmentEncryption } from '../utils/encryption-methods-util';
import { KeySystemFormats } from '../utils/mediakeys-helper';
import { mp4pssh } from '../utils/mp4-tools';
import { logger } from '../utils/logger';
Expand Down Expand Up @@ -51,13 +52,14 @@ export class LevelKey implements DecryptData {
this.keyFormatVersions = formatversions;
this.iv = iv;
this.encrypted = method ? method !== 'NONE' : false;
this.isCommonEncryption = this.encrypted && method !== 'AES-128';
this.isCommonEncryption =
this.encrypted && !isFullSegmentEncryption(method);
}

public isSupported(): boolean {
// If it's Segment encryption or No encryption, just select that key system
if (this.method) {
if (this.method === 'AES-128' || this.method === 'NONE') {
if (isFullSegmentEncryption(this.method) || this.method === 'NONE') {
return true;
}
if (this.keyFormat === 'identity') {
Expand Down Expand Up @@ -88,16 +90,15 @@ export class LevelKey implements DecryptData {
return null;
}

if (this.method === 'AES-128' && this.uri && !this.iv) {
if (isFullSegmentEncryption(this.method) && this.uri && !this.iv) {
if (typeof sn !== 'number') {
// We are fetching decryption data for a initialization segment
// If the segment was encrypted with AES-128
// If the segment was encrypted with AES-128/256
// It must have an IV defined. We cannot substitute the Segment Number in.
if (this.method === 'AES-128' && !this.iv) {
logger.warn(
`missing IV for initialization segment with method="${this.method}" - compliance issue`,
);
}
logger.warn(
`missing IV for initialization segment with method="${this.method}" - compliance issue`,
);

// Explicitly set sn to resulting value from implicit conversions 'initSegment' values for IV generation.
sn = 0;
}
Expand Down
21 changes: 21 additions & 0 deletions src/utils/encryption-methods-util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { DecrypterAesMode } from '../crypt/decrypter-aes-mode';

export function isFullSegmentEncryption(method: string): boolean {
return (
method === 'AES-128' || method === 'AES-256' || method === 'AES-256-CTR'
);
}

export function getAesModeFromFullSegmentMethod(
method: string,
): DecrypterAesMode {
switch (method) {
case 'AES-128':
case 'AES-256':
return DecrypterAesMode.cbc;
case 'AES-256-CTR':
return DecrypterAesMode.ctr;
default:
throw new Error(`invalid full segment method ${method}`);
}
}
1 change: 1 addition & 0 deletions tests/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import './unit/controller/subtitle-track-controller';
import './unit/controller/timeline-controller-nonnative';
import './unit/controller/timeline-controller';
import './unit/crypt/aes-decryptor';
import './unit/crypt/decrypter';
import './unit/demuxer/adts';
import './unit/demuxer/base-audio-demuxer';
import './unit/demuxer/id3';
Expand Down
Loading

0 comments on commit 55b4aac

Please sign in to comment.