mirror of
https://github.com/anidl/multi-downloader-nx.git
synced 2026-01-11 20:10:20 +00:00
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
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:
parent
51cb97e18f
commit
d0cc551b8d
14 changed files with 47 additions and 1541 deletions
|
|
@ -111,5 +111,4 @@ In order to decrypt DRM content, you will need to have a dumped CDM, after that
|
|||
### Instructions (Playready)
|
||||
|
||||
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!
|
||||
**IMPORTANT**: The Playready CDM (SL2000/SL3000) needs to be provisioned as a **V3 Device** by pyplayready (https://github.com/ready-dl/pyplayready).
|
||||
Place the CDM files (bgroupcert.dat and zgpriv.dat) in the `./playready/` directory and you're all set!
|
||||
|
|
|
|||
|
|
@ -2,42 +2,40 @@ import fs from 'fs';
|
|||
import { console } from './log';
|
||||
import { workingDir } from './module.cfg-loader';
|
||||
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 * as reqModule from './module.fetch';
|
||||
import Playready from 'node-playready';
|
||||
|
||||
const req = new reqModule.Req();
|
||||
|
||||
//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;
|
||||
prd_cdm: Playready | 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'));
|
||||
const bgroup_file_found = files_prd.find((f) => f === 'bgroupcert.dat');
|
||||
const zgpriv_file_found = files_prd.find((f) => f === 'zgpriv.dat');
|
||||
try {
|
||||
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);
|
||||
}
|
||||
if (bgroup_file_found && zgpriv_file_found) {
|
||||
const file_bgroup = path.join(workingDir, 'playready', bgroup_file_found);
|
||||
const file_zgpriv = path.join(workingDir, 'playready', zgpriv_file_found);
|
||||
|
||||
const bgroup_stats = fs.statSync(file_bgroup);
|
||||
const zgpriv_stats = fs.statSync(file_zgpriv);
|
||||
// Zgpriv is always 32 bytes long
|
||||
if (bgroup_stats.isFile() && zgpriv_stats.isFile() && zgpriv_stats.size === 32) {
|
||||
const bgroup = fs.readFileSync(file_bgroup);
|
||||
const zgpriv = fs.readFileSync(file_zgpriv);
|
||||
// Init Playready Client
|
||||
prd_cdm = Playready.init(bgroup, zgpriv);
|
||||
}
|
||||
}
|
||||
} 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.');
|
||||
prd = Buffer.from([]);
|
||||
console.error('Error loading Playready CDM. For more informations read the readme.');
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
const files_wvd = fs.readdirSync(path.join(workingDir, 'widevine'));
|
||||
|
|
@ -72,7 +70,7 @@ try {
|
|||
if (privateKey.length !== 0 && identifierBlob.length !== 0) {
|
||||
cdm = 'widevine';
|
||||
canDecrypt = true;
|
||||
} else if (prd.length !== 0) {
|
||||
} else if (prd_cdm) {
|
||||
cdm = 'playready';
|
||||
canDecrypt = true;
|
||||
} else if (privateKey.length === 0 && identifierBlob.length !== 0) {
|
||||
|
|
@ -81,8 +79,6 @@ try {
|
|||
} else if (identifierBlob.length === 0 && privateKey.length !== 0) {
|
||||
console.warn('Identifier blob missing');
|
||||
canDecrypt = false;
|
||||
} else if (prd.length == 0) {
|
||||
canDecrypt = false;
|
||||
} else {
|
||||
canDecrypt = false;
|
||||
}
|
||||
|
|
@ -93,10 +89,10 @@ try {
|
|||
|
||||
export async function getKeysWVD(pssh: string | undefined, licenseServer: string, authData: Record<string, string>): Promise<KeyContainer[]> {
|
||||
if (!pssh || !canDecrypt) return [];
|
||||
//pssh found in the mpd manifest
|
||||
// pssh found in the mpd manifest
|
||||
const psshBuffer = Buffer.from(pssh, 'base64');
|
||||
|
||||
//Create a new widevine session
|
||||
// Create a new widevine session
|
||||
const session = new Session({ privateKey, identifierBlob }, psshBuffer);
|
||||
|
||||
// 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[]> {
|
||||
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]);
|
||||
// Generate Playready challenge
|
||||
const session = await prd_cdm.generateChallenge(pssh);
|
||||
|
||||
//Generate license
|
||||
// Fetch license
|
||||
const licReq = await req.getData(licenseServer, {
|
||||
method: 'POST',
|
||||
body: session,
|
||||
|
|
@ -140,13 +135,12 @@ export async function getKeysPRD(pssh: string | undefined, licenseServer: string
|
|||
return [];
|
||||
}
|
||||
|
||||
//Parse License and return keys
|
||||
// Parse License and return keys
|
||||
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 {
|
||||
kid: k.key_id,
|
||||
kid: k.kid,
|
||||
key: k.key
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
// }
|
||||
// }
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
// }
|
||||
// }
|
||||
|
|
@ -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)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -41,24 +41,19 @@
|
|||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"cors": "^2.8.5",
|
||||
"elliptic": "^6.6.1",
|
||||
"express": "^5.1.0",
|
||||
"fast-xml-parser": "^5.3.2",
|
||||
"ffprobe": "^1.1.2",
|
||||
"fs-extra": "^11.3.2",
|
||||
"iso-639": "^0.2.2",
|
||||
"leven": "^4.1.0",
|
||||
"log4js": "^6.9.1",
|
||||
"long": "^5.3.2",
|
||||
"lookpath": "^1.2.3",
|
||||
"m3u8-parsed": "^2.0.0",
|
||||
"mpd-parser": "^1.3.1",
|
||||
"node-forge": "^1.3.1",
|
||||
"node-playready": "^1.0.2",
|
||||
"open": "^11.0.0",
|
||||
"protobufjs": "^7.5.4",
|
||||
"puppeteer-real-browser": "^1.4.4",
|
||||
|
|
@ -70,9 +65,7 @@
|
|||
"@bufbuild/buf": "^1.60.0",
|
||||
"@bufbuild/protoc-gen-es": "^2.10.1",
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/bn.js": "^5.2.0",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/elliptic": "^6.4.18",
|
||||
"@types/express": "^5.0.5",
|
||||
"@types/ffprobe": "^1.1.8",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
|
|
|
|||
|
|
@ -11,30 +11,15 @@ importers:
|
|||
'@bufbuild/protobuf':
|
||||
specifier: ^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:
|
||||
specifier: ^14.0.2
|
||||
version: 14.0.2
|
||||
cors:
|
||||
specifier: ^2.8.5
|
||||
version: 2.8.5
|
||||
elliptic:
|
||||
specifier: ^6.6.1
|
||||
version: 6.6.1
|
||||
express:
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0
|
||||
fast-xml-parser:
|
||||
specifier: ^5.3.2
|
||||
version: 5.3.2
|
||||
ffprobe:
|
||||
specifier: ^1.1.2
|
||||
version: 1.1.2
|
||||
|
|
@ -50,9 +35,6 @@ importers:
|
|||
log4js:
|
||||
specifier: ^6.9.1
|
||||
version: 6.9.1
|
||||
long:
|
||||
specifier: ^5.3.2
|
||||
version: 5.3.2
|
||||
lookpath:
|
||||
specifier: ^1.2.3
|
||||
version: 1.2.3
|
||||
|
|
@ -65,6 +47,9 @@ importers:
|
|||
node-forge:
|
||||
specifier: ^1.3.1
|
||||
version: 1.3.1
|
||||
node-playready:
|
||||
specifier: ^1.0.2
|
||||
version: 1.0.2
|
||||
open:
|
||||
specifier: ^11.0.0
|
||||
version: 11.0.0
|
||||
|
|
@ -93,15 +78,9 @@ importers:
|
|||
'@eslint/js':
|
||||
specifier: ^9.39.1
|
||||
version: 9.39.1
|
||||
'@types/bn.js':
|
||||
specifier: ^5.2.0
|
||||
version: 5.2.0
|
||||
'@types/cors':
|
||||
specifier: ^2.8.19
|
||||
version: 2.8.19
|
||||
'@types/elliptic':
|
||||
specifier: ^6.4.18
|
||||
version: 6.4.18
|
||||
'@types/express':
|
||||
specifier: ^5.0.5
|
||||
version: 5.0.5
|
||||
|
|
@ -543,9 +522,6 @@ packages:
|
|||
'@types/bezier-js@4.1.3':
|
||||
resolution: {integrity: sha512-FNVVCu5mx/rJCWBxLTcL7oOajmGtWtBTDjq6DSUWUI12GeePivrZZXz+UgE0D6VYsLEjvExRO03z4hVtu3pTEQ==}
|
||||
|
||||
'@types/bn.js@5.2.0':
|
||||
resolution: {integrity: sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==}
|
||||
|
||||
'@types/body-parser@1.19.6':
|
||||
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
||||
|
||||
|
|
@ -558,9 +534,6 @@ packages:
|
|||
'@types/debug@4.1.12':
|
||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||
|
||||
'@types/elliptic@6.4.18':
|
||||
resolution: {integrity: sha512-UseG6H5vjRiNpQvrhy4VF/JXdA3V/Fp5amvveaL+fs28BZ6xIKJBPnUPRlEaZpysD9MbpfaLi8lbl7PGUAkpWw==}
|
||||
|
||||
'@types/estree@1.0.8':
|
||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||
|
||||
|
|
@ -725,6 +698,10 @@ packages:
|
|||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
aes-cmac@4.0.0:
|
||||
resolution: {integrity: sha512-HhYx38lyXTYYVR7WdgN9LRdls63t3RXAUDUUpKrEVgyOTiqVanc1FBxh7Mncuu7Q1VyNU+HRSZHdAYefBL7Hcg==}
|
||||
engines: {node: '>=20.19.2'}
|
||||
|
||||
agent-base@6.0.2:
|
||||
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
|
||||
engines: {node: '>= 6.0.0'}
|
||||
|
|
@ -813,14 +790,6 @@ packages:
|
|||
bezier-js@6.1.4:
|
||||
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:
|
||||
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||
|
||||
|
|
@ -830,9 +799,6 @@ packages:
|
|||
bn.js@4.12.2:
|
||||
resolution: {integrity: sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==}
|
||||
|
||||
bn.js@5.2.2:
|
||||
resolution: {integrity: sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==}
|
||||
|
||||
body-parser@2.2.0:
|
||||
resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -1613,6 +1579,9 @@ packages:
|
|||
node-int64@0.4.0:
|
||||
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
|
||||
|
||||
node-playready@1.0.2:
|
||||
resolution: {integrity: sha512-FJQsSXrqgv3cqDW80WW6bWy43rG745JG47XjrYa0BiqNrle9DJloARi0PKYOkK5jstIVPttUIiTWQuY8su2XqQ==}
|
||||
|
||||
object-assign@4.1.1:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -2476,10 +2445,6 @@ snapshots:
|
|||
|
||||
'@types/bezier-js@4.1.3': {}
|
||||
|
||||
'@types/bn.js@5.2.0':
|
||||
dependencies:
|
||||
'@types/node': 24.10.1
|
||||
|
||||
'@types/body-parser@1.19.6':
|
||||
dependencies:
|
||||
'@types/connect': 3.4.38
|
||||
|
|
@ -2497,10 +2462,6 @@ snapshots:
|
|||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
|
||||
'@types/elliptic@6.4.18':
|
||||
dependencies:
|
||||
'@types/bn.js': 5.2.0
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
'@types/express-serve-static-core@5.1.0':
|
||||
|
|
@ -2744,6 +2705,8 @@ snapshots:
|
|||
|
||||
acorn@8.15.0: {}
|
||||
|
||||
aes-cmac@4.0.0: {}
|
||||
|
||||
agent-base@6.0.2:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
|
|
@ -2820,12 +2783,6 @@ snapshots:
|
|||
|
||||
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:
|
||||
dependencies:
|
||||
buffer: 5.7.1
|
||||
|
|
@ -2836,8 +2793,6 @@ snapshots:
|
|||
|
||||
bn.js@4.12.2: {}
|
||||
|
||||
bn.js@5.2.2: {}
|
||||
|
||||
body-parser@2.2.0:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
|
|
@ -3659,6 +3614,12 @@ snapshots:
|
|||
|
||||
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-inspect@1.13.4: {}
|
||||
|
|
|
|||
Loading…
Reference in a new issue