multi-downloader-nx_mirror/modules/playready/bcert.ts

450 lines
12 KiB
TypeScript

import * as fs from 'fs';
import { createHash } from 'crypto';
import { Parser } from 'binary-parser-encoder';
import ECCKey from './ecc_key';
import { console } from '../log';
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('key_type')
.uint16be('key_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_leaf_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 {
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: 3,
features: [4, 9, 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;
}
}