├── .gitignore ├── LICENSE ├── README.md ├── poetry.lock ├── pyproject.toml ├── templates ├── index.html └── wvproxy.js └── wvproxy.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | # Project-specific 141 | authorized_users.txt 142 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 nyuszika7h 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wvproxy 2 | 3 | Widevine CDM API 4 | 5 | ## Setup 6 | 1. Install Python 3.6 or newer and [Poetry](https://python-poetry.org/) 7 | 2. Install Python package dependencies using `poetry install` 8 | 3. Activate the virtual environment using `poetry shell` 9 | 4. Run the Flask web app using `python wvproxy.py` (this part can run on any OS) 10 | 5. Open the web app on a Windows computer with Chrome and [widevine-l3-guesser](https://github.com/Satsuoni/widevine-l3-guesser) installed and leave it running 11 | 6. (Optional but recommended) Put the API behind a reverse proxy such as nginx for HTTPS support 12 | 7. Put one or more users and keys in `authorized_users.txt` in TSV (tab-separated values) format: 13 | ``` 14 | alice e50af8c5-02ef-4546-8449-77ab4cf8a271 15 | bob 5fdb1375-e529-4f51-a8a4-9296bfadedfa 16 | ``` 17 | 18 | ## API request format 19 | 20 | ### Get challenge 21 | 22 | #### Request 23 | ```json 24 | { 25 | "method": "GetChallenge", 26 | "params": { 27 | "init": "", 28 | "cert": "" 29 | }, 30 | "token": "" 31 | } 32 | ``` 33 | 34 | #### Response 35 | ```json 36 | { 37 | "status_code": 200, 38 | "message": { 39 | "session_id": "", 40 | "challenge": "" 41 | } 42 | } 43 | ``` 44 | 45 | ### Get keys 46 | 47 | #### Request 48 | ```json 49 | { 50 | "method": "GetKeys", 51 | "params": { 52 | "session_id": "", 53 | "cdmkeyresponse": "" 54 | }, 55 | "token": "" 56 | } 57 | ``` 58 | 59 | #### Response 60 | ```json 61 | { 62 | "status_code": 200, 63 | "message": { 64 | "keys": [ 65 | { 66 | "kid": "", 67 | "key": "" 68 | }, 69 | { 70 | "kid": "", 71 | "key": "" 72 | }, 73 | ... 74 | ] 75 | } 76 | } 77 | ``` 78 | 79 | ### Error responses 80 | ```json 81 | { 82 | "status_code": 4XX|5XX, 83 | "message": "" 84 | } 85 | ``` 86 | 87 | ## Disclaimer 88 | This project is purely for educational purposes. The author claims no responsibility for what you do with it. 89 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "bidict" 3 | version = "0.21.2" 4 | description = "The bidirectional mapping library for Python." 5 | category = "main" 6 | optional = false 7 | python-versions = ">=3.6" 8 | 9 | [package.extras] 10 | coverage = ["coverage (<6)", "pytest-cov (<3)"] 11 | dev = ["setuptools-scm", "hypothesis (<6)", "py (<2)", "pytest (<7)", "pytest-benchmark (>=3.2.0,<4)", "sortedcollections (<2)", "sortedcontainers (<3)", "Sphinx (<4)", "sphinx-autodoc-typehints (<2)", "coverage (<6)", "pytest-cov (<3)", "pre-commit (<3)", "tox (<4)"] 12 | docs = ["Sphinx (<4)", "sphinx-autodoc-typehints (<2)"] 13 | precommit = ["pre-commit (<3)"] 14 | test = ["hypothesis (<6)", "py (<2)", "pytest (<7)", "pytest-benchmark (>=3.2.0,<4)", "sortedcollections (<2)", "sortedcontainers (<3)", "Sphinx (<4)", "sphinx-autodoc-typehints (<2)"] 15 | 16 | [[package]] 17 | name = "click" 18 | version = "8.0.1" 19 | description = "Composable command line interface toolkit" 20 | category = "main" 21 | optional = false 22 | python-versions = ">=3.6" 23 | 24 | [package.dependencies] 25 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 26 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 27 | 28 | [[package]] 29 | name = "colorama" 30 | version = "0.4.4" 31 | description = "Cross-platform colored terminal text." 32 | category = "main" 33 | optional = false 34 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 35 | 36 | [[package]] 37 | name = "construct" 38 | version = "2.8.8" 39 | description = "A powerful declarative parser/builder for binary data" 40 | category = "main" 41 | optional = false 42 | python-versions = "*" 43 | 44 | [[package]] 45 | name = "dataclasses" 46 | version = "0.8" 47 | description = "A backport of the dataclasses module for Python 3.6" 48 | category = "main" 49 | optional = false 50 | python-versions = ">=3.6, <3.7" 51 | 52 | [[package]] 53 | name = "flask" 54 | version = "2.0.1" 55 | description = "A simple framework for building complex web applications." 56 | category = "main" 57 | optional = false 58 | python-versions = ">=3.6" 59 | 60 | [package.dependencies] 61 | click = ">=7.1.2" 62 | itsdangerous = ">=2.0" 63 | Jinja2 = ">=3.0" 64 | Werkzeug = ">=2.0" 65 | 66 | [package.extras] 67 | async = ["asgiref (>=3.2)"] 68 | dotenv = ["python-dotenv"] 69 | 70 | [[package]] 71 | name = "flask-socketio" 72 | version = "5.1.1" 73 | description = "Socket.IO integration for Flask applications" 74 | category = "main" 75 | optional = false 76 | python-versions = ">=3.6" 77 | 78 | [package.dependencies] 79 | Flask = ">=0.9" 80 | python-socketio = ">=5.0.2" 81 | 82 | [[package]] 83 | name = "h11" 84 | version = "0.12.0" 85 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 86 | category = "main" 87 | optional = false 88 | python-versions = ">=3.6" 89 | 90 | [[package]] 91 | name = "importlib-metadata" 92 | version = "4.6.3" 93 | description = "Read metadata from Python packages" 94 | category = "main" 95 | optional = false 96 | python-versions = ">=3.6" 97 | 98 | [package.dependencies] 99 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 100 | zipp = ">=0.5" 101 | 102 | [package.extras] 103 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 104 | perf = ["ipython"] 105 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] 106 | 107 | [[package]] 108 | name = "itsdangerous" 109 | version = "2.0.1" 110 | description = "Safely pass data to untrusted environments and back." 111 | category = "main" 112 | optional = false 113 | python-versions = ">=3.6" 114 | 115 | [[package]] 116 | name = "jinja2" 117 | version = "3.0.1" 118 | description = "A very fast and expressive template engine." 119 | category = "main" 120 | optional = false 121 | python-versions = ">=3.6" 122 | 123 | [package.dependencies] 124 | MarkupSafe = ">=2.0" 125 | 126 | [package.extras] 127 | i18n = ["Babel (>=2.7)"] 128 | 129 | [[package]] 130 | name = "markupsafe" 131 | version = "2.0.1" 132 | description = "Safely add untrusted strings to HTML/XML markup." 133 | category = "main" 134 | optional = false 135 | python-versions = ">=3.6" 136 | 137 | [[package]] 138 | name = "pymp4" 139 | version = "1.2.0" 140 | description = "A Python parser for MP4 boxes" 141 | category = "main" 142 | optional = false 143 | python-versions = "*" 144 | 145 | [package.dependencies] 146 | construct = "2.8.8" 147 | 148 | [[package]] 149 | name = "python-engineio" 150 | version = "4.2.1" 151 | description = "Engine.IO server and client for Python" 152 | category = "main" 153 | optional = false 154 | python-versions = ">=3.6" 155 | 156 | [package.extras] 157 | asyncio_client = ["aiohttp (>=3.4)"] 158 | client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] 159 | 160 | [[package]] 161 | name = "python-socketio" 162 | version = "5.4.0" 163 | description = "Socket.IO server and client for Python" 164 | category = "main" 165 | optional = false 166 | python-versions = ">=3.6" 167 | 168 | [package.dependencies] 169 | bidict = ">=0.21.0" 170 | python-engineio = ">=4.1.0" 171 | 172 | [package.extras] 173 | asyncio_client = ["aiohttp (>=3.4)"] 174 | client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] 175 | 176 | [[package]] 177 | name = "simple-websocket" 178 | version = "0.3.0" 179 | description = "Simple WebSocket server and client for Python" 180 | category = "main" 181 | optional = false 182 | python-versions = ">=3.6" 183 | 184 | [package.dependencies] 185 | wsproto = "*" 186 | 187 | [[package]] 188 | name = "typing-extensions" 189 | version = "3.10.0.0" 190 | description = "Backported and Experimental Type Hints for Python 3.5+" 191 | category = "main" 192 | optional = false 193 | python-versions = "*" 194 | 195 | [[package]] 196 | name = "werkzeug" 197 | version = "2.0.1" 198 | description = "The comprehensive WSGI web application library." 199 | category = "main" 200 | optional = false 201 | python-versions = ">=3.6" 202 | 203 | [package.dependencies] 204 | dataclasses = {version = "*", markers = "python_version < \"3.7\""} 205 | 206 | [package.extras] 207 | watchdog = ["watchdog"] 208 | 209 | [[package]] 210 | name = "wsproto" 211 | version = "0.14.1" 212 | description = "WebSockets state-machine based protocol implementation" 213 | category = "main" 214 | optional = false 215 | python-versions = "*" 216 | 217 | [package.dependencies] 218 | h11 = ">=0.8.1" 219 | 220 | [[package]] 221 | name = "zipp" 222 | version = "3.5.0" 223 | description = "Backport of pathlib-compatible object wrapper for zip files" 224 | category = "main" 225 | optional = false 226 | python-versions = ">=3.6" 227 | 228 | [package.extras] 229 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 230 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] 231 | 232 | [metadata] 233 | lock-version = "1.1" 234 | python-versions = "^3.6" 235 | content-hash = "d41080be05e6a439073a4af4b462041589d6982bb7580578004e7aa8f9249064" 236 | 237 | [metadata.files] 238 | bidict = [ 239 | {file = "bidict-0.21.2-py2.py3-none-any.whl", hash = "sha256:929d056e8d0d9b17ceda20ba5b24ac388e2a4d39802b87f9f4d3f45ecba070bf"}, 240 | {file = "bidict-0.21.2.tar.gz", hash = "sha256:4fa46f7ff96dc244abfc437383d987404ae861df797e2fd5b190e233c302be09"}, 241 | ] 242 | click = [ 243 | {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, 244 | {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, 245 | ] 246 | colorama = [ 247 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 248 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 249 | ] 250 | construct = [ 251 | {file = "construct-2.8.8.tar.gz", hash = "sha256:1b84b8147f6fd15bcf64b737c3e8ac5100811ad80c830cb4b2545140511c4157"}, 252 | ] 253 | dataclasses = [ 254 | {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, 255 | {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, 256 | ] 257 | flask = [ 258 | {file = "Flask-2.0.1-py3-none-any.whl", hash = "sha256:a6209ca15eb63fc9385f38e452704113d679511d9574d09b2cf9183ae7d20dc9"}, 259 | {file = "Flask-2.0.1.tar.gz", hash = "sha256:1c4c257b1892aec1398784c63791cbaa43062f1f7aeb555c4da961b20ee68f55"}, 260 | ] 261 | flask-socketio = [ 262 | {file = "Flask-SocketIO-5.1.1.tar.gz", hash = "sha256:1efdaacc7a26e94f2b197a80079b1058f6aa644a6094c0a322349e2b9c41f6b1"}, 263 | {file = "Flask_SocketIO-5.1.1-py3-none-any.whl", hash = "sha256:07e1899e3b4851978b2ac8642080156c6294f8d0fc5212b4e4bcca713830306a"}, 264 | ] 265 | h11 = [ 266 | {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, 267 | {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, 268 | ] 269 | importlib-metadata = [ 270 | {file = "importlib_metadata-4.6.3-py3-none-any.whl", hash = "sha256:51c6635429c77cf1ae634c997ff9e53ca3438b495f10a55ba28594dd69764a8b"}, 271 | {file = "importlib_metadata-4.6.3.tar.gz", hash = "sha256:0645585859e9a6689c523927a5032f2ba5919f1f7d0e84bd4533312320de1ff9"}, 272 | ] 273 | itsdangerous = [ 274 | {file = "itsdangerous-2.0.1-py3-none-any.whl", hash = "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c"}, 275 | {file = "itsdangerous-2.0.1.tar.gz", hash = "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0"}, 276 | ] 277 | jinja2 = [ 278 | {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, 279 | {file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"}, 280 | ] 281 | markupsafe = [ 282 | {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, 283 | {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, 284 | {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, 285 | {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, 286 | {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, 287 | {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, 288 | {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, 289 | {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, 290 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, 291 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, 292 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, 293 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, 294 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, 295 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, 296 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, 297 | {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, 298 | {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, 299 | {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, 300 | {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, 301 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, 302 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, 303 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, 304 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, 305 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, 306 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, 307 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, 308 | {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, 309 | {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, 310 | {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, 311 | {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, 312 | {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, 313 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, 314 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, 315 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, 316 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, 317 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, 318 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, 319 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, 320 | {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, 321 | {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, 322 | {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, 323 | {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, 324 | {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, 325 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, 326 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, 327 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, 328 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, 329 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, 330 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, 331 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, 332 | {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, 333 | {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, 334 | {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, 335 | {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, 336 | ] 337 | pymp4 = [ 338 | {file = "pymp4-1.2.0.tar.gz", hash = "sha256:4a3d2e0838cfe28cd3dc64f45379e16d91b0212192f87a3e28f3804372727456"}, 339 | ] 340 | python-engineio = [ 341 | {file = "python-engineio-4.2.1.tar.gz", hash = "sha256:d510329b6d8ed5662547862f58bc73659ae62defa66b66d745ba021de112fa62"}, 342 | {file = "python_engineio-4.2.1-py3-none-any.whl", hash = "sha256:f3ef9a2c048d08990f294c5f8991f6f162c3b12ecbd368baa0d90441de907d1c"}, 343 | ] 344 | python-socketio = [ 345 | {file = "python-socketio-5.4.0.tar.gz", hash = "sha256:ca807c9e1f168e96dea412d64dd834fb47c470d27fd83da0504aa4b248ba2544"}, 346 | {file = "python_socketio-5.4.0-py3-none-any.whl", hash = "sha256:7ed57f6c024abdfeb9b25c74c0c00ffc18da47d903e8d72deecb87584370d1fc"}, 347 | ] 348 | simple-websocket = [ 349 | {file = "simple-websocket-0.3.0.tar.gz", hash = "sha256:86f7ed6eefc2db53648b2c7a3b731dd93a9572d52bc8e3889f098cdf55459b7e"}, 350 | {file = "simple_websocket-0.3.0-py3-none-any.whl", hash = "sha256:20015b3ca1b192c77bba575a8b23b6a7e227548a8d35022592b63a6aab10cdd4"}, 351 | ] 352 | typing-extensions = [ 353 | {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, 354 | {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, 355 | {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, 356 | ] 357 | werkzeug = [ 358 | {file = "Werkzeug-2.0.1-py3-none-any.whl", hash = "sha256:6c1ec500dcdba0baa27600f6a22f6333d8b662d22027ff9f6202e3367413caa8"}, 359 | {file = "Werkzeug-2.0.1.tar.gz", hash = "sha256:1de1db30d010ff1af14a009224ec49ab2329ad2cde454c8a708130642d579c42"}, 360 | ] 361 | wsproto = [ 362 | {file = "wsproto-0.14.1-py2.py3-none-any.whl", hash = "sha256:2b870f5b5b4a6d23dce080a4ee1cbb119b2378f82593bd6d66ae2cbd72a7c0ad"}, 363 | {file = "wsproto-0.14.1.tar.gz", hash = "sha256:ed222c812aaea55d72d18a87df429cfd602e15b6c992a07a53b495858f083a14"}, 364 | ] 365 | zipp = [ 366 | {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"}, 367 | {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"}, 368 | ] 369 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "wvproxy" 3 | version = "0.1.0" 4 | description = "Widevine CDM API" 5 | authors = ["nyuszika7h "] 6 | license = "MIT" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.6" 10 | Flask = "^2.0.1" 11 | Flask-SocketIO = "^5.1.1" 12 | pymp4 = "^1.2.0" 13 | simple-websocket = "^0.3.0" 14 | 15 | [tool.poetry.dev-dependencies] 16 | 17 | [build-system] 18 | requires = ["poetry-core>=1.0.0"] 19 | build-backend = "poetry.core.masonry.api" 20 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WVProxy 6 | 7 | 8 | 9 | 10 | 11 |

