multi-downloader-nx_mirror/modules/license.ts
2023-12-24 19:29:01 -08:00

161 lines
No EOL
5.2 KiB
TypeScript

//Originaly from https://github.com/Frooastside/node-widevine/blob/main/src/license.ts
import crypto from 'crypto';
import Long from 'long';
import { AES_CMAC } from './cmac';
import {
ClientIdentification,
License,
LicenseRequest,
LicenseRequest_RequestType,
LicenseType,
ProtocolVersion,
SignedMessage,
SignedMessage_MessageType,
SignedMessage_SessionKeyType,
WidevinePsshData
} from './license_protocol';
const WIDEVINE_SYSTEM_ID = new Uint8Array([237, 239, 139, 169, 121, 214, 74, 206, 163, 200, 39, 220, 213, 29, 33, 237]);
export type KeyContainer = {
kid: string;
key: string;
};
export type ContentDecryptionModule = {
privateKey: Buffer;
identifierBlob: Buffer;
};
export class Session {
private _devicePrivateKey: crypto.KeyObject;
private _identifierBlob: ClientIdentification;
private _identifier: Buffer;
private _pssh: Buffer;
private _rawLicenseRequest?: Buffer;
constructor(contentDecryptionModule: ContentDecryptionModule, pssh: Buffer) {
this._devicePrivateKey = crypto.createPrivateKey(contentDecryptionModule.privateKey);
this._identifierBlob = ClientIdentification.decode(contentDecryptionModule.identifierBlob);
this._identifier = this._generateIdentifier();
this._pssh = pssh;
}
createLicenseRequest(): Buffer {
if (!this._pssh.subarray(12, 28).equals(Buffer.from(WIDEVINE_SYSTEM_ID))) {
throw new Error('the pssh is not an actuall pssh');
}
const pssh = this._parsePSSH(this._pssh);
if (!pssh) {
throw new Error('pssh is invalid');
}
const licenseRequest: LicenseRequest = {
type: LicenseRequest_RequestType.NEW,
clientId: this._identifierBlob,
contentId: {
widevinePsshData: {
psshData: [this._pssh.subarray(32)],
licenseType: LicenseType.STREAMING,
requestId: this._identifier
}
},
requestTime: Long.fromNumber(Date.now()).divide(1000),
protocolVersion: ProtocolVersion.VERSION_2_1,
keyControlNonce: crypto.randomInt(2 ** 31),
keyControlNonceDeprecated: Buffer.alloc(0),
encryptedClientId: undefined
};
this._rawLicenseRequest = Buffer.from(LicenseRequest.encode(licenseRequest).finish());
const signature = crypto
.createSign('sha1')
.update(this._rawLicenseRequest)
.sign({ key: this._devicePrivateKey, padding: crypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: 20 });
const signedLicenseRequest: SignedMessage = {
type: SignedMessage_MessageType.LICENSE_REQUEST,
msg: this._rawLicenseRequest,
signature: Buffer.from(signature),
sessionKey: Buffer.alloc(0),
remoteAttestation: Buffer.alloc(0),
metricData: [],
serviceVersionInfo: undefined,
sessionKeyType: SignedMessage_SessionKeyType.UNDEFINED,
oemcryptoCoreMessage: Buffer.alloc(0)
};
return Buffer.from(SignedMessage.encode(signedLicenseRequest).finish());
}
parseLicense(rawLicense: Buffer) {
if (!this._rawLicenseRequest) {
throw new Error('please request a license first');
}
const signedLicense = SignedMessage.decode(rawLicense);
const sessionKey = crypto.privateDecrypt(this._devicePrivateKey, signedLicense.sessionKey);
const cmac = new AES_CMAC(Buffer.from(sessionKey));
const encKeyBase = Buffer.concat([
Buffer.from('ENCRYPTION'),
Buffer.from('\x00', 'ascii'),
this._rawLicenseRequest,
Buffer.from('\x00\x00\x00\x80', 'ascii')
]);
const authKeyBase = Buffer.concat([
Buffer.from('AUTHENTICATION'),
Buffer.from('\x00', 'ascii'),
this._rawLicenseRequest,
Buffer.from('\x00\x00\x02\x00', 'ascii')
]);
const encKey = cmac.calculate(Buffer.concat([Buffer.from('\x01'), encKeyBase]));
const serverKey = Buffer.concat([
cmac.calculate(Buffer.concat([Buffer.from('\x01'), authKeyBase])),
cmac.calculate(Buffer.concat([Buffer.from('\x02'), authKeyBase]))
]);
/*const clientKey = Buffer.concat([
cmac.calculate(Buffer.concat([Buffer.from("\x03"), authKeyBase])),
cmac.calculate(Buffer.concat([Buffer.from("\x04"), authKeyBase]))
]);*/
const calculatedSignature = crypto.createHmac('sha256', serverKey).update(signedLicense.msg).digest();
if (!calculatedSignature.equals(signedLicense.signature)) {
throw new Error('signatures do not match');
}
const license = License.decode(signedLicense.msg);
return license.key.map((keyContainer) => {
const keyId = keyContainer.id.length ? keyContainer.id.toString('hex') : keyContainer.type.toString();
const decipher = crypto.createDecipheriv(`aes-${encKey.length * 8}-cbc`, encKey, keyContainer.iv);
const decryptedKey = decipher.update(keyContainer.key);
decipher.destroy();
const key: KeyContainer = {
kid: keyId,
key: decryptedKey.toString('hex')
};
return key;
});
}
private _parsePSSH(pssh: Buffer): WidevinePsshData | null {
try {
return WidevinePsshData.decode(pssh.subarray(32));
} catch {
return null;
}
}
private _generateIdentifier(): Buffer {
return Buffer.from(`${crypto.randomBytes(8).toString('hex')}${'01'}${'00000000000000'}`);
}
get pssh(): Buffer {
return this._pssh;
}
}