-
Notifications
You must be signed in to change notification settings - Fork 1
/
polyv.py
192 lines (151 loc) · 5.4 KB
/
polyv.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
import base64
from dataclasses import dataclass, field
import hashlib
import json
import re
from mitmproxy.http import HTTPFlow
from pathlib import Path
from typing import Dict
from xepor import InterceptedAPI
try:
from Cryptodome.Cipher import AES
except ImportError:
pass
# mitmweb analysis filter: ! (~a | ~u "(woff|ttf)$" | ~m "OPTION")
REDACTED_CONSTANT = """
This script is used to bypass a video DRM system, to decrypt and download online coursewares,
which is *clearly* against its ToS. So the core implementation of decryption algorithm is
removed from this example. Other sensitive information is replaced with `REDACTED_CONSTANT`.
The script is only for demostration of Xepor's (unlimited) capability.
"""
print(REDACTED_CONSTANT)
HOST_DRM_VICTIM = REDACTED_CONSTANT
HOST_POLYV_INFO = "player.polyv.net"
HOST_POLYV_LICENSE = "hls.videocc.net"
VIDEO_DIR = Path("./videos")
KEYS_DIR = VIDEO_DIR / "keys"
KEYS_DIR.mkdir(parents=True, exist_ok=True)
@dataclass
class Lesson:
vid: str
title: str
category: str
uid: int
order: int
content_m3u8: bytes = None
content_key: bytes = None
content_info: bytes = None
def good(self):
return (
self.content_m3u8 is not None
and self.content_key is not None
and self.content_info is not None
)
@dataclass
class Ctx:
lessons: Dict[str, Lesson] = field(default_factory=dict)
title: str = ""
root_folder: str = "CISSP"
ctx = Ctx()
api = InterceptedAPI(HOST_DRM_VICTIM)
@api.route(
"/{REDACTED_CONSTANT}/",
HOST_DRM_VICTIM,
reqtype=InterceptedAPI.RESPONSE,
)
def lessons_list(flow: HTTPFlow):
"Get metadata from original server, optional, but output files hierarchy would be cleaner"
if flow.request.method == "OPTIONS":
# Skip preflight
return
payload = flow.response.json()
review_video_list = payload["data"]["reviewVideoList"]
def get_lessons(children, category):
"Find video items and dive into categories recursively, save everything into ctx.lessons"
count_vid = 0
count_cat = 0
for kid in children:
if kid.get("isPlay") is True:
count_vid += 1
lesson = Lesson(
kid["vid"][:-2],
kid["title"],
category,
kid["id"],
count_vid,
)
ctx.lessons[lesson.vid] = lesson
else:
count_cat += 1
get_lessons(kid["children"], f"{category}/{count_cat}_{kid['title']}")
get_lessons(review_video_list, ctx.root_folder)
print(
f"Got {len(ctx.lessons)} lessons"
)
@api.route("/videojson/{vid}_c.json", HOST_POLYV_INFO, reqtype=InterceptedAPI.RESPONSE)
def parse_videojson(flow: HTTPFlow, vid):
"Step 1, get video information from DRM provider, contain links to the video"
lesson = ctx.lessons[vid]
key1 = REDACTED_CONSTANT
key2 = REDACTED_CONSTANT
# json <body> key include encrypted payload
payload = bytes.fromhex(flow.response.json()["body"])
lesson.content_info = json.loads(base64.b64decode(decrypt(key1, key2, payload)))
print(
f"Decrypted videojson for {lesson.title}, seed={lesson.content_info[REDACTED_CONSTANT]}"
)
@api.route("/{}/{}/{vid}_{}.m3u8", HOST_POLYV_LICENSE, reqtype=InterceptedAPI.RESPONSE)
def lesson_m3u8(flow: HTTPFlow, *_, vid):
"""
Step 2, get video from DRM provider,
this is a m3u8 playlist containing links to .ts decryption key (but encrypted)
and .ts files (also encrypted).
.ts files are served from CDN, ffmpeg could directly access their contents.
No need to download inside our script.
"""
lesson = ctx.lessons[vid]
lesson.content_m3u8 = flow.response.get_content()
print("Found playlist for", lesson.uid, lesson.title)
@api.route(
"/playsafe/{}/{}/{vid}_{}.key", HOST_POLYV_LICENSE, reqtype=InterceptedAPI.RESPONSE
)
def lesson_key(flow: HTTPFlow, *_, vid):
"""
Step 3, get key from DRM provider.
We'll decrypt that key later.
"""
lesson = ctx.lessons[vid]
lesson.content_key = flow.response.get_content()
print("Found key for", lesson.uid, lesson.title)
# Everything needed is done. Good to go!
process_lesson(lesson)
def decrypt(_: bytes, __: bytes, ___: bytes):
return REDACTED_CONSTANT
def process_lesson(lesson: Lesson):
if not lesson.good():
print("Not enough info to decrypt the video:", lesson)
return False
decrypted_key = REDACTED_CONSTANT
print("Decrypted key:", decrypted_key.hex())
# Write video encryption keys
key_path = KEYS_DIR / f"{lesson.uid}_{lesson.vid}.key"
with key_path.open("wb") as f:
f.write(decrypted_key[:REDACTED_CONSTANT])
# Replace key URL with our decrypted ones (later served by python -m http.server)
m3u8_content = re.sub(
rb'(METHOD=AES-128,URI=")([^\"]+)',
rb"\1http://localhost:8000/keys/" + key_path.name.encode(),
lesson.content_m3u8,
)
# Write m3u8
m3u8_path = (
VIDEO_DIR / ctx.title / lesson.category / f"{lesson.order}_{lesson.title}.m3u8"
)
m3u8_path.parent.mkdir(parents=True, exist_ok=True)
with m3u8_path.open("wb") as f:
f.write(m3u8_content)
print("Saved m3u8 to", str(m3u8_path))
# Done! simply use ffmpeg to convert m3u8 to mp4.
return True
addons = [api]
print(__name__, "is loaded!")