Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent Widevine access request on "encrypted" w/o configured license url #6644

Merged
merged 3 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 40 additions & 16 deletions src/controller/eme-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import {
keySystemDomainToKeySystemFormat as keySystemToKeySystemFormat,
KeySystemFormats,
keySystemFormatToKeySystemDomain,
KeySystemIds,
keySystemIdToKeySystemDomain,
KeySystems,
requestMediaKeySystemAccess,
parsePlayReadyWRM,
} from '../utils/mediakeys-helper';
import { strToUtf8array } from '../utils/utf8-utils';
import { base64Decode } from '../utils/numeric-encoding-utils';
Expand All @@ -25,8 +25,8 @@ import {
bin2str,
parseMultiPssh,
parseSinf,
PsshData,
PsshInvalidResult,
type PsshData,
type PsshInvalidResult,
} from '../utils/mp4-tools';
import { EventEmitter } from 'eventemitter3';
import type Hls from '../hls';
Expand Down Expand Up @@ -127,7 +127,7 @@ class EMEController extends Logger implements ComponentAPI {
this.hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this);
}

private getLicenseServerUrl(keySystem: KeySystems): string | never {
private getLicenseServerUrl(keySystem: KeySystems): string | undefined {
const { drmSystems, widevineLicenseUrl } = this.config;
const keySystemConfiguration = drmSystems[keySystem];

Expand All @@ -139,10 +139,16 @@ class EMEController extends Logger implements ComponentAPI {
if (keySystem === KeySystems.WIDEVINE && widevineLicenseUrl) {
return widevineLicenseUrl;
}
}

throw new Error(
`no license server URL configured for key-system "${keySystem}"`,
);
private getLicenseServerUrlOrThrow(keySystem: KeySystems): string | never {
const url = this.getLicenseServerUrl(keySystem);
if (url === undefined) {
throw new Error(
`no license server URL configured for key-system "${keySystem}"`,
);
}
return url;
}

private getServerCertificateUrl(keySystem: KeySystems): string | void {
Expand Down Expand Up @@ -524,12 +530,12 @@ class EMEController extends Logger implements ComponentAPI {
return;
}

let keyId: Uint8Array | undefined;
let keyId: Uint8Array | null | undefined;
let keySystemDomain: KeySystems | undefined;

if (
initDataType === 'sinf' &&
this.config.drmSystems[KeySystems.FAIRPLAY]
this.getLicenseServerUrl(KeySystems.FAIRPLAY)
) {
// Match sinf keyId to playlist skd://keyId=
const json = bin2str(new Uint8Array(initData));
Expand All @@ -547,12 +553,25 @@ class EMEController extends Logger implements ComponentAPI {
this.warn(`${logMessage} Failed to parse sinf: ${error}`);
return;
}
} else {
} else if (this.getLicenseServerUrl(KeySystems.WIDEVINE)) {
// Support Widevine clear-lead key-session creation (otherwise depend on playlist keys)
const psshResults = parseMultiPssh(initData);
const psshInfo = psshResults.filter(
(pssh): pssh is PsshData => pssh.systemId === KeySystemIds.WIDEVINE,
)[0];

// TODO: If using keySystemAccessPromises we might want to wait until one is resolved
let keySystems = Object.keys(
this.keySystemAccessPromises,
) as KeySystems[];
if (!keySystems.length) {
keySystems = getKeySystemsForConfig(this.config);
}

const psshInfo = psshResults.filter((pssh): pssh is PsshData => {
const keySystem = pssh.systemId
? keySystemIdToKeySystemDomain(pssh.systemId)
: null;
return keySystem ? keySystems.indexOf(keySystem) > -1 : false;
})[0];

if (!psshInfo) {
if (
psshResults.length === 0 ||
Expand All @@ -568,10 +587,15 @@ class EMEController extends Logger implements ComponentAPI {
}
return;
}

keySystemDomain = keySystemIdToKeySystemDomain(psshInfo.systemId);
if (psshInfo.version === 0 && psshInfo.data) {
const offset = psshInfo.data.length - 22;
keyId = psshInfo.data.subarray(offset, offset + 16);
if (keySystemDomain === KeySystems.WIDEVINE) {
const offset = psshInfo.data.length - 22;
keyId = psshInfo.data.subarray(offset, offset + 16);
} else if (keySystemDomain === KeySystems.PLAYREADY) {
keyId = parsePlayReadyWRM(psshInfo.data);
}
}
}

Expand Down Expand Up @@ -1098,7 +1122,7 @@ class EMEController extends Logger implements ComponentAPI {
): Promise<ArrayBuffer> {
const keyLoadPolicy = this.config.keyLoadPolicy.default;
return new Promise((resolve, reject) => {
const url = this.getLicenseServerUrl(keySessionContext.keySystem);
const url = this.getLicenseServerUrlOrThrow(keySessionContext.keySystem);
this.log(`Sending license request to URL: ${url}`);
const xhr = new XMLHttpRequest();
xhr.responseType = 'arraybuffer';
Expand Down
44 changes: 8 additions & 36 deletions src/loader/level-key.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import {
changeEndianness,
convertDataUriToArrayBytes,
} from '../utils/keysystem-util';
import { convertDataUriToArrayBytes } from '../utils/keysystem-util';
import { isFullSegmentEncryption } from '../utils/encryption-methods-util';
import { KeySystemFormats } from '../utils/mediakeys-helper';
import { KeySystemFormats, parsePlayReadyWRM } from '../utils/mediakeys-helper';
import { mp4pssh } from '../utils/mp4-tools';
import { logger } from '../utils/logger';
import { base64Decode } from '../utils/numeric-encoding-utils';

let keyUriToKeyIdMap: { [uri: string]: Uint8Array } = {};

Expand Down Expand Up @@ -122,6 +118,8 @@ export class LevelKey implements DecryptData {
if (keyBytes) {
switch (this.keyFormat) {
case KeySystemFormats.WIDEVINE:
// Setting `pssh` on this LevelKey/DecryptData allows HLS.js to generate a session using
// the playlist-key before the "encrypted" event. (Comment out to only use "encrypted" path.)
this.pssh = keyBytes;
// In case of widevine keyID is embedded in PSSH box. Read Key ID.
if (keyBytes.length >= 22) {
Expand All @@ -137,38 +135,12 @@ export class LevelKey implements DecryptData {
0x5b, 0xe0, 0x88, 0x5f, 0x95,
]);

// Setting `pssh` on this LevelKey/DecryptData allows HLS.js to generate a session using
// the playlist-key before the "encrypted" event. (Comment out to only use "encrypted" path.)
this.pssh = mp4pssh(PlayReadyKeySystemUUID, null, keyBytes);

const keyBytesUtf16 = new Uint16Array(
keyBytes.buffer,
keyBytes.byteOffset,
keyBytes.byteLength / 2,
);
const keyByteStr = String.fromCharCode.apply(
null,
Array.from(keyBytesUtf16),
);

// Parse Playready WRMHeader XML
const xmlKeyBytes = keyByteStr.substring(
keyByteStr.indexOf('<'),
keyByteStr.length,
);
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlKeyBytes, 'text/xml');
const keyData = xmlDoc.getElementsByTagName('KID')[0];
if (keyData) {
const keyId = keyData.childNodes[0]
? keyData.childNodes[0].nodeValue
: keyData.getAttribute('VALUE');
if (keyId) {
const keyIdArray = base64Decode(keyId).subarray(0, 16);
// KID value in PRO is a base64-encoded little endian GUID interpretation of UUID
// KID value in ‘tenc’ is a big endian UUID GUID interpretation of UUID
changeEndianness(keyIdArray);
this.keyId = keyIdArray;
}
}
this.keyId = parsePlayReadyWRM(keyBytes);

break;
}
default: {
Expand Down
33 changes: 33 additions & 0 deletions src/utils/mediakeys-helper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { DRMSystemOptions, EMEControllerConfig } from '../config';
import { optionalSelf } from './global';
import { changeEndianness } from './keysystem-util';
import { base64Decode } from './numeric-encoding-utils';

/**
* @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/requestMediaKeySystemAccess
Expand Down Expand Up @@ -163,3 +165,34 @@ function createMediaKeySystemConfigurations(

return [baseConfig];
}

export function parsePlayReadyWRM(keyBytes: Uint8Array): Uint8Array | null {
const keyBytesUtf16 = new Uint16Array(
keyBytes.buffer,
keyBytes.byteOffset,
keyBytes.byteLength / 2,
);
const keyByteStr = String.fromCharCode.apply(null, Array.from(keyBytesUtf16));

// Parse Playready WRMHeader XML
const xmlKeyBytes = keyByteStr.substring(
keyByteStr.indexOf('<'),
keyByteStr.length,
);
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlKeyBytes, 'text/xml');
const keyData = xmlDoc.getElementsByTagName('KID')[0];
if (keyData) {
const keyId = keyData.childNodes[0]
? keyData.childNodes[0].nodeValue
: keyData.getAttribute('VALUE');
if (keyId) {
const keyIdArray = base64Decode(keyId).subarray(0, 16);
// KID value in PRO is a base64-encoded little endian GUID interpretation of UUID
// KID value in ‘tenc’ is a big endian UUID GUID interpretation of UUID
changeEndianness(keyIdArray);
return keyIdArray;
}
}
return null;
}
Loading