WVProxy

12 |

Just leave this page open and your browser will handle the requests.

13 |

Unable to detect widevine-l3-guesser status. Make sure to have JavaScript enabled.

14 | 15 | 16 | -------------------------------------------------------------------------------- /templates/wvproxy.js: -------------------------------------------------------------------------------- 1 | window.onload = function () { 2 | if (typeof WidevineCrypto !== 'undefined') { 3 | $('#status').css('color', 'green').text('widevine-l3-guesser is installed.'); 4 | } else { 5 | $('#status').css('color', 'red').text('widevine-l3-guesser is NOT installed. The API will not work.'); 6 | } 7 | }; 8 | 9 | (async function () { 10 | function base64ToBuffer(data) { 11 | return Uint8Array.from(atob(data), x => x.charCodeAt(0)); 12 | } 13 | 14 | let sessions = {}; 15 | 16 | let keySystemAccess = await navigator.requestMediaKeySystemAccess('com.widevine.alpha', [{ 17 | 'initDataTypes': ['cenc'], 18 | 'audioCapabilities': [{ 19 | 'contentType': 'audio/mp4;codecs="mp4a.40.2"', 20 | }], 21 | 'videoCapabilities': [{ 22 | 'contentType': 'video/mp4;codecs="avc1.42E01E"', 23 | }], 24 | }]); 25 | 26 | let socket = io(); 27 | 28 | console.log('Socket initialized'); 29 | 30 | socket.on('GetChallenge', async function(req, cb) { 31 | console.log('GetChallenge called with params: %o', req); 32 | 33 | let mediaKeys = await keySystemAccess.createMediaKeys(); 34 | await mediaKeys.setServerCertificate( 35 | DeviceCertificate.read(new Pbf(base64ToBuffer(req.params.cert))).serial_number 36 | ); 37 | 38 | let session = mediaKeys.createSession('temporary'); 39 | session.generateRequest('cenc', base64ToBuffer(req.params.init)); 40 | 41 | session.addEventListener('message', async function (event) { 42 | if (event.messageType == 'license-request') { 43 | socket.emit('SetChallenge', { 44 | session_id: req.params.session_id, 45 | challenge: event.message, 46 | }); 47 | sessions[req.params.session_id] = event.message; 48 | await session.close(); 49 | } 50 | }) 51 | }); 52 | 53 | socket.on('GetKeys', async function (req, cb) { 54 | console.log('GetKeys called with params: %o', req); 55 | 56 | let data = { 57 | licenseRequest: sessions[req.params.session_id], 58 | licenseResponse: base64ToBuffer(req.params.cdmkeyresponse), 59 | keys: new Map(), 60 | }; 61 | 62 | await WidevineCrypto.decryptContentKey(null, data); 63 | 64 | let keys = []; 65 | for (let [kid, key] of data.keys) { 66 | keys.push({ 67 | kid: kid, 68 | key: key, 69 | }); 70 | } 71 | 72 | console.log('Keys: %o', keys); 73 | 74 | socket.emit('SetKeys', { 75 | session_id: req.params.session_id, 76 | keys: keys, 77 | }); 78 | }); 79 | })(); 80 | -------------------------------------------------------------------------------- /wvproxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import base64 4 | import sys 5 | import time 6 | import uuid 7 | from collections import defaultdict 8 | 9 | from flask import Flask, render_template, request 10 | from flask_socketio import SocketIO 11 | from pymp4.parser import Box 12 | 13 | 14 | app = Flask(__name__) 15 | socketio = SocketIO(app, async_mode='threading') 16 | sessions = defaultdict(dict) 17 | 18 | 19 | def log(message, *args, **kwargs): 20 | kwargs.setdefault('file', sys.stderr) 21 | kwargs.setdefault('flush', True) 22 | print(message, *args, **kwargs) 23 | 24 | 25 | @app.route('/', methods=['GET']) 26 | def index(): 27 | return render_template('index.html') 28 | 29 | 30 | @app.route('/wvproxy.js', methods=['GET']) 31 | def wvproxy_js(): 32 | return render_template('wvproxy.js') 33 | 34 | 35 | @app.route('/api', methods=['POST']) 36 | def api(): 37 | req = request.get_json() 38 | 39 | method = req['method'] 40 | 41 | authorized = False 42 | with open('authorized_users.txt') as fd: 43 | for line in fd.read().splitlines(): 44 | name, token = line.split() 45 | if token == req['token']: 46 | log(f'[{name}] {req}') 47 | authorized = True 48 | break 49 | 50 | if not authorized: 51 | log(f'[anonymous] {req}') 52 | return { 53 | 'status_code': 403, 54 | 'message': 'Invalid token', 55 | } 56 | 57 | del req['token'] 58 | 59 | if method == 'GetChallenge': 60 | session_id = str(uuid.uuid4()) 61 | req['params']['session_id'] = session_id 62 | 63 | try: 64 | Box.parse(base64.b64decode(req['params']['init'])) 65 | except OSError: 66 | req['params']['init'] = base64.b64encode(Box.build(dict( 67 | type=b'pssh', 68 | version=0, 69 | flags=0, 70 | system_ID=uuid.UUID('edef8ba9-79d6-4ace-a3c8-27dcd51d21ed'), 71 | init_data=base64.b64decode(req['params']['init']), 72 | ))).decode() 73 | 74 | socketio.emit('GetChallenge', req) 75 | 76 | start = time.time() 77 | log('Waiting for challenge', end='') 78 | while not sessions[session_id]: 79 | if time.time() - start > 15: 80 | return { 81 | 'status_code': 504, 82 | 'message': 'Request timed out', 83 | } 84 | log('.', end='') 85 | time.sleep(0.5) 86 | 87 | return { 88 | 'status_code': 200, 89 | 'message': { 90 | 'session_id': session_id, 91 | 'challenge': base64.b64encode(sessions[session_id]['challenge']).decode(), 92 | } 93 | } 94 | elif method == 'GetKeys': 95 | session_id = req['params']['session_id'] 96 | if session_id not in sessions: 97 | return { 98 | 'status_code': 400, 99 | 'message': 'Invalid session ID', 100 | } 101 | 102 | socketio.emit('GetKeys', req) 103 | 104 | start = time.time() 105 | log('Waiting for keys', end='') 106 | while 'keys' not in sessions[session_id]: 107 | if time.time() - start > 15: 108 | return { 109 | 'status_code': 504, 110 | 'message': 'Request timed out', 111 | } 112 | log('.', end='') 113 | time.sleep(0.5) 114 | 115 | return { 116 | 'status_code': 200, 117 | 'message': { 118 | 'keys': sessions[session_id]['keys'], 119 | }, 120 | } 121 | elif method == 'GetKeysX': 122 | return { 123 | 'status_code': 400, 124 | 'message': 'Not implemented', 125 | } 126 | else: 127 | return { 128 | 'status_code': 400, 129 | 'message': 'Unknown method', 130 | } 131 | 132 | 133 | @socketio.on('SetChallenge') 134 | def on_set_challenge(res): 135 | log('\nGot challenge') 136 | log(base64.b64encode(res['challenge'])) 137 | sessions[res['session_id']]['challenge'] = res['challenge'] 138 | 139 | 140 | @socketio.on('SetKeys') 141 | def on_set_keys(res): 142 | log('\nGot keys') 143 | log(res['keys']) 144 | sessions[res['session_id']]['keys'] = res['keys'] 145 | 146 | 147 | if __name__ == '__main__': 148 | socketio.run(app) 149 | --------------------------------------------------------------------------------