pssh-tools
Version:
Tools to generate PSSH Data and PSSH Box
181 lines • 7.73 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.encodeKey = exports.decodeKey = exports.decodeData = exports.encodePssh = void 0;
const crypto = require("crypto");
const tools = require("./tools");
const DRM_AES_KEYSIZE_128 = 16;
const TEST_KEY_SEED = 'XVBovsmzhP9gRIZxWfFta3VVRPzVEWmJsazEJ46I';
const swapEndian = (keyId) => {
// Microsoft GUID endianness
const keyIdBytes = Buffer.from(keyId, 'hex');
const keyIdBuffer = Buffer.concat([
keyIdBytes.slice(0, 4).swap32(),
keyIdBytes.slice(4, 6).swap16(),
keyIdBytes.slice(6, 8).swap16(),
keyIdBytes.slice(8, 16)
], DRM_AES_KEYSIZE_128);
return keyIdBuffer;
};
// From: http://download.microsoft.com/download/2/3/8/238F67D9-1B8B-48D3-AB83-9C00112268B2/PlayReady%20Header%20Object%202015-08-13-FINAL-CL.PDF
const generateContentKey = (keyId, keySeed = TEST_KEY_SEED) => {
// Microsoft GUID endianness
const kidBuffer = swapEndian(keyId);
// Truncate if key seed > 30 bytes
const truncatedKeySeed = Buffer.alloc(30);
const originalKeySeed = Buffer.from(keySeed, 'base64');
originalKeySeed.copy(truncatedKeySeed, 0, 0, 30);
//
// Create shaA buffer. It is the SHA of the truncatedKeySeed and the keyId
//
const shaA = Buffer.concat([truncatedKeySeed, kidBuffer], truncatedKeySeed.length + kidBuffer.length);
const digestA = crypto.createHash('sha256').update(shaA).digest();
//
// Create shaB buffer. It is the SHA of the truncatedKeySeed, the keyId, and
// the truncatedKeySeed again.
//
const shaB = Buffer.concat([truncatedKeySeed, kidBuffer, truncatedKeySeed], (2 * truncatedKeySeed.length) + kidBuffer.length);
const digestB = crypto.createHash('sha256').update(shaB).digest();
//
// Create shaC buffer. It is the SHA of the truncatedKeySeed, the keyId,
// the truncatedKeySeed again, and the keyId again.
//
const shaC = Buffer.concat([truncatedKeySeed, kidBuffer, truncatedKeySeed, kidBuffer], (2 * truncatedKeySeed.length) + (2 * kidBuffer.length));
const digestC = crypto.createHash('sha256').update(shaC).digest();
// Calculate Content Key
const keyBuffer = Buffer.alloc(DRM_AES_KEYSIZE_128);
for (let i = 0; i < DRM_AES_KEYSIZE_128; i++) {
const value = digestA[i] ^ digestA[i + DRM_AES_KEYSIZE_128] ^ digestB[i] ^ digestB[i + DRM_AES_KEYSIZE_128] ^ digestC[i] ^ digestC[i + DRM_AES_KEYSIZE_128];
keyBuffer[i] = value;
}
// Calculate checksum
const cipher = crypto.createCipheriv('aes-128-ecb', keyBuffer, '').setAutoPadding(false);
const checksum = cipher.update(kidBuffer).slice(0, 8).toString('base64');
return {
kid: kidBuffer.toString('base64'),
key: swapEndian(keyBuffer.toString('hex')).toString('base64'),
checksum
};
};
const constructProXML4 = (keyPair, licenseUrl, keySeed, checksum = true) => {
const key = (0, exports.encodeKey)(keyPair, keySeed);
const xmlArray = ['<WRMHEADER xmlns="http://schemas.microsoft.com/DRM/2007/03/PlayReadyHeader" version="4.0.0.0">'];
xmlArray.push('<DATA>');
xmlArray.push('<PROTECTINFO><KEYLEN>16</KEYLEN><ALGID>AESCTR</ALGID></PROTECTINFO>');
xmlArray.push(`<KID>${key.kid}</KID>`);
if (checksum) {
xmlArray.push(`<CHECKSUM>${key.checksum}</CHECKSUM>`);
}
if (licenseUrl && licenseUrl !== '') {
xmlArray.push(`<LA_URL>${licenseUrl}</LA_URL>`);
}
xmlArray.push('<CUSTOMATTRIBUTES>');
xmlArray.push('<IIS_DRM_VERSION>8.0.1906.32</IIS_DRM_VERSION>');
xmlArray.push('</CUSTOMATTRIBUTES>');
xmlArray.push('</DATA>');
xmlArray.push('</WRMHEADER>');
return xmlArray.join('');
};
const constructProXML = (keyPairs, licenseUrl, keySeed, checksum = true) => {
const keyIds = keyPairs.map((k) => {
return (0, exports.encodeKey)(k, keySeed);
});
const xmlArray = ['<?xml version="1.0" encoding="UTF-8"?>'];
xmlArray.push('<WRMHEADER xmlns="http://schemas.microsoft.com/DRM/2007/03/PlayReadyHeader" version="4.2.0.0">');
xmlArray.push('<DATA>');
xmlArray.push('<PROTECTINFO><KIDS>');
// Construct Key
keyIds.forEach((key) => {
if (!checksum) {
xmlArray.push(`<KID ALGID="AESCTR" VALUE="${key.kid}">`);
}
else {
xmlArray.push(`<KID ALGID="AESCTR" CHECKSUM="${key.checksum}" VALUE="${key.kid}">`);
}
xmlArray.push('</KID>');
});
xmlArray.push('</KIDS></PROTECTINFO>');
// Construct License URL
if (licenseUrl && licenseUrl !== '') {
xmlArray.push('<LA_URL>');
xmlArray.push(`${licenseUrl}?cfg=`);
for (let i = 0; i < keyIds.length; i++) {
// TODO: Options to pass predefined contentkey
xmlArray.push(`(kid:${keyIds[i].kid})`);
if (i < keyPairs.length - 1) {
xmlArray.push(',');
}
}
xmlArray.push('</LA_URL>');
}
xmlArray.push('<CUSTOMATTRIBUTES>');
xmlArray.push('<IIS_DRM_VERSION>8.0.1906.32</IIS_DRM_VERSION>');
xmlArray.push('</CUSTOMATTRIBUTES>');
xmlArray.push('</DATA>');
xmlArray.push('</WRMHEADER>');
return xmlArray.join('');
};
const getPsshData = (request) => {
const licenseUrl = request.licenseUrl || '';
const keySeed = request.keySeed || '';
const emptyKey = { key: '', kid: '' };
const xmlData = request.compatibilityMode === true ? constructProXML4(request.keyPairs ? request.keyPairs[0] : emptyKey, licenseUrl, keySeed, request.checksum) : constructProXML(request.keyPairs ? request.keyPairs : [], licenseUrl, keySeed, request.checksum);
// Play Ready Object Header
const headerBytes = Buffer.from(xmlData, 'utf16le');
const headerLength = headerBytes.length;
const proLength = headerLength + 10;
const recordCount = 1;
const recordType = 1;
// Play Ready Object (PRO)
const data = Buffer.alloc(proLength);
data.writeInt32LE(proLength, 0);
data.writeInt16LE(recordCount, 4);
data.writeInt16LE(recordType, 6);
data.writeInt16LE(headerLength, 8);
data.write(xmlData, 10, proLength, 'utf16le');
// data
return Buffer.from(data).toString('base64');
};
const getPsshBox = (request) => {
// data
const data = getPsshData(request);
const requestData = {
systemId: tools.system.PLAYREADY.id,
keyIds: request.keyPairs ? request.keyPairs.map((k) => k.kid) : [],
data: data
};
const psshHeader = tools.getPsshHeader(requestData);
return psshHeader;
};
const encodePssh = (request) => {
if (request.dataOnly) {
return getPsshData(request);
}
return getPsshBox(request);
};
exports.encodePssh = encodePssh;
const decodeData = (data) => {
return tools.decodePsshData(tools.system.PLAYREADY.name, data);
};
exports.decodeData = decodeData;
const decodeKey = (keyData) => {
const keyBuffer = Buffer.from(keyData, 'base64');
return swapEndian(keyBuffer.toString('hex')).toString('hex');
};
exports.decodeKey = decodeKey;
const encodeKey = (keyPair, keySeed = '') => {
if (keySeed && keySeed.length) {
return generateContentKey(keyPair.kid, keySeed);
}
const kidBuffer = swapEndian(keyPair.kid);
// Calculate the checksum with provided key
const keyBuffer = Buffer.from(keyPair.key, 'hex');
const cipher = crypto.createCipheriv('aes-128-ecb', keyBuffer, '').setAutoPadding(false);
const checksum = cipher.update(kidBuffer).slice(0, 8).toString('base64');
return {
kid: kidBuffer.toString('base64'),
key: swapEndian(keyPair.key).toString('base64'),
checksum
};
};
exports.encodeKey = encodeKey;
//# sourceMappingURL=playready.js.map