├── .codeclimate.yml ├── .editorconfig ├── .eslintrc.json ├── .github └── workflows │ ├── npm-publish.yml │ ├── npm-test.yml │ └── scanner.yml ├── .gitignore ├── .nvmrc ├── .tool-versions ├── .travis.yml ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── sonar-project.properties ├── src ├── index.ts └── lib │ ├── pssh │ ├── index.ts │ ├── playready.ts │ ├── proto │ │ └── WidevineCencHeader.proto │ ├── tools.ts │ └── widevine.ts │ └── types.ts ├── test └── pssh.ts ├── tsconfig.json └── tslint.json /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: '2' # required to adjust maintainability checks 2 | checks: 3 | file-lines: 4 | config: 5 | threshold: 250 6 | method-lines: 7 | config: 8 | threshold: 25 9 | similar-code: 10 | enabled: false 11 | plugins: 12 | duplication: 13 | enabled: true 14 | editorconfig: 15 | enabled: true 16 | config: 17 | editorconfig: .editorconfig 18 | fixme: 19 | enabled: true 20 | config: 21 | strings: 22 | - FIXME 23 | - TODO 24 | markdownlint: 25 | enabled: true 26 | nodesecurity: 27 | enabled: true 28 | tslint: 29 | enabled: true 30 | config: tslint.json 31 | exclude_patterns: 32 | - 'dist/' 33 | - 'coverage/' 34 | - '.nyc_output/' 35 | - '**/node_modules/' 36 | - '**/test/' 37 | - '**/*.d.ts' 38 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig 2 | # https://EditorConfig.org 3 | 4 | # top-most EditorConfig file 5 | root = true 6 | 7 | # Unix-style newlines with a newline ending every file 8 | [*] 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | # Matches multiple files with brace expansion notation 14 | [*.{js,ts}] 15 | # Set default charset 16 | charset = utf-8 17 | # Set indent style 18 | indent_style = space 19 | indent_size = 2 20 | 21 | # Matches the exact files either package.json or .travis.yml 22 | [{package.json,.travis.yml}] 23 | # Set indent style 24 | indent_style = space 25 | indent_size = 2 26 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "standard" 8 | ], 9 | "parser": "@typescript-eslint/parser", 10 | "parserOptions": { 11 | "ecmaVersion": 12, 12 | "sourceType": "module" 13 | }, 14 | "plugins": [ 15 | "@typescript-eslint" 16 | ], 17 | "rules": {} 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: NPM Publish 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v2 16 | with: 17 | node-version: 14 18 | - run: npm ci && npm test 19 | 20 | publish: 21 | needs: build 22 | runs-on: ubuntu-latest 23 | permissions: 24 | contents: read 25 | packages: write 26 | steps: 27 | - uses: actions/checkout@v2 28 | - uses: actions/setup-node@v2 29 | with: 30 | node-version: 14 31 | registry-url: 'https://npm.pkg.github.com' 32 | scope: '@feedsbrain' 33 | 34 | - name: Get Package Version 35 | run: | 36 | # Strip git ref prefix from version 37 | VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') 38 | 39 | # Strip "v" prefix from tag name 40 | [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') 41 | 42 | # Save Image version info to GitHub environment variable 43 | echo "PACKAGE_VERSION=$VERSION" >> $GITHUB_ENV 44 | 45 | - name: NPM Version 46 | uses: reedyuk/npm-version@1.1.1 47 | with: 48 | version: ${{ env.PACKAGE_VERSION }} 49 | 50 | - name: Authenticate to GitHub Package Registry 51 | run: echo "//npm.pkg.github.com:_authToken=${{ secrets.GITHUB_TOKEN }}" > ~/.npmrc 52 | 53 | - name: Publishing to GitHub Package 54 | run: npm ci && npm publish 55 | env: 56 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | -------------------------------------------------------------------------------- /.github/workflows/npm-test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: NPM Test 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-node@v2 17 | with: 18 | node-version: 14 19 | - run: npm ci && npm test 20 | -------------------------------------------------------------------------------- /.github/workflows/scanner.yml: -------------------------------------------------------------------------------- 1 | name: Code Scanner 2 | # on: 3 | # push: 4 | # branches: 5 | # - master 6 | jobs: 7 | build: 8 | name: Code Scanner 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | fetch-depth: 0 14 | - uses: sonarsource/sonarqube-scan-action@master 15 | env: 16 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 17 | SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # IDE Settings 64 | .vscode 65 | 66 | # Build 67 | dist 68 | 69 | # OSX Thing 70 | .DS_Store 71 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.12.2 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 20.12.2 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | include: 3 | - language: node_js 4 | env: 5 | - CC_TEST_REPORTER_ID=a84e50756a52ef762a1a81bb3bbc3fa62ff2ac85db28990a8edaf5f3790b2c72 6 | node_js: 7 | - 'stable' 8 | - 'lts/*' 9 | before_script: 10 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 11 | - chmod +x ./cc-test-reporter 12 | - ./cc-test-reporter before-build 13 | script: 14 | - npm test 15 | after_script: 16 | - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Indra Gunawan 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pssh-tools 2 | 3 | [![Build Status](https://travis-ci.org/feedsbrain/pssh-tools.svg?branch=master)](https://travis-ci.org/feedsbrain/pssh-tools) [![Maintainability](https://api.codeclimate.com/v1/badges/916d04bffb3000cbda7d/maintainability)](https://codeclimate.com/github/feedsbrain/pssh-tools/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/916d04bffb3000cbda7d/test_coverage)](https://codeclimate.com/github/feedsbrain/pssh-tools/test_coverage) 4 | 5 | **Tools to generate PSSH Data and PSSH Box** 6 | 7 | For dealing with multi-drm using common encryption (cenc) we may need to generate pssh data and/or pssh box to use in our workflow. This tools helpful to easily provide the base64 pssh data for Widevine and PlayReady. 8 | 9 | ## Installation 10 | 11 | This module is installed via npm: 12 | 13 | ``` bash 14 | $ npm install pssh-tools 15 | ``` 16 | 17 | ## Supported PSSH 18 | 19 | Currently we're only focus on Widevine and PlayReady but we will support more in the future. 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@feedsbrain/pssh-tools", 3 | "version": "1.2.1", 4 | "description": "Tools to generate PSSH Data and PSSH Box", 5 | "main": "dist/src/index.js", 6 | "types": "dist/src/index.d.ts", 7 | "scripts": { 8 | "build": "eslint **/*.ts && tsc -p . && cp -R ./src/lib/pssh/proto ./dist/src/lib/pssh/", 9 | "prepare": "npm run build", 10 | "pretest": "npm run build", 11 | "test": "nyc ava", 12 | "lint": "eslint **/*.ts --fix", 13 | "pub": "npm version patch --force && npm publish" 14 | }, 15 | "keywords": [ 16 | "pssh", 17 | "tools", 18 | "pssh-box", 19 | "pssh-data", 20 | "cenc", 21 | "drm" 22 | ], 23 | "author": "Indra Gunawan ", 24 | "license": "BSD", 25 | "dependencies": { 26 | "protobufjs": "^7.2.4" 27 | }, 28 | "devDependencies": { 29 | "@istanbuljs/nyc-config-typescript": "^1.0.2", 30 | "@types/node": "^20.4.5", 31 | "@typescript-eslint/eslint-plugin": "^6.21.0", 32 | "@typescript-eslint/parser": "^6.21.0", 33 | "ava": "^5.3.1", 34 | "eslint": "^8.57.0", 35 | "eslint-config-standard": "^17.1.0", 36 | "eslint-plugin-import": "^2.27.5", 37 | "eslint-plugin-node": "^11.1.0", 38 | "eslint-plugin-promise": "^6.1.1", 39 | "nyc": "^15.1.0", 40 | "ts-node": "^10.9.2", 41 | "typescript": "^5.5.4" 42 | }, 43 | "files": [ 44 | "dist/src/*", 45 | "dist/src/**/*" 46 | ], 47 | "ava": { 48 | "files": [ 49 | "./test/**/*.ts" 50 | ], 51 | "extensions": [ 52 | "ts" 53 | ], 54 | "require": [ 55 | "ts-node/register" 56 | ] 57 | }, 58 | "nyc": { 59 | "reporter": [ 60 | "lcov", 61 | "text" 62 | ], 63 | "extends": "@istanbuljs/nyc-config-typescript", 64 | "all": true, 65 | "check-coverage": true 66 | }, 67 | "publishConfig": { 68 | "registry": "https://npm.pkg.github.com" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=feedsbrain_pssh-tools 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/pssh' 2 | export * from './lib/types' 3 | -------------------------------------------------------------------------------- /src/lib/pssh/index.ts: -------------------------------------------------------------------------------- 1 | import * as widevine from './widevine' 2 | import * as playready from './playready' 3 | import * as tools from './tools' 4 | 5 | export { 6 | widevine, 7 | playready, 8 | tools 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/pssh/playready.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from 'crypto' 2 | import * as tools from './tools' 3 | import * as T from '../types' 4 | 5 | const DRM_AES_KEYSIZE_128 = 16 6 | const TEST_KEY_SEED = 'XVBovsmzhP9gRIZxWfFta3VVRPzVEWmJsazEJ46I' 7 | 8 | interface KeyItem { 9 | kid: string 10 | key: string 11 | checksum: string 12 | } 13 | 14 | const swapEndian = (keyId: string): Buffer => { 15 | // Microsoft GUID endianness 16 | const keyIdBytes = Buffer.from(keyId, 'hex') 17 | const keyIdBuffer = Buffer.concat( 18 | [ 19 | keyIdBytes.slice(0, 4).swap32(), 20 | keyIdBytes.slice(4, 6).swap16(), 21 | keyIdBytes.slice(6, 8).swap16(), 22 | keyIdBytes.slice(8, 16) 23 | ], 24 | DRM_AES_KEYSIZE_128 25 | ) 26 | return keyIdBuffer 27 | } 28 | 29 | // From: http://download.microsoft.com/download/2/3/8/238F67D9-1B8B-48D3-AB83-9C00112268B2/PlayReady%20Header%20Object%202015-08-13-FINAL-CL.PDF 30 | const generateContentKey = (keyId: string, keySeed: string = TEST_KEY_SEED): KeyItem => { 31 | // Microsoft GUID endianness 32 | const kidBuffer = swapEndian(keyId) 33 | 34 | // Truncate if key seed > 30 bytes 35 | const truncatedKeySeed = Buffer.alloc(30) 36 | const originalKeySeed = Buffer.from(keySeed, 'base64') 37 | originalKeySeed.copy(truncatedKeySeed, 0, 0, 30) 38 | 39 | // 40 | // Create shaA buffer. It is the SHA of the truncatedKeySeed and the keyId 41 | // 42 | const shaA = Buffer.concat([truncatedKeySeed, kidBuffer], truncatedKeySeed.length + kidBuffer.length) 43 | const digestA = crypto.createHash('sha256').update(shaA).digest() 44 | 45 | // 46 | // Create shaB buffer. It is the SHA of the truncatedKeySeed, the keyId, and 47 | // the truncatedKeySeed again. 48 | // 49 | const shaB = Buffer.concat([truncatedKeySeed, kidBuffer, truncatedKeySeed], (2 * truncatedKeySeed.length) + kidBuffer.length) 50 | const digestB = crypto.createHash('sha256').update(shaB).digest() 51 | 52 | // 53 | // Create shaC buffer. It is the SHA of the truncatedKeySeed, the keyId, 54 | // the truncatedKeySeed again, and the keyId again. 55 | // 56 | const shaC = Buffer.concat([truncatedKeySeed, kidBuffer, truncatedKeySeed, kidBuffer], (2 * truncatedKeySeed.length) + (2 * kidBuffer.length)) 57 | const digestC = crypto.createHash('sha256').update(shaC).digest() 58 | 59 | // Calculate Content Key 60 | const keyBuffer = Buffer.alloc(DRM_AES_KEYSIZE_128) 61 | for (let i = 0; i < DRM_AES_KEYSIZE_128; i++) { 62 | 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] 63 | keyBuffer[i] = value 64 | } 65 | 66 | // Calculate checksum 67 | const cipher = crypto.createCipheriv('aes-128-ecb', keyBuffer, '').setAutoPadding(false) 68 | const checksum = cipher.update(kidBuffer).slice(0, 8).toString('base64') 69 | 70 | return { 71 | kid: kidBuffer.toString('base64'), 72 | key: swapEndian(keyBuffer.toString('hex')).toString('base64'), 73 | checksum 74 | } 75 | } 76 | 77 | const constructProXML4 = (keyPair: T.KeyPair, licenseUrl: string, keySeed: string, checksum: boolean = true): string => { 78 | const key = encodeKey(keyPair, keySeed) 79 | 80 | const xmlArray = [''] 81 | 82 | xmlArray.push('') 83 | xmlArray.push('16AESCTR') 84 | xmlArray.push(`${key.kid}`) 85 | 86 | if (checksum) { 87 | xmlArray.push(`${key.checksum}`) 88 | } 89 | 90 | if (licenseUrl && licenseUrl !== '') { 91 | xmlArray.push(`${licenseUrl}`) 92 | } 93 | xmlArray.push('') 94 | xmlArray.push('8.0.1906.32') 95 | xmlArray.push('') 96 | xmlArray.push('') 97 | xmlArray.push('') 98 | 99 | return xmlArray.join('') 100 | } 101 | 102 | const constructProXML = (keyPairs: T.KeyPair[], licenseUrl: string, keySeed: string, checksum: boolean = true): string => { 103 | const keyIds = keyPairs.map((k) => { 104 | return encodeKey(k, keySeed) 105 | }) 106 | const xmlArray = [''] 107 | xmlArray.push('') 108 | xmlArray.push('') 109 | xmlArray.push('') 110 | // Construct Key 111 | keyIds.forEach((key) => { 112 | if (!checksum) { 113 | xmlArray.push(``) 114 | } else { 115 | xmlArray.push(``) 116 | } 117 | xmlArray.push('') 118 | }) 119 | xmlArray.push('') 120 | // Construct License URL 121 | if (licenseUrl && licenseUrl !== '') { 122 | xmlArray.push('') 123 | xmlArray.push(`${licenseUrl}?cfg=`) 124 | for (let i = 0; i < keyIds.length; i++) { 125 | // TODO: Options to pass predefined contentkey 126 | xmlArray.push(`(kid:${keyIds[i].kid})`) 127 | if (i < keyPairs.length - 1) { 128 | xmlArray.push(',') 129 | } 130 | } 131 | xmlArray.push('') 132 | } 133 | xmlArray.push('') 134 | xmlArray.push('8.0.1906.32') 135 | xmlArray.push('') 136 | xmlArray.push('') 137 | xmlArray.push('') 138 | return xmlArray.join('') 139 | } 140 | 141 | const getPsshData = (request: T.PlayReadyDataEncodeConfig): string => { 142 | const licenseUrl = request.licenseUrl || '' 143 | const keySeed = request.keySeed || '' 144 | const emptyKey = { key: '', kid: '' } 145 | 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) 146 | 147 | // Play Ready Object Header 148 | const headerBytes = Buffer.from(xmlData, 'utf16le') 149 | const headerLength = headerBytes.length 150 | const proLength = headerLength + 10 151 | const recordCount = 1 152 | const recordType = 1 153 | 154 | // Play Ready Object (PRO) 155 | const data = Buffer.alloc(proLength) 156 | data.writeInt32LE(proLength, 0) 157 | data.writeInt16LE(recordCount, 4) 158 | data.writeInt16LE(recordType, 6) 159 | data.writeInt16LE(headerLength, 8) 160 | data.write(xmlData, 10, proLength, 'utf16le') 161 | 162 | // data 163 | return Buffer.from(data).toString('base64') 164 | } 165 | 166 | const getPsshBox = (request: T.PlayReadyDataEncodeConfig) => { 167 | // data 168 | const data = getPsshData(request) 169 | const requestData: T.HeaderConfig = { 170 | systemId: tools.system.PLAYREADY.id, 171 | keyIds: request.keyPairs ? request.keyPairs.map((k) => k.kid) : [], 172 | data: data 173 | } 174 | const psshHeader = tools.getPsshHeader(requestData) 175 | return psshHeader 176 | } 177 | 178 | export const encodePssh = (request: T.PlayReadyEncodeConfig) => { 179 | if (request.dataOnly) { 180 | return getPsshData(request) 181 | } 182 | return getPsshBox(request) 183 | } 184 | 185 | export const decodeData = (data: string) => { 186 | return tools.decodePsshData(tools.system.PLAYREADY.name, data) 187 | } 188 | 189 | export const decodeKey = (keyData: string) => { 190 | const keyBuffer = Buffer.from(keyData, 'base64') 191 | return swapEndian(keyBuffer.toString('hex')).toString('hex') 192 | } 193 | 194 | export const encodeKey = (keyPair: T.KeyPair, keySeed: string = ''): KeyItem => { 195 | if (keySeed && keySeed.length) { 196 | return generateContentKey(keyPair.kid, keySeed) 197 | } 198 | 199 | const kidBuffer = swapEndian(keyPair.kid) 200 | 201 | // Calculate the checksum with provided key 202 | const keyBuffer = Buffer.from(keyPair.key, 'hex') 203 | const cipher = crypto.createCipheriv('aes-128-ecb', keyBuffer, '').setAutoPadding(false) 204 | const checksum = cipher.update(kidBuffer).slice(0, 8).toString('base64') 205 | 206 | return { 207 | kid: kidBuffer.toString('base64'), 208 | key: swapEndian(keyPair.key).toString('base64'), 209 | checksum 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/lib/pssh/proto/WidevineCencHeader.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | package proto; 3 | 4 | message WidevineCencHeader { 5 | enum Algorithm { 6 | UNENCRYPTED = 0; 7 | AESCTR = 1; 8 | } 9 | optional Algorithm algorithm = 1; 10 | repeated bytes key_id = 2; 11 | 12 | // Content provider name. 13 | optional string provider = 3; 14 | 15 | // A content identifier, specificed by content provider. 16 | optional bytes content_id = 4; 17 | 18 | // Track type. Acceptable values are SD, HD, and AUDIO. Used to 19 | // differentiate content keys used by an asset. 20 | optional string track_type_deprecated = 5; 21 | 22 | // The name of a registered policy to be used for this asset. 23 | optional string policy = 6; 24 | 25 | // Crypto period index, for media using key rotation. 26 | optional uint32 crypto_period_index = 7; 27 | 28 | // Optional protected context for group content. The grouped_license is a 29 | // serialized SignedMessage. 30 | optional bytes grouped_license = 8; 31 | 32 | // Protection scheme identifying the encryption algorithm. 33 | // Represented as one of the following 4CC values: 34 | // 'cenc' (AES­CTR), 'cbc1' (AES­CBC), 35 | // 'cens' (AES­CTR subsample), 'cbcs' (AES­CBC subsample). 36 | optional uint32 protection_scheme = 9; 37 | 38 | // Optional. For media using key rotation, this represents the duration 39 | // of each crypto period in seconds. 40 | optional uint32 crypto_period_seconds = 10; 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/pssh/tools.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as protobuf from 'protobufjs' 3 | import * as T from '../types' 4 | 5 | export const system = { 6 | WIDEVINE: { id: 'EDEF8BA979D64ACEA3C827DCD51D21ED', name: 'Widevine' }, 7 | PLAYREADY: { id: '9A04F07998404286AB92E65BE0885F95', name: 'PlayReady' }, 8 | MARLIN: { id: '5E629AF538DA4063897797FFBD9902D4', name: 'Marlin' }, 9 | COMMON: { id: '1077EFECC0B24D02ACE33C1E52E2FB4B', name: 'Common' } 10 | } 11 | 12 | const decodeWidevineHeader = (data: Buffer) => { 13 | const protoFile = path.join(__dirname, 'proto', 'WidevineCencHeader.proto') 14 | const root = protobuf.loadSync(protoFile) 15 | 16 | const WidevineCencHeader = root.lookupType('proto.WidevineCencHeader') 17 | const message = WidevineCencHeader.decode(data) 18 | const header = WidevineCencHeader.toObject(message, { 19 | enums: String, 20 | bytes: String, 21 | defaults: false, 22 | arrays: false 23 | }) 24 | 25 | return header 26 | } 27 | 28 | const createPsshHeader = (version: number) => { 29 | const psshHeaderBuffer = Buffer.from('pssh') 30 | 31 | const versionBuffer = Buffer.alloc(2) 32 | versionBuffer.writeInt16LE(version, 0) 33 | 34 | const flagBuffer = Buffer.alloc(2) 35 | flagBuffer.writeInt16BE(0, 0) 36 | 37 | return Buffer.concat([psshHeaderBuffer, versionBuffer, flagBuffer]) 38 | } 39 | 40 | export const getPsshHeader = (request: T.HeaderConfig): string => { 41 | const pssh = [] 42 | const keyIds = request.keyIds || [] 43 | 44 | // Set default to 0 for Widevine backward compatibility 45 | let version = 0 46 | if (request.systemId !== system.WIDEVINE.id && keyIds.length > 0) { 47 | version = 1 48 | } 49 | 50 | // pssh header 51 | const psshHeader = createPsshHeader(version) 52 | pssh.push(psshHeader) 53 | 54 | // system id 55 | const systemIdBuffer = Buffer.from(request.systemId, 'hex') 56 | pssh.push(systemIdBuffer) 57 | 58 | // key ids 59 | if (version === 1) { 60 | const keyCountBuffer = Buffer.alloc(4) 61 | keyCountBuffer.writeInt32BE(keyIds.length, 0) 62 | pssh.push(keyCountBuffer) 63 | 64 | const kidsBufferArray = [] 65 | for (let i = 0; i < keyIds.length; i++) { 66 | kidsBufferArray.push(Buffer.from(keyIds[i], 'hex')) 67 | } 68 | const kidsBuffer = Buffer.concat(kidsBufferArray) 69 | if (kidsBuffer.length > 0) { 70 | pssh.push(kidsBuffer) 71 | } 72 | } 73 | 74 | // data 75 | const dataBuffer = Buffer.from(request.data, 'base64') 76 | const dataSizeBuffer = Buffer.alloc(4) 77 | dataSizeBuffer.writeInt32BE(dataBuffer.length, 0) 78 | 79 | pssh.push(dataSizeBuffer) 80 | pssh.push(dataBuffer) 81 | 82 | // total size 83 | const psshSizeBuffer = Buffer.alloc(4) 84 | let totalLength = 4 85 | pssh.forEach((data) => { 86 | totalLength += data.length 87 | }) 88 | psshSizeBuffer.writeInt32BE(totalLength, 0) 89 | pssh.unshift(psshSizeBuffer) 90 | 91 | return Buffer.concat(pssh).toString('base64') 92 | } 93 | 94 | export const decodePssh = (data: string) => { 95 | const result: T.DecodeResult = {} 96 | const decodedData = Buffer.from(data, 'base64') 97 | 98 | // pssh header 99 | const psshSize = Buffer.alloc(4) 100 | decodedData.copy(psshSize, 0, 0, 4) 101 | const psshHeader = Buffer.alloc(4) 102 | decodedData.copy(psshHeader, 0, 4, 8) 103 | 104 | // fullbox header 105 | const headerVersion = Buffer.alloc(2) 106 | decodedData.copy(headerVersion, 0, 8, 10) 107 | const psshVersion = headerVersion.readInt16LE(0) 108 | 109 | const headerFlag = Buffer.alloc(2) 110 | decodedData.copy(headerFlag, 0, 10, 12) 111 | 112 | // system id 113 | const systemId = Buffer.alloc(16) 114 | decodedData.copy(systemId, 0, 12, 28) 115 | 116 | let dataStartPosition = 28 117 | 118 | let keyCountInt = 0 119 | if (psshVersion === 1) { 120 | // key count 121 | const keyCount = Buffer.alloc(4) 122 | decodedData.copy(keyCount, 0, 28, 32) 123 | 124 | keyCountInt = keyCount.readInt32BE(0) 125 | 126 | if (keyCountInt > 0) { 127 | result.keyIds = [] 128 | for (let i = 0; i < keyCountInt; i++) { 129 | // key id 130 | const keyId = Buffer.alloc(16) 131 | decodedData.copy(keyId, 0, 32 + (i * 16), 32 + ((i + 1) * 16)) 132 | result.keyIds.push(keyId.toString('hex')) 133 | } 134 | } 135 | dataStartPosition = 32 + (16 * keyCountInt) 136 | } 137 | 138 | // data size 139 | const dataSize = Buffer.alloc(4) 140 | decodedData.copy(dataSize, 0, dataStartPosition, dataStartPosition + dataSize.length) 141 | const psshDataSize = dataSize.readInt32BE(0) 142 | 143 | // data 144 | const psshData = Buffer.alloc(psshDataSize) 145 | decodedData.copy(psshData, 0, dataStartPosition + dataSize.length, dataStartPosition + dataSize.length + psshData.length) 146 | 147 | let systemName = '' 148 | let widevineKeyCount = 0 149 | 150 | switch (systemId.toString('hex').toUpperCase()) { 151 | case system.WIDEVINE.id: 152 | systemName = system.WIDEVINE.name 153 | result.dataObject = decodeWVData(psshData) 154 | widevineKeyCount = result.dataObject.widevineKeyCount || 0 155 | break 156 | case system.PLAYREADY.id: 157 | systemName = system.PLAYREADY.name 158 | result.dataObject = decodePRData(psshData) 159 | break 160 | case system.MARLIN.id: 161 | systemName = system.MARLIN.name 162 | break 163 | default: 164 | systemName = 'common' 165 | } 166 | 167 | result.systemId = systemId 168 | result.systemName = systemName 169 | result.version = psshVersion 170 | result.keyCount = keyCountInt + widevineKeyCount 171 | 172 | result.printPssh = () => { 173 | // pssh version 174 | const psshArray = [`PSSH Box v${psshVersion}`] 175 | 176 | // system id 177 | psshArray.push(` System ID: ${systemName} ${stringHexToGuid(systemId.toString('hex'))}`) 178 | 179 | // key ids 180 | if (result.keyIds && result.keyIds.length) { 181 | psshArray.push(` Key IDs (${result.keyIds.length}):`) 182 | result.keyIds.forEach((key) => { 183 | const keyGuid = stringHexToGuid(key) 184 | psshArray.push(` ${keyGuid}`) 185 | }) 186 | } 187 | 188 | // pssh data size 189 | psshArray.push(` PSSH Data (size: ${psshDataSize}):`) 190 | if (psshDataSize > 0) { 191 | psshArray.push(` ${systemName} Data:`) 192 | 193 | // widevine data 194 | if (systemName === system.WIDEVINE.name && result.dataObject) { 195 | const dataObject: T.WidevineData = result.dataObject as T.WidevineData 196 | if (dataObject.keyId) { 197 | psshArray.push(` Key IDs (${dataObject.keyId.length})`) 198 | dataObject.keyId.forEach((key) => { 199 | const keyGuid = stringHexToGuid(key) 200 | psshArray.push(` ${keyGuid}`) 201 | }) 202 | } 203 | if (dataObject.provider) { 204 | psshArray.push(` Provider: ${dataObject.provider}`) 205 | } 206 | if (dataObject.contentId) { 207 | psshArray.push(' Content ID') 208 | psshArray.push(` - UTF-8: ${Buffer.from(dataObject.contentId, 'hex').toString('utf8')}`) 209 | psshArray.push(` - HEX : ${dataObject.contentId}`) 210 | } 211 | } 212 | 213 | // playready data 214 | if (systemName === system.PLAYREADY.name && result.dataObject) { 215 | const dataObject: T.PlayReadyData = result.dataObject as T.PlayReadyData 216 | psshArray.push(` Record size(${dataObject.recordSize})`) 217 | if (dataObject.recordType) { 218 | switch (dataObject.recordType) { 219 | case 1: 220 | psshArray.push(` Record Type: Rights Management Header (${dataObject.recordType})`) 221 | break 222 | case 3: 223 | psshArray.push(` Record Type: Embedded License Store (${dataObject.recordType})`) 224 | break 225 | } 226 | } 227 | if (dataObject.recordXml) { 228 | psshArray.push(' Record XML:') 229 | psshArray.push(` ${dataObject.recordXml}`) 230 | } 231 | } 232 | } 233 | 234 | // line break 235 | psshArray.push('\n') 236 | return psshArray.join('\n') 237 | } 238 | 239 | return result 240 | } 241 | 242 | const decodeWVData = (psshData: Buffer): T.WidevineData => { 243 | // cenc header 244 | const header = decodeWidevineHeader(psshData) 245 | const wvData: T.WidevineData = {} 246 | 247 | if (header.keyId && header.keyId.length > 0) { 248 | wvData.widevineKeyCount = header.keyId.length 249 | const decodedKeys = header.keyId.map((key: string) => { 250 | return Buffer.from(key, 'base64').toString('hex') 251 | }) 252 | wvData.keyId = decodedKeys 253 | } 254 | if (header.provider) { 255 | wvData.provider = header.provider 256 | } 257 | if (header.contentId) { 258 | wvData.contentId = Buffer.from(header.contentId, 'base64').toString('hex').toUpperCase() 259 | } 260 | return wvData 261 | } 262 | 263 | const decodePRData = (psshData: Buffer): T.PlayReadyData => { 264 | // pro header 265 | const proHeader = Buffer.alloc(10) 266 | psshData.copy(proHeader, 0, 0, 10) 267 | 268 | const proHeaderLength = proHeader.readInt32LE(0) 269 | // let proRecordCount = proHeader.readInt16LE(4) 270 | const proRecordType = proHeader.readInt16LE(6) 271 | const proDataLength = proHeader.readInt16LE(8) 272 | const proData = Buffer.alloc(proDataLength) 273 | psshData.copy(proData, 0, 10, proHeaderLength) 274 | 275 | return { 276 | recordSize: proDataLength, 277 | recordType: proRecordType, 278 | recordXml: proData.toString('utf16le') 279 | } 280 | } 281 | 282 | const stringHexToGuid = (value: string) => { 283 | const guidArray = [] 284 | guidArray.push(value.slice(0, 8)) 285 | guidArray.push(value.slice(8, 12)) 286 | guidArray.push(value.slice(12, 16)) 287 | guidArray.push(value.slice(16, 20)) 288 | guidArray.push(value.slice(20, 32)) 289 | return guidArray.join('-') 290 | } 291 | 292 | export const decodePsshData = (targetSystem: string, data: string) => { 293 | const dataBuffer = Buffer.from(data, 'base64') 294 | if (system.WIDEVINE.name === targetSystem) { 295 | return decodeWVData(dataBuffer) 296 | } 297 | if (system.PLAYREADY.name === targetSystem) { 298 | return decodePRData(dataBuffer) 299 | } 300 | return null 301 | } 302 | -------------------------------------------------------------------------------- /src/lib/pssh/widevine.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as protobuf from 'protobufjs' 3 | import * as T from '../types'; 4 | 5 | import * as tools from './tools' 6 | 7 | interface WidevineProtoPayload { 8 | algorithm: number, 9 | keyId?: Buffer[], 10 | contentId?: Buffer, 11 | trackType?: string, 12 | provider?: string, 13 | protectionScheme?: number 14 | } 15 | 16 | const getPsshData = (request: T.WidevineDataEncodeConfig) => { 17 | const protoFile = path.join(__dirname, 'proto', 'WidevineCencHeader.proto') 18 | const root = protobuf.loadSync(protoFile) 19 | 20 | const WidevineCencHeader = root.lookupType('proto.WidevineCencHeader') 21 | const payload: WidevineProtoPayload = { 22 | algorithm: 1 // 0: Unencrypted - 1: AESCTR 23 | } 24 | if (request.keyIds && request.keyIds.length > 0) { 25 | const keyIdsBuffer = request.keyIds.map((key) => { 26 | return Buffer.from(key, 'hex') 27 | }) 28 | payload.keyId = keyIdsBuffer 29 | } 30 | if (request.contentId) { 31 | payload.contentId = Buffer.from(request.contentId) 32 | } 33 | if (request.trackType !== '') { 34 | payload.trackType = request.trackType 35 | } 36 | if (request.provider !== '') { 37 | payload.provider = request.provider 38 | } 39 | if (request.protectionScheme) { 40 | payload.protectionScheme = Buffer.from(request.protectionScheme).readInt32BE(0) 41 | } 42 | 43 | const errMsg = WidevineCencHeader.verify(payload) 44 | if (errMsg) { 45 | throw new Error(errMsg) 46 | } 47 | 48 | const message = WidevineCencHeader.create(payload) 49 | const buffer = WidevineCencHeader.encode(message).finish() 50 | 51 | return Buffer.from(buffer).toString('base64') 52 | } 53 | 54 | const getPsshBox = (request: T.WidevineDataEncodeConfig) => { 55 | // data 56 | const data = getPsshData(request) 57 | const requestData: T.HeaderConfig = { 58 | systemId: tools.system.WIDEVINE.id, 59 | keyIds: request.keyIds , 60 | data: data 61 | } 62 | const psshHeader = tools.getPsshHeader(requestData) 63 | return psshHeader 64 | } 65 | 66 | export const encodePssh = (request: T.WidevineEncodeConfig) => { 67 | if (request.dataOnly) { 68 | return getPsshData(request) 69 | } 70 | return getPsshBox(request) 71 | } 72 | 73 | export const decodeData = (data: string) => { 74 | return tools.decodePsshData(tools.system.WIDEVINE.name, data) 75 | } 76 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | type Omit = Pick> 2 | 3 | export interface WidevineEncodeConfig { 4 | contentId: string 5 | dataOnly: boolean 6 | keyIds?: string[] 7 | provider?: string 8 | protectionScheme?: string 9 | trackType?: string 10 | } 11 | 12 | export type WidevineDataEncodeConfig = Omit 13 | 14 | export interface KeyPair { 15 | kid: string 16 | key: string 17 | } 18 | 19 | export interface PlayReadyEncodeConfig { 20 | keyPairs?: KeyPair[] 21 | licenseUrl?: string 22 | keySeed?: string 23 | compatibilityMode: boolean 24 | dataOnly: boolean 25 | checksum?: boolean 26 | } 27 | 28 | export type PlayReadyDataEncodeConfig = Omit 29 | 30 | export interface HeaderConfig { 31 | systemId: string 32 | keyIds?: string[] 33 | data: string 34 | } 35 | 36 | export interface PlayReadyData { 37 | recordSize: number 38 | recordType: number 39 | recordXml: string 40 | } 41 | 42 | export interface WidevineData { 43 | widevineKeyCount?: number 44 | keyId?: string[] 45 | provider?: string 46 | contentId?: string 47 | } 48 | 49 | export type PsshData = PlayReadyData | WidevineData 50 | 51 | export interface DecodeResult { 52 | keyIds?: string[] 53 | dataObject?: PsshData 54 | systemId?: Buffer 55 | systemName?: string 56 | version?: number 57 | keyCount?: number 58 | printPssh?: Function 59 | } 60 | -------------------------------------------------------------------------------- /test/pssh.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import * as pssh from '../src/index' 3 | import { WidevineEncodeConfig, PlayReadyEncodeConfig, PlayReadyData } from '../src/lib/types' 4 | 5 | const KID = '6f651ae1dbe44434bcb4690d1564c41c' 6 | const KEY = '2a85da88fae41e2e36aeb2d5c94997b1' 7 | 8 | const KEY_SEED = 'XVBovsmzhP9gRIZxWfFta3VVRPzVEWmJsazEJ46I' 9 | const KS_KEY = '88da852ae4fa2e1e36aeb2d5c94997b1' 10 | 11 | const PRO_KID = '4Rplb+TbNES8tGkNFWTEHA==' 12 | const PRO_CONTENT_KEY = 'iNqFKuT6Lh42rrLVyUmXsQ==' 13 | const PRO_KS_CONTENT_KEY = 'KoXaiPrkHi42rrLVyUmXsQ==' 14 | const PRO_CHECKSUM_KEY = 'f8Acn4I4wU0=' 15 | 16 | const LA_URL = 'https://test.playready.microsoft.com/service/rightsmanager.asmx' 17 | const PSSH_TEST = 'AAAAQXBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAACESEJjp6TNjifjKjuoDBeg+VrUaCmludGVydHJ1c3QiASo=' 18 | const PSSH_DATA_PR = 'pAIAAAEAAQCaAjwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4AWgAwAFUAagBBAGEAdQBKADcAOAAwAEIASQAwAFYAbgBpAGEAdgBOADcAdwA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgAwAHgAOQAxAFcARgB0AEcAWABCAEkAPQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APABMAEEAXwBVAFIATAA+AGgAdAB0AHAAOgAvAC8AdABlAHMAdAAuAHAAbABhAHkAcgBlAGEAZAB5AC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAHMAZQByAHYAaQBjAGUALwByAGkAZwBoAHQAcwBtAGEAbgBhAGcAZQByAC4AYQBzAG0AeAA8AC8ATABBAF8AVQBSAEwAPgA8AC8ARABBAFQAQQA+ADwALwBXAFIATQBIAEUAQQBEAEUAUgA+AA==' 19 | 20 | test('Should return Widevine PSSH version 0 without KID', t => { 21 | const payload: WidevineEncodeConfig = { contentId: 'cenc-content-id', trackType: 'HD', provider: 'widevine_test', keyIds: [], protectionScheme: 'cenc', dataOnly: false } 22 | 23 | const data = pssh.widevine.encodePssh(payload) 24 | const result = pssh.tools.decodePssh(data) 25 | 26 | t.is(result.version, 0) 27 | t.is(result.keyCount, 0) 28 | }) 29 | 30 | test('Should return Widevine PSSH version 0 with KIDs', t => { 31 | const payload: WidevineEncodeConfig = { contentId: 'cenc-content-id', trackType: 'HD', keyIds: [KID], provider: 'widevine_test', protectionScheme: 'cenc', dataOnly: false } 32 | 33 | const data = pssh.widevine.encodePssh(payload) 34 | const result = pssh.tools.decodePssh(data) 35 | 36 | if (result.printPssh) { 37 | console.log(result.printPssh()) 38 | } 39 | 40 | t.is(result.version, 0) 41 | t.not(result.keyCount, 0) 42 | }) 43 | 44 | test('Should return Widevine PSSH version 0 with Multiple KIDs', t => { 45 | const payload: WidevineEncodeConfig = { contentId: 'cenc-content-id', keyIds: [KID, KS_KEY], provider: 'widevine_test', protectionScheme: 'cenc', dataOnly: false } 46 | 47 | const data = pssh.widevine.encodePssh(payload) 48 | const result = pssh.tools.decodePssh(data) 49 | 50 | if (result.printPssh) { 51 | console.log(result.printPssh()) 52 | } 53 | 54 | t.is(result.version, 0) 55 | t.not(result.keyCount, 0) 56 | }) 57 | 58 | test('Should return PlayReady PSSH version 1 with KID', t => { 59 | const payload: PlayReadyEncodeConfig = { keyPairs: [{ kid: KID, key: KEY }], licenseUrl: LA_URL, keySeed: '', compatibilityMode: false, dataOnly: false } 60 | 61 | const data = pssh.playready.encodePssh(payload) 62 | const result = pssh.tools.decodePssh(data) 63 | 64 | if (result.printPssh) { 65 | console.log(result.printPssh()) 66 | } 67 | 68 | t.is(result.version, 1) 69 | t.is(result.keyCount, 1) 70 | }) 71 | 72 | test('Should return PlayReady PSSH version 1 with Header Version 4.0.0.0 and KID', t => { 73 | const payload: PlayReadyEncodeConfig = { keyPairs: [{ kid: KID, key: KEY }], licenseUrl: LA_URL, keySeed: '', compatibilityMode: true, dataOnly: false } 74 | 75 | const data = pssh.playready.encodePssh(payload) 76 | const result = pssh.tools.decodePssh(data) 77 | 78 | if (result.printPssh) { 79 | console.log(result.printPssh()) 80 | } 81 | 82 | t.is(result.version, 1) 83 | t.is(result.keyCount, 1) 84 | }) 85 | 86 | test('Should return PRO w/ Checksum', t => { 87 | const payload: PlayReadyEncodeConfig = { keyPairs: [{ kid: KID, key: KEY }], licenseUrl: LA_URL, keySeed: '', compatibilityMode: true, dataOnly: true } 88 | const data = pssh.playready.encodePssh(payload) 89 | const result: PlayReadyData = pssh.playready.decodeData(data) as PlayReadyData 90 | 91 | if (result && result.recordXml && result.recordSize) { 92 | t.is(result.recordXml.includes('CHECKSUM'), true) 93 | } else { 94 | t.fail() 95 | } 96 | }) 97 | 98 | test('Should return PRO w/o Checksum', t => { 99 | const payload: PlayReadyEncodeConfig = { keyPairs: [{ kid: KID, key: KEY }], licenseUrl: LA_URL, keySeed: '', compatibilityMode: true, dataOnly: true, checksum: false } 100 | const data = pssh.playready.encodePssh(payload) 101 | const result: PlayReadyData = pssh.playready.decodeData(data) as PlayReadyData 102 | 103 | if (result && result.recordXml && result.recordSize) { 104 | t.is(result.recordXml.includes('CHECKSUM'), false) 105 | } else { 106 | t.fail() 107 | } 108 | }) 109 | 110 | test('Should be able to decode PSSH generated from PSSH-BOX', t => { 111 | const result = pssh.tools.decodePssh(PSSH_TEST) 112 | if (result.printPssh) { 113 | console.log(result.printPssh()) 114 | } 115 | 116 | t.is(result.version, 0) 117 | t.is(result.keyCount, 1) 118 | }) 119 | 120 | test('Should be able to decode PlayReady PSSH data', t => { 121 | const result: PlayReadyData = pssh.playready.decodeData(PSSH_DATA_PR) as PlayReadyData 122 | if (result && result.recordXml && result.recordSize) { 123 | console.log('PR Data:', result.recordXml) 124 | t.not(result.recordSize, 0) 125 | } 126 | }) 127 | 128 | test('Should be able to decode PlayReady static content key', t => { 129 | const result = pssh.playready.decodeKey(PRO_CONTENT_KEY) 130 | console.log(`\nKey ID: ${result}\n`) 131 | 132 | t.is(result, KEY) 133 | }) 134 | 135 | test('Should be able to decode PlayReady generated content key', t => { 136 | const result = pssh.playready.decodeKey(PRO_KS_CONTENT_KEY) 137 | console.log(`\nKey ID: ${result}\n`) 138 | 139 | t.is(result, KS_KEY) 140 | }) 141 | 142 | test('Should be able to encode PlayReady content key with correct checksum', t => { 143 | const result = pssh.playready.encodeKey({ kid: KID, key: KEY }) 144 | console.log(`\nKey: ${JSON.stringify(result, null, 2)}\n`) 145 | 146 | t.not(result, undefined) 147 | t.is(result.kid, PRO_KID) 148 | t.is(result.key, PRO_CONTENT_KEY) 149 | t.is(result.checksum, PRO_CHECKSUM_KEY) 150 | }) 151 | 152 | test('Should be able to encode PlayReady content key using test key seed with correct checksum', t => { 153 | const result = pssh.playready.encodeKey({ kid: KID, key: '' }, KEY_SEED) 154 | const control = pssh.playready.encodeKey({ kid: KID, key: KS_KEY }) 155 | 156 | console.log(`\nKey: ${JSON.stringify(result, null, 2)}\n`) 157 | 158 | t.not(result, undefined) 159 | t.is(result.kid, PRO_KID) 160 | t.is(result.key, control.key) 161 | t.is(result.checksum, control.checksum) 162 | }) 163 | 164 | test('Should be able to encode PlayReady content key using kid and key with correct checksum', t => { 165 | const result = pssh.playready.encodeKey({ kid: KID, key: KEY }) 166 | console.log(`\nKey: ${JSON.stringify(result, null, 2)}\n`) 167 | 168 | t.not(result, undefined) 169 | t.is(result.kid, PRO_KID) 170 | t.is(result.key, PRO_CONTENT_KEY) 171 | t.is(result.checksum, PRO_CHECKSUM_KEY) 172 | }) 173 | 174 | test('Should return PlayReady PRO without LA_URL', t => { 175 | const payload: PlayReadyEncodeConfig = { keyPairs: [{ kid: KID, key: KEY }], keySeed: '', compatibilityMode: true, dataOnly: false } 176 | 177 | const data = pssh.playready.encodePssh(payload) 178 | const result = pssh.tools.decodePssh(data) 179 | 180 | if (result.printPssh) { 181 | console.log(result.printPssh()) 182 | } 183 | 184 | if (result.dataObject) { 185 | const pro: PlayReadyData = result.dataObject as PlayReadyData 186 | t.is(pro.recordXml.includes(LA_URL), false) 187 | } 188 | }) 189 | 190 | test('Should return PlayReady PRO with LA_URL', t => { 191 | const payload: PlayReadyEncodeConfig = { keyPairs: [{ kid: KID, key: KEY }], licenseUrl: LA_URL, keySeed: '', compatibilityMode: true, dataOnly: false } 192 | 193 | const data = pssh.playready.encodePssh(payload) 194 | const result = pssh.tools.decodePssh(data) 195 | 196 | if (result.printPssh) { 197 | console.log(result.printPssh()) 198 | } 199 | 200 | if (result.dataObject) { 201 | const pro: PlayReadyData = result.dataObject as PlayReadyData 202 | t.is(pro.recordXml.includes(LA_URL), true) 203 | } 204 | }) 205 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "allowUnreachableCode": false, 5 | "allowUnusedLabels": false, 6 | "declaration": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "module": "commonjs", 9 | "noEmitOnError": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "noImplicitReturns": true, 12 | "pretty": true, 13 | "sourceMap": true, 14 | "strict": true, 15 | "outDir": "dist", 16 | "lib": [ 17 | "es5", 18 | "dom", 19 | "es2015", 20 | "es2016", 21 | "es2017", 22 | "es2018", 23 | "esnext" 24 | ], 25 | "experimentalDecorators": true, 26 | "emitDecoratorMetadata": true, 27 | "resolveJsonModule": true 28 | }, 29 | "exclude": [ 30 | "dist" 31 | ], 32 | "typeAcquisition": { 33 | "enable": true 34 | } 35 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-config-standard", 3 | "rules": { 4 | "semicolon": [false], 5 | "no-default-export": [false], 6 | "variable-name": [false], 7 | "forin": [false], 8 | "indent": [ 9 | true, 10 | "spaces", 11 | 2 12 | ] 13 | } 14 | } 15 | --------------------------------------------------------------------------------