mirror of
https://github.com/anidl/multi-downloader-nx.git
synced 2026-04-21 00:12:05 +00:00
added prd v3 device support
This commit is contained in:
parent
1f12a75009
commit
37b8bbad1f
8 changed files with 305 additions and 328 deletions
|
|
@ -111,8 +111,8 @@ export class BCertStructs {
|
|||
});
|
||||
|
||||
static DrmBCertExtDataSignKeyInfo = new Parser()
|
||||
.uint16be('type')
|
||||
.uint16be('length')
|
||||
.uint16be('key_type')
|
||||
.uint16be('key_length')
|
||||
.uint32be('flags')
|
||||
.buffer('key', {
|
||||
length: function () {
|
||||
|
|
@ -219,7 +219,7 @@ export class Certificate {
|
|||
}
|
||||
|
||||
// UNSTABLE
|
||||
static new_key_cert(
|
||||
static new_leaf_cert(
|
||||
cert_id: Buffer,
|
||||
security_level: number,
|
||||
client_id: Buffer,
|
||||
|
|
@ -232,13 +232,6 @@ export class Certificate {
|
|||
max_header: number = 15360,
|
||||
max_chain_depth: number = 2
|
||||
): Certificate {
|
||||
if (!cert_id) {
|
||||
throw new Error('Certificate ID is required');
|
||||
}
|
||||
if (!client_id) {
|
||||
throw new Error('Client ID is required');
|
||||
}
|
||||
|
||||
const basic_info = {
|
||||
cert_id: cert_id,
|
||||
security_level: security_level,
|
||||
|
|
@ -269,8 +262,8 @@ export class Certificate {
|
|||
};
|
||||
|
||||
const feature = {
|
||||
feature_count: 1,
|
||||
features: [4, 13],
|
||||
feature_count: 3,
|
||||
features: [4, 9, 13],
|
||||
};
|
||||
const feature_attribute = {
|
||||
flags: 1,
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import { CertificateChain } from './bcert';
|
|||
import ECCKey from './ecc_key';
|
||||
import ElGamal, { Point } from './elgamal';
|
||||
import XmlKey from './xml_key';
|
||||
import { CipherType, getCipherType, Key } from './key';
|
||||
import { XMRLicense } from './xmrlicense';
|
||||
import { Key } from './key';
|
||||
import { XmrUtil } from './xmrlicense';
|
||||
import crypto from 'crypto';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { createHash } from 'crypto';
|
||||
|
|
@ -79,7 +79,7 @@ export default class Cdm {
|
|||
|
||||
private getCipherData(): Buffer {
|
||||
const b64_chain = this.certificate_chain.dumps().toString('base64');
|
||||
const body = `<Data><CertificateChains><CertificateChain>${b64_chain}</CertificateChain></CertificateChains></Data>`;
|
||||
const body = `<Data><CertificateChains><CertificateChain>${b64_chain}</CertificateChain></CertificateChains><Features><Feature Name="AESCBC"></Feature></Features></Data>`;
|
||||
|
||||
const cipher = crypto.createCipheriv(
|
||||
'aes-128-cbc',
|
||||
|
|
@ -105,7 +105,7 @@ export default class Cdm {
|
|||
|
||||
return (
|
||||
'<LA xmlns="http://schemas.microsoft.com/DRM/2007/03/protocols" Id="SignedData" xml:space="preserve">' +
|
||||
`<Version>${this.la_version}</Version>` +
|
||||
'<Version>4</Version>' +
|
||||
`<ContentHeader>${content_header}</ContentHeader>` +
|
||||
'<CLIENTINFO>' +
|
||||
`<CLIENTVERSION>${this.client_version}</CLIENTVERSION>` +
|
||||
|
|
@ -209,7 +209,7 @@ export default class Cdm {
|
|||
return main_body;
|
||||
}
|
||||
|
||||
private _decryptEcc256Key(encrypted_key: Buffer): Buffer {
|
||||
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')
|
||||
|
|
@ -253,19 +253,21 @@ export default class Cdm {
|
|||
const keys = [];
|
||||
|
||||
for (const licenseElement of licenses) {
|
||||
for (const key of XMRLicense.loads(licenseElement).get_content_keys()) {
|
||||
if (getCipherType(key.cipher_type) === CipherType.ECC256) {
|
||||
keys.push(
|
||||
new Key(
|
||||
this.fixUUID(key.key_id),
|
||||
key.key_type,
|
||||
key.cipher_type,
|
||||
key.key_length,
|
||||
this._decryptEcc256Key(key.encrypted_key)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
|
@ -273,13 +275,4 @@ export default class Cdm {
|
|||
throw new Error(`Unable to parse license, ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
fixUUID(data: Buffer): Buffer {
|
||||
return Buffer.concat([
|
||||
Buffer.from(data.subarray(0, 4).reverse()),
|
||||
Buffer.from(data.subarray(4, 6).reverse()),
|
||||
Buffer.from(data.subarray(6, 8).reverse()),
|
||||
data.subarray(8, 16),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,10 +30,19 @@ class DeviceStructs {
|
|||
.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.v2;
|
||||
static CURRENT_STRUCT = DeviceStructs.v3;
|
||||
|
||||
group_certificate: CertificateChain;
|
||||
encryption_key: ECCKey;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export enum KeyType {
|
||||
enum KeyType {
|
||||
Invalid = 0x0000,
|
||||
AES128CTR = 0x0001,
|
||||
RC4 = 0x0002,
|
||||
|
|
@ -8,7 +8,7 @@ export enum KeyType {
|
|||
UNKNOWN = 0xffff,
|
||||
}
|
||||
|
||||
export function getKeyType(value: number): KeyType {
|
||||
function getKeyType(value: number): KeyType {
|
||||
switch (value) {
|
||||
case KeyType.Invalid:
|
||||
case KeyType.AES128CTR:
|
||||
|
|
@ -22,7 +22,7 @@ export function getKeyType(value: number): KeyType {
|
|||
}
|
||||
}
|
||||
|
||||
export enum CipherType {
|
||||
enum CipherType {
|
||||
Invalid = 0x0000,
|
||||
RSA128 = 0x0001,
|
||||
ChainedLicense = 0x0002,
|
||||
|
|
@ -32,7 +32,7 @@ export enum CipherType {
|
|||
UNKNOWN = 0xffff,
|
||||
}
|
||||
|
||||
export function getCipherType(value: number): CipherType {
|
||||
function getCipherType(value: number): CipherType {
|
||||
switch (value) {
|
||||
case CipherType.Invalid:
|
||||
case CipherType.RSA128:
|
||||
|
|
@ -54,17 +54,16 @@ export class Key {
|
|||
key: string;
|
||||
|
||||
constructor(
|
||||
key_id: Buffer | string,
|
||||
key_id: string,
|
||||
key_type: number,
|
||||
cipher_type: number,
|
||||
key_length: number,
|
||||
key: Buffer | string
|
||||
key: Buffer
|
||||
) {
|
||||
this.key_id = Buffer.isBuffer(key_id) ? key_id.toString('hex') : key_id;
|
||||
|
||||
this.key_id = key_id;
|
||||
this.key_type = getKeyType(key_type);
|
||||
this.cipher_type = getCipherType(cipher_type);
|
||||
this.key_length = key_length;
|
||||
this.key = Buffer.isBuffer(key) ? key.toString('hex') : key;
|
||||
this.key = key.toString('hex');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,31 +5,37 @@ 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' });
|
||||
.buffer('data', {
|
||||
length: 'data_length',
|
||||
});
|
||||
|
||||
const PlayreadyObject = new Parser()
|
||||
.endianess('little')
|
||||
.useContextVars()
|
||||
.uint16('type')
|
||||
.uint16('length')
|
||||
.choice('data', {
|
||||
tag: 'type',
|
||||
choices: {
|
||||
1: new Parser().string('data', {
|
||||
length: 'length',
|
||||
length: function () {
|
||||
return (this as any).$parent.length;
|
||||
},
|
||||
encoding: 'utf16le',
|
||||
}),
|
||||
},
|
||||
defaultChoice: new Parser().buffer('data', { length: 'length' }),
|
||||
defaultChoice: new Parser().buffer('data', {
|
||||
length: function () {
|
||||
return (this as any).$parent.length;
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const PlayreadyHeader = new Parser()
|
||||
.endianess('little')
|
||||
.uint32('length')
|
||||
.uint16('record_count')
|
||||
.array('records', {
|
||||
|
|
@ -38,7 +44,7 @@ const PlayreadyHeader = new Parser()
|
|||
});
|
||||
|
||||
function isPlayreadyPsshBox(data: Buffer): boolean {
|
||||
if (data.length < 28) return false; // Ensure enough length
|
||||
if (data.length < 28) return false;
|
||||
return data.subarray(12, 28).equals(SYSTEM_ID);
|
||||
}
|
||||
|
||||
|
|
@ -111,10 +117,12 @@ export class PSSH {
|
|||
|
||||
// Header downgrade
|
||||
public get_wrm_headers(downgrade_to_v4: boolean = false): string[] {
|
||||
return this.wrm_headers.map(downgrade_to_v4 ? this._downgrade : (_) => _);
|
||||
return this.wrm_headers.map(
|
||||
downgrade_to_v4 ? this.downgradePSSH : (_) => _
|
||||
);
|
||||
}
|
||||
|
||||
private _downgrade(wrm_header: string): string {
|
||||
private downgradePSSH(wrm_header: string): string {
|
||||
const header = new WRMHeader(wrm_header);
|
||||
return header.to_v4_0_0_0();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,10 +51,6 @@ export default class WRMHeader {
|
|||
return 'UNKNOWN';
|
||||
}
|
||||
|
||||
private static ensureList(element: any): any[] {
|
||||
return Array.isArray(element) ? element : [element];
|
||||
}
|
||||
|
||||
to_v4_0_0_0(): string {
|
||||
const [key_ids, la_url, lui_url, ds_id] = this.readAttributes();
|
||||
if (key_ids.length === 0) throw new Error('No Key IDs available');
|
||||
|
|
@ -98,18 +94,16 @@ export default class WRMHeader {
|
|||
|
||||
private static read_vX(data: any): ReturnStructure {
|
||||
const protectInfo = data.PROTECTINFO;
|
||||
const signedKeyIDs: SignedKeyID[] = protectInfo?.KID
|
||||
? WRMHeader.ensureList(protectInfo.KID).map(
|
||||
(kid: any) =>
|
||||
new SignedKeyID(
|
||||
kid['@_ALGID'] || '',
|
||||
kid['@_VALUE'],
|
||||
kid['@_CHECKSUM']
|
||||
)
|
||||
|
||||
const signedKeyID: SignedKeyID | undefined = protectInfo.KIDS.KID
|
||||
? new SignedKeyID(
|
||||
protectInfo.KIDS.KID['@_ALGID'] || '',
|
||||
protectInfo.KIDS.KID['@_VALUE'],
|
||||
protectInfo.KIDS.KID['@_CHECKSUM']
|
||||
)
|
||||
: [];
|
||||
: undefined;
|
||||
return [
|
||||
signedKeyIDs,
|
||||
signedKeyID ? [signedKeyID] : [],
|
||||
data.LA_URL || null,
|
||||
data.LUI_URL || null,
|
||||
data.DS_ID || null,
|
||||
|
|
|
|||
|
|
@ -26,20 +26,20 @@ export default class XmlKey {
|
|||
}
|
||||
|
||||
// Make it more undetectable (not working right now)
|
||||
// import { ec as EC } from 'elliptic'
|
||||
// import { Point } from './elgamal.js'
|
||||
// import { randomBytes } from 'crypto';
|
||||
|
||||
// import { randomBytes } from 'crypto'
|
||||
// export default class XmlKey {
|
||||
// public aesIv: Uint8Array;
|
||||
// public aesKey: Uint8Array;
|
||||
// public aesIv: Uint8Array
|
||||
// public aesKey: Uint8Array
|
||||
// public bytes: Uint8Array
|
||||
|
||||
// constructor() {
|
||||
// this.aesIv = randomBytes(16);
|
||||
// this.aesKey = randomBytes(16);
|
||||
// }
|
||||
// this.aesIv = randomBytes(16)
|
||||
// this.aesKey = randomBytes(16)
|
||||
// this.bytes = new Uint8Array([...this.aesIv, ...this.aesKey])
|
||||
|
||||
// getPoint(curve: EC): Point {
|
||||
// return curve.curve.point(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,270 +1,251 @@
|
|||
import { Parser } from 'binary-parser';
|
||||
import * as fs from 'fs';
|
||||
|
||||
export class XMRLicenseStructs {
|
||||
static PlayEnablerType = new Parser().buffer('player_enabler_type', {
|
||||
length: 16,
|
||||
});
|
||||
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;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
static DomainRestrictionObject = new Parser()
|
||||
.buffer('account_id', { length: 16 })
|
||||
.uint32('revision');
|
||||
export class XMRLicenseStructsV2 {
|
||||
static CONTENT_KEY = new Parser()
|
||||
.buffer('kid', { length: 16 })
|
||||
.uint16('keytype')
|
||||
.uint16('ciphertype')
|
||||
.uint16('length')
|
||||
.buffer('value', {
|
||||
length: 'length',
|
||||
});
|
||||
|
||||
static IssueDateObject = new Parser().uint32('issue_date');
|
||||
static ECC_KEY = new Parser()
|
||||
.uint16('curve')
|
||||
.uint16('length')
|
||||
.buffer('value', {
|
||||
length: 'length',
|
||||
});
|
||||
|
||||
static RevInfoVersionObject = new Parser().uint32('sequence');
|
||||
|
||||
static SecurityLevelObject = new Parser().uint16('minimum_security_level');
|
||||
|
||||
static EmbeddedLicenseSettingsObject = new Parser().uint16('indicator');
|
||||
|
||||
static ECCKeyObject = new Parser()
|
||||
.uint16('curve_type')
|
||||
.uint16('key_length')
|
||||
.buffer('key', {
|
||||
static FTLV = new Parser()
|
||||
.uint16('flags')
|
||||
.uint16('type')
|
||||
.uint32('length')
|
||||
.buffer('value', {
|
||||
length: function () {
|
||||
return (this as any).key_length;
|
||||
return (this as any).length - 8;
|
||||
},
|
||||
});
|
||||
|
||||
static SignatureObject = new Parser()
|
||||
.uint16('signature_type')
|
||||
.uint16('signature_data_length')
|
||||
.buffer('signature_data', {
|
||||
length: function () {
|
||||
return (this as any).signature_data_length;
|
||||
},
|
||||
});
|
||||
|
||||
static ContentKeyObject = new Parser()
|
||||
.buffer('key_id', { length: 16 })
|
||||
.uint16('key_type')
|
||||
.uint16('cipher_type')
|
||||
.uint16('key_length')
|
||||
.buffer('encrypted_key', {
|
||||
length: function () {
|
||||
return (this as any).key_length;
|
||||
},
|
||||
});
|
||||
|
||||
static RightsSettingsObject = new Parser().uint16('rights');
|
||||
|
||||
static OutputProtectionLevelRestrictionObject = new Parser()
|
||||
.uint16('minimum_compressed_digital_video_opl')
|
||||
.uint16('minimum_uncompressed_digital_video_opl')
|
||||
.uint16('minimum_analog_video_opl')
|
||||
.uint16('minimum_digital_compressed_audio_opl')
|
||||
.uint16('minimum_digital_uncompressed_audio_opl');
|
||||
|
||||
static ExpirationRestrictionObject = new Parser()
|
||||
.uint32('begin_date')
|
||||
.uint32('end_date');
|
||||
|
||||
static RemovalDateObject = new Parser().uint32('removal_date');
|
||||
|
||||
static UplinkKIDObject = new Parser()
|
||||
.buffer('uplink_kid', { length: 16 })
|
||||
.uint16('chained_checksum_type')
|
||||
.uint16('chained_checksum_length')
|
||||
.buffer('chained_checksum', {
|
||||
length: function () {
|
||||
return (this as any).chained_checksum_length;
|
||||
},
|
||||
});
|
||||
|
||||
static AnalogVideoOutputConfigurationRestriction = new Parser()
|
||||
.buffer('video_output_protection_id', { length: 16 })
|
||||
.buffer('binary_configuration_data', {
|
||||
length: function () {
|
||||
return (this as any).$parent.length - 16;
|
||||
},
|
||||
});
|
||||
|
||||
static DigitalVideoOutputRestrictionObject = new Parser()
|
||||
.buffer('video_output_protection_id', { length: 16 })
|
||||
.buffer('binary_configuration_data', {
|
||||
length: function () {
|
||||
return (this as any).$parent.length - 16;
|
||||
},
|
||||
});
|
||||
|
||||
static DigitalAudioOutputRestrictionObject = new Parser()
|
||||
.buffer('audio_output_protection_id', { length: 16 })
|
||||
.buffer('binary_configuration_data', {
|
||||
length: function () {
|
||||
return (this as any).$parent.length - 16;
|
||||
},
|
||||
});
|
||||
|
||||
static PolicyMetadataObject = new Parser()
|
||||
.buffer('metadata_type', { length: 16 })
|
||||
.buffer('policy_data', {
|
||||
length: function () {
|
||||
return (this as any).$parent.length - 16;
|
||||
},
|
||||
});
|
||||
|
||||
static SecureStopRestrictionObject = new Parser().buffer('metering_id', {
|
||||
length: 16,
|
||||
});
|
||||
|
||||
static MeteringRestrictionObject = new Parser().buffer('metering_id', {
|
||||
length: 16,
|
||||
});
|
||||
|
||||
static ExpirationAfterFirstPlayRestrictionObject = new Parser().uint32(
|
||||
'seconds'
|
||||
);
|
||||
|
||||
static GracePeriodObject = new Parser().uint32('grace_period');
|
||||
|
||||
static SourceIdObject = new Parser().uint32('source_id');
|
||||
|
||||
static AuxiliaryKey = new Parser()
|
||||
static AUXILIARY_LOCATIONS = new Parser()
|
||||
.uint32('location')
|
||||
.buffer('key', { length: 16 });
|
||||
.buffer('value', { length: 16 });
|
||||
|
||||
static AuxiliaryKeysObject = new Parser()
|
||||
static AUXILIARY_KEY_OBJECT = new Parser()
|
||||
.uint16('count')
|
||||
.array('auxiliary_keys', {
|
||||
.array('locations', {
|
||||
length: 'count',
|
||||
type: XMRLicenseStructs.AuxiliaryKey,
|
||||
type: XMRLicenseStructsV2.AUXILIARY_LOCATIONS,
|
||||
});
|
||||
|
||||
static UplinkKeyObject3 = new Parser()
|
||||
.buffer('uplink_key_id', { length: 16 })
|
||||
.uint16('chained_length')
|
||||
.buffer('checksum', {
|
||||
length: function () {
|
||||
return (this as any).chained_length;
|
||||
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,
|
||||
},
|
||||
})
|
||||
.uint16('count')
|
||||
.array('entries', {
|
||||
length: 'count',
|
||||
type: new Parser().uint32('entry'),
|
||||
});
|
||||
};
|
||||
const container = parsed_license.license;
|
||||
const data = xmr.data;
|
||||
|
||||
static CopyEnablerObject = new Parser().buffer('copy_enabler_type', {
|
||||
length: 16,
|
||||
});
|
||||
let pos = 0;
|
||||
while (pos < data.length - 16) {
|
||||
const value = XMRLicenseStructsV2.FTLV.parse(data.value.slice(pos));
|
||||
|
||||
static CopyCountRestrictionObject = new Parser().uint32('count');
|
||||
// XMR_SIGNATURE_OBJECT
|
||||
if (value.type === XMRTYPE.XMR_SIGNATURE_OBJECT) {
|
||||
const signature = XMRLicenseStructsV2.SIGNATURE.parse(value.value);
|
||||
|
||||
static MoveObject = new Parser().uint32('minimum_move_protection_level');
|
||||
|
||||
static XMRObject = (): Parser =>
|
||||
new Parser()
|
||||
.namely('self')
|
||||
.int16('flags')
|
||||
.int16('type')
|
||||
.int32('length')
|
||||
.choice('data', {
|
||||
tag: 'type',
|
||||
choices: {
|
||||
0x0005: XMRLicenseStructs.OutputProtectionLevelRestrictionObject,
|
||||
0x0008: XMRLicenseStructs.AnalogVideoOutputConfigurationRestriction,
|
||||
0x000a: XMRLicenseStructs.ContentKeyObject,
|
||||
0x000b: XMRLicenseStructs.SignatureObject,
|
||||
0x000d: XMRLicenseStructs.RightsSettingsObject,
|
||||
0x0012: XMRLicenseStructs.ExpirationRestrictionObject,
|
||||
0x0013: XMRLicenseStructs.IssueDateObject,
|
||||
0x0016: XMRLicenseStructs.MeteringRestrictionObject,
|
||||
0x001a: XMRLicenseStructs.GracePeriodObject,
|
||||
0x0022: XMRLicenseStructs.SourceIdObject,
|
||||
0x002a: XMRLicenseStructs.ECCKeyObject,
|
||||
0x002c: XMRLicenseStructs.PolicyMetadataObject,
|
||||
0x0029: XMRLicenseStructs.DomainRestrictionObject,
|
||||
0x0030: XMRLicenseStructs.ExpirationAfterFirstPlayRestrictionObject,
|
||||
0x0031: XMRLicenseStructs.DigitalAudioOutputRestrictionObject,
|
||||
0x0032: XMRLicenseStructs.RevInfoVersionObject,
|
||||
0x0033: XMRLicenseStructs.EmbeddedLicenseSettingsObject,
|
||||
0x0034: XMRLicenseStructs.SecurityLevelObject,
|
||||
0x0037: XMRLicenseStructs.MoveObject,
|
||||
0x0039: XMRLicenseStructs.PlayEnablerType,
|
||||
0x003a: XMRLicenseStructs.CopyEnablerObject,
|
||||
0x003b: XMRLicenseStructs.UplinkKIDObject,
|
||||
0x003d: XMRLicenseStructs.CopyCountRestrictionObject,
|
||||
0x0050: XMRLicenseStructs.RemovalDateObject,
|
||||
0x0051: XMRLicenseStructs.AuxiliaryKeysObject,
|
||||
0x0052: XMRLicenseStructs.UplinkKeyObject3,
|
||||
0x005a: XMRLicenseStructs.SecureStopRestrictionObject,
|
||||
0x0059: XMRLicenseStructs.DigitalVideoOutputRestrictionObject,
|
||||
},
|
||||
defaultChoice: 'self',
|
||||
});
|
||||
|
||||
static XmrLicense = new Parser()
|
||||
.useContextVars()
|
||||
.buffer('signature', { length: 4 })
|
||||
.int32('xmr_version')
|
||||
.buffer('rights_id', { length: 16 })
|
||||
.array('containers', {
|
||||
type: XMRLicenseStructs.XMRObject(),
|
||||
readUntil: 'eof',
|
||||
});
|
||||
}
|
||||
|
||||
export class XMRLicense extends XMRLicenseStructs {
|
||||
parsed: any;
|
||||
_LICENSE: Parser;
|
||||
|
||||
constructor(
|
||||
parsed_license: any,
|
||||
license_obj: Parser = XMRLicenseStructs.XmrLicense
|
||||
) {
|
||||
super();
|
||||
this.parsed = parsed_license;
|
||||
this._LICENSE = license_obj;
|
||||
}
|
||||
|
||||
static loads(data: string | Buffer): XMRLicense {
|
||||
if (typeof data === 'string') {
|
||||
data = Buffer.from(data, 'base64');
|
||||
}
|
||||
if (!Buffer.isBuffer(data)) {
|
||||
throw new Error(`Expecting Bytes or Base64 input, got ${data}`);
|
||||
}
|
||||
|
||||
const licence = XMRLicenseStructs.XmrLicense;
|
||||
const parsed_license = licence.parse(data);
|
||||
return new XMRLicense(parsed_license, licence);
|
||||
}
|
||||
|
||||
static load(filePath: string): XMRLicense {
|
||||
if (typeof filePath !== 'string') {
|
||||
throw new Error(`Expecting path string, got ${filePath}`);
|
||||
}
|
||||
const data = fs.readFileSync(filePath);
|
||||
return XMRLicense.loads(data);
|
||||
}
|
||||
|
||||
dumps(): Buffer {
|
||||
return this._LICENSE.parse(this.parsed);
|
||||
}
|
||||
|
||||
struct(): Parser {
|
||||
return this._LICENSE;
|
||||
}
|
||||
|
||||
private _locate(container: any): any {
|
||||
if (container.flags === 2 || container.flags === 3) {
|
||||
return this._locate(container.data);
|
||||
} else {
|
||||
return container;
|
||||
}
|
||||
}
|
||||
|
||||
*get_object(type_: number): Generator<any> {
|
||||
for (const obj of this.parsed.containers) {
|
||||
const container = this._locate(obj);
|
||||
if (container.type === type_) {
|
||||
yield container.data;
|
||||
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);
|
||||
}
|
||||
|
||||
get_content_keys(): Generator<any> {
|
||||
return this.get_object(0x000a);
|
||||
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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue