mirror of
https://github.com/anidl/multi-downloader-nx.git
synced 2026-01-11 20:10:20 +00:00
289 lines
10 KiB
TypeScript
289 lines
10 KiB
TypeScript
import { CertificateChain } from './bcert'
|
|
import ECCKey from './ecc_key'
|
|
import ElGamal, { Point } from './elgamal'
|
|
import XmlKey from './xml_key'
|
|
import { CipherType, getCipherType, Key } from './key'
|
|
import { XMRLicense } from './xmrlicense'
|
|
import crypto from 'crypto'
|
|
import { randomBytes } from 'crypto'
|
|
import { createHash } from 'crypto'
|
|
import elliptic from 'elliptic'
|
|
import { Device } from './device'
|
|
import { XMLParser } from 'fast-xml-parser'
|
|
|
|
export default class Cdm {
|
|
security_level: number
|
|
certificate_chain: CertificateChain
|
|
encryption_key: ECCKey
|
|
signing_key: ECCKey
|
|
client_version: string
|
|
la_version: number
|
|
|
|
curve: elliptic.ec
|
|
elgamal: ElGamal
|
|
|
|
private wmrm_key: elliptic.ec.KeyPair
|
|
private xml_key: XmlKey
|
|
|
|
constructor(
|
|
security_level: number,
|
|
certificate_chain: CertificateChain,
|
|
encryption_key: ECCKey,
|
|
signing_key: ECCKey,
|
|
client_version: string = '2.4.117.27',
|
|
la_version: number = 1
|
|
) {
|
|
this.security_level = security_level
|
|
this.certificate_chain = certificate_chain
|
|
this.encryption_key = encryption_key
|
|
this.signing_key = signing_key
|
|
this.client_version = client_version
|
|
this.la_version = la_version
|
|
|
|
this.curve = new elliptic.ec('p256')
|
|
this.elgamal = new ElGamal(this.curve)
|
|
|
|
const x =
|
|
'c8b6af16ee941aadaa5389b4af2c10e356be42af175ef3face93254e7b0b3d9b'
|
|
const y =
|
|
'982b27b5cb2341326e56aa857dbfd5c634ce2cf9ea74fca8f2af5957efeea562'
|
|
this.wmrm_key = this.curve.keyFromPublic({ x, y }, 'hex')
|
|
this.xml_key = new XmlKey()
|
|
}
|
|
|
|
static fromDevice(device: Device): Cdm {
|
|
return new Cdm(
|
|
device.security_level,
|
|
device.group_certificate,
|
|
device.encryption_key,
|
|
device.signing_key
|
|
)
|
|
}
|
|
|
|
private getKeyData(): Buffer {
|
|
const messagePoint = this.xml_key.getPoint(this.elgamal.curve)
|
|
const [point1, point2] = this.elgamal.encrypt(
|
|
messagePoint,
|
|
this.wmrm_key.getPublic() as Point
|
|
)
|
|
|
|
const bufferArray = Buffer.concat([
|
|
ElGamal.toBytes(point1.getX()),
|
|
ElGamal.toBytes(point1.getY()),
|
|
ElGamal.toBytes(point2.getX()),
|
|
ElGamal.toBytes(point2.getY())
|
|
])
|
|
|
|
return bufferArray
|
|
}
|
|
|
|
private getCipherData(): Buffer {
|
|
const b64_chain = this.certificate_chain.dumps().toString('base64')
|
|
const body = `<Data><CertificateChains><CertificateChain>${b64_chain}</CertificateChain></CertificateChains></Data>`
|
|
|
|
const cipher = crypto.createCipheriv(
|
|
'aes-128-cbc',
|
|
this.xml_key.aesKey,
|
|
this.xml_key.aesIv
|
|
)
|
|
|
|
const ciphertext = Buffer.concat([
|
|
cipher.update(Buffer.from(body, 'utf-8')),
|
|
cipher.final()
|
|
])
|
|
|
|
return Buffer.concat([this.xml_key.aesIv, ciphertext])
|
|
}
|
|
|
|
private buildDigestContent(
|
|
content_header: string,
|
|
nonce: string,
|
|
wmrm_cipher: string,
|
|
cert_cipher: string
|
|
): string {
|
|
const clientTime = Math.floor(Date.now() / 1000)
|
|
|
|
return (
|
|
`<LA xmlns="http://schemas.microsoft.com/DRM/2007/03/protocols" Id="SignedData" xml:space="preserve">` +
|
|
`<Version>${this.la_version}</Version>` +
|
|
`<ContentHeader>${content_header}</ContentHeader>` +
|
|
`<CLIENTINFO>` +
|
|
`<CLIENTVERSION>${this.client_version}</CLIENTVERSION>` +
|
|
`</CLIENTINFO>` +
|
|
`<LicenseNonce>${nonce}</LicenseNonce>` +
|
|
`<ClientTime>${clientTime}</ClientTime>` +
|
|
`<EncryptedData xmlns="http://www.w3.org/2001/04/xmlenc#" Type="http://www.w3.org/2001/04/xmlenc#Element">` +
|
|
`<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"></EncryptionMethod>` +
|
|
`<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">` +
|
|
`<EncryptedKey xmlns="http://www.w3.org/2001/04/xmlenc#">` +
|
|
`<EncryptionMethod Algorithm="http://schemas.microsoft.com/DRM/2007/03/protocols#ecc256"></EncryptionMethod>` +
|
|
`<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">` +
|
|
`<KeyName>WMRMServer</KeyName>` +
|
|
`</KeyInfo>` +
|
|
`<CipherData>` +
|
|
`<CipherValue>${wmrm_cipher}</CipherValue>` +
|
|
`</CipherData>` +
|
|
`</EncryptedKey>` +
|
|
`</KeyInfo>` +
|
|
`<CipherData>` +
|
|
`<CipherValue>${cert_cipher}</CipherValue>` +
|
|
`</CipherData>` +
|
|
`</EncryptedData>` +
|
|
`</LA>`
|
|
)
|
|
}
|
|
|
|
private static buildSignedInfo(digest_value: string): string {
|
|
return (
|
|
`<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">` +
|
|
`<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></CanonicalizationMethod>` +
|
|
`<SignatureMethod Algorithm="http://schemas.microsoft.com/DRM/2007/03/protocols#ecdsa-sha256"></SignatureMethod>` +
|
|
`<Reference URI="#SignedData">` +
|
|
`<DigestMethod Algorithm="http://schemas.microsoft.com/DRM/2007/03/protocols#sha256"></DigestMethod>` +
|
|
`<DigestValue>${digest_value}</DigestValue>` +
|
|
`</Reference>` +
|
|
`</SignedInfo>`
|
|
)
|
|
}
|
|
|
|
getLicenseChallenge(content_header: string): string {
|
|
const nonce = randomBytes(16).toString('base64')
|
|
const wmrm_cipher = this.getKeyData().toString('base64')
|
|
const cert_cipher = this.getCipherData().toString('base64')
|
|
|
|
const la_content = this.buildDigestContent(
|
|
content_header,
|
|
nonce,
|
|
wmrm_cipher,
|
|
cert_cipher
|
|
)
|
|
|
|
const la_hash = createHash('sha256')
|
|
.update(la_content, 'utf-8')
|
|
.digest()
|
|
|
|
const signed_info = Cdm.buildSignedInfo(la_hash.toString('base64'))
|
|
const signed_info_digest = createHash('sha256')
|
|
.update(signed_info, 'utf-8')
|
|
.digest()
|
|
|
|
const signatureObj = this.signing_key.keyPair.sign(signed_info_digest)
|
|
|
|
const r = signatureObj.r.toArrayLike(Buffer, 'be', 32)
|
|
const s = signatureObj.s.toArrayLike(Buffer, 'be', 32)
|
|
|
|
const rawSignature = Buffer.concat([r, s])
|
|
const signatureValue = rawSignature.toString('base64')
|
|
|
|
const publicKeyBytes = this.signing_key.keyPair
|
|
.getPublic()
|
|
.encode('array', false)
|
|
const publicKeyBuffer = Buffer.from(publicKeyBytes)
|
|
const publicKeyBase64 = publicKeyBuffer.toString('base64')
|
|
|
|
const main_body =
|
|
'<?xml version="1.0" encoding="utf-8"?>' +
|
|
'<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' +
|
|
'xmlns:xsd="http://www.w3.org/2001/XMLSchema" ' +
|
|
'xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">' +
|
|
'<soap:Body>' +
|
|
'<AcquireLicense xmlns="http://schemas.microsoft.com/DRM/2007/03/protocols">' +
|
|
'<challenge>' +
|
|
'<Challenge xmlns="http://schemas.microsoft.com/DRM/2007/03/protocols/messages">' +
|
|
la_content +
|
|
'<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">' +
|
|
signed_info +
|
|
`<SignatureValue>${signatureValue}</SignatureValue>` +
|
|
'<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">' +
|
|
'<KeyValue>' +
|
|
'<ECCKeyValue>' +
|
|
`<PublicKey>${publicKeyBase64}</PublicKey>` +
|
|
'</ECCKeyValue>' +
|
|
'</KeyValue>' +
|
|
'</KeyInfo>' +
|
|
'</Signature>' +
|
|
'</Challenge>' +
|
|
'</challenge>' +
|
|
'</AcquireLicense>' +
|
|
'</soap:Body>' +
|
|
'</soap:Envelope>'
|
|
|
|
return main_body
|
|
}
|
|
|
|
private _decryptEcc256Key(encrypted_key: Buffer): Buffer {
|
|
const point1 = this.curve.curve.point(
|
|
encrypted_key.subarray(0, 32).toString('hex'),
|
|
encrypted_key.subarray(32, 64).toString('hex')
|
|
)
|
|
const point2 = this.curve.curve.point(
|
|
encrypted_key.subarray(64, 96).toString('hex'),
|
|
encrypted_key.subarray(96, 128).toString('hex')
|
|
)
|
|
|
|
const decrypted = ElGamal.decrypt(
|
|
[point1, point2],
|
|
this.encryption_key.keyPair.getPrivate()
|
|
)
|
|
const decryptedBytes = decrypted.getX().toArray('be', 32).slice(16, 32)
|
|
|
|
return Buffer.from(decryptedBytes)
|
|
}
|
|
|
|
parseLicense(license: string | Buffer): {
|
|
key_id: string
|
|
key_type: number
|
|
cipher_type: number
|
|
key_length: number
|
|
key: string
|
|
}[] {
|
|
try {
|
|
const parser = new XMLParser({
|
|
removeNSPrefix: true
|
|
})
|
|
const result = parser.parse(license)
|
|
|
|
let licenses =
|
|
result['Envelope']['Body']['AcquireLicenseResponse'][
|
|
'AcquireLicenseResult'
|
|
]['Response']['LicenseResponse']['Licenses']['License']
|
|
|
|
if (!Array.isArray(licenses)) {
|
|
licenses = [licenses]
|
|
}
|
|
|
|
var keys = []
|
|
|
|
for (const licenseElement of licenses) {
|
|
for (const key of XMRLicense.loads(
|
|
licenseElement
|
|
).get_content_keys()) {
|
|
if (getCipherType(key.cipher_type) === CipherType.ECC256) {
|
|
keys.push(
|
|
new Key(
|
|
this.fixUUID(key.key_id),
|
|
key.key_type,
|
|
key.cipher_type,
|
|
key.key_length,
|
|
this._decryptEcc256Key(key.encrypted_key)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
return keys
|
|
} catch (error) {
|
|
throw new Error(`Unable to parse license, ${error}`)
|
|
}
|
|
}
|
|
|
|
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)
|
|
])
|
|
}
|
|
}
|