Skip to content

Commit

Permalink
Shaka Packager Raw Key support (#63)
Browse files Browse the repository at this point in the history
Closes #21
  • Loading branch information
cstranex authored Nov 12, 2020
1 parent e7ff160 commit ddfd796
Show file tree
Hide file tree
Showing 5 changed files with 339 additions and 12 deletions.
74 changes: 74 additions & 0 deletions config_files/pipeline_vod_encrypted_raw_config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# This is a sample pipeline configuration file for Shaka Streamer in VOD mode.
# Here you configure resolutions, manifest formats, segment size, and more.

# Streaming mode. Can be live or vod.
streaming_mode: vod

# A list of resolutions to encode.
# For VOD, you can specify many more resolutions than you would with live,
# since the encoding does not need to be done in real time.
resolutions:
- 4k
- 1080p
- 720p
- 480p
- 360p

# The number of audio channels to output.
channels: 6

# Manifest format (dash, hls or both)
manifest_format:
- dash
- hls

# Length of each segment in seconds.
segment_size: 10

# Forces the use of SegmentTemplate in DASH.
segment_per_file: True

encryption:
# Enables encryption.
# If disabled, the following settings are ignored.
enable: True
# Set to 'raw' to use the Raw Key Encryption mode. Default is widevine.
encryption_mode: raw
# List of keys. Key and key id are 32 digit hex strings
# Optionally 'label' can be specified. If no label is specified, it
# is assumed to be the default key.
keys:
- key_id: 8858d6731bee84d3b6e3d12f3c767a26
key: 1ae8ccd0e7985cc0b6203a55855a1034
# Optional IV. If not specified one will be randomly created
# Must be either 16 digit or 32 digit hex
iv: 8858d6731bee84d3b6e3d12f3c767a26
# One or more pssh boxes in hex string format.
pssh: "000000317073736800000000EDEF8BA979D64ACEA3C827DCD\
51D21ED00000011220F7465737420636F6E74656E74206964"
# Optional protection systems to be generated
protection_systems:
- Widevine
- FairPlay
- PlayReady
- Marlin
- CommonSystem
# Protection scheme (cenc or cbcs)
# These are different methods of using a block cipher to encrypt media.
protection_scheme: cenc
# Seconds of unencrypted media at the beginning of the stream.
clear_lead: 10
10 changes: 10 additions & 0 deletions streamer/input_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,16 @@ class Input(configuration.Base):
Not supported with media_type of 'text'.
"""

drm_label = configuration.Field(str).cast()
"""Optional value for a custom DRM label, which defines the encryption key
applied to the stream. If not provided, the DRM label is derived from stream
type (video, audio), resolutions, etc. Note that it is case sensitive.
Applies to 'raw' encryption_mode only."""

skip_encryption = configuration.Field(int, default=0).cast()
"""If set, no encryption of the stream will be made"""

# TODO: Figure out why mypy 0.720 and Python 3.7.5 don't correctly deduce the
# type parameter here if we don't specify it explicitly with brackets after
# "Field".
Expand Down
68 changes: 57 additions & 11 deletions streamer/packager_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from . import pipeline_configuration

from streamer.output_stream import OutputStream
from streamer.pipeline_configuration import PipelineConfig
from streamer.pipeline_configuration import EncryptionMode, PipelineConfig
from typing import List, Optional, Union

# Alias a few classes to avoid repeating namespaces later.
Expand Down Expand Up @@ -127,6 +127,12 @@ def _setup_stream(self, stream: OutputStream) -> str:
'stream': stream.type.value,
}

if stream.input.skip_encryption:
dict['skip_encryption'] = str(stream.input.skip_encryption)

if stream.input.drm_label:
dict['drm_label'] = stream.input.drm_label

# Note: Shaka Packager will not accept 'und' as a language, but Shaka
# Player will fill that in if the language metadata is missing from the
# manifest/playlist.
Expand Down Expand Up @@ -177,17 +183,57 @@ def _setup_manifest_format(self) -> List[str]:
]
return args

def _setup_encryption_keys(self) -> List[str]:
# Sets up encryption keys for raw encryption mode
keys = []
for key in self._pipeline_config.encryption.keys:
key_str = ''
if key.label:
key_str = 'label=' + key.label + ':'
key_str += 'key_id=' + key.key_id + ':key=' + key.key
keys.append(key_str)
return keys

def _setup_encryption(self) -> List[str]:
# Sets up encryption of content.
args = [
'--enable_widevine_encryption',
'--key_server_url', self._pipeline_config.encryption.key_server_url,
'--content_id', self._pipeline_config.encryption.content_id,
'--signer', self._pipeline_config.encryption.signer,
'--aes_signing_key', self._pipeline_config.encryption.signing_key,
'--aes_signing_iv', self._pipeline_config.encryption.signing_iv,

encryption = self._pipeline_config.encryption

args = []

if encryption.encryption_mode == EncryptionMode.WIDEVINE:
args = [
'--enable_widevine_encryption',
'--key_server_url', encryption.key_server_url,
'--content_id', encryption.content_id,
'--signer', encryption.signer,
'--aes_signing_key', encryption.signing_key,
'--aes_signing_iv', encryption.signing_iv,
]
elif encryption.encryption_mode == EncryptionMode.RAW:
# raw key encryption mode
args = [
'--enable_raw_key_encryption',
'--keys',
','.join(self._setup_encryption_keys()),
]
if encryption.iv:
args.extend(['--iv', encryption.iv])
if encryption.pssh:
args.extend(['--pssh', encryption.pssh])

# Common arguments
args.extend([
'--protection_scheme',
self._pipeline_config.encryption.protection_scheme.value,
'--clear_lead', str(self._pipeline_config.encryption.clear_lead),
]
encryption.protection_scheme.value,
'--clear_lead', str(encryption.clear_lead),
])

if encryption.protection_systems:
args.extend([
'--protection_systems', ','.join(
[p.value for p in encryption.protection_systems]
)
])

return args
90 changes: 90 additions & 0 deletions streamer/pipeline_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,33 @@ class ProtectionScheme(enum.Enum):
CBCS = 'cbcs'
"""AES-128-CBC mode with pattern encryption."""

class ProtectionSystem(enum.Enum):
WIDEVINE = 'Widevine'
FAIRPLAY = 'FairPlay'
PLAYREADY = 'PlayReady'
MARLIN = 'Marlin'
COMMON = 'CommonSystem'

class EncryptionMode(enum.Enum):
WIDEVINE = 'widevine'
"""Widevine key server mode"""

RAW = 'raw'
"""Raw key mode"""

class RawKeyConfig(configuration.Base):
"""An object representing a list of keys for Raw key encryption"""

label = configuration.Field(str).cast()
"""An arbitary string or a predefined DRM label like AUDIO, SD, HD, etc.
If not specified, indicates the default key and key_id."""

key_id = configuration.Field(configuration.HexString, required=True).cast()
"""A key identifier as a 32-digit hex string"""

key = configuration.Field(configuration.HexString, required=True).cast()
"""The encryption key to use as a 32-digit hex string"""


class EncryptionConfig(configuration.Base):
"""An object representing the encryption config for Shaka Streamer."""
Expand All @@ -72,37 +99,77 @@ class EncryptionConfig(configuration.Base):
Otherwise, all other encryption settings are ignored.
"""

encryption_mode = configuration.Field(
EncryptionMode, default=EncryptionMode.WIDEVINE).cast()
"""Encryption mode to use. By default it is widevine but can be changed
to raw."""

protection_systems = configuration.Field(List[ProtectionSystem]).cast()
"""Protection Systems to be generated. Supported protection systems include
Widevine, PlayReady, FairPlay, Marin and CommonSystem.
"""

pssh = configuration.Field(configuration.HexString).cast()
"""One or more concatenated PSSH boxes in hex string format. If this and
`protection_systems` is not specified, a v1 common PSSH box will be
generated.
Applies to 'raw' encryption_mode only.
"""

iv = configuration.Field(configuration.HexString).cast()
"""IV in hex string format. If not specified, a random IV will be
generated.
Applies to 'raw' encryption_mode only.
"""

keys = configuration.Field(List[RawKeyConfig]).cast()
"""A list of encryption keys to use.
Applies to 'raw' encryption_mode only."""

content_id = configuration.Field(
configuration.HexString, default=RANDOM_CONTENT_ID).cast()
"""The content ID, in hex.
If omitted, a random content ID will be chosen for you.
Applies to 'widevine' encryption_mode only.
"""

key_server_url = configuration.Field(str, default=UAT_SERVER).cast()
"""The URL of your key server.
This is used to generate an encryption key. By default, it is Widevine's UAT
server.
Applies to 'widevine' encryption_mode only.
"""

signer = configuration.Field(str, default=WIDEVINE_TEST_ACCOUNT).cast()
"""The name of the signer when authenticating to the key server.
Applies to 'widevine' encryption_mode only.
Defaults to the Widevine test account.
"""

signing_key = configuration.Field(
configuration.HexString, default=WIDEVINE_TEST_SIGNING_KEY).cast()
"""The signing key, in hex, when authenticating to the key server.
Applies to 'widevine' encryption_mode only.
Defaults to the Widevine test account's key.
"""

signing_iv = configuration.Field(
configuration.HexString, default=WIDEVINE_TEST_SIGNING_IV).cast()
"""The signing IV, in hex, when authenticating to the key server.
Applies to 'widevine' encryption_mode only.
Defaults to the Widevine test account's IV.
"""

Expand All @@ -113,6 +180,29 @@ class EncryptionConfig(configuration.Base):
clear_lead = configuration.Field(int, default=10).cast()
"""The seconds of unencrypted media at the beginning of the stream."""

def __init__(self, *args) -> None:
super().__init__(*args)

# Don't do any further checks if encryption is disabled
if not self.enable:
return

if self.encryption_mode == EncryptionMode.WIDEVINE:
field_names = ['keys', 'pssh', 'iv']
for field_name in field_names:
if getattr(self, field_name):
field = getattr(self.__class__, field_name)
reason = 'cannot be set when encryption_mode is "%s"' % \
self.encryption_mode
raise configuration.MalformedField(
self.__class__, field_name, field, reason)
elif self.encryption_mode == EncryptionMode.RAW:
# Check at least one key has been specified
if not self.keys:
field = self.__class__.keys
reason = 'at least one key must be specified'
raise configuration.MalformedField(
self.__class__, 'keys', field, reason)

class PipelineConfig(configuration.Base):
"""An object representing the entire pipeline config for Shaka Streamer."""
Expand Down
Loading

0 comments on commit ddfd796

Please sign in to comment.