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

489 lines
15 KiB
TypeScript

import * as fs from 'fs'
import { createHash } from 'crypto'
import { Parser } from 'binary-parser-encoder'
import ECCKey from './ecc_key'
function alignUp(length: number, alignment: number): number {
return Math.ceil(length / alignment) * alignment
}
export class BCertStructs {
static DrmBCertBasicInfo = new Parser()
.buffer('cert_id', { length: 16 })
.uint32be('security_level')
.uint32be('flags')
.uint32be('cert_type')
.buffer('public_key_digest', { length: 32 })
.uint32be('expiration_date')
.buffer('client_id', { length: 16 })
static DrmBCertDomainInfo = new Parser()
.buffer('service_id', { length: 16 })
.buffer('account_id', { length: 16 })
.uint32be('revision_timestamp')
.uint32be('domain_url_length')
.buffer('domain_url', {
length: function () {
return alignUp((this as any).domain_url_length, 4)
}
})
static DrmBCertPCInfo = new Parser().uint32be('security_version')
static DrmBCertDeviceInfo = new Parser()
.uint32be('max_license')
.uint32be('max_header')
.uint32be('max_chain_depth')
static DrmBCertFeatureInfo = new Parser()
.uint32be('feature_count')
.array('features', {
type: 'uint32be',
length: 'feature_count'
})
static CertKey = new Parser()
.uint16be('type')
.uint16be('length')
.uint32be('flags')
.buffer('key', {
length: function () {
return (this as any).length / 8
}
})
.uint32be('usages_count')
.array('usages', {
type: 'uint32be',
length: 'usages_count'
})
static DrmBCertKeyInfo = new Parser()
.uint32be('key_count')
.array('cert_keys', {
type: BCertStructs.CertKey,
length: 'key_count'
})
static DrmBCertManufacturerInfo = new Parser()
.uint32be('flags')
.uint32be('manufacturer_name_length')
.buffer('manufacturer_name', {
length: function () {
return alignUp((this as any).manufacturer_name_length, 4)
}
})
.uint32be('model_name_length')
.buffer('model_name', {
length: function () {
return alignUp((this as any).model_name_length, 4)
}
})
.uint32be('model_number_length')
.buffer('model_number', {
length: function () {
return alignUp((this as any).model_number_length, 4)
}
})
static DrmBCertSignatureInfo = new Parser()
.uint16be('signature_type')
.uint16be('signature_size')
.buffer('signature', { length: 'signature_size' })
.uint32be('signature_key_size')
.buffer('signature_key', {
length: function () {
return (this as any).signature_key_size / 8
}
})
static DrmBCertSilverlightInfo = new Parser()
.uint32be('security_version')
.uint32be('platform_identifier')
static DrmBCertMeteringInfo = new Parser()
.buffer('metering_id', { length: 16 })
.uint32be('metering_url_length')
.buffer('metering_url', {
length: function () {
return alignUp((this as any).metering_url_length, 4)
}
})
static DrmBCertExtDataSignKeyInfo = new Parser()
.uint16be('type')
.uint16be('length')
.uint32be('flags')
.buffer('key', {
length: function () {
return (this as any).length / 8
}
})
static BCertExtDataRecord = new Parser()
.uint32be('data_size')
.buffer('data', {
length: 'data_size'
})
static DrmBCertExtDataSignature = new Parser()
.uint16be('signature_type')
.uint16be('signature_size')
.buffer('signature', {
length: 'signature_size'
})
static BCertExtDataContainer = new Parser()
.uint32be('record_count')
.array('records', {
length: 'record_count',
type: BCertStructs.BCertExtDataRecord
})
.nest('signature', {
type: BCertStructs.DrmBCertExtDataSignature
})
static DrmBCertServerInfo = new Parser().uint32be('warning_days')
static DrmBcertSecurityVersion = new Parser()
.uint32be('security_version')
.uint32be('platform_identifier')
static Attribute = new Parser()
.uint16be('flags')
.uint16be('tag')
.uint32be('length')
.choice('attribute', {
tag: 'tag',
choices: {
1: BCertStructs.DrmBCertBasicInfo,
2: BCertStructs.DrmBCertDomainInfo,
3: BCertStructs.DrmBCertPCInfo,
4: BCertStructs.DrmBCertDeviceInfo,
5: BCertStructs.DrmBCertFeatureInfo,
6: BCertStructs.DrmBCertKeyInfo,
7: BCertStructs.DrmBCertManufacturerInfo,
8: BCertStructs.DrmBCertSignatureInfo,
9: BCertStructs.DrmBCertSilverlightInfo,
10: BCertStructs.DrmBCertMeteringInfo,
11: BCertStructs.DrmBCertExtDataSignKeyInfo,
12: BCertStructs.BCertExtDataContainer,
13: BCertStructs.DrmBCertExtDataSignature,
14: new Parser().buffer('data', {
length: function () {
return (this as any).length - 8
}
}),
15: BCertStructs.DrmBCertServerInfo,
16: BCertStructs.DrmBcertSecurityVersion,
17: BCertStructs.DrmBcertSecurityVersion
},
defaultChoice: new Parser().buffer('data', {
length: function () {
return (this as any).length - 8
}
})
})
static BCert = new Parser()
.string('signature', { length: 4, assert: 'CERT' })
.int32be('version')
.int32be('total_length')
.int32be('certificate_length')
.array('attributes', {
type: BCertStructs.Attribute,
lengthInBytes: function () {
return (this as any).total_length - 16
}
})
static BCertChain = new Parser()
.string('signature', { length: 4, assert: 'CHAI' })
.int32be('version')
.int32be('total_length')
.int32be('flags')
.int32be('certificate_count')
.array('certificates', {
type: BCertStructs.BCert,
length: 'certificate_count'
})
}
export class Certificate {
parsed: any
_BCERT: Parser
constructor(parsed_bcert: any, bcert_obj: Parser = BCertStructs.BCert) {
this.parsed = parsed_bcert
this._BCERT = bcert_obj
}
// UNSTABLE
static new_key_cert(
cert_id: Buffer,
security_level: number,
client_id: Buffer,
signing_key: ECCKey,
encryption_key: ECCKey,
group_key: ECCKey,
parent: CertificateChain,
expiry: number = 0xffffffff,
max_license: number = 10240,
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,
flags: 0,
cert_type: 2,
public_key_digest: signing_key.publicSha256Digest(),
expiration_date: expiry,
client_id: client_id
}
const basic_info_attribute = {
flags: 1,
tag: 1,
length:
BCertStructs.DrmBCertBasicInfo.encode(basic_info).length + 8,
attribute: basic_info
}
const device_info = {
max_license: max_license,
max_header: max_header,
max_chain_depth: max_chain_depth
}
const device_info_attribute = {
flags: 1,
tag: 4,
length:
BCertStructs.DrmBCertDeviceInfo.encode(device_info).length + 8,
attribute: device_info
}
const feature = {
feature_count: 1,
features: [4, 13]
}
const feature_attribute = {
flags: 1,
tag: 5,
length: BCertStructs.DrmBCertFeatureInfo.encode(feature).length + 8,
attribute: feature
}
const cert_key_sign = {
type: 1,
length: 512, // bits
flags: 0,
key: signing_key.privateBytes(),
usages_count: 1,
usages: [1]
}
const cert_key_encrypt = {
type: 1,
length: 512, // bits
flags: 0,
key: encryption_key.privateBytes(),
usages_count: 1,
usages: [2]
}
const key_info = {
key_count: 2,
cert_keys: [cert_key_sign, cert_key_encrypt]
}
const key_info_attribute = {
flags: 1,
tag: 6,
length: BCertStructs.DrmBCertKeyInfo.encode(key_info).length + 8,
attribute: key_info
}
const manufacturer_info = parent.get_certificate(0).get_attribute(7)
const new_bcert_container = {
signature: 'CERT',
version: 1,
total_length: 0,
certificate_length: 0,
attributes: [
basic_info_attribute,
device_info_attribute,
feature_attribute,
key_info_attribute,
manufacturer_info
]
}
let payload = BCertStructs.BCert.encode(new_bcert_container)
new_bcert_container.certificate_length = payload.length
new_bcert_container.total_length = payload.length + 144
payload = BCertStructs.BCert.encode(new_bcert_container)
const hash = createHash('sha256')
hash.update(payload)
const digest = hash.digest()
const signatureObj = group_key.keyPair.sign(digest)
const r = Buffer.from(signatureObj.r.toArray('be', 32))
const s = Buffer.from(signatureObj.s.toArray('be', 32))
const signature = Buffer.concat([r, s])
const signature_info = {
signature_type: 1,
signature_size: 64,
signature: signature,
signature_key_size: 512, // bits
signature_key: group_key.publicBytes()
}
const signature_info_attribute = {
flags: 1,
tag: 8,
length:
BCertStructs.DrmBCertSignatureInfo.encode(signature_info)
.length + 8,
attribute: signature_info
}
new_bcert_container.attributes.push(signature_info_attribute)
return new Certificate(new_bcert_container)
}
static loads(data: string | Buffer): Certificate {
if (typeof data === 'string') {
data = Buffer.from(data, 'base64')
}
if (!Buffer.isBuffer(data)) {
throw new Error(`Expecting Bytes or Base64 input, got ${data}`)
}
const cert = BCertStructs.BCert
const parsed_bcert = cert.parse(data)
return new Certificate(parsed_bcert, cert)
}
static load(filePath: string): Certificate {
const data = fs.readFileSync(filePath)
return Certificate.loads(data)
}
get_attribute(type_: number) {
for (const attribute of this.parsed.attributes) {
if (attribute.tag === type_) {
return attribute
}
}
}
get_security_level(): number {
const basic_info_attribute = this.get_attribute(1)
if (basic_info_attribute) {
return basic_info_attribute.attribute.security_level
}
return 0
}
private static _unpad(name: Buffer): string {
return name.toString('utf8').replace(/\0+$/, '')
}
get_name(): string {
const manufacturer_info_attribute = this.get_attribute(7)
if (manufacturer_info_attribute) {
const manufacturer_info = manufacturer_info_attribute.attribute
const manufacturer_name = Certificate._unpad(
manufacturer_info.manufacturer_name
)
const model_name = Certificate._unpad(manufacturer_info.model_name)
const model_number = Certificate._unpad(
manufacturer_info.model_number
)
return `${manufacturer_name} ${model_name} ${model_number}`
}
return ''
}
dumps(): Buffer {
return this._BCERT.encode(this.parsed)
}
struct(): Parser {
return this._BCERT
}
}
export class CertificateChain {
parsed: any
_BCERT_CHAIN: Parser
constructor(
parsed_bcert_chain: any,
bcert_chain_obj: Parser = BCertStructs.BCertChain
) {
this.parsed = parsed_bcert_chain
this._BCERT_CHAIN = bcert_chain_obj
}
static loads(data: string | Buffer): CertificateChain {
if (typeof data === 'string') {
data = Buffer.from(data, 'base64')
}
if (!Buffer.isBuffer(data)) {
throw new Error(`Expecting Bytes or Base64 input, got ${data}`)
}
const cert_chain = BCertStructs.BCertChain
try {
const parsed_bcert_chain = cert_chain.parse(data)
return new CertificateChain(parsed_bcert_chain, cert_chain)
} catch (error) {
console.error('Error during parsing:', error)
throw error
}
}
static load(filePath: string): CertificateChain {
const data = fs.readFileSync(filePath)
return CertificateChain.loads(data)
}
dumps(): Buffer {
return this._BCERT_CHAIN.encode(this.parsed)
}
struct(): Parser {
return this._BCERT_CHAIN
}
get_certificate(index: number): Certificate {
return new Certificate(this.parsed.certificates[index])
}
get_security_level(): number {
return this.get_certificate(0).get_security_level()
}
get_name(): string {
return this.get_certificate(0).get_name()
}
append(bcert: Certificate): void {
this.parsed.certificate_count += 1
this.parsed.certificates.push(bcert.parsed)
this.parsed.total_length += bcert.dumps().length
}
prepend(bcert: Certificate): void {
this.parsed.certificate_count += 1
this.parsed.certificates.unshift(bcert.parsed)
this.parsed.total_length += bcert.dumps().length
}
}