added new playready client
Some checks are pending
auto-documentation / documentation (push) Waiting to run
build and push docker image / build-node (push) Waiting to run
Style and build test / tsc (push) Waiting to run
Style and build test / eslint (push) Blocked by required conditions
Style and build test / prettier (push) Blocked by required conditions
Style and build test / build-test-windows-arm64 (push) Blocked by required conditions
Style and build test / build-test-linux-arm64 (push) Blocked by required conditions
Style and build test / build-test-macos-arm64 (push) Blocked by required conditions
Style and build test / build-test-windows-x64 (push) Blocked by required conditions
Style and build test / build-test-linux-x64 (push) Blocked by required conditions
Style and build test / build-test-macos-x64 (push) Blocked by required conditions

This commit is contained in:
stratumadev 2025-11-20 04:42:41 +01:00
parent 51cb97e18f
commit d0cc551b8d
14 changed files with 47 additions and 1541 deletions

View file

@ -111,5 +111,4 @@ In order to decrypt DRM content, you will need to have a dumped CDM, after that
### Instructions (Playready) ### Instructions (Playready)
Playready CDMs are very easy to obtain, you can find them even on Github. Playready CDMs are very easy to obtain, you can find them even on Github.
Place the CDM in the `./playready/` directory and you're all set! Place the CDM files (bgroupcert.dat and zgpriv.dat) in the `./playready/` directory and you're all set!
**IMPORTANT**: The Playready CDM (SL2000/SL3000) needs to be provisioned as a **V3 Device** by pyplayready (https://github.com/ready-dl/pyplayready).

View file

@ -2,42 +2,40 @@ import fs from 'fs';
import { console } from './log'; import { console } from './log';
import { workingDir } from './module.cfg-loader'; import { workingDir } from './module.cfg-loader';
import path from 'path'; import path from 'path';
import { Device } from './playready/device';
import Cdm from './playready/cdm';
import { PSSH } from './playready/pssh';
import { KeyContainer, Session } from './widevine/license'; import { KeyContainer, Session } from './widevine/license';
import * as reqModule from './module.fetch'; import * as reqModule from './module.fetch';
import Playready from 'node-playready';
const req = new reqModule.Req(); const req = new reqModule.Req();
//read cdm files located in the same directory //read cdm files located in the same directory
let privateKey: Buffer = Buffer.from([]), let privateKey: Buffer = Buffer.from([]),
identifierBlob: Buffer = Buffer.from([]), identifierBlob: Buffer = Buffer.from([]),
prd: Buffer = Buffer.from([]), prd_cdm: Playready | undefined;
prd_cdm: Cdm | undefined;
export let cdm: 'widevine' | 'playready'; export let cdm: 'widevine' | 'playready';
export let canDecrypt: boolean; export let canDecrypt: boolean;
try { try {
const files_prd = fs.readdirSync(path.join(workingDir, 'playready')); const files_prd = fs.readdirSync(path.join(workingDir, 'playready'));
const prd_file_found = files_prd.find((f) => f.includes('.prd')); const bgroup_file_found = files_prd.find((f) => f === 'bgroupcert.dat');
const zgpriv_file_found = files_prd.find((f) => f === 'zgpriv.dat');
try { try {
if (prd_file_found) { if (bgroup_file_found && zgpriv_file_found) {
const file_prd = path.join(workingDir, 'playready', prd_file_found); const file_bgroup = path.join(workingDir, 'playready', bgroup_file_found);
const stats = fs.statSync(file_prd); const file_zgpriv = path.join(workingDir, 'playready', zgpriv_file_found);
if (stats.size < 1024 * 8 && stats.isFile()) {
const fileContents = fs.readFileSync(file_prd, { const bgroup_stats = fs.statSync(file_bgroup);
encoding: 'utf8' const zgpriv_stats = fs.statSync(file_zgpriv);
}); // Zgpriv is always 32 bytes long
if (fileContents.includes('CERT')) { if (bgroup_stats.isFile() && zgpriv_stats.isFile() && zgpriv_stats.size === 32) {
prd = fs.readFileSync(file_prd); const bgroup = fs.readFileSync(file_bgroup);
const device = Device.loads(prd); const zgpriv = fs.readFileSync(file_zgpriv);
prd_cdm = Cdm.fromDevice(device); // Init Playready Client
} prd_cdm = Playready.init(bgroup, zgpriv);
} }
} }
} catch (e) { } catch (e) {
console.error('Error loading Playready CDM, ensure the CDM is provisioned as a V3 Device and not malformed. For more informations read the readme.'); console.error('Error loading Playready CDM. For more informations read the readme.');
prd = Buffer.from([]); console.error(e);
} }
const files_wvd = fs.readdirSync(path.join(workingDir, 'widevine')); const files_wvd = fs.readdirSync(path.join(workingDir, 'widevine'));
@ -72,7 +70,7 @@ try {
if (privateKey.length !== 0 && identifierBlob.length !== 0) { if (privateKey.length !== 0 && identifierBlob.length !== 0) {
cdm = 'widevine'; cdm = 'widevine';
canDecrypt = true; canDecrypt = true;
} else if (prd.length !== 0) { } else if (prd_cdm) {
cdm = 'playready'; cdm = 'playready';
canDecrypt = true; canDecrypt = true;
} else if (privateKey.length === 0 && identifierBlob.length !== 0) { } else if (privateKey.length === 0 && identifierBlob.length !== 0) {
@ -81,8 +79,6 @@ try {
} else if (identifierBlob.length === 0 && privateKey.length !== 0) { } else if (identifierBlob.length === 0 && privateKey.length !== 0) {
console.warn('Identifier blob missing'); console.warn('Identifier blob missing');
canDecrypt = false; canDecrypt = false;
} else if (prd.length == 0) {
canDecrypt = false;
} else { } else {
canDecrypt = false; canDecrypt = false;
} }
@ -93,10 +89,10 @@ try {
export async function getKeysWVD(pssh: string | undefined, licenseServer: string, authData: Record<string, string>): Promise<KeyContainer[]> { export async function getKeysWVD(pssh: string | undefined, licenseServer: string, authData: Record<string, string>): Promise<KeyContainer[]> {
if (!pssh || !canDecrypt) return []; if (!pssh || !canDecrypt) return [];
//pssh found in the mpd manifest // pssh found in the mpd manifest
const psshBuffer = Buffer.from(pssh, 'base64'); const psshBuffer = Buffer.from(pssh, 'base64');
//Create a new widevine session // Create a new widevine session
const session = new Session({ privateKey, identifierBlob }, psshBuffer); const session = new Session({ privateKey, identifierBlob }, psshBuffer);
// Request License // Request License
@ -123,12 +119,11 @@ export async function getKeysWVD(pssh: string | undefined, licenseServer: string
export async function getKeysPRD(pssh: string | undefined, licenseServer: string, authData: Record<string, string>): Promise<KeyContainer[]> { export async function getKeysPRD(pssh: string | undefined, licenseServer: string, authData: Record<string, string>): Promise<KeyContainer[]> {
if (!pssh || !canDecrypt || !prd_cdm) return []; if (!pssh || !canDecrypt || !prd_cdm) return [];
const pssh_parsed = new PSSH(pssh);
//Create a new playready session // Generate Playready challenge
const session = prd_cdm.getLicenseChallenge(pssh_parsed.get_wrm_headers(true)[0]); const session = await prd_cdm.generateChallenge(pssh);
//Generate license // Fetch license
const licReq = await req.getData(licenseServer, { const licReq = await req.getData(licenseServer, {
method: 'POST', method: 'POST',
body: session, body: session,
@ -140,13 +135,12 @@ export async function getKeysPRD(pssh: string | undefined, licenseServer: string
return []; return [];
} }
//Parse License and return keys // Parse License and return keys
try { try {
const keys = prd_cdm.parseLicense(await licReq.res.text()); const keys = await prd_cdm.parseLicense(Buffer.from(await licReq.res.text(), 'utf-8'));
return keys.map((k) => { return keys.map((k) => {
return { return {
kid: k.key_id, kid: k.kid,
key: k.key key: k.key
}; };
}); });

View file

@ -1,450 +0,0 @@
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;
}
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('key_type')
.uint16be('key_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_leaf_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 {
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: 3,
features: [4, 9, 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');
}
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);
}
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;
}
}
}
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;
}
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);
}
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);
}
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;
}
}

View file

@ -1,228 +0,0 @@
import { CertificateChain } from './bcert';
import ECCKey from './ecc_key';
import ElGamal, { Point } from './elgamal';
import XmlKey from './xml_key';
import { Key } from './key';
import { XmrUtil } 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><Features><Feature Name="AESCBC"></Feature></Features></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>4</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) {
const keyMaterial = XmrUtil.parse(Buffer.from(licenseElement, 'base64')).license.license.keyMaterial;
if (!keyMaterial || !keyMaterial.contentKey) throw new Error('No Content Keys retrieved');
keys.push(
new Key(
keyMaterial.contentKey.kid,
keyMaterial.contentKey.keyType,
keyMaterial.contentKey.ciphertype,
keyMaterial.contentKey.length,
this.decryptEcc256Key(keyMaterial.contentKey.value)
)
);
}
return keys;
} catch (error) {
throw new Error(`Unable to parse license, ${error}`);
}
}
}

