Encrypting for Widevine
Suggested softwares​
Shaka packager​
Shaka packager is a media packager maintained by Google. It has options for generating dash streaming protocol outputs. It supports encryption protocols for Widevine.
Encryption steps​
Creating a content id​
Content-ID is associated with a single encrypted output. One content-id maps to multiple encryption keys. The license URL is also generated using this content-id. We have an adaptive output with multiple tracks for video at different bitrates. Each of these tracks will have a different encryption key. To make things easier to handle and generate license URLs, there is a single content-id associated with a set of encryption keys. This way, you don't have to maintain different encryption keys for each track and the license server takes care of each track individually. Save this content-id in your database corresponding to the each video.
Sample code generating content-id of 16 bytes. Use a cryptographically secure function to generate this.
- Shell
- NodeJS
openssl rand -hex 16
const crypto = require('crypto');
crypto.randomBytes(16, (err, data) => {
const contentId = data.toString('hex');
console.log({contentId});
});
- In the licensing API, the content id needs to be passed in hex format
- The format when passing to shaka packager has to be
hex. When used with Encryption API, the format has to bebase64. This is because shaka packager converts the hex input to base64. - The minimum length of content id in bytes should be 16 bytes. This translates to 32 characters of hex bytes and 24 characters in base64. Using smaller content id size will result in HTTP 400 status code.
Package with shaka packager​
Widevine encryption is done after transcoding and before sending the content on CDN. The CENC encryption is done to MPEG-DASH streaming protocol. To enable encryption, you need to create a unique content key for every content as describe in the previous section. You need not have separate content keys for each track. Internally, shaka packager will automatically request multiple keys for each track.
packager \
in=h264_baseline_360p_600.mp4,stream=audio,output=audio_enc.mp4 \
in=h264_main_480p_1000.mp4,stream=video,output=h264_480p.mp4 \
in=h264_main_720p_3000.mp4,stream=video,output=h264_720p.mp4 \
in=h264_high_1080p_6000.mp4,stream=video,output=h264_1080p.mp4 \
--enable_widevine_encryption \
--key_server_url $URL \
--content_id $content_id \
--signer $signer \
--aes_signing_key $wv_key \
--aes_signing_iv $wv_iv \
--mpd_output h264.mpd
- The above command assumes packager command added in
$PATH - Words beginning with
$are variables and can be added as string or environment
File output from above command​
├── h264.mpd
├── audio_enc.mp4
├── h264_480.mpd
├── h264_720.mp4
└── h264_1080.mpd
In the above command, shaka packager connects with the license server. It sends a request for new set of encryption
keys for the video files. It encrypts this request with the {key, iv} for authentication. In response the license
server responds with a set of keys annotated with track information. Shaka packager uses these keys to encrypt the
input files into encryption versions.
Obtaining content keys via API​
Below is the alternative method where the process of obtaining keys and encrypting is done in two separate methods. This is useful if you are using any other software other than shaka packager.
Request​
- NodeJS
const crypto = require('crypto');
const request = require('request');
require('dotenv').config();
if (process.argv.length !== 3) throw new Error('wrong command');
// This is to be saved to database. For this example, reading from command arguments
const content_id = Buffer.from(process.argv[2], 'hex').toString('base64');
const wvSign = (requestMessage, key, iv) => {
const shasum = crypto.createHash('sha1');
shasum.update(requestMessage);
const input = shasum.digest('base64');
const cipher = crypto.createCipheriv('aes256', key, iv);
let result = cipher.update(input, 'base64', 'base64');
result += cipher.final('base64');
return result;
};
const requestMessage = {
content_id,
drm_types: ['WIDEVINE'],
policy: '',
tracks: [
{type: 'SD'},
{type: 'HD'},
{type: 'UHD1'},
{type: 'UHD2'},
{type: 'AUDIO'},
],
};
const requestStr = JSON.stringify(requestMessage);
const signature = wvSign(
requestStr,
Buffer.from(process.env.widevine_AES_key, 'hex'),
Buffer.from(process.env.widevine_IV, 'hex'),
);
const wvReqOptions = {
url: process.env.widevine_content_key_URL,
method: 'POST',
json: true,
body: {
request: Buffer.from(requestStr).toString('base64'),
signature: signature,
signer: process.env.widevine_signer_name,
},
};
request(wvReqOptions, (e, response, responseBody) => {
if (e) {
throw e;
}
if (response.statusCode === 200) {
const responseStr = Buffer.from(responseBody.response, 'base64').toString();
const responseObj = JSON.parse(responseStr);
console.log(JSON.stringify(responseObj, null, 2));
} else {
console.log('HTTP Status: ', response.statusCode);
console.log('Response Body: ', responseBody);
}
});
Running this code
npm install request dotenv
# provide the envrionment variables used in above code
vim .env
node generate-keys.js 00f90fd12df92b2c22356b8a80ad59bd
Response​
{
"status": "OK",
"drm": [
{
"type": "WIDEVINE",
"system_id": "edef8ba979d64acea3c827dcd51d21ed"
}
],
"tracks": [
{
"type": "SD",
"key_id": "umVaXZfiW6y8ScA77+RRcA==",
"key": "UbHLeXDtMzA7qHJITrOv/A==",
"pssh": [
{
"drm_type": "WIDEVINE",
"data": "IjI3NjY0NmY2MzY5NzA2ODY1NzJkNGIxMDM0ZGRlMzA0MTYzOGQ4MGJhN2FlMGJhMDJiN0jj3JWbBg==",
"boxes": "AAAAWnBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADoiMjc2NjQ2ZjYzNjk3MDY4NjU3MmQ0YjEwMzRkZGUzMDQxNjM4ZDgwYmE3YWUwYmEwMmI3SOPclZsG"
}
],
"entitled_key": [],
"content_groups": [],
"feature": ""
},
{
"type": "HD",
"key_id": "at74fvwCVNicAPHBC9ZkpQ==",
"key": "LQi3jMI2hWVbRtRLxLMM4A==",
"pssh": [
{
"drm_type": "WIDEVINE",
"data": "IjI3NjY0NmY2MzY5NzA2ODY1NzJkNGIxMDM0ZGRlMzA0MTYzOGQ4MGJhN2FlMGJhMDJiN0jj3JWbBg==",
"boxes": "AAAAWnBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADoiMjc2NjQ2ZjYzNjk3MDY4NjU3MmQ0YjEwMzRkZGUzMDQxNjM4ZDgwYmE3YWUwYmEwMmI3SOPclZsG"
}
],
"entitled_key": [],
"content_groups": [],
"feature": ""
},
{
"type": "UHD1",
"key_id": "rXIrKJZXVGOo9VmlWF3InQ==",
"key": "+BMAoTmILmIoy71yqwCRfA==",
"pssh": [
{
"drm_type": "WIDEVINE",
"data": "IjI3NjY0NmY2MzY5NzA2ODY1NzJkNGIxMDM0ZGRlMzA0MTYzOGQ4MGJhN2FlMGJhMDJiN0jj3JWbBg==",
"boxes": "AAAAWnBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADoiMjc2NjQ2ZjYzNjk3MDY4NjU3MmQ0YjEwMzRkZGUzMDQxNjM4ZDgwYmE3YWUwYmEwMmI3SOPclZsG"
}
],
"entitled_key": [],
"content_groups": [],
"feature": ""
},
{
"type": "UHD2",
"key_id": "11yzqGiVXiqzgWeBXmcidA==",
"key": "ILznLEknXl7Lc44JxoBJnQ==",
"pssh": [
{
"drm_type": "WIDEVINE",
"data": "IjI3NjY0NmY2MzY5NzA2ODY1NzJkNGIxMDM0ZGRlMzA0MTYzOGQ4MGJhN2FlMGJhMDJiN0jj3JWbBg==",
"boxes": "AAAAWnBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADoiMjc2NjQ2ZjYzNjk3MDY4NjU3MmQ0YjEwMzRkZGUzMDQxNjM4ZDgwYmE3YWUwYmEwMmI3SOPclZsG"
}
],
"entitled_key": [],
"content_groups": [],
"feature": ""
},
{
"type": "AUDIO",
"key_id": "bqrk32h3XlGypSia+PN1ig==",
"key": "B2TlmuW7BWkPF1qS7brysA==",
"pssh": [
{
"drm_type": "WIDEVINE",
"data": "IjI3NjY0NmY2MzY5NzA2ODY1NzJkNGIxMDM0ZGRlMzA0MTYzOGQ4MGJhN2FlMGJhMDJiN0jj3JWbBg==",
"boxes": "AAAAWnBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADoiMjc2NjQ2ZjYzNjk3MDY4NjU3MmQ0YjEwMzRkZGUzMDQxNjM4ZDgwYmE3YWUwYmEwMmI3SOPclZsG"
}
],
"entitled_key": [],
"content_groups": [],
"feature": ""
}
]
}