Skip to content

Commit

Permalink
Prevent Widevine access request on "encrypted" w/o configured license…
Browse files Browse the repository at this point in the history
… url (#6644)

* Gate use of Widevine PSSH on "encrypted" when config is missing corresponding license
* Support playlist or "encrypted" key workflow with PlayReady
  • Loading branch information
robwalch committed Aug 27, 2024
1 parent 3268053 commit 4ac240c
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 52 deletions.
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;
}

0 comments on commit 4ac240c

Please sign in to comment.