UNPKG

pssh-tools

Version:

Tools to generate PSSH Data and PSSH Box

181 lines 7.73 kB
"use strict"; 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