View file

@ -1,124 +0,0 @@
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;
};
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 v3 = new Parser()
.string('signature', { length: 3, assert: DeviceStructs.magic })
.uint8('version')
.buffer('group_key', { length: 96 })
.buffer('encryption_key', { length: 96 })
.buffer('signing_key', { length: 96 })
.uint32('group_certificate_length')
.buffer('group_certificate', { length: 'group_certificate_length' });
}
export class Device {
static CURRENT_STRUCT = DeviceStructs.v3;
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
// export class Device {
// group_certificate: CertificateChain
// encryption_key: ECCKey
// signing_key: ECCKey
// security_level: number
// constructor(group_certificate: Buffer, group_key: Buffer) {
// this.group_certificate = CertificateChain.loads(group_certificate)
// this.encryption_key = ECCKey.generate()
// this.signing_key = ECCKey.generate()
// this.security_level = this.group_certificate.get_security_level()
// const new_certificate = Certificate.new_key_cert(
// randomBytes(16),
// this.group_certificate.get_security_level(),
// randomBytes(16),
// this.signing_key,
// this.encryption_key,
// ECCKey.loads(group_key),
// this.group_certificate
// )
// this.group_certificate.prepend(new_certificate)
// }
// }

View file

@ -1,91 +0,0 @@
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);
}
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');
return new ECCKey(keyPair);
}
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);
}
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();
}
}

