Skip to content

Commit 751e0ba

Browse files
add key rotation method
update readme
1 parent 3e986c5 commit 751e0ba

6 files changed

Lines changed: 137 additions & 15 deletions

File tree

README.md

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ This package uses the **[FFmpeg](https://ffmpeg.org)** to package media content
1919
- [Opening a Resource](#opening-a-resource)
2020
- [DASH](#dash)
2121
- [HLS](#hls)
22-
- [Encrypted HLS](#encrypted-hls)
22+
- [DRM (Encrypted HLS)](#drm-encrypted-hls)
2323
- [Progress](#progress)
2424
- [Saving Files](#saving-files)
2525
- [Probe](#probe)
@@ -46,13 +46,13 @@ pip install python-ffmpeg-video-streaming
4646
### Opening a Resource
4747
There are two ways to open a file:
4848

49-
#### 1. From a FFmpeg supported resources
49+
#### 1. From a FFmpeg supported resource
5050
You can pass a local path of video(or a supported resource) to the method(`hls` or `dash`):
5151
```python
5252
video = '/var/www/media/videos/video.mp4'
5353
```
5454

55-
For opening a file from a supported FFmpeg resource such as `http`, `ftp`, `pipe`, `rtmp` and etc. please see **[FFmpeg Protocols Documentation](https://ffmpeg.org/ffmpeg-protocols.html)**
55+
For opening a file from a supported resource such as `http`, `ftp`, `pipe`, `rtmp` and etc. please see **[FFmpeg Protocols Documentation](https://ffmpeg.org/ffmpeg-protocols.html)**
5656

5757
**For example:**
5858
```python
@@ -142,11 +142,14 @@ r_720p = Representation(width=1280, height=720, kilo_bitrate=2048)
142142
```
143143
**NOTE:** You cannot use HEVC(libx265) and VP9 formats for HLS packaging.
144144

145-
#### Encrypted HLS
145+
#### DRM (Encrypted HLS)
146146
The encryption process requires some kind of secret (key) together with an encryption algorithm. HLS uses AES in cipher block chaining (CBC) mode. This means each block is encrypted using the ciphertext of the preceding block. [Learn more](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation)
147147

148148
You must specify a path to save a random key to your local machine and also a URL(or a path) to access the key on your website(the key you will save must be accessible from your website). You must pass both these parameters to the `encryption` method:
149149

150+
##### Single Key
151+
The following code generates a key for all TS files.
152+
150153
```python
151154
import ffmpeg_streaming
152155

@@ -166,7 +169,46 @@ url = 'https://www.aminyazdanpanah.com/PATH_TO_KEY_DIRECTORY/random_key.key'
166169
.package('/var/www/media/videos/hls/hls-stream.m3u8')
167170
)
168171
```
169-
**NOTE:** It is very important to protect your key on your website using a token or a session/cookie(****It is highly recommended****).
172+
173+
##### Key Rotation
174+
The code below, allows you to encrypt each TS file with a new encryption key. This can improve security and allows for more flexibility. You can also modify the code to use a different key for each set of segments(i.e. if 10 TS files has been generated then rotate the key) or you can generate a new encryption key at every periodic time(i.e. every 10 seconds).
175+
```python
176+
import tempfile
177+
from os.path import join
178+
from random import randrange
179+
180+
import ffmpeg_streaming
181+
from ffmpeg_streaming.key_info_file import generate_key_info_file
182+
183+
save_to = '/home/public_html/PATH_TO_KEY_DIRECTORY/key_rotation'
184+
url = 'https://www.aminyazdanpanah.com/PATH_TO_KEY_DIRECTORY/key_rotation'
185+
key_info_file_path = join(tempfile.gettempdir(), str(randrange(1000, 100000)) + '_py_ff_vi_st.tmp')
186+
k_num = 1
187+
188+
189+
def k_format(name, num):
190+
return str(name) + "_" + str(num)
191+
192+
193+
def progress(per, ffmpeg):
194+
global k_num
195+
if ".ts' for writing" in ffmpeg:
196+
# A new TS file has been created!
197+
generate_key_info_file(k_format(url, k_num), k_format(save_to, k_num), key_info_path=key_info_file_path)
198+
k_num += 1
199+
200+
(
201+
ffmpeg_streaming
202+
.hls(video, hls_flags="periodic_rekey")
203+
.encryption(url, save_to, key_info_path=key_info_file_path)
204+
.format('libx264')
205+
.auto_rep()
206+
.package('/var/www/media/videos/hls/hls-stream.m3u8', progress=progress)
207+
)
208+
```
209+
**NOTE:** It is very important to protect your key(s) on your website using a token or a session/cookie(****It is highly recommended****).
210+
211+
**NOTE:** However HLS supports AES encryption, that you can encrypt your streams, it is not a full DRM solution. If you want to use a full DRM solution, I recommend to try **[FairPlay Streaming](https://developer.apple.com/streaming/fps/)** solution which then securely exchange keys, and protect playback on devices.
170212

171213
See **[HLS examples](https://github.com/aminyazdanpanah/python-ffmpeg-video-streaming/tree/master/examples/hls)** and **[HLS options](https://ffmpeg.org/ffmpeg-formats.html#hls-2)** for more information.
172214

@@ -276,7 +318,7 @@ A path can also be passed to save a copy of files to your local machine.
276318
progress=progress)
277319
)
278320
```
279-
**NOTE:** This option(Save To Clouds) is only for **[VOD](https://en.wikipedia.org/wiki/Video_on_demand)** (it does not support live streaming).
321+
**NOTE:** This option(Save To Clouds) is only valid for **[VOD](https://en.wikipedia.org/wiki/Video_on_demand)** (it does not support live streaming).
280322

281323
**Schema:** The relation is `one-to-many`.
282324

examples/hls/hls_encryption.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def main():
6262
(
6363
ffmpeg_streaming
6464
.hls(args.input)
65-
.encryption(args.key, args.url)
65+
.encryption(args.url, args.key)
6666
.format('libx264')
6767
.auto_rep()
6868
.package(args.output, progress=transcode_progress)
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""
2+
examples.hls.hls_encryption
3+
~~~~~~~~~~~~
4+
5+
Create encrypted HlS streams and manifests
6+
7+
8+
:copyright: (c) 2019 by Amin Yazdanpanah.
9+
:website: https://www.aminyazdanpanah.com
10+
11+
:license: MIT, see LICENSE for more details.
12+
"""
13+
14+
import argparse
15+
import sys
16+
import logging
17+
import tempfile
18+
from os.path import join
19+
from random import randrange
20+
21+
import ffmpeg_streaming
22+
from ffmpeg_streaming.key_info_file import generate_key_info_file
23+
24+
logging.basicConfig(filename='streaming.log', level=logging.NOTSET, format='[%(asctime)s] %(levelname)s: %(message)s')
25+
26+
parser = argparse.ArgumentParser()
27+
parser.add_argument('-i', '--input', required=True, help='The path to the video file (required).')
28+
parser.add_argument('-key', '--key', required=True, help='The full pathname of the file where a random key will '
29+
'be created (required). Note: The path of the key should '
30+
'be accessible from your website(e.g. '
31+
'"/var/www/public_html/keys/enc.key")')
32+
parser.add_argument('-url', '--url', required=True, help='A URL (or a path) to access the key on your website ('
33+
'required). It is highly recommended to protect the key '
34+
'on your website(e.g using a token or check a '
35+
'session/cookie)')
36+
parser.add_argument('-o', '--output', default=None, help='The output to write files.')
37+
args = parser.parse_args()
38+
39+
40+
key_info_file_path = join(tempfile.gettempdir(), str(randrange(1000, 100000)) + '_py_ff_vi_st.tmp')
41+
k_num = 1
42+
43+
44+
def k_format(name, num):
45+
return str(name) + "_" + str(num)
46+
47+
48+
def progress(per, ffmpeg):
49+
global k_num
50+
if ".ts' for writing" in ffmpeg:
51+
generate_key_info_file(k_format(args.url, k_num), k_format(args.key, k_num), key_info_path=key_info_file_path)
52+
k_num += 1
53+
54+
55+
def main():
56+
(
57+
ffmpeg_streaming
58+
.hls(args.input, hls_flags="periodic_rekey")
59+
.encryption(args.url, args.key, key_info_path=key_info_file_path)
60+
.format('libx264')
61+
.auto_rep()
62+
.package(args.output, progress=progress)
63+
)
64+
65+
66+
if __name__ == "__main__":
67+
sys.exit(main())

ffmpeg_streaming/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from .media import *
33
from .rep import *
44
from .clouds import *
5+
from .key_info_file import *
56

67
VERSION = '0.0.13'
7-
__all__ = _ffprobe.__all__ + media.__all__ + rep.__all__ + clouds.__all__
8+
__all__ = _ffprobe.__all__ + media.__all__ + rep.__all__ + clouds.__all__ + key_info_file.__all__

ffmpeg_streaming/key_info_file.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,20 @@
1515
from secrets import token_bytes, token_hex
1616

1717

18-
def generate_key_info_file(url, path, length=16):
18+
def generate_key_info_file(url, path, key_info_path=None, length=16):
1919
with open(path, 'wb') as key:
2020
key.write(token_bytes(length))
21-
with tempfile.NamedTemporaryFile(mode='w', suffix='_py_ff_vi_st.tmp', delete=False) as key_info:
21+
22+
if key_info_path is None:
23+
_key_info_file = tempfile.NamedTemporaryFile(mode='w', suffix='_py_ff_vi_st.tmp', delete=False)
24+
else:
25+
_key_info_file = open(key_info_path, mode="w")
26+
27+
with _key_info_file as key_info:
2228
key_info.write("\n".join([url, path, token_hex(length)]))
2329
return key_info.name
30+
31+
32+
__all__ = [
33+
'generate_key_info_file',
34+
]

ffmpeg_streaming/media.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ def _save_hls_master_playlist(output, master_playlist_path, dirname, name):
5555
class Export(object):
5656
video_format = str
5757
audio_format = str
58-
_ffprobe = dict
58+
_ffprobe = None
59+
_ffmpeg = None
5960
reps = list
6061
output = str
6162
_is_tmp_directory = False
@@ -113,14 +114,14 @@ def package(
113114
_save_hls_master_playlist(output, self.master_playlist_path, dirname, name)
114115

115116
with Process(progress, build_command(cmd, self), c_stdout, c_stderr, c_stdin) as process:
116-
p = process.run(c_input, timeout)
117+
Export._ffmpeg = process.run(c_input, timeout)
117118

118119
save_to_clouds(clouds, dirname)
119120

120121
if output is not None and clouds is not None:
121122
shutil.move(dirname, os.path.dirname(output))
122123

123-
return self, p, Export._ffprobe
124+
return self
124125

125126

126127
class HLS(Export):
@@ -133,8 +134,8 @@ def __init__(self, filename, **options):
133134
self.hls_key_info_file = options.pop('hls_key_info_file', None)
134135
super(HLS, self).__init__(filename, options)
135136

136-
def encryption(self, url, path, length=16):
137-
self.hls_key_info_file = generate_key_info_file(url, path, length)
137+
def encryption(self, url, path, key_info_path=None, length=16):
138+
self.hls_key_info_file = generate_key_info_file(url, path, key_info_path, length)
138139
return self
139140

140141

0 commit comments

Comments
 (0)