From 1f12a75009c6709f28975944ecc5fd9856125e3c Mon Sep 17 00:00:00 2001 From: stratumadev Date: Wed, 11 Dec 2024 17:40:45 +0100 Subject: [PATCH] linting fix --- ao.ts | 2 +- crunchy.ts | 6 +- hidive.ts | 1 - modules/cdm.ts | 408 +++++++------- modules/playready/bcert.ts | 918 ++++++++++++++++---------------- modules/playready/cdm.ts | 562 ++++++++++--------- modules/playready/device.ts | 162 +++--- modules/playready/ecc_key.ts | 162 +++--- modules/playready/elgamal.ts | 66 +-- modules/playready/key.ts | 108 ++-- modules/playready/pssh.ts | 192 ++++--- modules/playready/wrmheader.ts | 182 ++++--- modules/playready/xml_key.ts | 42 +- modules/playready/xmrlicense.ts | 510 +++++++++--------- 14 files changed, 1653 insertions(+), 1668 deletions(-) diff --git a/ao.ts b/ao.ts index 09fac63..2892c78 100644 --- a/ao.ts +++ b/ao.ts @@ -679,7 +679,7 @@ export default class AnimeOnegai implements ServiceClass { //Handle Decryption if needed if ((chosenVideoSegments.pssh_wvd || chosenAudioSegments.pssh_wvd) && (videoDownloaded || audioDownloaded)) { console.info('Decryption Needed, attempting to decrypt'); - var encryptionKeys; + let encryptionKeys; if (cdm === 'widevine') { encryptionKeys = await getKeysWVD(chosenVideoSegments.pssh_wvd, streamData.widevine_proxy, {}); diff --git a/crunchy.ts b/crunchy.ts index 81ca512..1d29cb5 100644 --- a/crunchy.ts +++ b/crunchy.ts @@ -1742,20 +1742,20 @@ export default class Crunchy implements ServiceClass { } const authData = await decReq.res.json() as {'custom_data': string, 'token': string}; - var encryptionKeys; + let encryptionKeys; if (cdm === 'widevine') { encryptionKeys = await getKeysWVD(chosenVideoSegments.pssh_wvd, 'https://lic.drmtoday.com/license-proxy-widevine/cenc/', { 'dt-custom-data': authData.custom_data, 'x-dt-auth-token': authData.token - }) + }); } if (cdm === 'playready') { encryptionKeys = await getKeysPRD(chosenVideoSegments.pssh_prd, 'https://lic.drmtoday.com/license-proxy-headerauth/drmtoday/RightsManager.asmx', { 'dt-custom-data': authData.custom_data, 'x-dt-auth-token': authData.token - }) + }); } if (!encryptionKeys || encryptionKeys.length == 0) { diff --git a/hidive.ts b/hidive.ts index b705bd7..a1a85a9 100644 --- a/hidive.ts +++ b/hidive.ts @@ -657,7 +657,6 @@ export default class Hidive implements ServiceClass { const chosenFontSize = options.originalFontSize ? undefined : options.fontSize; let encryptionKeys: KeyContainer[] = []; if (!canDecrypt) console.warn('Decryption not enabled!'); - if (canDecrypt && cdm === 'playready') console.warn("Hidive doesn't support Playready CDM!"); if (!this.cfg.bin.ffmpeg) this.cfg.bin = await yamlCfg.loadBinCfg(); diff --git a/modules/cdm.ts b/modules/cdm.ts index f512701..0bded5d 100644 --- a/modules/cdm.ts +++ b/modules/cdm.ts @@ -1,235 +1,231 @@ -import { KeyContainer, Session } from "./license"; -import fs from "fs"; -import { console } from "./log"; -import got from "got"; -import { workingDir } from "./module.cfg-loader"; -import path from "path"; -import { ReadError, Response } from "got"; -import { Device } from "./playready/device"; -import Cdm from "./playready/cdm"; -import { PSSH } from "./playready/pssh"; +import { KeyContainer, Session } from './license'; +import fs from 'fs'; +import { console } from './log'; +import got from 'got'; +import { workingDir } from './module.cfg-loader'; +import path from 'path'; +import { ReadError, Response } from 'got'; +import { Device } from './playready/device'; +import Cdm from './playready/cdm'; +import { PSSH } from './playready/pssh'; //read cdm files located in the same directory let privateKey: Buffer = Buffer.from([]), - identifierBlob: Buffer = Buffer.from([]), - prd: Buffer = Buffer.from([]), - prd_cdm: Cdm | undefined; -export let cdm: "widevine" | "playready"; + identifierBlob: Buffer = Buffer.from([]), + prd: Buffer = Buffer.from([]), + prd_cdm: Cdm | undefined; +export let cdm: 'widevine' | 'playready'; export let canDecrypt: boolean; try { - const files_prd = fs.readdirSync(path.join(workingDir, "playready")); - const prd_file_found = files_prd.find(f => f.includes('.prd')) - if (prd_file_found) { - const file_prd = path.join(workingDir, "playready", prd_file_found); - const stats = fs.statSync(file_prd); - if (stats.size < 1024 * 8 && stats.isFile()) { - const fileContents = fs.readFileSync(file_prd, { - encoding: "utf8", - }); - if (fileContents.includes("CERT")) { - prd = fs.readFileSync(file_prd); - const device = Device.loads(prd); - prd_cdm = Cdm.fromDevice(device); - } - } + const files_prd = fs.readdirSync(path.join(workingDir, 'playready')); + const prd_file_found = files_prd.find((f) => f.includes('.prd')); + if (prd_file_found) { + const file_prd = path.join(workingDir, 'playready', prd_file_found); + const stats = fs.statSync(file_prd); + if (stats.size < 1024 * 8 && stats.isFile()) { + const fileContents = fs.readFileSync(file_prd, { + encoding: 'utf8', + }); + if (fileContents.includes('CERT')) { + prd = fs.readFileSync(file_prd); + const device = Device.loads(prd); + prd_cdm = Cdm.fromDevice(device); + } } + } - const files_wvd = fs.readdirSync(path.join(workingDir, "widevine")); - files_wvd.forEach(function (file) { - file = path.join(workingDir, "widevine", file); - const stats = fs.statSync(file); - if (stats.size < 1024 * 8 && stats.isFile()) { - const fileContents = fs.readFileSync(file, { encoding: "utf8" }); - if ( - fileContents.includes("-BEGIN PRIVATE KEY-") || - fileContents.includes("-BEGIN RSA PRIVATE KEY-") - ) { - privateKey = fs.readFileSync(file); - } - if (fileContents.includes("widevine_cdm_version")) { - identifierBlob = fs.readFileSync(file); - } - } - }); - - if (privateKey.length !== 0 && identifierBlob.length !== 0) { - cdm = "widevine"; - canDecrypt = true; - } else if (prd.length !== 0) { - cdm = "playready"; - canDecrypt = true; - } else if (privateKey.length == 0) { - console.warn("Private key missing"); - canDecrypt = false; - } else if (identifierBlob.length == 0) { - console.warn("Identifier blob missing"); - canDecrypt = false; - } else if (prd.length == 0) { - console.warn("PRD is missing"); - canDecrypt = false; + const files_wvd = fs.readdirSync(path.join(workingDir, 'widevine')); + files_wvd.forEach(function (file) { + file = path.join(workingDir, 'widevine', file); + const stats = fs.statSync(file); + if (stats.size < 1024 * 8 && stats.isFile()) { + const fileContents = fs.readFileSync(file, { encoding: 'utf8' }); + if ( + fileContents.includes('-BEGIN PRIVATE KEY-') || + fileContents.includes('-BEGIN RSA PRIVATE KEY-') + ) { + privateKey = fs.readFileSync(file); + } + if (fileContents.includes('widevine_cdm_version')) { + identifierBlob = fs.readFileSync(file); + } } -} catch (e) { - console.error(e); + }); + + if (privateKey.length !== 0 && identifierBlob.length !== 0) { + cdm = 'widevine'; + canDecrypt = true; + } else if (prd.length !== 0) { + cdm = 'playready'; + canDecrypt = true; + } else if (privateKey.length == 0) { + console.warn('Private key missing'); + canDecrypt = false; + } else if (identifierBlob.length == 0) { + console.warn('Identifier blob missing'); + canDecrypt = false; + } else if (prd.length == 0) { + console.warn('PRD is missing'); canDecrypt = false; + } +} catch (e) { + console.error(e); + canDecrypt = false; } export async function getKeysWVD( - pssh: string | undefined, - licenseServer: string, - authData: Record + pssh: string | undefined, + licenseServer: string, + authData: Record ): Promise { - if (!pssh || !canDecrypt) return []; - //pssh found in the mpd manifest - const psshBuffer = Buffer.from(pssh, "base64"); + if (!pssh || !canDecrypt) return []; + //pssh found in the mpd manifest + const psshBuffer = Buffer.from(pssh, 'base64'); - //Create a new widevine session - const session = new Session({ privateKey, identifierBlob }, psshBuffer); + //Create a new widevine session + const session = new Session({ privateKey, identifierBlob }, psshBuffer); - //Generate license - let response; - try { - response = await got(licenseServer, { - method: "POST", - body: session.createLicenseRequest(), - headers: authData, - responseType: "text", - }); - } catch (_error) { - const error = _error as { - name: string; - } & ReadError & { - res: Response; - }; - if ( - error.response && - error.response.statusCode && - error.response.statusMessage - ) { - console.error( - `${error.name} ${error.response.statusCode}: ${error.response.statusMessage}` - ); - } else { - console.error(`${error.name}: ${error.code || error.message}`); - } - if (error.response && !error.res) { - error.res = error.response; - const docTitle = (error.res.body as string).match( - /(.*)<\/title>/ - ); - if (error.res.body && docTitle) { - console.error(docTitle[1]); - } - } - if ( - error.res && - error.res.body && - error.response.statusCode && - error.response.statusCode != 404 && - error.response.statusCode != 403 - ) { - console.error("Body:", error.res.body); - } - return []; + //Generate license + let response; + try { + response = await got(licenseServer, { + method: 'POST', + body: session.createLicenseRequest(), + headers: authData, + responseType: 'text', + }); + } catch (_error) { + const error = _error as { + name: string; + } & ReadError & { + res: Response<unknown>; + }; + if ( + error.response && + error.response.statusCode && + error.response.statusMessage + ) { + console.error( + `${error.name} ${error.response.statusCode}: ${error.response.statusMessage}` + ); + } else { + console.error(`${error.name}: ${error.code || error.message}`); + } + if (error.response && !error.res) { + error.res = error.response; + const docTitle = (error.res.body as string).match(/<title>(.*)<\/title>/); + if (error.res.body && docTitle) { + console.error(docTitle[1]); + } + } + if ( + error.res && + error.res.body && + error.response.statusCode && + error.response.statusCode != 404 && + error.response.statusCode != 403 + ) { + console.error('Body:', error.res.body); } + return []; + } - if (response.statusCode === 200) { - //Parse License and return keys - try { - const json = JSON.parse(response.body); - return session.parseLicense(Buffer.from(json["license"], "base64")); - } catch { - return session.parseLicense(response.rawBody); - } - } else { - console.info( - "License request failed:", - response.statusMessage, - response.body - ); - return []; + if (response.statusCode === 200) { + //Parse License and return keys + try { + const json = JSON.parse(response.body); + return session.parseLicense(Buffer.from(json['license'], 'base64')); + } catch { + return session.parseLicense(response.rawBody); } + } else { + console.info( + 'License request failed:', + response.statusMessage, + response.body + ); + return []; + } } export async function getKeysPRD( - pssh: string | undefined, - licenseServer: string, - authData: Record<string, string> + pssh: string | undefined, + licenseServer: string, + authData: Record<string, string> ): Promise<KeyContainer[] | undefined> { - if (!pssh || !canDecrypt || !prd_cdm) return []; - const pssh_parsed = new PSSH(pssh); + if (!pssh || !canDecrypt || !prd_cdm) return []; + const pssh_parsed = new PSSH(pssh); - //Create a new playready session - const session = prd_cdm.getLicenseChallenge( - pssh_parsed.get_wrm_headers(true)[0] - ); + //Create a new playready session + const session = prd_cdm.getLicenseChallenge( + pssh_parsed.get_wrm_headers(true)[0] + ); - //Generate license - let response; - try { - response = await got(licenseServer, { - method: "POST", - body: session, - headers: authData, - responseType: "text", - }); - } catch (_error) { - const error = _error as { - name: string; - } & ReadError & { - res: Response<unknown>; - }; - if ( - error.response && - error.response.statusCode && - error.response.statusMessage - ) { - console.error( - `${error.name} ${error.response.statusCode}: ${error.response.statusMessage}` - ); - } else { - console.error(`${error.name}: ${error.code || error.message}`); - } - if (error.response && !error.res) { - error.res = error.response; - const docTitle = (error.res.body as string).match( - /<title>(.*)<\/title>/ - ); - if (error.res.body && docTitle) { - console.error(docTitle[1]); - } - } - if ( - error.res && - error.res.body && - error.response.statusCode && - error.response.statusCode != 404 && - error.response.statusCode != 403 - ) { - console.error("Body:", error.res.body); - } - return []; + //Generate license + let response; + try { + response = await got(licenseServer, { + method: 'POST', + body: session, + headers: authData, + responseType: 'text', + }); + } catch (_error) { + const error = _error as { + name: string; + } & ReadError & { + res: Response<unknown>; + }; + if ( + error.response && + error.response.statusCode && + error.response.statusMessage + ) { + console.error( + `${error.name} ${error.response.statusCode}: ${error.response.statusMessage}` + ); + } else { + console.error(`${error.name}: ${error.code || error.message}`); + } + if (error.response && !error.res) { + error.res = error.response; + const docTitle = (error.res.body as string).match(/<title>(.*)<\/title>/); + if (error.res.body && docTitle) { + console.error(docTitle[1]); + } + } + if ( + error.res && + error.res.body && + error.response.statusCode && + error.response.statusCode != 404 && + error.response.statusCode != 403 + ) { + console.error('Body:', error.res.body); } + return []; + } - if (response.statusCode === 200) { - //Parse License and return keys - try { - const keys = prd_cdm.parseLicense(response.body); + if (response.statusCode === 200) { + //Parse License and return keys + try { + const keys = prd_cdm.parseLicense(response.body); - return keys.map((k) => { - return { - kid: k.key_id, - key: k.key, - }; - }); - } catch { - return undefined; - } - } else { - console.info( - "License request failed:", - response.statusMessage, - response.body - ); - return []; + return keys.map((k) => { + return { + kid: k.key_id, + key: k.key, + }; + }); + } catch { + return undefined; } + } else { + console.info( + 'License request failed:', + response.statusMessage, + response.body + ); + return []; + } } diff --git a/modules/playready/bcert.ts b/modules/playready/bcert.ts index 188614f..b79e9f7 100644 --- a/modules/playready/bcert.ts +++ b/modules/playready/bcert.ts @@ -1,489 +1,485 @@ -import * as fs from 'fs' -import { createHash } from 'crypto' -import { Parser } from 'binary-parser-encoder' -import ECCKey from './ecc_key' +import * as fs from 'fs'; +import { createHash } from 'crypto'; +import { Parser } from 'binary-parser-encoder'; +import ECCKey from './ecc_key'; +import { console } from '../log'; function alignUp(length: number, alignment: number): number { - return Math.ceil(length / alignment) * alignment + return Math.ceil(length / alignment) * alignment; } export class BCertStructs { - static DrmBCertBasicInfo = new Parser() - .buffer('cert_id', { length: 16 }) - .uint32be('security_level') - .uint32be('flags') - .uint32be('cert_type') - .buffer('public_key_digest', { length: 32 }) - .uint32be('expiration_date') - .buffer('client_id', { length: 16 }) - - static DrmBCertDomainInfo = new Parser() - .buffer('service_id', { length: 16 }) - .buffer('account_id', { length: 16 }) - .uint32be('revision_timestamp') - .uint32be('domain_url_length') - .buffer('domain_url', { - length: function () { - return alignUp((this as any).domain_url_length, 4) - } - }) - - static DrmBCertPCInfo = new Parser().uint32be('security_version') - - static DrmBCertDeviceInfo = new Parser() - .uint32be('max_license') - .uint32be('max_header') - .uint32be('max_chain_depth') - - static DrmBCertFeatureInfo = new Parser() - .uint32be('feature_count') - .array('features', { - type: 'uint32be', - length: 'feature_count' - }) - - static CertKey = new Parser() - .uint16be('type') - .uint16be('length') - .uint32be('flags') - .buffer('key', { - length: function () { - return (this as any).length / 8 - } - }) - .uint32be('usages_count') - .array('usages', { - type: 'uint32be', - length: 'usages_count' - }) - - static DrmBCertKeyInfo = new Parser() - .uint32be('key_count') - .array('cert_keys', { - type: BCertStructs.CertKey, - length: 'key_count' - }) - - static DrmBCertManufacturerInfo = new Parser() - .uint32be('flags') - .uint32be('manufacturer_name_length') - .buffer('manufacturer_name', { - length: function () { - return alignUp((this as any).manufacturer_name_length, 4) - } - }) - .uint32be('model_name_length') - .buffer('model_name', { - length: function () { - return alignUp((this as any).model_name_length, 4) - } - }) - .uint32be('model_number_length') - .buffer('model_number', { - length: function () { - return alignUp((this as any).model_number_length, 4) - } - }) - - static DrmBCertSignatureInfo = new Parser() - .uint16be('signature_type') - .uint16be('signature_size') - .buffer('signature', { length: 'signature_size' }) - .uint32be('signature_key_size') - .buffer('signature_key', { - length: function () { - return (this as any).signature_key_size / 8 - } - }) - - static DrmBCertSilverlightInfo = new Parser() - .uint32be('security_version') - .uint32be('platform_identifier') - - static DrmBCertMeteringInfo = new Parser() - .buffer('metering_id', { length: 16 }) - .uint32be('metering_url_length') - .buffer('metering_url', { - length: function () { - return alignUp((this as any).metering_url_length, 4) - } - }) - - static DrmBCertExtDataSignKeyInfo = new Parser() - .uint16be('type') - .uint16be('length') - .uint32be('flags') - .buffer('key', { - length: function () { - return (this as any).length / 8 - } - }) - - static BCertExtDataRecord = new Parser() - .uint32be('data_size') - .buffer('data', { - length: 'data_size' - }) - - static DrmBCertExtDataSignature = new Parser() - .uint16be('signature_type') - .uint16be('signature_size') - .buffer('signature', { - length: 'signature_size' - }) - - static BCertExtDataContainer = new Parser() - .uint32be('record_count') - .array('records', { - length: 'record_count', - type: BCertStructs.BCertExtDataRecord - }) - .nest('signature', { - type: BCertStructs.DrmBCertExtDataSignature - }) - - static DrmBCertServerInfo = new Parser().uint32be('warning_days') - - static DrmBcertSecurityVersion = new Parser() - .uint32be('security_version') - .uint32be('platform_identifier') - - static Attribute = new Parser() - .uint16be('flags') - .uint16be('tag') - .uint32be('length') - .choice('attribute', { - tag: 'tag', - choices: { - 1: BCertStructs.DrmBCertBasicInfo, - 2: BCertStructs.DrmBCertDomainInfo, - 3: BCertStructs.DrmBCertPCInfo, - 4: BCertStructs.DrmBCertDeviceInfo, - 5: BCertStructs.DrmBCertFeatureInfo, - 6: BCertStructs.DrmBCertKeyInfo, - 7: BCertStructs.DrmBCertManufacturerInfo, - 8: BCertStructs.DrmBCertSignatureInfo, - 9: BCertStructs.DrmBCertSilverlightInfo, - 10: BCertStructs.DrmBCertMeteringInfo, - 11: BCertStructs.DrmBCertExtDataSignKeyInfo, - 12: BCertStructs.BCertExtDataContainer, - 13: BCertStructs.DrmBCertExtDataSignature, - 14: new Parser().buffer('data', { - length: function () { - return (this as any).length - 8 - } - }), - 15: BCertStructs.DrmBCertServerInfo, - 16: BCertStructs.DrmBcertSecurityVersion, - 17: BCertStructs.DrmBcertSecurityVersion - }, - defaultChoice: new Parser().buffer('data', { - length: function () { - return (this as any).length - 8 - } - }) - }) - - static BCert = new Parser() - .string('signature', { length: 4, assert: 'CERT' }) - .int32be('version') - .int32be('total_length') - .int32be('certificate_length') - .array('attributes', { - type: BCertStructs.Attribute, - lengthInBytes: function () { - return (this as any).total_length - 16 - } - }) - - static BCertChain = new Parser() - .string('signature', { length: 4, assert: 'CHAI' }) - .int32be('version') - .int32be('total_length') - .int32be('flags') - .int32be('certificate_count') - .array('certificates', { - type: BCertStructs.BCert, - length: 'certificate_count' - }) + static DrmBCertBasicInfo = new Parser() + .buffer('cert_id', { length: 16 }) + .uint32be('security_level') + .uint32be('flags') + .uint32be('cert_type') + .buffer('public_key_digest', { length: 32 }) + .uint32be('expiration_date') + .buffer('client_id', { length: 16 }); + + static DrmBCertDomainInfo = new Parser() + .buffer('service_id', { length: 16 }) + .buffer('account_id', { length: 16 }) + .uint32be('revision_timestamp') + .uint32be('domain_url_length') + .buffer('domain_url', { + length: function () { + return alignUp((this as any).domain_url_length, 4); + }, + }); + + static DrmBCertPCInfo = new Parser().uint32be('security_version'); + + static DrmBCertDeviceInfo = new Parser() + .uint32be('max_license') + .uint32be('max_header') + .uint32be('max_chain_depth'); + + static DrmBCertFeatureInfo = new Parser() + .uint32be('feature_count') + .array('features', { + type: 'uint32be', + length: 'feature_count', + }); + + static CertKey = new Parser() + .uint16be('type') + .uint16be('length') + .uint32be('flags') + .buffer('key', { + length: function () { + return (this as any).length / 8; + }, + }) + .uint32be('usages_count') + .array('usages', { + type: 'uint32be', + length: 'usages_count', + }); + + static DrmBCertKeyInfo = new Parser() + .uint32be('key_count') + .array('cert_keys', { + type: BCertStructs.CertKey, + length: 'key_count', + }); + + static DrmBCertManufacturerInfo = new Parser() + .uint32be('flags') + .uint32be('manufacturer_name_length') + .buffer('manufacturer_name', { + length: function () { + return alignUp((this as any).manufacturer_name_length, 4); + }, + }) + .uint32be('model_name_length') + .buffer('model_name', { + length: function () { + return alignUp((this as any).model_name_length, 4); + }, + }) + .uint32be('model_number_length') + .buffer('model_number', { + length: function () { + return alignUp((this as any).model_number_length, 4); + }, + }); + + static DrmBCertSignatureInfo = new Parser() + .uint16be('signature_type') + .uint16be('signature_size') + .buffer('signature', { length: 'signature_size' }) + .uint32be('signature_key_size') + .buffer('signature_key', { + length: function () { + return (this as any).signature_key_size / 8; + }, + }); + + static DrmBCertSilverlightInfo = new Parser() + .uint32be('security_version') + .uint32be('platform_identifier'); + + static DrmBCertMeteringInfo = new Parser() + .buffer('metering_id', { length: 16 }) + .uint32be('metering_url_length') + .buffer('metering_url', { + length: function () { + return alignUp((this as any).metering_url_length, 4); + }, + }); + + static DrmBCertExtDataSignKeyInfo = new Parser() + .uint16be('type') + .uint16be('length') + .uint32be('flags') + .buffer('key', { + length: function () { + return (this as any).length / 8; + }, + }); + + static BCertExtDataRecord = new Parser() + .uint32be('data_size') + .buffer('data', { + length: 'data_size', + }); + + static DrmBCertExtDataSignature = new Parser() + .uint16be('signature_type') + .uint16be('signature_size') + .buffer('signature', { + length: 'signature_size', + }); + + static BCertExtDataContainer = new Parser() + .uint32be('record_count') + .array('records', { + length: 'record_count', + type: BCertStructs.BCertExtDataRecord, + }) + .nest('signature', { + type: BCertStructs.DrmBCertExtDataSignature, + }); + + static DrmBCertServerInfo = new Parser().uint32be('warning_days'); + + static DrmBcertSecurityVersion = new Parser() + .uint32be('security_version') + .uint32be('platform_identifier'); + + static Attribute = new Parser() + .uint16be('flags') + .uint16be('tag') + .uint32be('length') + .choice('attribute', { + tag: 'tag', + choices: { + 1: BCertStructs.DrmBCertBasicInfo, + 2: BCertStructs.DrmBCertDomainInfo, + 3: BCertStructs.DrmBCertPCInfo, + 4: BCertStructs.DrmBCertDeviceInfo, + 5: BCertStructs.DrmBCertFeatureInfo, + 6: BCertStructs.DrmBCertKeyInfo, + 7: BCertStructs.DrmBCertManufacturerInfo, + 8: BCertStructs.DrmBCertSignatureInfo, + 9: BCertStructs.DrmBCertSilverlightInfo, + 10: BCertStructs.DrmBCertMeteringInfo, + 11: BCertStructs.DrmBCertExtDataSignKeyInfo, + 12: BCertStructs.BCertExtDataContainer, + 13: BCertStructs.DrmBCertExtDataSignature, + 14: new Parser().buffer('data', { + length: function () { + return (this as any).length - 8; + }, + }), + 15: BCertStructs.DrmBCertServerInfo, + 16: BCertStructs.DrmBcertSecurityVersion, + 17: BCertStructs.DrmBcertSecurityVersion, + }, + defaultChoice: new Parser().buffer('data', { + length: function () { + return (this as any).length - 8; + }, + }), + }); + + static BCert = new Parser() + .string('signature', { length: 4, assert: 'CERT' }) + .int32be('version') + .int32be('total_length') + .int32be('certificate_length') + .array('attributes', { + type: BCertStructs.Attribute, + lengthInBytes: function () { + return (this as any).total_length - 16; + }, + }); + + static BCertChain = new Parser() + .string('signature', { length: 4, assert: 'CHAI' }) + .int32be('version') + .int32be('total_length') + .int32be('flags') + .int32be('certificate_count') + .array('certificates', { + type: BCertStructs.BCert, + length: 'certificate_count', + }); } export class Certificate { - parsed: any - _BCERT: Parser - - constructor(parsed_bcert: any, bcert_obj: Parser = BCertStructs.BCert) { - this.parsed = parsed_bcert - this._BCERT = bcert_obj - } - - // UNSTABLE - static new_key_cert( - cert_id: Buffer, - security_level: number, - client_id: Buffer, - signing_key: ECCKey, - encryption_key: ECCKey, - group_key: ECCKey, - parent: CertificateChain, - expiry: number = 0xffffffff, - max_license: number = 10240, - max_header: number = 15360, - max_chain_depth: number = 2 - ): Certificate { - if (!cert_id) { - throw new Error('Certificate ID is required') - } - if (!client_id) { - throw new Error('Client ID is required') - } - - const basic_info = { - cert_id: cert_id, - security_level: security_level, - flags: 0, - cert_type: 2, - public_key_digest: signing_key.publicSha256Digest(), - expiration_date: expiry, - client_id: client_id - } - const basic_info_attribute = { - flags: 1, - tag: 1, - length: - BCertStructs.DrmBCertBasicInfo.encode(basic_info).length + 8, - attribute: basic_info - } - - const device_info = { - max_license: max_license, - max_header: max_header, - max_chain_depth: max_chain_depth - } - - const device_info_attribute = { - flags: 1, - tag: 4, - length: - BCertStructs.DrmBCertDeviceInfo.encode(device_info).length + 8, - attribute: device_info - } - - const feature = { - feature_count: 1, - features: [4, 13] - } - const feature_attribute = { - flags: 1, - tag: 5, - length: BCertStructs.DrmBCertFeatureInfo.encode(feature).length + 8, - attribute: feature - } - - const cert_key_sign = { - type: 1, - length: 512, // bits - flags: 0, - key: signing_key.privateBytes(), - usages_count: 1, - usages: [1] - } - const cert_key_encrypt = { - type: 1, - length: 512, // bits - flags: 0, - key: encryption_key.privateBytes(), - usages_count: 1, - usages: [2] - } - const key_info = { - key_count: 2, - cert_keys: [cert_key_sign, cert_key_encrypt] - } - const key_info_attribute = { - flags: 1, - tag: 6, - length: BCertStructs.DrmBCertKeyInfo.encode(key_info).length + 8, - attribute: key_info - } - - const manufacturer_info = parent.get_certificate(0).get_attribute(7) - - const new_bcert_container = { - signature: 'CERT', - version: 1, - total_length: 0, - certificate_length: 0, - attributes: [ - basic_info_attribute, - device_info_attribute, - feature_attribute, - key_info_attribute, - manufacturer_info - ] - } - - let payload = BCertStructs.BCert.encode(new_bcert_container) - new_bcert_container.certificate_length = payload.length - new_bcert_container.total_length = payload.length + 144 - payload = BCertStructs.BCert.encode(new_bcert_container) - - const hash = createHash('sha256') - hash.update(payload) - const digest = hash.digest() - - const signatureObj = group_key.keyPair.sign(digest) - const r = Buffer.from(signatureObj.r.toArray('be', 32)) - const s = Buffer.from(signatureObj.s.toArray('be', 32)) - const signature = Buffer.concat([r, s]) - - const signature_info = { - signature_type: 1, - signature_size: 64, - signature: signature, - signature_key_size: 512, // bits - signature_key: group_key.publicBytes() - } - const signature_info_attribute = { - flags: 1, - tag: 8, - length: - BCertStructs.DrmBCertSignatureInfo.encode(signature_info) - .length + 8, - attribute: signature_info - } - new_bcert_container.attributes.push(signature_info_attribute) - - return new Certificate(new_bcert_container) + parsed: any; + _BCERT: Parser; + + constructor(parsed_bcert: any, bcert_obj: Parser = BCertStructs.BCert) { + this.parsed = parsed_bcert; + this._BCERT = bcert_obj; + } + + // UNSTABLE + static new_key_cert( + cert_id: Buffer, + security_level: number, + client_id: Buffer, + signing_key: ECCKey, + encryption_key: ECCKey, + group_key: ECCKey, + parent: CertificateChain, + expiry: number = 0xffffffff, + max_license: number = 10240, + max_header: number = 15360, + max_chain_depth: number = 2 + ): Certificate { + if (!cert_id) { + throw new Error('Certificate ID is required'); } - - static loads(data: string | Buffer): Certificate { - if (typeof data === 'string') { - data = Buffer.from(data, 'base64') - } - if (!Buffer.isBuffer(data)) { - throw new Error(`Expecting Bytes or Base64 input, got ${data}`) - } - - const cert = BCertStructs.BCert - const parsed_bcert = cert.parse(data) - return new Certificate(parsed_bcert, cert) + if (!client_id) { + throw new Error('Client ID is required'); } - static load(filePath: string): Certificate { - const data = fs.readFileSync(filePath) - return Certificate.loads(data) + const basic_info = { + cert_id: cert_id, + security_level: security_level, + flags: 0, + cert_type: 2, + public_key_digest: signing_key.publicSha256Digest(), + expiration_date: expiry, + client_id: client_id, + }; + const basic_info_attribute = { + flags: 1, + tag: 1, + length: BCertStructs.DrmBCertBasicInfo.encode(basic_info).length + 8, + attribute: basic_info, + }; + + const device_info = { + max_license: max_license, + max_header: max_header, + max_chain_depth: max_chain_depth, + }; + + const device_info_attribute = { + flags: 1, + tag: 4, + length: BCertStructs.DrmBCertDeviceInfo.encode(device_info).length + 8, + attribute: device_info, + }; + + const feature = { + feature_count: 1, + features: [4, 13], + }; + const feature_attribute = { + flags: 1, + tag: 5, + length: BCertStructs.DrmBCertFeatureInfo.encode(feature).length + 8, + attribute: feature, + }; + + const cert_key_sign = { + type: 1, + length: 512, // bits + flags: 0, + key: signing_key.privateBytes(), + usages_count: 1, + usages: [1], + }; + const cert_key_encrypt = { + type: 1, + length: 512, // bits + flags: 0, + key: encryption_key.privateBytes(), + usages_count: 1, + usages: [2], + }; + const key_info = { + key_count: 2, + cert_keys: [cert_key_sign, cert_key_encrypt], + }; + const key_info_attribute = { + flags: 1, + tag: 6, + length: BCertStructs.DrmBCertKeyInfo.encode(key_info).length + 8, + attribute: key_info, + }; + + const manufacturer_info = parent.get_certificate(0).get_attribute(7); + + const new_bcert_container = { + signature: 'CERT', + version: 1, + total_length: 0, + certificate_length: 0, + attributes: [ + basic_info_attribute, + device_info_attribute, + feature_attribute, + key_info_attribute, + manufacturer_info, + ], + }; + + let payload = BCertStructs.BCert.encode(new_bcert_container); + new_bcert_container.certificate_length = payload.length; + new_bcert_container.total_length = payload.length + 144; + payload = BCertStructs.BCert.encode(new_bcert_container); + + const hash = createHash('sha256'); + hash.update(payload); + const digest = hash.digest(); + + const signatureObj = group_key.keyPair.sign(digest); + const r = Buffer.from(signatureObj.r.toArray('be', 32)); + const s = Buffer.from(signatureObj.s.toArray('be', 32)); + const signature = Buffer.concat([r, s]); + + const signature_info = { + signature_type: 1, + signature_size: 64, + signature: signature, + signature_key_size: 512, // bits + signature_key: group_key.publicBytes(), + }; + const signature_info_attribute = { + flags: 1, + tag: 8, + length: + BCertStructs.DrmBCertSignatureInfo.encode(signature_info).length + 8, + attribute: signature_info, + }; + new_bcert_container.attributes.push(signature_info_attribute); + + return new Certificate(new_bcert_container); + } + + static loads(data: string | Buffer): Certificate { + if (typeof data === 'string') { + data = Buffer.from(data, 'base64'); } - - get_attribute(type_: number) { - for (const attribute of this.parsed.attributes) { - if (attribute.tag === type_) { - return attribute - } - } + if (!Buffer.isBuffer(data)) { + throw new Error(`Expecting Bytes or Base64 input, got ${data}`); } - get_security_level(): number { - const basic_info_attribute = this.get_attribute(1) - if (basic_info_attribute) { - return basic_info_attribute.attribute.security_level - } - return 0 + const cert = BCertStructs.BCert; + const parsed_bcert = cert.parse(data); + return new Certificate(parsed_bcert, cert); + } + + static load(filePath: string): Certificate { + const data = fs.readFileSync(filePath); + return Certificate.loads(data); + } + + get_attribute(type_: number) { + for (const attribute of this.parsed.attributes) { + if (attribute.tag === type_) { + return attribute; + } } + } - private static _unpad(name: Buffer): string { - return name.toString('utf8').replace(/\0+$/, '') + get_security_level(): number { + const basic_info_attribute = this.get_attribute(1); + if (basic_info_attribute) { + return basic_info_attribute.attribute.security_level; } - - get_name(): string { - const manufacturer_info_attribute = this.get_attribute(7) - if (manufacturer_info_attribute) { - const manufacturer_info = manufacturer_info_attribute.attribute - const manufacturer_name = Certificate._unpad( - manufacturer_info.manufacturer_name - ) - const model_name = Certificate._unpad(manufacturer_info.model_name) - const model_number = Certificate._unpad( - manufacturer_info.model_number - ) - return `${manufacturer_name} ${model_name} ${model_number}` - } - return '' + return 0; + } + + private static _unpad(name: Buffer): string { + return name.toString('utf8').replace(/\0+$/, ''); + } + + get_name(): string { + const manufacturer_info_attribute = this.get_attribute(7); + if (manufacturer_info_attribute) { + const manufacturer_info = manufacturer_info_attribute.attribute; + const manufacturer_name = Certificate._unpad( + manufacturer_info.manufacturer_name + ); + const model_name = Certificate._unpad(manufacturer_info.model_name); + const model_number = Certificate._unpad(manufacturer_info.model_number); + return `${manufacturer_name} ${model_name} ${model_number}`; } + return ''; + } - dumps(): Buffer { - return this._BCERT.encode(this.parsed) - } + dumps(): Buffer { + return this._BCERT.encode(this.parsed); + } - struct(): Parser { - return this._BCERT - } + struct(): Parser { + return this._BCERT; + } } export class CertificateChain { - parsed: any - _BCERT_CHAIN: Parser - - constructor( - parsed_bcert_chain: any, - bcert_chain_obj: Parser = BCertStructs.BCertChain - ) { - this.parsed = parsed_bcert_chain - this._BCERT_CHAIN = bcert_chain_obj - } - - static loads(data: string | Buffer): CertificateChain { - if (typeof data === 'string') { - data = Buffer.from(data, 'base64') - } - if (!Buffer.isBuffer(data)) { - throw new Error(`Expecting Bytes or Base64 input, got ${data}`) - } - - const cert_chain = BCertStructs.BCertChain - try { - const parsed_bcert_chain = cert_chain.parse(data) - return new CertificateChain(parsed_bcert_chain, cert_chain) - } catch (error) { - console.error('Error during parsing:', error) - throw error - } - } - - static load(filePath: string): CertificateChain { - const data = fs.readFileSync(filePath) - return CertificateChain.loads(data) + parsed: any; + _BCERT_CHAIN: Parser; + + constructor( + parsed_bcert_chain: any, + bcert_chain_obj: Parser = BCertStructs.BCertChain + ) { + this.parsed = parsed_bcert_chain; + this._BCERT_CHAIN = bcert_chain_obj; + } + + static loads(data: string | Buffer): CertificateChain { + if (typeof data === 'string') { + data = Buffer.from(data, 'base64'); } - - dumps(): Buffer { - return this._BCERT_CHAIN.encode(this.parsed) - } - - struct(): Parser { - return this._BCERT_CHAIN - } - - get_certificate(index: number): Certificate { - return new Certificate(this.parsed.certificates[index]) - } - - get_security_level(): number { - return this.get_certificate(0).get_security_level() - } - - get_name(): string { - return this.get_certificate(0).get_name() - } - - append(bcert: Certificate): void { - this.parsed.certificate_count += 1 - this.parsed.certificates.push(bcert.parsed) - this.parsed.total_length += bcert.dumps().length + if (!Buffer.isBuffer(data)) { + throw new Error(`Expecting Bytes or Base64 input, got ${data}`); } - prepend(bcert: Certificate): void { - this.parsed.certificate_count += 1 - this.parsed.certificates.unshift(bcert.parsed) - this.parsed.total_length += bcert.dumps().length + const cert_chain = BCertStructs.BCertChain; + try { + const parsed_bcert_chain = cert_chain.parse(data); + return new CertificateChain(parsed_bcert_chain, cert_chain); + } catch (error) { + console.error('Error during parsing:', error); + throw error; } + } + + static load(filePath: string): CertificateChain { + const data = fs.readFileSync(filePath); + return CertificateChain.loads(data); + } + + dumps(): Buffer { + return this._BCERT_CHAIN.encode(this.parsed); + } + + struct(): Parser { + return this._BCERT_CHAIN; + } + + get_certificate(index: number): Certificate { + return new Certificate(this.parsed.certificates[index]); + } + + get_security_level(): number { + return this.get_certificate(0).get_security_level(); + } + + get_name(): string { + return this.get_certificate(0).get_name(); + } + + append(bcert: Certificate): void { + this.parsed.certificate_count += 1; + this.parsed.certificates.push(bcert.parsed); + this.parsed.total_length += bcert.dumps().length; + } + + prepend(bcert: Certificate): void { + this.parsed.certificate_count += 1; + this.parsed.certificates.unshift(bcert.parsed); + this.parsed.total_length += bcert.dumps().length; + } } diff --git a/modules/playready/cdm.ts b/modules/playready/cdm.ts index 0647f51..ea9b0fb 100644 --- a/modules/playready/cdm.ts +++ b/modules/playready/cdm.ts @@ -1,289 +1,285 @@ -import { CertificateChain } from './bcert' -import ECCKey from './ecc_key' -import ElGamal, { Point } from './elgamal' -import XmlKey from './xml_key' -import { CipherType, getCipherType, Key } from './key' -import { XMRLicense } from './xmrlicense' -import crypto from 'crypto' -import { randomBytes } from 'crypto' -import { createHash } from 'crypto' -import elliptic from 'elliptic' -import { Device } from './device' -import { XMLParser } from 'fast-xml-parser' +import { CertificateChain } from './bcert'; +import ECCKey from './ecc_key'; +import ElGamal, { Point } from './elgamal'; +import XmlKey from './xml_key'; +import { CipherType, getCipherType, Key } from './key'; +import { XMRLicense } from './xmrlicense'; +import crypto from 'crypto'; +import { randomBytes } from 'crypto'; +import { createHash } from 'crypto'; +import elliptic from 'elliptic'; +import { Device } from './device'; +import { XMLParser } from 'fast-xml-parser'; export default class Cdm { - security_level: number - certificate_chain: CertificateChain - encryption_key: ECCKey - signing_key: ECCKey - client_version: string - la_version: number - - curve: elliptic.ec - elgamal: ElGamal - - private wmrm_key: elliptic.ec.KeyPair - private xml_key: XmlKey - - constructor( - security_level: number, - certificate_chain: CertificateChain, - encryption_key: ECCKey, - signing_key: ECCKey, - client_version: string = '2.4.117.27', - la_version: number = 1 - ) { - this.security_level = security_level - this.certificate_chain = certificate_chain - this.encryption_key = encryption_key - this.signing_key = signing_key - this.client_version = client_version - this.la_version = la_version - - this.curve = new elliptic.ec('p256') - this.elgamal = new ElGamal(this.curve) - - const x = - 'c8b6af16ee941aadaa5389b4af2c10e356be42af175ef3face93254e7b0b3d9b' - const y = - '982b27b5cb2341326e56aa857dbfd5c634ce2cf9ea74fca8f2af5957efeea562' - this.wmrm_key = this.curve.keyFromPublic({ x, y }, 'hex') - this.xml_key = new XmlKey() - } - - static fromDevice(device: Device): Cdm { - return new Cdm( - device.security_level, - device.group_certificate, - device.encryption_key, - device.signing_key - ) - } - - private getKeyData(): Buffer { - const messagePoint = this.xml_key.getPoint(this.elgamal.curve) - const [point1, point2] = this.elgamal.encrypt( - messagePoint, - this.wmrm_key.getPublic() as Point - ) - - const bufferArray = Buffer.concat([ - ElGamal.toBytes(point1.getX()), - ElGamal.toBytes(point1.getY()), - ElGamal.toBytes(point2.getX()), - ElGamal.toBytes(point2.getY()) - ]) - - return bufferArray - } - - private getCipherData(): Buffer { - const b64_chain = this.certificate_chain.dumps().toString('base64') - const body = `<Data><CertificateChains><CertificateChain>${b64_chain}</CertificateChain></CertificateChains></Data>` - - const cipher = crypto.createCipheriv( - 'aes-128-cbc', - this.xml_key.aesKey, - this.xml_key.aesIv - ) - - const ciphertext = Buffer.concat([ - cipher.update(Buffer.from(body, 'utf-8')), - cipher.final() - ]) - - return Buffer.concat([this.xml_key.aesIv, ciphertext]) - } - - private buildDigestContent( - content_header: string, - nonce: string, - wmrm_cipher: string, - cert_cipher: string - ): string { - const clientTime = Math.floor(Date.now() / 1000) - - return ( - `<LA xmlns="http://schemas.microsoft.com/DRM/2007/03/protocols" Id="SignedData" xml:space="preserve">` + - `<Version>${this.la_version}</Version>` + - `<ContentHeader>${content_header}</ContentHeader>` + - `<CLIENTINFO>` + - `<CLIENTVERSION>${this.client_version}</CLIENTVERSION>` + - `</CLIENTINFO>` + - `<LicenseNonce>${nonce}</LicenseNonce>` + - `<ClientTime>${clientTime}</ClientTime>` + - `<EncryptedData xmlns="http://www.w3.org/2001/04/xmlenc#" Type="http://www.w3.org/2001/04/xmlenc#Element">` + - `<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"></EncryptionMethod>` + - `<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">` + - `<EncryptedKey xmlns="http://www.w3.org/2001/04/xmlenc#">` + - `<EncryptionMethod Algorithm="http://schemas.microsoft.com/DRM/2007/03/protocols#ecc256"></EncryptionMethod>` + - `<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">` + - `<KeyName>WMRMServer</KeyName>` + - `</KeyInfo>` + - `<CipherData>` + - `<CipherValue>${wmrm_cipher}</CipherValue>` + - `</CipherData>` + - `</EncryptedKey>` + - `</KeyInfo>` + - `<CipherData>` + - `<CipherValue>${cert_cipher}</CipherValue>` + - `</CipherData>` + - `</EncryptedData>` + - `</LA>` - ) - } - - private static buildSignedInfo(digest_value: string): string { - return ( - `<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">` + - `<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></CanonicalizationMethod>` + - `<SignatureMethod Algorithm="http://schemas.microsoft.com/DRM/2007/03/protocols#ecdsa-sha256"></SignatureMethod>` + - `<Reference URI="#SignedData">` + - `<DigestMethod Algorithm="http://schemas.microsoft.com/DRM/2007/03/protocols#sha256"></DigestMethod>` + - `<DigestValue>${digest_value}</DigestValue>` + - `</Reference>` + - `</SignedInfo>` - ) - } - - getLicenseChallenge(content_header: string): string { - const nonce = randomBytes(16).toString('base64') - const wmrm_cipher = this.getKeyData().toString('base64') - const cert_cipher = this.getCipherData().toString('base64') - - const la_content = this.buildDigestContent( - content_header, - nonce, - wmrm_cipher, - cert_cipher - ) - - const la_hash = createHash('sha256') - .update(la_content, 'utf-8') - .digest() - - const signed_info = Cdm.buildSignedInfo(la_hash.toString('base64')) - const signed_info_digest = createHash('sha256') - .update(signed_info, 'utf-8') - .digest() - - const signatureObj = this.signing_key.keyPair.sign(signed_info_digest) - - const r = signatureObj.r.toArrayLike(Buffer, 'be', 32) - const s = signatureObj.s.toArrayLike(Buffer, 'be', 32) - - const rawSignature = Buffer.concat([r, s]) - const signatureValue = rawSignature.toString('base64') - - const publicKeyBytes = this.signing_key.keyPair - .getPublic() - .encode('array', false) - const publicKeyBuffer = Buffer.from(publicKeyBytes) - const publicKeyBase64 = publicKeyBuffer.toString('base64') - - const main_body = - '<?xml version="1.0" encoding="utf-8"?>' + - '<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' + - 'xmlns:xsd="http://www.w3.org/2001/XMLSchema" ' + - 'xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">' + - '<soap:Body>' + - '<AcquireLicense xmlns="http://schemas.microsoft.com/DRM/2007/03/protocols">' + - '<challenge>' + - '<Challenge xmlns="http://schemas.microsoft.com/DRM/2007/03/protocols/messages">' + - la_content + - '<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">' + - signed_info + - `<SignatureValue>${signatureValue}</SignatureValue>` + - '<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">' + - '<KeyValue>' + - '<ECCKeyValue>' + - `<PublicKey>${publicKeyBase64}</PublicKey>` + - '</ECCKeyValue>' + - '</KeyValue>' + - '</KeyInfo>' + - '</Signature>' + - '</Challenge>' + - '</challenge>' + - '</AcquireLicense>' + - '</soap:Body>' + - '</soap:Envelope>' - - return main_body - } - - private _decryptEcc256Key(encrypted_key: Buffer): Buffer { - const point1 = this.curve.curve.point( - encrypted_key.subarray(0, 32).toString('hex'), - encrypted_key.subarray(32, 64).toString('hex') - ) - const point2 = this.curve.curve.point( - encrypted_key.subarray(64, 96).toString('hex'), - encrypted_key.subarray(96, 128).toString('hex') - ) - - const decrypted = ElGamal.decrypt( - [point1, point2], - this.encryption_key.keyPair.getPrivate() - ) - const decryptedBytes = decrypted.getX().toArray('be', 32).slice(16, 32) - - return Buffer.from(decryptedBytes) - } - - parseLicense(license: string | Buffer): { - key_id: string - key_type: number - cipher_type: number - key_length: number - key: string - }[] { - try { - const parser = new XMLParser({ - removeNSPrefix: true - }) - const result = parser.parse(license) - - let licenses = - result['Envelope']['Body']['AcquireLicenseResponse'][ - 'AcquireLicenseResult' - ]['Response']['LicenseResponse']['Licenses']['License'] - - if (!Array.isArray(licenses)) { - licenses = [licenses] - } - - var keys = [] - - for (const licenseElement of licenses) { - for (const key of XMRLicense.loads( - licenseElement - ).get_content_keys()) { - if (getCipherType(key.cipher_type) === CipherType.ECC256) { - keys.push( - new Key( - this.fixUUID(key.key_id), - key.key_type, - key.cipher_type, - key.key_length, - this._decryptEcc256Key(key.encrypted_key) - ) - ) - } - } - } - - return keys - } catch (error) { - throw new Error(`Unable to parse license, ${error}`) + security_level: number; + certificate_chain: CertificateChain; + encryption_key: ECCKey; + signing_key: ECCKey; + client_version: string; + la_version: number; + + curve: elliptic.ec; + elgamal: ElGamal; + + private wmrm_key: elliptic.ec.KeyPair; + private xml_key: XmlKey; + + constructor( + security_level: number, + certificate_chain: CertificateChain, + encryption_key: ECCKey, + signing_key: ECCKey, + client_version: string = '2.4.117.27', + la_version: number = 1 + ) { + this.security_level = security_level; + this.certificate_chain = certificate_chain; + this.encryption_key = encryption_key; + this.signing_key = signing_key; + this.client_version = client_version; + this.la_version = la_version; + + this.curve = new elliptic.ec('p256'); + this.elgamal = new ElGamal(this.curve); + + const x = + 'c8b6af16ee941aadaa5389b4af2c10e356be42af175ef3face93254e7b0b3d9b'; + const y = + '982b27b5cb2341326e56aa857dbfd5c634ce2cf9ea74fca8f2af5957efeea562'; + this.wmrm_key = this.curve.keyFromPublic({ x, y }, 'hex'); + this.xml_key = new XmlKey(); + } + + static fromDevice(device: Device): Cdm { + return new Cdm( + device.security_level, + device.group_certificate, + device.encryption_key, + device.signing_key + ); + } + + private getKeyData(): Buffer { + const messagePoint = this.xml_key.getPoint(this.elgamal.curve); + const [point1, point2] = this.elgamal.encrypt( + messagePoint, + this.wmrm_key.getPublic() as Point + ); + + const bufferArray = Buffer.concat([ + ElGamal.toBytes(point1.getX()), + ElGamal.toBytes(point1.getY()), + ElGamal.toBytes(point2.getX()), + ElGamal.toBytes(point2.getY()), + ]); + + return bufferArray; + } + + private getCipherData(): Buffer { + const b64_chain = this.certificate_chain.dumps().toString('base64'); + const body = `<Data><CertificateChains><CertificateChain>${b64_chain}</CertificateChain></CertificateChains></Data>`; + + const cipher = crypto.createCipheriv( + 'aes-128-cbc', + this.xml_key.aesKey, + this.xml_key.aesIv + ); + + const ciphertext = Buffer.concat([ + cipher.update(Buffer.from(body, 'utf-8')), + cipher.final(), + ]); + + return Buffer.concat([this.xml_key.aesIv, ciphertext]); + } + + private buildDigestContent( + content_header: string, + nonce: string, + wmrm_cipher: string, + cert_cipher: string + ): string { + const clientTime = Math.floor(Date.now() / 1000); + + return ( + '<LA xmlns="http://schemas.microsoft.com/DRM/2007/03/protocols" Id="SignedData" xml:space="preserve">' + + `<Version>${this.la_version}</Version>` + + `<ContentHeader>${content_header}</ContentHeader>` + + '<CLIENTINFO>' + + `<CLIENTVERSION>${this.client_version}</CLIENTVERSION>` + + '</CLIENTINFO>' + + `<LicenseNonce>${nonce}</LicenseNonce>` + + `<ClientTime>${clientTime}</ClientTime>` + + '<EncryptedData xmlns="http://www.w3.org/2001/04/xmlenc#" Type="http://www.w3.org/2001/04/xmlenc#Element">' + + '<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"></EncryptionMethod>' + + '<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">' + + '<EncryptedKey xmlns="http://www.w3.org/2001/04/xmlenc#">' + + '<EncryptionMethod Algorithm="http://schemas.microsoft.com/DRM/2007/03/protocols#ecc256"></EncryptionMethod>' + + '<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">' + + '<KeyName>WMRMServer</KeyName>' + + '</KeyInfo>' + + '<CipherData>' + + `<CipherValue>${wmrm_cipher}</CipherValue>` + + '</CipherData>' + + '</EncryptedKey>' + + '</KeyInfo>' + + '<CipherData>' + + `<CipherValue>${cert_cipher}</CipherValue>` + + '</CipherData>' + + '</EncryptedData>' + + '</LA>' + ); + } + + private static buildSignedInfo(digest_value: string): string { + return ( + '<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">' + + '<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></CanonicalizationMethod>' + + '<SignatureMethod Algorithm="http://schemas.microsoft.com/DRM/2007/03/protocols#ecdsa-sha256"></SignatureMethod>' + + '<Reference URI="#SignedData">' + + '<DigestMethod Algorithm="http://schemas.microsoft.com/DRM/2007/03/protocols#sha256"></DigestMethod>' + + `<DigestValue>${digest_value}</DigestValue>` + + '</Reference>' + + '</SignedInfo>' + ); + } + + getLicenseChallenge(content_header: string): string { + const nonce = randomBytes(16).toString('base64'); + const wmrm_cipher = this.getKeyData().toString('base64'); + const cert_cipher = this.getCipherData().toString('base64'); + + const la_content = this.buildDigestContent( + content_header, + nonce, + wmrm_cipher, + cert_cipher + ); + + const la_hash = createHash('sha256').update(la_content, 'utf-8').digest(); + + const signed_info = Cdm.buildSignedInfo(la_hash.toString('base64')); + const signed_info_digest = createHash('sha256') + .update(signed_info, 'utf-8') + .digest(); + + const signatureObj = this.signing_key.keyPair.sign(signed_info_digest); + + const r = signatureObj.r.toArrayLike(Buffer, 'be', 32); + const s = signatureObj.s.toArrayLike(Buffer, 'be', 32); + + const rawSignature = Buffer.concat([r, s]); + const signatureValue = rawSignature.toString('base64'); + + const publicKeyBytes = this.signing_key.keyPair + .getPublic() + .encode('array', false); + const publicKeyBuffer = Buffer.from(publicKeyBytes); + const publicKeyBase64 = publicKeyBuffer.toString('base64'); + + const main_body = + '<?xml version="1.0" encoding="utf-8"?>' + + '<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' + + 'xmlns:xsd="http://www.w3.org/2001/XMLSchema" ' + + 'xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">' + + '<soap:Body>' + + '<AcquireLicense xmlns="http://schemas.microsoft.com/DRM/2007/03/protocols">' + + '<challenge>' + + '<Challenge xmlns="http://schemas.microsoft.com/DRM/2007/03/protocols/messages">' + + la_content + + '<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">' + + signed_info + + `<SignatureValue>${signatureValue}</SignatureValue>` + + '<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">' + + '<KeyValue>' + + '<ECCKeyValue>' + + `<PublicKey>${publicKeyBase64}</PublicKey>` + + '</ECCKeyValue>' + + '</KeyValue>' + + '</KeyInfo>' + + '</Signature>' + + '</Challenge>' + + '</challenge>' + + '</AcquireLicense>' + + '</soap:Body>' + + '</soap:Envelope>'; + + return main_body; + } + + private _decryptEcc256Key(encrypted_key: Buffer): Buffer { + const point1 = this.curve.curve.point( + encrypted_key.subarray(0, 32).toString('hex'), + encrypted_key.subarray(32, 64).toString('hex') + ); + const point2 = this.curve.curve.point( + encrypted_key.subarray(64, 96).toString('hex'), + encrypted_key.subarray(96, 128).toString('hex') + ); + + const decrypted = ElGamal.decrypt( + [point1, point2], + this.encryption_key.keyPair.getPrivate() + ); + const decryptedBytes = decrypted.getX().toArray('be', 32).slice(16, 32); + + return Buffer.from(decryptedBytes); + } + + parseLicense(license: string | Buffer): { + key_id: string; + key_type: number; + cipher_type: number; + key_length: number; + key: string; + }[] { + try { + const parser = new XMLParser({ + removeNSPrefix: true, + }); + const result = parser.parse(license); + + let licenses = + result['Envelope']['Body']['AcquireLicenseResponse'][ + 'AcquireLicenseResult' + ]['Response']['LicenseResponse']['Licenses']['License']; + + if (!Array.isArray(licenses)) { + licenses = [licenses]; + } + + const keys = []; + + for (const licenseElement of licenses) { + for (const key of XMRLicense.loads(licenseElement).get_content_keys()) { + if (getCipherType(key.cipher_type) === CipherType.ECC256) { + keys.push( + new Key( + this.fixUUID(key.key_id), + key.key_type, + key.cipher_type, + key.key_length, + this._decryptEcc256Key(key.encrypted_key) + ) + ); + } } - } + } - fixUUID(data: Buffer): Buffer { - return Buffer.concat([ - Buffer.from(data.subarray(0, 4).reverse()), - Buffer.from(data.subarray(4, 6).reverse()), - Buffer.from(data.subarray(6, 8).reverse()), - data.subarray(8, 16) - ]) + return keys; + } catch (error) { + throw new Error(`Unable to parse license, ${error}`); } + } + + fixUUID(data: Buffer): Buffer { + return Buffer.concat([ + Buffer.from(data.subarray(0, 4).reverse()), + Buffer.from(data.subarray(4, 6).reverse()), + Buffer.from(data.subarray(6, 8).reverse()), + data.subarray(8, 16), + ]); + } } diff --git a/modules/playready/device.ts b/modules/playready/device.ts index 3649112..8894679 100644 --- a/modules/playready/device.ts +++ b/modules/playready/device.ts @@ -1,90 +1,92 @@ -import { Parser } from 'binary-parser-encoder' -import { CertificateChain } from './bcert' -import ECCKey from './ecc_key' -import * as fs from 'fs' +import { Parser } from 'binary-parser-encoder'; +import { CertificateChain } from './bcert'; +import ECCKey from './ecc_key'; +import * as fs from 'fs'; type RawDeviceV2 = { - signature: string - version: number - group_certificate_length: number - group_certificate: Buffer - encryption_key: Buffer - signing_key: Buffer -} + signature: string; + version: number; + group_certificate_length: number; + group_certificate: Buffer; + encryption_key: Buffer; + signing_key: Buffer; +}; class DeviceStructs { - static magic = 'PRD' - - static v1 = new Parser() - .string('signature', { length: 3, assert: DeviceStructs.magic }) - .uint8('version') - .uint32('group_key_length') - .buffer('group_key', { length: 'group_key_length' }) - .uint32('group_certificate_length') - .buffer('group_certificate', { length: 'group_certificate_length' }) - - static v2 = new Parser() - .string('signature', { length: 3, assert: DeviceStructs.magic }) - .uint8('version') - .uint32('group_certificate_length') - .buffer('group_certificate', { length: 'group_certificate_length' }) - .buffer('encryption_key', { length: 96 }) - .buffer('signing_key', { length: 96 }) + static magic = 'PRD'; + + static v1 = new Parser() + .string('signature', { length: 3, assert: DeviceStructs.magic }) + .uint8('version') + .uint32('group_key_length') + .buffer('group_key', { length: 'group_key_length' }) + .uint32('group_certificate_length') + .buffer('group_certificate', { length: 'group_certificate_length' }); + + static v2 = new Parser() + .string('signature', { length: 3, assert: DeviceStructs.magic }) + .uint8('version') + .uint32('group_certificate_length') + .buffer('group_certificate', { length: 'group_certificate_length' }) + .buffer('encryption_key', { length: 96 }) + .buffer('signing_key', { length: 96 }); } export class Device { - static CURRENT_STRUCT = DeviceStructs.v2 - - group_certificate: CertificateChain - encryption_key: ECCKey - signing_key: ECCKey - security_level: number - - constructor(parsedData: RawDeviceV2) { - this.group_certificate = CertificateChain.loads( - parsedData.group_certificate - ) - this.encryption_key = ECCKey.loads(parsedData.encryption_key) - this.signing_key = ECCKey.loads(parsedData.signing_key) - this.security_level = this.group_certificate.get_security_level() - } - - static loads(data: Buffer): Device { - const parsedData = Device.CURRENT_STRUCT.parse(data) - return new Device(parsedData) - } - - static load(filePath: string): Device { - const data = fs.readFileSync(filePath) - return Device.loads(data) - } - - dumps(): Buffer { - const groupCertBytes = this.group_certificate.dumps() - const encryptionKeyBytes = this.encryption_key.dumps() - const signingKeyBytes = this.signing_key.dumps() - - const buildData = { - signature: DeviceStructs.magic, - version: 2, - group_certificate_length: groupCertBytes.length, - group_certificate: groupCertBytes, - encryption_key: encryptionKeyBytes, - signing_key: signingKeyBytes - } - - return Device.CURRENT_STRUCT.encode(buildData) - } - - dump(filePath: string): void { - const data = this.dumps() - fs.writeFileSync(filePath, data) - } - - get_name(): string { - const name = `${this.group_certificate.get_name()}_sl${this.security_level}` - return name.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase() - } + static CURRENT_STRUCT = DeviceStructs.v2; + + group_certificate: CertificateChain; + encryption_key: ECCKey; + signing_key: ECCKey; + security_level: number; + + constructor(parsedData: RawDeviceV2) { + this.group_certificate = CertificateChain.loads( + parsedData.group_certificate + ); + this.encryption_key = ECCKey.loads(parsedData.encryption_key); + this.signing_key = ECCKey.loads(parsedData.signing_key); + this.security_level = this.group_certificate.get_security_level(); + } + + static loads(data: Buffer): Device { + const parsedData = Device.CURRENT_STRUCT.parse(data); + return new Device(parsedData); + } + + static load(filePath: string): Device { + const data = fs.readFileSync(filePath); + return Device.loads(data); + } + + dumps(): Buffer { + const groupCertBytes = this.group_certificate.dumps(); + const encryptionKeyBytes = this.encryption_key.dumps(); + const signingKeyBytes = this.signing_key.dumps(); + + const buildData = { + signature: DeviceStructs.magic, + version: 2, + group_certificate_length: groupCertBytes.length, + group_certificate: groupCertBytes, + encryption_key: encryptionKeyBytes, + signing_key: signingKeyBytes, + }; + + return Device.CURRENT_STRUCT.encode(buildData); + } + + dump(filePath: string): void { + const data = this.dumps(); + fs.writeFileSync(filePath, data); + } + + get_name(): string { + const name = `${this.group_certificate.get_name()}_sl${ + this.security_level + }`; + return name.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase(); + } } // Device V2 disabled because unstable provisioning diff --git a/modules/playready/ecc_key.ts b/modules/playready/ecc_key.ts index 0f5795f..2e7b79d 100644 --- a/modules/playready/ecc_key.ts +++ b/modules/playready/ecc_key.ts @@ -1,93 +1,93 @@ -import elliptic from 'elliptic' -import { createHash } from 'crypto' -import * as fs from 'fs' +import elliptic from 'elliptic'; +import { createHash } from 'crypto'; +import * as fs from 'fs'; export default class ECCKey { - keyPair: elliptic.ec.KeyPair - - constructor(keyPair: elliptic.ec.KeyPair) { - this.keyPair = keyPair - } - - static generate(): ECCKey { - const EC = new elliptic.ec('p256') - const keyPair = EC.genKeyPair() - return new ECCKey(keyPair) + keyPair: elliptic.ec.KeyPair; + + constructor(keyPair: elliptic.ec.KeyPair) { + this.keyPair = keyPair; + } + + static generate(): ECCKey { + const EC = new elliptic.ec('p256'); + const keyPair = EC.genKeyPair(); + return new ECCKey(keyPair); + } + + static construct(privateKey: Buffer | string | number): ECCKey { + if (Buffer.isBuffer(privateKey)) { + privateKey = privateKey.toString('hex'); + } else if (typeof privateKey === 'number') { + privateKey = privateKey.toString(16); } - static construct(privateKey: Buffer | string | number): ECCKey { - if (Buffer.isBuffer(privateKey)) { - privateKey = privateKey.toString('hex') - } else if (typeof privateKey === 'number') { - privateKey = privateKey.toString(16) - } + const EC = new elliptic.ec('p256'); + const keyPair = EC.keyFromPrivate(privateKey, 'hex'); - const EC = new elliptic.ec('p256') - const keyPair = EC.keyFromPrivate(privateKey, 'hex') + return new ECCKey(keyPair); + } - return new ECCKey(keyPair) + static loads(data: string | Buffer): ECCKey { + if (typeof data === 'string') { + data = Buffer.from(data, 'base64'); } - - static loads(data: string | Buffer): ECCKey { - if (typeof data === 'string') { - data = Buffer.from(data, 'base64') - } - if (!Buffer.isBuffer(data)) { - throw new Error(`Expecting Bytes or Base64 input, got ${data}`) - } - - if (data.length !== 96 && data.length !== 32) { - throw new Error( - `Invalid data length. Expecting 96 or 32 bytes, got ${data.length}` - ) - } - - const privateKey = data.subarray(0, 32) - return ECCKey.construct(privateKey) + if (!Buffer.isBuffer(data)) { + throw new Error(`Expecting Bytes or Base64 input, got ${data}`); } - static load(filePath: string): ECCKey { - const data = fs.readFileSync(filePath) - return ECCKey.loads(data) + if (data.length !== 96 && data.length !== 32) { + throw new Error( + `Invalid data length. Expecting 96 or 32 bytes, got ${data.length}` + ); } - dumps(): Buffer { - return Buffer.concat([this.privateBytes(), this.publicBytes()]) - } - - dump(filePath: string): void { - fs.writeFileSync(filePath, this.dumps()) - } - - getPoint(): { x: string; y: string } { - const publicKey = this.keyPair.getPublic() - return { - x: publicKey.getX().toString('hex'), - y: publicKey.getY().toString('hex') - } - } - - privateBytes(): Buffer { - const privateKey = this.keyPair.getPrivate() - return Buffer.from(privateKey.toArray('be', 32)) - } - - privateSha256Digest(): Buffer { - const hash = createHash('sha256') - hash.update(this.privateBytes()) - return hash.digest() - } - - publicBytes(): Buffer { - const publicKey = this.keyPair.getPublic() - const x = publicKey.getX().toArray('be', 32) - const y = publicKey.getY().toArray('be', 32) - return Buffer.concat([Buffer.from(x), Buffer.from(y)]) - } - - publicSha256Digest(): Buffer { - const hash = createHash('sha256') - hash.update(this.publicBytes()) - return hash.digest() - } + const privateKey = data.subarray(0, 32); + return ECCKey.construct(privateKey); + } + + static load(filePath: string): ECCKey { + const data = fs.readFileSync(filePath); + return ECCKey.loads(data); + } + + dumps(): Buffer { + return Buffer.concat([this.privateBytes(), this.publicBytes()]); + } + + dump(filePath: string): void { + fs.writeFileSync(filePath, this.dumps()); + } + + getPoint(): { x: string; y: string } { + const publicKey = this.keyPair.getPublic(); + return { + x: publicKey.getX().toString('hex'), + y: publicKey.getY().toString('hex'), + }; + } + + privateBytes(): Buffer { + const privateKey = this.keyPair.getPrivate(); + return Buffer.from(privateKey.toArray('be', 32)); + } + + privateSha256Digest(): Buffer { + const hash = createHash('sha256'); + hash.update(this.privateBytes()); + return hash.digest(); + } + + publicBytes(): Buffer { + const publicKey = this.keyPair.getPublic(); + const x = publicKey.getX().toArray('be', 32); + const y = publicKey.getY().toArray('be', 32); + return Buffer.concat([Buffer.from(x), Buffer.from(y)]); + } + + publicSha256Digest(): Buffer { + const hash = createHash('sha256'); + hash.update(this.publicBytes()); + return hash.digest(); + } } diff --git a/modules/playready/elgamal.ts b/modules/playready/elgamal.ts index 220d342..580c6a8 100644 --- a/modules/playready/elgamal.ts +++ b/modules/playready/elgamal.ts @@ -1,45 +1,45 @@ -import { ec as EC } from 'elliptic' -import { randomBytes } from 'crypto' -import BN from 'bn.js' +import { ec as EC } from 'elliptic'; +import { randomBytes } from 'crypto'; +import BN from 'bn.js'; export interface Point { - getY(): BN - getX(): BN - add(point: Point): Point - mul(n: BN | bigint | number): Point - neg(): Point + getY(): BN; + getX(): BN; + add(point: Point): Point; + mul(n: BN | bigint | number): Point; + neg(): Point; } export default class ElGamal { - curve: EC + curve: EC; - constructor(curve: EC) { - this.curve = curve - } + constructor(curve: EC) { + this.curve = curve; + } - static toBytes(n: BN): Uint8Array { - const byteArray = n.toString(16).padStart(2, '0') - if (byteArray.length % 2 !== 0) { - return Uint8Array.from(Buffer.from('0' + byteArray, 'hex')) - } - return Uint8Array.from(Buffer.from(byteArray, 'hex')) + static toBytes(n: BN): Uint8Array { + const byteArray = n.toString(16).padStart(2, '0'); + if (byteArray.length % 2 !== 0) { + return Uint8Array.from(Buffer.from('0' + byteArray, 'hex')); } + return Uint8Array.from(Buffer.from(byteArray, 'hex')); + } - encrypt(messagePoint: Point, publicKey: Point): [Point, Point] { - const ephemeralKey = new BN(randomBytes(32).toString('hex'), 16).mod( - this.curve.n! - ) - const ephemeralKeyBigInt = BigInt(ephemeralKey.toString(10)) - const point1 = this.curve.g.mul(ephemeralKeyBigInt) - const point2 = messagePoint.add(publicKey.mul(ephemeralKeyBigInt)) + encrypt(messagePoint: Point, publicKey: Point): [Point, Point] { + const ephemeralKey = new BN(randomBytes(32).toString('hex'), 16).mod( + this.curve.n! + ); + const ephemeralKeyBigInt = BigInt(ephemeralKey.toString(10)); + const point1 = this.curve.g.mul(ephemeralKeyBigInt); + const point2 = messagePoint.add(publicKey.mul(ephemeralKeyBigInt)); - return [point1, point2] - } + return [point1, point2]; + } - static decrypt(encrypted: [Point, Point], privateKey: BN): Point { - const [point1, point2] = encrypted - const sharedSecret = point1.mul(privateKey) - const decryptedMessage = point2.add(sharedSecret.neg()) - return decryptedMessage - } + static decrypt(encrypted: [Point, Point], privateKey: BN): Point { + const [point1, point2] = encrypted; + const sharedSecret = point1.mul(privateKey); + const decryptedMessage = point2.add(sharedSecret.neg()); + return decryptedMessage; + } } diff --git a/modules/playready/key.ts b/modules/playready/key.ts index 7076974..1037544 100644 --- a/modules/playready/key.ts +++ b/modules/playready/key.ts @@ -1,70 +1,70 @@ export enum KeyType { - Invalid = 0x0000, - AES128CTR = 0x0001, - RC4 = 0x0002, - AES128ECB = 0x0003, - Cocktail = 0x0004, - AESCBC = 0x0005, - UNKNOWN = 0xffff + Invalid = 0x0000, + AES128CTR = 0x0001, + RC4 = 0x0002, + AES128ECB = 0x0003, + Cocktail = 0x0004, + AESCBC = 0x0005, + UNKNOWN = 0xffff, } export function getKeyType(value: number): KeyType { - switch (value) { - case KeyType.Invalid: - case KeyType.AES128CTR: - case KeyType.RC4: - case KeyType.AES128ECB: - case KeyType.Cocktail: - case KeyType.AESCBC: - return value - default: - return KeyType.UNKNOWN - } + switch (value) { + case KeyType.Invalid: + case KeyType.AES128CTR: + case KeyType.RC4: + case KeyType.AES128ECB: + case KeyType.Cocktail: + case KeyType.AESCBC: + return value; + default: + return KeyType.UNKNOWN; + } } export enum CipherType { - Invalid = 0x0000, - RSA128 = 0x0001, - ChainedLicense = 0x0002, - ECC256 = 0x0003, - ECCforScalableLicenses = 0x0004, - Scalable = 0x0005, - UNKNOWN = 0xffff + Invalid = 0x0000, + RSA128 = 0x0001, + ChainedLicense = 0x0002, + ECC256 = 0x0003, + ECCforScalableLicenses = 0x0004, + Scalable = 0x0005, + UNKNOWN = 0xffff, } export function getCipherType(value: number): CipherType { - switch (value) { - case CipherType.Invalid: - case CipherType.RSA128: - case CipherType.ChainedLicense: - case CipherType.ECC256: - case CipherType.ECCforScalableLicenses: - case CipherType.Scalable: - return value - default: - return CipherType.UNKNOWN - } + switch (value) { + case CipherType.Invalid: + case CipherType.RSA128: + case CipherType.ChainedLicense: + case CipherType.ECC256: + case CipherType.ECCforScalableLicenses: + case CipherType.Scalable: + return value; + default: + return CipherType.UNKNOWN; + } } export class Key { - key_id: string - key_type: KeyType - cipher_type: CipherType - key_length: number - key: string + key_id: string; + key_type: KeyType; + cipher_type: CipherType; + key_length: number; + key: string; - constructor( - key_id: Buffer | string, - key_type: number, - cipher_type: number, - key_length: number, - key: Buffer | string - ) { - this.key_id = Buffer.isBuffer(key_id) ? key_id.toString('hex') : key_id + constructor( + key_id: Buffer | string, + key_type: number, + cipher_type: number, + key_length: number, + key: Buffer | string + ) { + this.key_id = Buffer.isBuffer(key_id) ? key_id.toString('hex') : key_id; - this.key_type = getKeyType(key_type) - this.cipher_type = getCipherType(cipher_type) - this.key_length = key_length - this.key = Buffer.isBuffer(key) ? key.toString('hex') : key - } + this.key_type = getKeyType(key_type); + this.cipher_type = getCipherType(cipher_type); + this.key_length = key_length; + this.key = Buffer.isBuffer(key) ? key.toString('hex') : key; + } } diff --git a/modules/playready/pssh.ts b/modules/playready/pssh.ts index c3fb469..93f733f 100644 --- a/modules/playready/pssh.ts +++ b/modules/playready/pssh.ts @@ -1,131 +1,121 @@ -import { Parser } from 'binary-parser' -import { Buffer } from 'buffer' -import WRMHeader from './wrmheader' +import { Parser } from 'binary-parser'; +import { Buffer } from 'buffer'; +import WRMHeader from './wrmheader'; -const SYSTEM_ID = Buffer.from('9a04f07998404286ab92e65be0885f95', 'hex') +const SYSTEM_ID = Buffer.from('9a04f07998404286ab92e65be0885f95', 'hex'); const PSSHBox = new Parser() - .uint32('length') - .string('pssh', { length: 4, assert: 'pssh' }) - .uint32('fullbox') - .buffer('system_id', { length: 16 }) - .uint32('data_length') - .buffer('data', { length: 'data_length' }) + .uint32('length') + .string('pssh', { length: 4, assert: 'pssh' }) + .uint32('fullbox') + .buffer('system_id', { length: 16 }) + .uint32('data_length') + .buffer('data', { length: 'data_length' }); const PlayreadyObject = new Parser() - .endianess('little') - .uint16('type') - .uint16('length') - .choice('data', { - tag: 'type', - choices: { - 1: new Parser().string('data', { - length: 'length', - encoding: 'utf16le' - }) - }, - defaultChoice: new Parser().buffer('data', { length: 'length' }) - }) + .endianess('little') + .uint16('type') + .uint16('length') + .choice('data', { + tag: 'type', + choices: { + 1: new Parser().string('data', { + length: 'length', + encoding: 'utf16le', + }), + }, + defaultChoice: new Parser().buffer('data', { length: 'length' }), + }); const PlayreadyHeader = new Parser() - .endianess('little') - .uint32('length') - .uint16('record_count') - .array('records', { - length: 'record_count', - type: PlayreadyObject - }) + .endianess('little') + .uint32('length') + .uint16('record_count') + .array('records', { + length: 'record_count', + type: PlayreadyObject, + }); function isPlayreadyPsshBox(data: Buffer): boolean { - if (data.length < 28) return false // Ensure enough length - return data.subarray(12, 28).equals(SYSTEM_ID) + if (data.length < 28) return false; // Ensure enough length + return data.subarray(12, 28).equals(SYSTEM_ID); } function isUtf16(data: Buffer): boolean { - for (let i = 1; i < data.length; i += 2) { - if (data[i] !== 0) { - return false - } + for (let i = 1; i < data.length; i += 2) { + if (data[i] !== 0) { + return false; } - return true + } + return true; } function* getWrmHeaders(wrm_header: any): IterableIterator<string> { - for (const record of wrm_header.records) { - if (record.type === 1 && typeof record.data === 'string') { - yield record.data - } + for (const record of wrm_header.records) { + if (record.type === 1 && typeof record.data === 'string') { + yield record.data; } + } } export class PSSH { - public wrm_headers: string[] + public wrm_headers: string[]; - constructor(data: string | Buffer) { - if (!data) { - throw new Error('Data must not be empty') - } + constructor(data: string | Buffer) { + if (!data) { + throw new Error('Data must not be empty'); + } - if (typeof data === 'string') { - try { - data = Buffer.from(data, 'base64') - } catch (e) { - throw new Error(`Could not decode data as Base64: ${e}`) - } - } + if (typeof data === 'string') { + try { + data = Buffer.from(data, 'base64'); + } catch (e) { + throw new Error(`Could not decode data as Base64: ${e}`); + } + } - try { - if (isPlayreadyPsshBox(data)) { - const pssh_box = PSSHBox.parse(data) - const psshData = pssh_box.data + try { + if (isPlayreadyPsshBox(data)) { + const pssh_box = PSSHBox.parse(data); + const psshData = pssh_box.data; - if (isUtf16(psshData)) { - this.wrm_headers = [psshData.toString('utf16le')] - } else if (isUtf16(psshData.subarray(6))) { - this.wrm_headers = [ - psshData.subarray(6).toString('utf16le') - ] - } else if (isUtf16(psshData.subarray(10))) { - this.wrm_headers = [ - psshData.subarray(10).toString('utf16le') - ] - } else { - const playready_header = PlayreadyHeader.parse(psshData) - this.wrm_headers = Array.from( - getWrmHeaders(playready_header) - ) - } - } else { - if (isUtf16(data)) { - this.wrm_headers = [data.toString('utf16le')] - } else if (isUtf16(data.subarray(6))) { - this.wrm_headers = [data.subarray(6).toString('utf16le')] - } else if (isUtf16(data.subarray(10))) { - this.wrm_headers = [data.subarray(10).toString('utf16le')] - } else { - const playready_header = PlayreadyHeader.parse(data) - this.wrm_headers = Array.from( - getWrmHeaders(playready_header) - ) - } - } - } catch (e) { - throw new Error( - 'Could not parse data as a PSSH Box nor a PlayReadyHeader' - ) + if (isUtf16(psshData)) { + this.wrm_headers = [psshData.toString('utf16le')]; + } else if (isUtf16(psshData.subarray(6))) { + this.wrm_headers = [psshData.subarray(6).toString('utf16le')]; + } else if (isUtf16(psshData.subarray(10))) { + this.wrm_headers = [psshData.subarray(10).toString('utf16le')]; + } else { + const playready_header = PlayreadyHeader.parse(psshData); + this.wrm_headers = Array.from(getWrmHeaders(playready_header)); + } + } else { + if (isUtf16(data)) { + this.wrm_headers = [data.toString('utf16le')]; + } else if (isUtf16(data.subarray(6))) { + this.wrm_headers = [data.subarray(6).toString('utf16le')]; + } else if (isUtf16(data.subarray(10))) { + this.wrm_headers = [data.subarray(10).toString('utf16le')]; + } else { + const playready_header = PlayreadyHeader.parse(data); + this.wrm_headers = Array.from(getWrmHeaders(playready_header)); } + } + } catch (e) { + throw new Error( + 'Could not parse data as a PSSH Box nor a PlayReadyHeader' + ); } + } - // Header downgrade - public get_wrm_headers(downgrade_to_v4: boolean = false): string[] { - return this.wrm_headers.map( - downgrade_to_v4 ? this._downgrade : (_) => _ - ) - } + // Header downgrade + public get_wrm_headers(downgrade_to_v4: boolean = false): string[] { + return this.wrm_headers.map(downgrade_to_v4 ? this._downgrade : (_) => _); + } - private _downgrade(wrm_header: string): string { - const header = new WRMHeader(wrm_header) - return header.to_v4_0_0_0() - } + private _downgrade(wrm_header: string): string { + const header = new WRMHeader(wrm_header); + return header.to_v4_0_0_0(); + } } diff --git a/modules/playready/wrmheader.ts b/modules/playready/wrmheader.ts index 7b43773..2416822 100644 --- a/modules/playready/wrmheader.ts +++ b/modules/playready/wrmheader.ts @@ -1,112 +1,118 @@ -import { XMLParser } from 'fast-xml-parser' +import { XMLParser } from 'fast-xml-parser'; export class SignedKeyID { - constructor( - public alg_id: string, - public value: string, - public checksum?: string - ) {} + constructor( + public alg_id: string, + public value: string, + public checksum?: string + ) {} } -export type Version = '4.0.0.0' | '4.1.0.0' | '4.2.0.0' | '4.3.0.0' | 'UNKNOWN' +export type Version = '4.0.0.0' | '4.1.0.0' | '4.2.0.0' | '4.3.0.0' | 'UNKNOWN'; export type ReturnStructure = [ - SignedKeyID[], - string | null, - string | null, - string | null -] + SignedKeyID[], + string | null, + string | null, + string | null +]; interface ParsedWRMHeader { - WRMHEADER: { - '@_version': string - DATA?: any - } + WRMHEADER: { + '@_version': string; + DATA?: any; + }; } export default class WRMHeader { - private header: ParsedWRMHeader['WRMHEADER'] - version: Version + private header: ParsedWRMHeader['WRMHEADER']; + version: Version; - constructor(data: string) { - if (!data) throw new Error('Data must not be empty') + constructor(data: string) { + if (!data) throw new Error('Data must not be empty'); - const parser = new XMLParser({ - ignoreAttributes: false, - removeNSPrefix: true, - attributeNamePrefix: '@_' - }) - const parsed = parser.parse(data) as ParsedWRMHeader + const parser = new XMLParser({ + ignoreAttributes: false, + removeNSPrefix: true, + attributeNamePrefix: '@_', + }); + const parsed = parser.parse(data) as ParsedWRMHeader; - if (!parsed.WRMHEADER) throw new Error('Data is not a valid WRMHEADER') + if (!parsed.WRMHEADER) throw new Error('Data is not a valid WRMHEADER'); - this.header = parsed.WRMHEADER - this.version = WRMHeader.fromString(this.header['@_version']) - } + this.header = parsed.WRMHEADER; + this.version = WRMHeader.fromString(this.header['@_version']); + } - private static fromString(value: string): Version { - if (['4.0.0.0', '4.1.0.0', '4.2.0.0', '4.3.0.0'].includes(value)) { - return value as Version - } - return 'UNKNOWN' + private static fromString(value: string): Version { + if (['4.0.0.0', '4.1.0.0', '4.2.0.0', '4.3.0.0'].includes(value)) { + return value as Version; } + return 'UNKNOWN'; + } - private static ensureList(element: any): any[] { - return Array.isArray(element) ? element : [element] - } + private static ensureList(element: any): any[] { + return Array.isArray(element) ? element : [element]; + } - to_v4_0_0_0(): string { - const [key_ids, la_url, lui_url, ds_id] = this.readAttributes() - if (key_ids.length === 0) throw new Error('No Key IDs available') - const key_id = key_ids[0] - return `<WRMHEADER xmlns="http://schemas.microsoft.com/DRM/2007/03/PlayReadyHeader" version="4.0.0.0"><DATA><PROTECTINFO><KEYLEN>16</KEYLEN><ALGID>AESCTR</ALGID></PROTECTINFO><KID>${key_id.value}</KID>${la_url ? `<LA_URL>${la_url}</LA_URL>` : ''}${lui_url ? `<LUI_URL>${lui_url}</LUI_URL>` : ''}${ds_id ? `<DS_ID>${ds_id}</DS_ID>` : ''}${key_id.checksum ? `<CHECKSUM>${key_id.checksum}</CHECKSUM>` : ''}</DATA></WRMHEADER>` - } + to_v4_0_0_0(): string { + const [key_ids, la_url, lui_url, ds_id] = this.readAttributes(); + if (key_ids.length === 0) throw new Error('No Key IDs available'); + const key_id = key_ids[0]; + return `<WRMHEADER xmlns="http://schemas.microsoft.com/DRM/2007/03/PlayReadyHeader" version="4.0.0.0"><DATA><PROTECTINFO><KEYLEN>16</KEYLEN><ALGID>AESCTR</ALGID></PROTECTINFO><KID>${ + key_id.value + }</KID>${la_url ? `<LA_URL>${la_url}</LA_URL>` : ''}${ + lui_url ? `<LUI_URL>${lui_url}</LUI_URL>` : '' + }${ds_id ? `<DS_ID>${ds_id}</DS_ID>` : ''}${ + key_id.checksum ? `<CHECKSUM>${key_id.checksum}</CHECKSUM>` : '' + }</DATA></WRMHEADER>`; + } - readAttributes(): ReturnStructure { - const data = this.header.DATA - if (!data) - throw new Error( - 'Not a valid PlayReady Header Record, WRMHEADER/DATA required' - ) - switch (this.version) { - case '4.0.0.0': - return WRMHeader.read_v4(data) - case '4.1.0.0': - case '4.2.0.0': - case '4.3.0.0': - return WRMHeader.read_vX(data) - default: - throw new Error(`Unsupported version: ${this.version}`) - } + readAttributes(): ReturnStructure { + const data = this.header.DATA; + if (!data) + throw new Error( + 'Not a valid PlayReady Header Record, WRMHEADER/DATA required' + ); + switch (this.version) { + case '4.0.0.0': + return WRMHeader.read_v4(data); + case '4.1.0.0': + case '4.2.0.0': + case '4.3.0.0': + return WRMHeader.read_vX(data); + default: + throw new Error(`Unsupported version: ${this.version}`); } + } - private static read_v4(data: any): ReturnStructure { - const protectInfo = data.PROTECTINFO - return [ - [new SignedKeyID(protectInfo.ALGID, data.KID, data.CHECKSUM)], - data.LA_URL || null, - data.LUI_URL || null, - data.DS_ID || null - ] - } + private static read_v4(data: any): ReturnStructure { + const protectInfo = data.PROTECTINFO; + return [ + [new SignedKeyID(protectInfo.ALGID, data.KID, data.CHECKSUM)], + data.LA_URL || null, + data.LUI_URL || null, + data.DS_ID || null, + ]; + } - private static read_vX(data: any): ReturnStructure { - const protectInfo = data.PROTECTINFO - const signedKeyIDs: SignedKeyID[] = protectInfo?.KID - ? WRMHeader.ensureList(protectInfo.KID).map( - (kid: any) => - new SignedKeyID( - kid['@_ALGID'] || '', - kid['@_VALUE'], - kid['@_CHECKSUM'] - ) - ) - : [] - return [ - signedKeyIDs, - data.LA_URL || null, - data.LUI_URL || null, - data.DS_ID || null - ] - } + private static read_vX(data: any): ReturnStructure { + const protectInfo = data.PROTECTINFO; + const signedKeyIDs: SignedKeyID[] = protectInfo?.KID + ? WRMHeader.ensureList(protectInfo.KID).map( + (kid: any) => + new SignedKeyID( + kid['@_ALGID'] || '', + kid['@_VALUE'], + kid['@_CHECKSUM'] + ) + ) + : []; + return [ + signedKeyIDs, + data.LA_URL || null, + data.LUI_URL || null, + data.DS_ID || null, + ]; + } } diff --git a/modules/playready/xml_key.ts b/modules/playready/xml_key.ts index 783521f..dc6c923 100644 --- a/modules/playready/xml_key.ts +++ b/modules/playready/xml_key.ts @@ -1,28 +1,28 @@ -import BN from 'bn.js' -import { ec as EC } from 'elliptic' -import ECCKey from './ecc_key' -import ElGamal, { Point } from './elgamal' +import BN from 'bn.js'; +import { ec as EC } from 'elliptic'; +import ECCKey from './ecc_key'; +import ElGamal, { Point } from './elgamal'; export default class XmlKey { - private _sharedPoint: ECCKey - public sharedKeyX: BN - public sharedKeyY: BN - public _shared_key_x_bytes: Uint8Array - public aesIv: Uint8Array - public aesKey: Uint8Array + private _sharedPoint: ECCKey; + public sharedKeyX: BN; + public sharedKeyY: BN; + public _shared_key_x_bytes: Uint8Array; + public aesIv: Uint8Array; + public aesKey: Uint8Array; - constructor() { - this._sharedPoint = ECCKey.generate() - this.sharedKeyX = this._sharedPoint.keyPair.getPublic().getX() - this.sharedKeyY = this._sharedPoint.keyPair.getPublic().getY() - this._shared_key_x_bytes = ElGamal.toBytes(this.sharedKeyX) - this.aesIv = this._shared_key_x_bytes.subarray(0, 16) - this.aesKey = this._shared_key_x_bytes.subarray(16, 32) - } + constructor() { + this._sharedPoint = ECCKey.generate(); + this.sharedKeyX = this._sharedPoint.keyPair.getPublic().getX(); + this.sharedKeyY = this._sharedPoint.keyPair.getPublic().getY(); + this._shared_key_x_bytes = ElGamal.toBytes(this.sharedKeyX); + this.aesIv = this._shared_key_x_bytes.subarray(0, 16); + this.aesKey = this._shared_key_x_bytes.subarray(16, 32); + } - getPoint(curve: EC): Point { - return curve.curve.point(this.sharedKeyX, this.sharedKeyY) - } + getPoint(curve: EC): Point { + return curve.curve.point(this.sharedKeyX, this.sharedKeyY); + } } // Make it more undetectable (not working right now) diff --git a/modules/playready/xmrlicense.ts b/modules/playready/xmrlicense.ts index 26b3679..f242d33 100644 --- a/modules/playready/xmrlicense.ts +++ b/modules/playready/xmrlicense.ts @@ -1,270 +1,270 @@ -import { Parser } from 'binary-parser' -import * as fs from 'fs' +import { Parser } from 'binary-parser'; +import * as fs from 'fs'; export class XMRLicenseStructs { - static PlayEnablerType = new Parser().buffer('player_enabler_type', { - length: 16 + static PlayEnablerType = new Parser().buffer('player_enabler_type', { + length: 16, + }); + + static DomainRestrictionObject = new Parser() + .buffer('account_id', { length: 16 }) + .uint32('revision'); + + static IssueDateObject = new Parser().uint32('issue_date'); + + static RevInfoVersionObject = new Parser().uint32('sequence'); + + static SecurityLevelObject = new Parser().uint16('minimum_security_level'); + + static EmbeddedLicenseSettingsObject = new Parser().uint16('indicator'); + + static ECCKeyObject = new Parser() + .uint16('curve_type') + .uint16('key_length') + .buffer('key', { + length: function () { + return (this as any).key_length; + }, + }); + + static SignatureObject = new Parser() + .uint16('signature_type') + .uint16('signature_data_length') + .buffer('signature_data', { + length: function () { + return (this as any).signature_data_length; + }, + }); + + static ContentKeyObject = new Parser() + .buffer('key_id', { length: 16 }) + .uint16('key_type') + .uint16('cipher_type') + .uint16('key_length') + .buffer('encrypted_key', { + length: function () { + return (this as any).key_length; + }, + }); + + static RightsSettingsObject = new Parser().uint16('rights'); + + static OutputProtectionLevelRestrictionObject = new Parser() + .uint16('minimum_compressed_digital_video_opl') + .uint16('minimum_uncompressed_digital_video_opl') + .uint16('minimum_analog_video_opl') + .uint16('minimum_digital_compressed_audio_opl') + .uint16('minimum_digital_uncompressed_audio_opl'); + + static ExpirationRestrictionObject = new Parser() + .uint32('begin_date') + .uint32('end_date'); + + static RemovalDateObject = new Parser().uint32('removal_date'); + + static UplinkKIDObject = new Parser() + .buffer('uplink_kid', { length: 16 }) + .uint16('chained_checksum_type') + .uint16('chained_checksum_length') + .buffer('chained_checksum', { + length: function () { + return (this as any).chained_checksum_length; + }, + }); + + static AnalogVideoOutputConfigurationRestriction = new Parser() + .buffer('video_output_protection_id', { length: 16 }) + .buffer('binary_configuration_data', { + length: function () { + return (this as any).$parent.length - 16; + }, + }); + + static DigitalVideoOutputRestrictionObject = new Parser() + .buffer('video_output_protection_id', { length: 16 }) + .buffer('binary_configuration_data', { + length: function () { + return (this as any).$parent.length - 16; + }, + }); + + static DigitalAudioOutputRestrictionObject = new Parser() + .buffer('audio_output_protection_id', { length: 16 }) + .buffer('binary_configuration_data', { + length: function () { + return (this as any).$parent.length - 16; + }, + }); + + static PolicyMetadataObject = new Parser() + .buffer('metadata_type', { length: 16 }) + .buffer('policy_data', { + length: function () { + return (this as any).$parent.length - 16; + }, + }); + + static SecureStopRestrictionObject = new Parser().buffer('metering_id', { + length: 16, + }); + + static MeteringRestrictionObject = new Parser().buffer('metering_id', { + length: 16, + }); + + static ExpirationAfterFirstPlayRestrictionObject = new Parser().uint32( + 'seconds' + ); + + static GracePeriodObject = new Parser().uint32('grace_period'); + + static SourceIdObject = new Parser().uint32('source_id'); + + static AuxiliaryKey = new Parser() + .uint32('location') + .buffer('key', { length: 16 }); + + static AuxiliaryKeysObject = new Parser() + .uint16('count') + .array('auxiliary_keys', { + length: 'count', + type: XMRLicenseStructs.AuxiliaryKey, + }); + + static UplinkKeyObject3 = new Parser() + .buffer('uplink_key_id', { length: 16 }) + .uint16('chained_length') + .buffer('checksum', { + length: function () { + return (this as any).chained_length; + }, }) - - static DomainRestrictionObject = new Parser() - .buffer('account_id', { length: 16 }) - .uint32('revision') - - static IssueDateObject = new Parser().uint32('issue_date') - - static RevInfoVersionObject = new Parser().uint32('sequence') - - static SecurityLevelObject = new Parser().uint16('minimum_security_level') - - static EmbeddedLicenseSettingsObject = new Parser().uint16('indicator') - - static ECCKeyObject = new Parser() - .uint16('curve_type') - .uint16('key_length') - .buffer('key', { - length: function () { - return (this as any).key_length - } - }) - - static SignatureObject = new Parser() - .uint16('signature_type') - .uint16('signature_data_length') - .buffer('signature_data', { - length: function () { - return (this as any).signature_data_length - } - }) - - static ContentKeyObject = new Parser() - .buffer('key_id', { length: 16 }) - .uint16('key_type') - .uint16('cipher_type') - .uint16('key_length') - .buffer('encrypted_key', { - length: function () { - return (this as any).key_length - } - }) - - static RightsSettingsObject = new Parser().uint16('rights') - - static OutputProtectionLevelRestrictionObject = new Parser() - .uint16('minimum_compressed_digital_video_opl') - .uint16('minimum_uncompressed_digital_video_opl') - .uint16('minimum_analog_video_opl') - .uint16('minimum_digital_compressed_audio_opl') - .uint16('minimum_digital_uncompressed_audio_opl') - - static ExpirationRestrictionObject = new Parser() - .uint32('begin_date') - .uint32('end_date') - - static RemovalDateObject = new Parser().uint32('removal_date') - - static UplinkKIDObject = new Parser() - .buffer('uplink_kid', { length: 16 }) - .uint16('chained_checksum_type') - .uint16('chained_checksum_length') - .buffer('chained_checksum', { - length: function () { - return (this as any).chained_checksum_length - } - }) - - static AnalogVideoOutputConfigurationRestriction = new Parser() - .buffer('video_output_protection_id', { length: 16 }) - .buffer('binary_configuration_data', { - length: function () { - return (this as any).$parent.length - 16 - } - }) - - static DigitalVideoOutputRestrictionObject = new Parser() - .buffer('video_output_protection_id', { length: 16 }) - .buffer('binary_configuration_data', { - length: function () { - return (this as any).$parent.length - 16 - } - }) - - static DigitalAudioOutputRestrictionObject = new Parser() - .buffer('audio_output_protection_id', { length: 16 }) - .buffer('binary_configuration_data', { - length: function () { - return (this as any).$parent.length - 16 - } - }) - - static PolicyMetadataObject = new Parser() - .buffer('metadata_type', { length: 16 }) - .buffer('policy_data', { - length: function () { - return (this as any).$parent.length - 16 - } - }) - - static SecureStopRestrictionObject = new Parser().buffer('metering_id', { - length: 16 - }) - - static MeteringRestrictionObject = new Parser().buffer('metering_id', { - length: 16 - }) - - static ExpirationAfterFirstPlayRestrictionObject = new Parser().uint32( - 'seconds' - ) - - static GracePeriodObject = new Parser().uint32('grace_period') - - static SourceIdObject = new Parser().uint32('source_id') - - static AuxiliaryKey = new Parser() - .uint32('location') - .buffer('key', { length: 16 }) - - static AuxiliaryKeysObject = new Parser() - .uint16('count') - .array('auxiliary_keys', { - length: 'count', - type: XMRLicenseStructs.AuxiliaryKey - }) - - static UplinkKeyObject3 = new Parser() - .buffer('uplink_key_id', { length: 16 }) - .uint16('chained_length') - .buffer('checksum', { - length: function () { - return (this as any).chained_length - } - }) - .uint16('count') - .array('entries', { - length: 'count', - type: new Parser().uint32('entry') - }) - - static CopyEnablerObject = new Parser().buffer('copy_enabler_type', { - length: 16 - }) - - static CopyCountRestrictionObject = new Parser().uint32('count') - - static MoveObject = new Parser().uint32('minimum_move_protection_level') - - static XMRObject = (): Parser => - new Parser() - .namely('self') - .int16('flags') - .int16('type') - .int32('length') - .choice('data', { - tag: 'type', - choices: { - 0x0005: XMRLicenseStructs.OutputProtectionLevelRestrictionObject, - 0x0008: XMRLicenseStructs.AnalogVideoOutputConfigurationRestriction, - 0x000a: XMRLicenseStructs.ContentKeyObject, - 0x000b: XMRLicenseStructs.SignatureObject, - 0x000d: XMRLicenseStructs.RightsSettingsObject, - 0x0012: XMRLicenseStructs.ExpirationRestrictionObject, - 0x0013: XMRLicenseStructs.IssueDateObject, - 0x0016: XMRLicenseStructs.MeteringRestrictionObject, - 0x001a: XMRLicenseStructs.GracePeriodObject, - 0x0022: XMRLicenseStructs.SourceIdObject, - 0x002a: XMRLicenseStructs.ECCKeyObject, - 0x002c: XMRLicenseStructs.PolicyMetadataObject, - 0x0029: XMRLicenseStructs.DomainRestrictionObject, - 0x0030: XMRLicenseStructs.ExpirationAfterFirstPlayRestrictionObject, - 0x0031: XMRLicenseStructs.DigitalAudioOutputRestrictionObject, - 0x0032: XMRLicenseStructs.RevInfoVersionObject, - 0x0033: XMRLicenseStructs.EmbeddedLicenseSettingsObject, - 0x0034: XMRLicenseStructs.SecurityLevelObject, - 0x0037: XMRLicenseStructs.MoveObject, - 0x0039: XMRLicenseStructs.PlayEnablerType, - 0x003a: XMRLicenseStructs.CopyEnablerObject, - 0x003b: XMRLicenseStructs.UplinkKIDObject, - 0x003d: XMRLicenseStructs.CopyCountRestrictionObject, - 0x0050: XMRLicenseStructs.RemovalDateObject, - 0x0051: XMRLicenseStructs.AuxiliaryKeysObject, - 0x0052: XMRLicenseStructs.UplinkKeyObject3, - 0x005a: XMRLicenseStructs.SecureStopRestrictionObject, - 0x0059: XMRLicenseStructs.DigitalVideoOutputRestrictionObject - }, - defaultChoice: 'self' - }) - - static XmrLicense = new Parser() - .useContextVars() - .buffer('signature', { length: 4 }) - .int32('xmr_version') - .buffer('rights_id', { length: 16 }) - .array('containers', { - type: XMRLicenseStructs.XMRObject(), - readUntil: 'eof' - }) + .uint16('count') + .array('entries', { + length: 'count', + type: new Parser().uint32('entry'), + }); + + static CopyEnablerObject = new Parser().buffer('copy_enabler_type', { + length: 16, + }); + + static CopyCountRestrictionObject = new Parser().uint32('count'); + + static MoveObject = new Parser().uint32('minimum_move_protection_level'); + + static XMRObject = (): Parser => + new Parser() + .namely('self') + .int16('flags') + .int16('type') + .int32('length') + .choice('data', { + tag: 'type', + choices: { + 0x0005: XMRLicenseStructs.OutputProtectionLevelRestrictionObject, + 0x0008: XMRLicenseStructs.AnalogVideoOutputConfigurationRestriction, + 0x000a: XMRLicenseStructs.ContentKeyObject, + 0x000b: XMRLicenseStructs.SignatureObject, + 0x000d: XMRLicenseStructs.RightsSettingsObject, + 0x0012: XMRLicenseStructs.ExpirationRestrictionObject, + 0x0013: XMRLicenseStructs.IssueDateObject, + 0x0016: XMRLicenseStructs.MeteringRestrictionObject, + 0x001a: XMRLicenseStructs.GracePeriodObject, + 0x0022: XMRLicenseStructs.SourceIdObject, + 0x002a: XMRLicenseStructs.ECCKeyObject, + 0x002c: XMRLicenseStructs.PolicyMetadataObject, + 0x0029: XMRLicenseStructs.DomainRestrictionObject, + 0x0030: XMRLicenseStructs.ExpirationAfterFirstPlayRestrictionObject, + 0x0031: XMRLicenseStructs.DigitalAudioOutputRestrictionObject, + 0x0032: XMRLicenseStructs.RevInfoVersionObject, + 0x0033: XMRLicenseStructs.EmbeddedLicenseSettingsObject, + 0x0034: XMRLicenseStructs.SecurityLevelObject, + 0x0037: XMRLicenseStructs.MoveObject, + 0x0039: XMRLicenseStructs.PlayEnablerType, + 0x003a: XMRLicenseStructs.CopyEnablerObject, + 0x003b: XMRLicenseStructs.UplinkKIDObject, + 0x003d: XMRLicenseStructs.CopyCountRestrictionObject, + 0x0050: XMRLicenseStructs.RemovalDateObject, + 0x0051: XMRLicenseStructs.AuxiliaryKeysObject, + 0x0052: XMRLicenseStructs.UplinkKeyObject3, + 0x005a: XMRLicenseStructs.SecureStopRestrictionObject, + 0x0059: XMRLicenseStructs.DigitalVideoOutputRestrictionObject, + }, + defaultChoice: 'self', + }); + + static XmrLicense = new Parser() + .useContextVars() + .buffer('signature', { length: 4 }) + .int32('xmr_version') + .buffer('rights_id', { length: 16 }) + .array('containers', { + type: XMRLicenseStructs.XMRObject(), + readUntil: 'eof', + }); } export class XMRLicense extends XMRLicenseStructs { - parsed: any - _LICENSE: Parser - - constructor( - parsed_license: any, - license_obj: Parser = XMRLicenseStructs.XmrLicense - ) { - super() - this.parsed = parsed_license - this._LICENSE = license_obj + parsed: any; + _LICENSE: Parser; + + constructor( + parsed_license: any, + license_obj: Parser = XMRLicenseStructs.XmrLicense + ) { + super(); + this.parsed = parsed_license; + this._LICENSE = license_obj; + } + + static loads(data: string | Buffer): XMRLicense { + if (typeof data === 'string') { + data = Buffer.from(data, 'base64'); } - - static loads(data: string | Buffer): XMRLicense { - if (typeof data === 'string') { - data = Buffer.from(data, 'base64') - } - if (!Buffer.isBuffer(data)) { - throw new Error(`Expecting Bytes or Base64 input, got ${data}`) - } - - const licence = XMRLicenseStructs.XmrLicense - const parsed_license = licence.parse(data) - return new XMRLicense(parsed_license, licence) + if (!Buffer.isBuffer(data)) { + throw new Error(`Expecting Bytes or Base64 input, got ${data}`); } - static load(filePath: string): XMRLicense { - if (typeof filePath !== 'string') { - throw new Error(`Expecting path string, got ${filePath}`) - } - const data = fs.readFileSync(filePath) - return XMRLicense.loads(data) - } + const licence = XMRLicenseStructs.XmrLicense; + const parsed_license = licence.parse(data); + return new XMRLicense(parsed_license, licence); + } - dumps(): Buffer { - return this._LICENSE.parse(this.parsed) + static load(filePath: string): XMRLicense { + if (typeof filePath !== 'string') { + throw new Error(`Expecting path string, got ${filePath}`); } - - struct(): Parser { - return this._LICENSE + const data = fs.readFileSync(filePath); + return XMRLicense.loads(data); + } + + dumps(): Buffer { + return this._LICENSE.parse(this.parsed); + } + + struct(): Parser { + return this._LICENSE; + } + + private _locate(container: any): any { + if (container.flags === 2 || container.flags === 3) { + return this._locate(container.data); + } else { + return container; } - - private _locate(container: any): any { - if (container.flags === 2 || container.flags === 3) { - return this._locate(container.data) - } else { - return container - } + } + + *get_object(type_: number): Generator<any> { + for (const obj of this.parsed.containers) { + const container = this._locate(obj); + if (container.type === type_) { + yield container.data; + } } + } - *get_object(type_: number): Generator<any> { - for (const obj of this.parsed.containers) { - const container = this._locate(obj) - if (container.type === type_) { - yield container.data - } - } - } - - get_content_keys(): Generator<any> { - return this.get_object(0x000a) - } + get_content_keys(): Generator<any> { + return this.get_object(0x000a); + } }