View file

@ -1,43 +0,0 @@
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;
}
export default class ElGamal {
curve: EC;
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'));
}
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];
}
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;
}
}

View file

@ -1,63 +0,0 @@
enum KeyType {
Invalid = 0x0000,
AES128CTR = 0x0001,
RC4 = 0x0002,
AES128ECB = 0x0003,
Cocktail = 0x0004,
AESCBC = 0x0005,
UNKNOWN = 0xffff
}
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;
}
}
enum CipherType {
Invalid = 0x0000,
RSA128 = 0x0001,
ChainedLicense = 0x0002,
ECC256 = 0x0003,
ECCforScalableLicenses = 0x0004,
Scalable = 0x0005,
UNKNOWN = 0xffff
}
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;
}
}
export class Key {
key_id: string;
key_type: KeyType;
cipher_type: CipherType;
key_length: number;
key: string;
constructor(key_id: string, key_type: number, cipher_type: number, key_length: number, key: Buffer) {
this.key_id = key_id;
this.key_type = getKeyType(key_type);
this.cipher_type = getCipherType(cipher_type);
this.key_length = key_length;
this.key = key.toString('hex');
}
}

View file

@ -1,81 +0,0 @@
import { Parser } from 'binary-parser';
import { Buffer } from 'buffer';
import WRMHeader from './wrmheader';
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'
});
export function isPlayreadyPsshBox(data: Buffer): boolean {
if (data.length < 28) return false;
return data.subarray(12, 28).equals(SYSTEM_ID);
}
export class PSSH {
public wrm_headers: string[];
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}`);
}
}
try {
if (isPlayreadyPsshBox(data)) {
const header = this.extractPlayreadyHeader(data);
if (header) {
this.wrm_headers = [header];
} else {
throw new Error('Invalid PlayReady Header');
}
} else {
const repairedHeader = this.extractPlayreadyHeader(data);
if (repairedHeader) {
this.wrm_headers = [repairedHeader];
} else {
throw new Error('Could not extract PlayReady header from repaired data');
}
}
} catch (e) {
throw new Error(`Could not parse or repair PSSH data: ${e}`);
}
}
private extractPlayreadyHeader(data: Buffer): string | null {
try {
const utf16Data = data.toString('utf16le');
const wrmHeaderMatch = utf16Data.match(/<WRMHEADER[^>]*>.*<\/WRMHEADER>/i);
if (wrmHeaderMatch && wrmHeaderMatch.length > 0) {
return wrmHeaderMatch[0];
}
return null;
} catch (e) {
return null;
}
}
public get_wrm_headers(downgrade_to_v4: boolean = false): string[] {
return this.wrm_headers.map(downgrade_to_v4 ? this.downgradePSSH : (_) => _);
}
private downgradePSSH(wrm_header: string): string {
const header = new WRMHeader(wrm_header);
return header.to_v4_0_0_0();
}
}

View file

@ -1,88 +0,0 @@
import { XMLParser } from 'fast-xml-parser';
export class SignedKeyID {
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 ReturnStructure = [SignedKeyID[], string | null, string | null, string | null];
interface ParsedWRMHeader {
WRMHEADER: {
'@_version': string;
DATA?: any;
};
}
export default class WRMHeader {
private header: ParsedWRMHeader['WRMHEADER'];
version: Version;
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;
if (!parsed.WRMHEADER) throw new Error('Data is not a valid WRMHEADER');
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';
}
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}`);
}
}
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 signedKeyID: SignedKeyID | undefined = protectInfo.KIDS.KID
? new SignedKeyID(protectInfo.KIDS.KID['@_ALGID'] || '', protectInfo.KIDS.KID['@_VALUE'], protectInfo.KIDS.KID['@_CHECKSUM'])
: undefined;
return [signedKeyID ? [signedKeyID] : [], data.LA_URL || null, data.LUI_URL || null, data.DS_ID || null];
}
}

