multi-downloader-nx_mirror/modules/playready/cdm.ts
2024-12-07 03:35:22 +01:00

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)
])
}
}