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 = `${b64_chain}` 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 ( `` + `${this.la_version}` + `${content_header}` + `` + `${this.client_version}` + `` + `${nonce}` + `${clientTime}` + `` + `` + `` + `` + `` + `` + `WMRMServer` + `` + `` + `${wmrm_cipher}` + `` + `` + `` + `` + `${cert_cipher}` + `` + `` + `` ) } private static buildSignedInfo(digest_value: string): string { return ( `` + `` + `` + `` + `` + `${digest_value}` + `` + `` ) } 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 = '' + '' + '' + '' + '' + '' + la_content + '' + signed_info + `${signatureValue}` + '' + '' + '' + `${publicKeyBase64}` + '' + '' + '' + '' + '' + '' + '' + '' + '' 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) ]) } }