View file

@ -1,45 +0,0 @@
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;
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);
}
}
// Make it more undetectable (not working right now)
// import { randomBytes } from 'crypto'
// export default class XmlKey {
// public aesIv: Uint8Array
// public aesKey: Uint8Array
// public bytes: Uint8Array
// constructor() {
// this.aesIv = randomBytes(16)
// this.aesKey = randomBytes(16)
// this.bytes = new Uint8Array([...this.aesIv, ...this.aesKey])
// console.log('XML key (AES/CBC)')
// console.log('iv:', Buffer.from(this.aesIv).toString('hex'))
// console.log('key:', Buffer.from(this.aesKey).toString('hex'))
// console.log('bytes:', this.bytes)
// }
// }

View file

@ -1,228 +0,0 @@
import { Parser } from 'binary-parser';
type ParsedLicense = {
version: number;
rights: string;
length: number;
license: {
length: number;
signature?: {
length: number;
type: string;
value: string;
};
global_container?: {
revocationInfo?: {
version: number;
};
securityLevel?: {
level: number;
};
};
keyMaterial?: {
contentKey?: {
kid: string;
keyType: number;
ciphertype: number;
length: number;
value: Buffer;
};
encryptionKey?: {
curve: number;
length: number;
value: string;
};
auxKeys?: {
count: number;
value: {
location: number;
value: string;
};
};
};
};
};
export class XMRLicenseStructsV2 {
static CONTENT_KEY = new Parser().buffer('kid', { length: 16 }).uint16('keytype').uint16('ciphertype').uint16('length').buffer('value', {
length: 'length'
});
static ECC_KEY = new Parser().uint16('curve').uint16('length').buffer('value', {
length: 'length'
});
static FTLV = new Parser()
.uint16('flags')
.uint16('type')
.uint32('length')
.buffer('value', {
length: function () {
return (this as any).length - 8;
}
});
static AUXILIARY_LOCATIONS = new Parser().uint32('location').buffer('value', { length: 16 });
static AUXILIARY_KEY_OBJECT = new Parser().uint16('count').array('locations', {
length: 'count',
type: XMRLicenseStructsV2.AUXILIARY_LOCATIONS
});
static SIGNATURE = new Parser().uint16('type').uint16('siglength').buffer('signature', {
length: 'siglength'
});
static XMR = new Parser().string('constant', { length: 4, assert: 'XMR\x00' }).int32('version').buffer('rightsid', { length: 16 }).nest('data', {
type: XMRLicenseStructsV2.FTLV
});
}
enum XMRTYPE {
XMR_OUTER_CONTAINER = 0x0001,
XMR_GLOBAL_POLICY_CONTAINER = 0x0002,
XMR_PLAYBACK_POLICY_CONTAINER = 0x0004,
XMR_KEY_MATERIAL_CONTAINER = 0x0009,
XMR_RIGHTS_SETTINGS = 0x000d,
XMR_EMBEDDED_LICENSE_SETTINGS = 0x0033,
XMR_REVOCATION_INFORMATION_VERSION = 0x0032,
XMR_SECURITY_LEVEL = 0x0034,
XMR_CONTENT_KEY_OBJECT = 0x000a,
XMR_ECC_KEY_OBJECT = 0x002a,
XMR_SIGNATURE_OBJECT = 0x000b,
XMR_OUTPUT_LEVEL_RESTRICTION = 0x0005,
XMR_AUXILIARY_KEY_OBJECT = 0x0051,
XMR_EXPIRATION_RESTRICTION = 0x0012,
XMR_ISSUE_DATE = 0x0013,
XMR_EXPLICIT_ANALOG_CONTAINER = 0x0007
}
export class XmrUtil {
public data: Buffer;
public license: ParsedLicense;
constructor(data: Buffer, license: ParsedLicense) {
this.data = data;
this.license = license;
}
static parse(license: Buffer) {
const xmr = XMRLicenseStructsV2.XMR.parse(license);
const parsed_license: ParsedLicense = {
version: xmr.version,
rights: Buffer.from(xmr.rightsid).toString('hex'),
length: license.length,
license: {
length: xmr.data.length
}
};
const container = parsed_license.license;
const data = xmr.data;
let pos = 0;
while (pos < data.length - 16) {
const value = XMRLicenseStructsV2.FTLV.parse(data.value.slice(pos));
// XMR_SIGNATURE_OBJECT
if (value.type === XMRTYPE.XMR_SIGNATURE_OBJECT) {
const signature = XMRLicenseStructsV2.SIGNATURE.parse(value.value);
container.signature = {
length: value.length,
type: signature.type,
value: Buffer.from(signature.signature).toString('hex')
};
}
// XMRTYPE.XMR_GLOBAL_POLICY_CONTAINER
if (value.type === XMRTYPE.XMR_GLOBAL_POLICY_CONTAINER) {
container.global_container = {};
let index = 0;
while (index < value.length - 16) {
const data = XMRLicenseStructsV2.FTLV.parse(value.value.slice(index));
// XMRTYPE.XMR_REVOCATION_INFORMATION_VERSION
if (data.type === XMRTYPE.XMR_REVOCATION_INFORMATION_VERSION) {
container.global_container.revocationInfo = {
version: data.value.readUInt32BE(0)
};
}
// XMRTYPE.XMR_SECURITY_LEVEL
if (data.type === XMRTYPE.XMR_SECURITY_LEVEL) {
container.global_container.securityLevel = {
level: data.value.readUInt16BE(0)
};
}
index += data.length;
}
}
// XMRTYPE.XMR_KEY_MATERIAL_CONTAINER
if (value.type === XMRTYPE.XMR_KEY_MATERIAL_CONTAINER) {
container.keyMaterial = {};
let index = 0;
while (index < value.length - 16) {
const data = XMRLicenseStructsV2.FTLV.parse(value.value.slice(index));
// XMRTYPE.XMR_CONTENT_KEY_OBJECT
if (data.type === XMRTYPE.XMR_CONTENT_KEY_OBJECT) {
const content_key = XMRLicenseStructsV2.CONTENT_KEY.parse(data.value);
container.keyMaterial.contentKey = {
kid: XmrUtil.fixUUID(content_key.kid).toString('hex'),
keyType: content_key.keytype,
ciphertype: content_key.ciphertype,
length: content_key.length,
value: content_key.value
};
}
// XMRTYPE.XMR_ECC_KEY_OBJECT
if (data.type === XMRTYPE.XMR_ECC_KEY_OBJECT) {
const ecc_key = XMRLicenseStructsV2.ECC_KEY.parse(data.value);
container.keyMaterial.encryptionKey = {
curve: ecc_key.curve,
length: ecc_key.length,
value: Buffer.from(ecc_key.value).toString('hex')
};
}
// XMRTYPE.XMR_AUXILIARY_KEY_OBJECT
if (data.type === XMRTYPE.XMR_AUXILIARY_KEY_OBJECT) {
const aux_keys = XMRLicenseStructsV2.AUXILIARY_KEY_OBJECT.parse(data.value);
container.keyMaterial.auxKeys = {
count: aux_keys.count,
value: aux_keys.locations.map((a: any) => {
return {
location: a.location,
value: Buffer.from(a.value).toString('hex')
};
})
};
}
index += data.length;
}
}
pos += value.length;
}
return new XmrUtil(license, parsed_license);
}
static 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)
]);
}
}

