mirror of
https://github.com/video-dev/hls.js.git
synced 2026-06-16 13:34:44 +00:00
Fix AES CBC byterange addressed I-Frame decryption (for non-16byte boundary m2ts content) (#7892)
This commit is contained in:
@@ -974,12 +974,20 @@ export class CaptionScreen {
|
||||
//
|
||||
// @public (undocumented)
|
||||
export class ChunkMetadata {
|
||||
constructor(level: number, sn: number, id: number, size?: number, part?: number, partial?: boolean, duration?: number, iframe?: boolean);
|
||||
constructor(level: number, sn: number, id: number, size?: number, part?: number, partial?: boolean, duration?: number, iframe?: boolean, decryptRange?: {
|
||||
start: number;
|
||||
end: number;
|
||||
});
|
||||
// (undocumented)
|
||||
readonly buffering: {
|
||||
[key in SourceBufferName]: HlsChunkPerformanceTiming;
|
||||
};
|
||||
// (undocumented)
|
||||
readonly decryptRange?: {
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
// (undocumented)
|
||||
readonly duration: number;
|
||||
// (undocumented)
|
||||
readonly id: number;
|
||||
@@ -1191,7 +1199,10 @@ export interface DecryptData {
|
||||
export class Decrypter {
|
||||
constructor(config: HlsConfig, useSoftware?: boolean);
|
||||
// (undocumented)
|
||||
decrypt(data: Uint8Array | ArrayBuffer, key: ArrayBuffer, iv: ArrayBuffer, aesMode: DecrypterAesMode, plainTextLength?: number): Promise<ArrayBuffer>;
|
||||
decrypt(data: Uint8Array | ArrayBuffer, key: ArrayBuffer, iv: ArrayBuffer, aesMode: DecrypterAesMode, decryptRange?: {
|
||||
start: number;
|
||||
end: number;
|
||||
}): Promise<ArrayBuffer>;
|
||||
// (undocumented)
|
||||
destroy(): void;
|
||||
// (undocumented)
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
mediaFragmentsAreEqual,
|
||||
type Part,
|
||||
} from '../loader/fragment';
|
||||
import FragmentLoader from '../loader/fragment-loader';
|
||||
import FragmentLoader, { getAESAdjustments } from '../loader/fragment-loader';
|
||||
import TaskLoop from '../task-loop';
|
||||
import { PlaylistLevelType } from '../types/loader';
|
||||
import { ChunkMetadata } from '../types/transmuxer';
|
||||
@@ -736,17 +736,18 @@ export default class BaseStreamController
|
||||
) {
|
||||
const startTime = self.performance.now();
|
||||
// decrypt init segment data
|
||||
const byteRange = frag.byteRange;
|
||||
const plainTextLength = byteRange.length
|
||||
? byteRange[1] - byteRange[0]
|
||||
: 0;
|
||||
const { decryptRange } = getAESAdjustments(
|
||||
frag,
|
||||
null,
|
||||
this.iframesOnly,
|
||||
);
|
||||
return this.decrypter
|
||||
.decrypt(
|
||||
new Uint8Array(payload),
|
||||
decryptData.key.buffer,
|
||||
decryptData.iv.buffer,
|
||||
getAesModeFromFullSegmentMethod(decryptData.method),
|
||||
plainTextLength,
|
||||
decryptRange,
|
||||
)
|
||||
.catch((err) => {
|
||||
hls.trigger(Events.ERROR, {
|
||||
@@ -906,6 +907,7 @@ export default class BaseStreamController
|
||||
!partsLoaded ||
|
||||
partsLoaded.length === 0 ||
|
||||
partsLoaded.some((fragLoaded) => !fragLoaded);
|
||||
const { decryptRange } = getAESAdjustments(frag, part);
|
||||
const chunkMeta = new ChunkMetadata(
|
||||
frag.level,
|
||||
frag.sn,
|
||||
@@ -915,6 +917,7 @@ export default class BaseStreamController
|
||||
!complete,
|
||||
frag.duration,
|
||||
this.iframesOnly,
|
||||
decryptRange,
|
||||
);
|
||||
this.log(
|
||||
`load complete ${frag.type} sn: ${frag.sn}${
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ErrorDetails } from '../errors';
|
||||
import { Events } from '../events';
|
||||
import { changeTypeSupported } from '../is-supported';
|
||||
import { ElementaryStreamTypes, isMediaFragment } from '../loader/fragment';
|
||||
import { getAESAdjustments } from '../loader/fragment-loader';
|
||||
import { LoaderContextType, PlaylistLevelType } from '../types/loader';
|
||||
import { ChunkMetadata } from '../types/transmuxer';
|
||||
import { BufferHelper } from '../utils/buffer-helper';
|
||||
@@ -808,16 +809,21 @@ export default class StreamController
|
||||
));
|
||||
const partIndex = part ? part.index : -1;
|
||||
const partial = partIndex !== -1;
|
||||
const byteRange = frag.byteRange;
|
||||
const { decryptRange, byteRange } = getAESAdjustments(
|
||||
frag,
|
||||
part,
|
||||
this.iframesOnly,
|
||||
);
|
||||
const chunkMeta = new ChunkMetadata(
|
||||
frag.level,
|
||||
frag.sn,
|
||||
frag.stats.chunkCount,
|
||||
byteRange.length ? byteRange[1] - byteRange[0] : payload.byteLength,
|
||||
byteRange ? byteRange.end - byteRange.start : payload.byteLength,
|
||||
partIndex,
|
||||
partial,
|
||||
frag.duration,
|
||||
this.iframesOnly,
|
||||
decryptRange,
|
||||
);
|
||||
const initPTS = this.initPTS[frag.cc];
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ const CHUNK_SIZE = 16; // 16 bytes, 128 bits
|
||||
|
||||
export default class Decrypter {
|
||||
private logEnabled: boolean = true;
|
||||
private plainTextLength: number = 0;
|
||||
private decryptRange?: { start: number; end: number };
|
||||
private subtle: SubtleCrypto | null = null;
|
||||
private softwareDecrypter: AESDecryptor | null = null;
|
||||
private key: ArrayBuffer | null = null;
|
||||
@@ -60,8 +60,8 @@ export default class Decrypter {
|
||||
}
|
||||
const data = new Uint8Array(currentResult);
|
||||
this.reset();
|
||||
if (this.plainTextLength) {
|
||||
return data.slice(0, this.plainTextLength);
|
||||
if (this.decryptRange) {
|
||||
return data.slice(this.decryptRange.start, this.decryptRange.end);
|
||||
}
|
||||
return removePadding(data);
|
||||
}
|
||||
@@ -80,10 +80,10 @@ export default class Decrypter {
|
||||
key: ArrayBuffer,
|
||||
iv: ArrayBuffer,
|
||||
aesMode: DecrypterAesMode,
|
||||
plainTextLength: number = 0,
|
||||
decryptRange?: { start: number; end: number },
|
||||
): Promise<ArrayBuffer> {
|
||||
this.plainTextLength = plainTextLength;
|
||||
if (this.useSoftware || plainTextLength) {
|
||||
this.decryptRange = decryptRange;
|
||||
if (this.useSoftware || decryptRange) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const dataView = ArrayBuffer.isView(data) ? data : new Uint8Array(data);
|
||||
this.softwareDecrypt(dataView, key, iv, aesMode);
|
||||
|
||||
@@ -124,12 +124,6 @@ export default class Transmuxer {
|
||||
if (keyData && isFullSegmentEncryption(keyData.method)) {
|
||||
const decrypter = this.getDecrypter();
|
||||
const aesMode = getAesModeFromFullSegmentMethod(keyData.method);
|
||||
|
||||
const plainTextLength =
|
||||
chunkMeta.size !== data.byteLength && chunkMeta.iframe
|
||||
? chunkMeta.size
|
||||
: 0;
|
||||
|
||||
this.asyncResult = true;
|
||||
this.decryptionPromise = decrypter
|
||||
.decrypt(
|
||||
@@ -137,7 +131,7 @@ export default class Transmuxer {
|
||||
keyData.key.buffer,
|
||||
keyData.iv.buffer,
|
||||
aesMode,
|
||||
plainTextLength,
|
||||
chunkMeta.decryptRange,
|
||||
)
|
||||
.then((decryptedData) => {
|
||||
// Calling push here is important; if flush() is called while this is still resolving, this ensures that
|
||||
|
||||
@@ -369,11 +369,31 @@ function createLoaderContext(
|
||||
rangeStart: 0,
|
||||
rangeEnd: 0,
|
||||
};
|
||||
const aesAdjustments = getAESAdjustments(frag, part, isIFrame);
|
||||
if (aesAdjustments.resetIV) {
|
||||
loaderContext.resetIV = true;
|
||||
}
|
||||
if (aesAdjustments.byteRange) {
|
||||
loaderContext.rangeStart = aesAdjustments.byteRange.start;
|
||||
loaderContext.rangeEnd = aesAdjustments.byteRange.end;
|
||||
}
|
||||
return loaderContext;
|
||||
}
|
||||
|
||||
export function getAESAdjustments(
|
||||
frag: Fragment,
|
||||
part: Part | null = null,
|
||||
isIFrame?: boolean,
|
||||
) {
|
||||
const result: {
|
||||
resetIV?: boolean;
|
||||
byteRange?: { start: number; end: number };
|
||||
decryptRange?: { start: number; end: number };
|
||||
} = {};
|
||||
const segment: BaseSegment = part || frag;
|
||||
const start = segment.byteRangeStartOffset as number;
|
||||
const end = segment.byteRangeEndOffset as number;
|
||||
if (Number.isFinite(start) && Number.isFinite(end)) {
|
||||
let byteRangeStart = start;
|
||||
let byteRangeEnd = end;
|
||||
if (
|
||||
(frag.sn === 'initSegment' || isIFrame) &&
|
||||
isMethodFullSegmentAesCbc(frag.decryptdata?.method)
|
||||
@@ -381,19 +401,28 @@ function createLoaderContext(
|
||||
// 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;
|
||||
if (fragmentLen % 16) {
|
||||
byteRangeEnd = end + (16 - (fragmentLen % 16));
|
||||
}
|
||||
if (start !== 0) {
|
||||
loaderContext.resetIV = true;
|
||||
byteRangeStart = start - 16;
|
||||
let byteRangeStart = Math.floor(start / 16) * 16; // round down to 16-byte boundary;
|
||||
const byteRangeEnd = Math.ceil(end / 16) * 16; // round up to 16-byte boundary
|
||||
result.decryptRange = {
|
||||
start: start - byteRangeStart,
|
||||
end: end - byteRangeStart,
|
||||
};
|
||||
if (byteRangeStart >= 16) {
|
||||
result.resetIV = true;
|
||||
byteRangeStart = byteRangeStart - 16;
|
||||
}
|
||||
result.byteRange = {
|
||||
start: byteRangeStart,
|
||||
end: byteRangeEnd,
|
||||
};
|
||||
} else {
|
||||
result.byteRange = {
|
||||
start,
|
||||
end,
|
||||
};
|
||||
}
|
||||
loaderContext.rangeStart = byteRangeStart;
|
||||
loaderContext.rangeEnd = byteRangeEnd;
|
||||
}
|
||||
return loaderContext;
|
||||
return result;
|
||||
}
|
||||
|
||||
function createGapLoadError(frag: Fragment, part?: Part): LoadError {
|
||||
|
||||
@@ -16,6 +16,7 @@ export class ChunkMetadata {
|
||||
public readonly partial: boolean;
|
||||
public readonly iframe: boolean;
|
||||
public readonly duration: number;
|
||||
public readonly decryptRange?: { start: number; end: number };
|
||||
public readonly transmuxing: HlsChunkPerformanceTiming =
|
||||
getNewPerformanceTiming();
|
||||
public readonly buffering: {
|
||||
@@ -35,6 +36,7 @@ export class ChunkMetadata {
|
||||
partial = false,
|
||||
duration?: number,
|
||||
iframe?: boolean,
|
||||
decryptRange?: { start: number; end: number },
|
||||
) {
|
||||
this.level = level;
|
||||
this.sn = sn;
|
||||
@@ -43,6 +45,7 @@ export class ChunkMetadata {
|
||||
this.part = part;
|
||||
this.partial = partial;
|
||||
this.duration = duration || 0;
|
||||
this.decryptRange = decryptRange;
|
||||
this.iframe = iframe || false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user