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