View file

@ -41,24 +41,19 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@bufbuild/protobuf": "^2.10.1", "@bufbuild/protobuf": "^2.10.1",
"binary-parser": "^2.2.1",
"binary-parser-encoder": "^1.5.3",
"bn.js": "^5.2.2",
"commander": "^14.0.2", "commander": "^14.0.2",
"cors": "^2.8.5", "cors": "^2.8.5",
"elliptic": "^6.6.1",
"express": "^5.1.0", "express": "^5.1.0",
"fast-xml-parser": "^5.3.2",
"ffprobe": "^1.1.2", "ffprobe": "^1.1.2",
"fs-extra": "^11.3.2", "fs-extra": "^11.3.2",
"iso-639": "^0.2.2", "iso-639": "^0.2.2",
"leven": "^4.1.0", "leven": "^4.1.0",
"log4js": "^6.9.1", "log4js": "^6.9.1",
"long": "^5.3.2",
"lookpath": "^1.2.3", "lookpath": "^1.2.3",
"m3u8-parsed": "^2.0.0", "m3u8-parsed": "^2.0.0",
"mpd-parser": "^1.3.1", "mpd-parser": "^1.3.1",
"node-forge": "^1.3.1", "node-forge": "^1.3.1",
"node-playready": "^1.0.2",
"open": "^11.0.0", "open": "^11.0.0",
"protobufjs": "^7.5.4", "protobufjs": "^7.5.4",
"puppeteer-real-browser": "^1.4.4", "puppeteer-real-browser": "^1.4.4",
@ -70,9 +65,7 @@
"@bufbuild/buf": "^1.60.0", "@bufbuild/buf": "^1.60.0",
"@bufbuild/protoc-gen-es": "^2.10.1", "@bufbuild/protoc-gen-es": "^2.10.1",
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",
"@types/bn.js": "^5.2.0",
"@types/cors": "^2.8.19", "@types/cors": "^2.8.19",
"@types/elliptic": "^6.4.18",
"@types/express": "^5.0.5", "@types/express": "^5.0.5",
"@types/ffprobe": "^1.1.8", "@types/ffprobe": "^1.1.8",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",

View file

@ -11,30 +11,15 @@ importers:
'@bufbuild/protobuf': '@bufbuild/protobuf':
specifier: ^2.10.1 specifier: ^2.10.1
version: 2.10.1 version: 2.10.1
binary-parser:
specifier: ^2.2.1
version: 2.2.1
binary-parser-encoder:
specifier: ^1.5.3
version: 1.5.3
bn.js:
specifier: ^5.2.2
version: 5.2.2
commander: commander:
specifier: ^14.0.2 specifier: ^14.0.2
version: 14.0.2 version: 14.0.2
cors: cors:
specifier: ^2.8.5 specifier: ^2.8.5
version: 2.8.5 version: 2.8.5
elliptic:
specifier: ^6.6.1
version: 6.6.1
express: express:
specifier: ^5.1.0 specifier: ^5.1.0
version: 5.1.0 version: 5.1.0
fast-xml-parser:
specifier: ^5.3.2
version: 5.3.2
ffprobe: ffprobe:
specifier: ^1.1.2 specifier: ^1.1.2
version: 1.1.2 version: 1.1.2
@ -50,9 +35,6 @@ importers:
log4js: log4js:
specifier: ^6.9.1 specifier: ^6.9.1
version: 6.9.1 version: 6.9.1
long:
specifier: ^5.3.2
version: 5.3.2
lookpath: lookpath:
specifier: ^1.2.3 specifier: ^1.2.3
version: 1.2.3 version: 1.2.3
@ -65,6 +47,9 @@ importers:
node-forge: node-forge:
specifier: ^1.3.1 specifier: ^1.3.1
version: 1.3.1 version: 1.3.1
node-playready:
specifier: ^1.0.2
version: 1.0.2
open: open:
specifier: ^11.0.0 specifier: ^11.0.0
version: 11.0.0 version: 11.0.0
@ -93,15 +78,9 @@ importers:
'@eslint/js': '@eslint/js':
specifier: ^9.39.1 specifier: ^9.39.1
version: 9.39.1 version: 9.39.1
'@types/bn.js':
specifier: ^5.2.0
version: 5.2.0
'@types/cors': '@types/cors':
specifier: ^2.8.19 specifier: ^2.8.19
version: 2.8.19 version: 2.8.19
'@types/elliptic':
specifier: ^6.4.18
version: 6.4.18
'@types/express': '@types/express':
specifier: ^5.0.5 specifier: ^5.0.5
version: 5.0.5 version: 5.0.5
@ -543,9 +522,6 @@ packages:
'@types/bezier-js@4.1.3': '@types/bezier-js@4.1.3':
resolution: {integrity: sha512-FNVVCu5mx/rJCWBxLTcL7oOajmGtWtBTDjq6DSUWUI12GeePivrZZXz+UgE0D6VYsLEjvExRO03z4hVtu3pTEQ==} resolution: {integrity: sha512-FNVVCu5mx/rJCWBxLTcL7oOajmGtWtBTDjq6DSUWUI12GeePivrZZXz+UgE0D6VYsLEjvExRO03z4hVtu3pTEQ==}
'@types/bn.js@5.2.0':
resolution: {integrity: sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==}
'@types/body-parser@1.19.6': '@types/body-parser@1.19.6':
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
@ -558,9 +534,6 @@ packages:
'@types/debug@4.1.12': '@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
'@types/elliptic@6.4.18':
resolution: {integrity: sha512-UseG6H5vjRiNpQvrhy4VF/JXdA3V/Fp5amvveaL+fs28BZ6xIKJBPnUPRlEaZpysD9MbpfaLi8lbl7PGUAkpWw==}
'@types/estree@1.0.8': '@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
@ -725,6 +698,10 @@ packages:
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
hasBin: true hasBin: true
aes-cmac@4.0.0:
resolution: {integrity: sha512-HhYx38lyXTYYVR7WdgN9LRdls63t3RXAUDUUpKrEVgyOTiqVanc1FBxh7Mncuu7Q1VyNU+HRSZHdAYefBL7Hcg==}
engines: {node: '>=20.19.2'}
agent-base@6.0.2: agent-base@6.0.2:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'} engines: {node: '>= 6.0.0'}
@ -813,14 +790,6 @@ packages:
bezier-js@6.1.4: bezier-js@6.1.4:
resolution: {integrity: sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==} resolution: {integrity: sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==}
binary-parser-encoder@1.5.3:
resolution: {integrity: sha512-yu3tdLBYqPIwGRaXyswLoLrhaffkuZkNuXveq/jYoyBHQbFMjamHCWPFOmI2Qz+Go0Rh6wE9f6tt0EAvsgDD0g==}
engines: {node: '>=8.9.0'}
binary-parser@2.2.1:
resolution: {integrity: sha512-5ATpz/uPDgq5GgEDxTB4ouXCde7q2lqAQlSdBRQVl/AJnxmQmhIfyxJx+0MGu//D5rHQifkfGbWWlaysG0o9NA==}
engines: {node: '>=12'}
bl@4.1.0: bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
@ -830,9 +799,6 @@ packages:
bn.js@4.12.2: bn.js@4.12.2:
resolution: {integrity: sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==} resolution: {integrity: sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==}
bn.js@5.2.2:
resolution: {integrity: sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==}
body-parser@2.2.0: body-parser@2.2.0:
resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -1613,6 +1579,9 @@ packages:
node-int64@0.4.0: node-int64@0.4.0:
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
node-playready@1.0.2:
resolution: {integrity: sha512-FJQsSXrqgv3cqDW80WW6bWy43rG745JG47XjrYa0BiqNrle9DJloARi0PKYOkK5jstIVPttUIiTWQuY8su2XqQ==}
object-assign@4.1.1: object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -2476,10 +2445,6 @@ snapshots:
'@types/bezier-js@4.1.3': {} '@types/bezier-js@4.1.3': {}
'@types/bn.js@5.2.0':
dependencies:
'@types/node': 24.10.1
'@types/body-parser@1.19.6': '@types/body-parser@1.19.6':
dependencies: dependencies:
'@types/connect': 3.4.38 '@types/connect': 3.4.38
@ -2497,10 +2462,6 @@ snapshots:
dependencies: dependencies:
'@types/ms': 2.1.0 '@types/ms': 2.1.0
'@types/elliptic@6.4.18':
dependencies:
'@types/bn.js': 5.2.0
'@types/estree@1.0.8': {} '@types/estree@1.0.8': {}
'@types/express-serve-static-core@5.1.0': '@types/express-serve-static-core@5.1.0':
@ -2744,6 +2705,8 @@ snapshots:
acorn@8.15.0: {} acorn@8.15.0: {}
aes-cmac@4.0.0: {}
agent-base@6.0.2: agent-base@6.0.2:
dependencies: dependencies:
debug: 4.4.3 debug: 4.4.3
@ -2820,12 +2783,6 @@ snapshots:
bezier-js@6.1.4: {} bezier-js@6.1.4: {}
binary-parser-encoder@1.5.3:
dependencies:
smart-buffer: 4.2.0
binary-parser@2.2.1: {}
bl@4.1.0: bl@4.1.0:
dependencies: dependencies:
buffer: 5.7.1 buffer: 5.7.1
@ -2836,8 +2793,6 @@ snapshots:
bn.js@4.12.2: {} bn.js@4.12.2: {}
bn.js@5.2.2: {}
body-parser@2.2.0: body-parser@2.2.0:
dependencies: dependencies:
bytes: 3.1.2 bytes: 3.1.2
@ -3659,6 +3614,12 @@ snapshots:
node-int64@0.4.0: {} node-int64@0.4.0: {}
node-playready@1.0.2:
dependencies:
aes-cmac: 4.0.0
elliptic: 6.6.1
fast-xml-parser: 5.3.2
object-assign@4.1.1: {} object-assign@4.1.1: {}
object-inspect@1.13.4: {} object-inspect@1.13.4: {}