Reupload after github DMCA

This commit is contained in:
TPD94 2025-04-24 17:06:14 -04:00
parent 9c76337a27
commit 27184f0abd
66 changed files with 8309 additions and 2 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
*.pyc
*.prd
*.wvd
*.db
.idea/
configs/config.yaml

View File

@ -1,3 +1,29 @@
# CDRM-Project
## CDRM-Project
![forthebadge](https://forthebadge.com/images/badges/uses-html.svg) ![forthebadge](https://forthebadge.com/images/badges/uses-css.svg) ![forthebadge](https://forthebadge.com/images/badges/uses-javascript.svg) ![forthebadge](https://forthebadge.com/images/badges/made-with-python.svg)
## What is this?
An open source web application written in python to decrypt Widevine and PlayReady protected content.
## Prerequisites
- [Python](https://www.python.org/downloads/) with PIP installed
> Python 3.13 was used at the time of writing
## Installation
- Open your terminal and navigate to where you'd like to store the application
- Create a new python virtual environment using `python -m venv CDRM-Project`
- Change directory into the new `CDRM-Project` folder
- Activate the virtual environment
> Windows - change directory into the `Scripts` directory then `activate.bat`
>
> Linux - `source bin/activate`
- Extract CDRM-Project 2.0 git contents into the newly created `CDRM-Project` folder
- Install python dependencies `pip install -r requirements.txt`
- (Optional) Place your .WVD file into `/configs/CDMs/WV`
- (Optional) Place your .PRD file into `/configs/CDMs/PR`
- Run the application `python main.py`
Self Hosted Widevine & Playready web application written in Python/JavaScript utilizing the Flask/Tailwind Framework and ReactJS library.

23
cdrm-frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

8
cdrm-frontend/README.md Normal file
View File

@ -0,0 +1,8 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

5
cdrm-frontend/dist/discord.svg vendored Normal file
View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<circle cx="512" cy="512" r="512" style="fill:#5865f2"/>
<path d="M689.43 349a422.21 422.21 0 0 0-104.22-32.32 1.58 1.58 0 0 0-1.68.79 294.11 294.11 0 0 0-13 26.66 389.78 389.78 0 0 0-117.05 0 269.75 269.75 0 0 0-13.18-26.66 1.64 1.64 0 0 0-1.68-.79A421 421 0 0 0 334.44 349a1.49 1.49 0 0 0-.69.59c-66.37 99.17-84.55 195.9-75.63 291.41a1.76 1.76 0 0 0 .67 1.2 424.58 424.58 0 0 0 127.85 64.63 1.66 1.66 0 0 0 1.8-.59 303.45 303.45 0 0 0 26.15-42.54 1.62 1.62 0 0 0-.89-2.25 279.6 279.6 0 0 1-39.94-19 1.64 1.64 0 0 1-.16-2.72c2.68-2 5.37-4.1 7.93-6.22a1.58 1.58 0 0 1 1.65-.22c83.79 38.26 174.51 38.26 257.31 0a1.58 1.58 0 0 1 1.68.2c2.56 2.11 5.25 4.23 8 6.24a1.64 1.64 0 0 1-.14 2.72 262.37 262.37 0 0 1-40 19 1.63 1.63 0 0 0-.87 2.28 340.72 340.72 0 0 0 26.13 42.52 1.62 1.62 0 0 0 1.8.61 423.17 423.17 0 0 0 128-64.63 1.64 1.64 0 0 0 .67-1.18c10.68-110.44-17.88-206.38-75.7-291.42a1.3 1.3 0 0 0-.63-.63zM427.09 582.85c-25.23 0-46-23.16-46-51.6s20.38-51.6 46-51.6c25.83 0 46.42 23.36 46 51.6.02 28.44-20.37 51.6-46 51.6zm170.13 0c-25.23 0-46-23.16-46-51.6s20.38-51.6 46-51.6c25.83 0 46.42 23.36 46 51.6.01 28.44-20.17 51.6-46 51.6z" style="fill:#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

9
cdrm-frontend/dist/docu_logo.svg vendored Normal file
View File

@ -0,0 +1,9 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 1024 1024" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="#000000">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier">
<path d="M877.685565 727.913127l-0.584863-0.365539a32.898541 32.898541 0 0 1-8.041866-46.423497 411.816631 411.816631 0 1 0-141.829267 145.777092c14.621574-8.992268 33.62962-5.117551 43.645398 8.772944l0.146216 0.073108a30.412874 30.412874 0 0 1-7.968758 43.206751l-6.141061 4.020933a475.201154 475.201154 0 1 1 163.615412-164.419599 29.974227 29.974227 0 0 1-42.841211 9.357807z m-537.342843-398.584106c7.164571-7.091463 24.71046-9.650239 33.26408 0 10.600641 11.185504 7.164571 29.462472 0 37.138798l-110.612207 107.468569L370.901811 576.14119c7.164571 7.091463 8.114974 27.342343 0 35.384209-9.796455 9.723347-29.828011 8.188081-36.480827 1.535265L208.309909 487.388236a18.423183 18.423183 0 0 1 0-25.953294l132.032813-132.032813z m343.314556 0l132.032813 132.032813a18.423183 18.423183 0 0 1 0 25.953294L689.652124 613.133772c-6.652816 6.579708-25.587754 10.746857-36.553935 0-10.30821-10.235102-7.091463-31.290168 0-38.381632l108.345863-100.669537-111.855041-108.638294c-7.164571-7.676326-9.504023-26.611265 0-36.04218 9.284699-9.138484 26.903696-7.091463 34.068267 0z m-135.54199-26.318833c3.582286-9.504023 21.347498-15.498868 32.679217-11.258612 10.819965 4.020933 17.180349 19.008046 14.256035 28.512069l-119.896906 329.716493c-3.509178 9.504023-20.616419 13.305632-30.193551 9.723347-10.161994-3.509178-21.201282-17.545889-17.545888-26.976804l120.627985-329.716493z" fill="#ffffff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
cdrm-frontend/dist/favico.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

19
cdrm-frontend/dist/github.svg vendored Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>github [#142]</title>
<desc>Created with Sketch.</desc>
<defs>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Dribbble-Light-Preview" transform="translate(-140.000000, -7559.000000)" fill="#000000">
<g id="icons" transform="translate(56.000000, 160.000000)">
<path d="M94,7399 C99.523,7399 104,7403.59 104,7409.253 C104,7413.782 101.138,7417.624 97.167,7418.981 C96.66,7419.082 96.48,7418.762 96.48,7418.489 C96.48,7418.151 96.492,7417.047 96.492,7415.675 C96.492,7414.719 96.172,7414.095 95.813,7413.777 C98.04,7413.523 100.38,7412.656 100.38,7408.718 C100.38,7407.598 99.992,7406.684 99.35,7405.966 C99.454,7405.707 99.797,7404.664 99.252,7403.252 C99.252,7403.252 98.414,7402.977 96.505,7404.303 C95.706,7404.076 94.85,7403.962 94,7403.958 C93.15,7403.962 92.295,7404.076 91.497,7404.303 C89.586,7402.977 88.746,7403.252 88.746,7403.252 C88.203,7404.664 88.546,7405.707 88.649,7405.966 C88.01,7406.684 87.619,7407.598 87.619,7408.718 C87.619,7412.646 89.954,7413.526 92.175,7413.785 C91.889,7414.041 91.63,7414.493 91.54,7415.156 C90.97,7415.418 89.522,7415.871 88.63,7414.304 C88.63,7414.304 88.101,7413.319 87.097,7413.247 C87.097,7413.247 86.122,7413.234 87.029,7413.87 C87.029,7413.87 87.684,7414.185 88.139,7415.37 C88.139,7415.37 88.726,7417.2 91.508,7416.58 C91.513,7417.437 91.522,7418.245 91.522,7418.489 C91.522,7418.76 91.338,7419.077 90.839,7418.982 C86.865,7417.627 84,7413.783 84,7409.253 C84,7403.59 88.478,7399 94,7399" id="github-[#142]">
</path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

1
cdrm-frontend/dist/home.svg vendored Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0,0,256,256" width="50px" height="50px" fill-rule="nonzero"><g fill="#ffffff" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><g transform="scale(5.12,5.12)"><path d="M24.96289,1.05469c-0.20987,0.00724 -0.41214,0.08036 -0.57812,0.20898l-23,17.94727c-0.43579,0.33978 -0.51361,0.96851 -0.17383,1.4043c0.33978,0.43579 0.96851,0.51361 1.4043,0.17383l1.38477,-1.08008v26.29102c0.00006,0.55226 0.44774,0.99994 1,1h13.83203c0.10799,0.01785 0.21818,0.01785 0.32617,0h11.67383c0.10799,0.01785 0.21818,0.01785 0.32617,0h13.8418c0.55226,-0.00006 0.99994,-0.44774 1,-1v-26.29102l1.38477,1.08008c0.2819,0.21983 0.65967,0.27257 0.991,0.13833c0.33133,-0.13423 0.56586,-0.43504 0.61526,-0.7891c0.0494,-0.35406 -0.09386,-0.70757 -0.37579,-0.92736l-7.61523,-5.94141v-7.26953h-6v2.58594l-9.38477,-7.32227c-0.18607,-0.14428 -0.41707,-0.21828 -0.65234,-0.20898zM25,3.32227l19,14.82617v26.85156h-12v-19h-14v19h-12v-26.85156zM37,8h2v3.70898l-2,-1.5625zM20,28h10v17h-10z"></path></g></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

21
cdrm-frontend/dist/index.html vendored Normal file
View File

@ -0,0 +1,21 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favico.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Decrypt Widevine and PlayReady protected content." />
<meta name="keywords" content="CDRM, Widevine, PlayReady, DRM, Decrypt, CDM, CDM-Project, CDRM-Project, TPD94, Decryption" />
<meta property='og:title' content='CDRM-Project' />
<meta property='og:description' content='Decrypt Widevine & PlayReady Content' />
<meta property='og:image' content='https://cdrm-project.com/lockforog.png' />
<meta property='og:url' content='https://cdrm-project.com/' />
<meta property='og:locale' content='en_US' />
<title>CDRM-Project</title>
<script type="module" crossorigin src="/assets/index-BCBsxJZZ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-0Rv9u7Qs.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

BIN
cdrm-frontend/dist/lockforog.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

1
cdrm-frontend/dist/logo.svg vendored Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.5 13.100000381469727 321.79998779296875 36.69999694824219" data-asc="0.984"><g fill="#ffffff"><g fill="#ffffff" transform="translate(0, 0)"><path d="M13.15 49.80Q10.10 49.80 7.60 48.58Q5.10 47.35 3.28 45.10Q1.45 42.85 0.47 39.68Q-0.50 36.50-0.50 32.60Q-0.50 27.40 1.25 23.58Q3 19.75 6.13 17.68Q9.25 15.60 13.45 15.60Q16.30 15.60 18.45 16.80Q20.60 18 21.85 19.50L20.85 20.60Q19.50 19 17.65 18.05Q15.80 17.10 13.45 17.10Q9.65 17.10 6.90 19Q4.15 20.90 2.67 24.38Q1.20 27.85 1.20 32.60Q1.20 37.35 2.67 40.88Q4.15 44.40 6.85 46.35Q9.55 48.30 13.25 48.30Q15.90 48.30 17.95 47.25Q20.00 46.20 21.85 44.10L22.85 45.10Q21.00 47.25 18.70 48.53Q16.40 49.80 13.15 49.80ZM30.80 49.20L30.80 16.20L38.20 16.20Q43.30 16.20 46.58 18.25Q49.85 20.30 51.42 24Q53 27.70 53 32.60Q53 37.50 51.42 41.25Q49.85 45 46.58 47.10Q43.30 49.20 38.20 49.20L30.80 49.20M32.40 47.80L38 47.80Q42.70 47.80 45.63 45.80Q48.55 43.80 49.92 40.38Q51.30 36.95 51.30 32.60Q51.30 28.25 49.92 24.85Q48.55 21.45 45.63 19.53Q42.70 17.60 38 17.60L32.40 17.60L32.40 47.80ZM62 49.20L62 16.20L71.10 16.20Q74.35 16.20 76.72 17.05Q79.10 17.90 80.40 19.78Q81.70 21.65 81.70 24.70Q81.70 27.60 80.40 29.58Q79.10 31.55 76.72 32.58Q74.35 33.60 71.10 33.60L63.60 33.60L63.60 49.20L62 49.20M63.60 32.20L70.50 32.20Q75.10 32.20 77.55 30.35Q80 28.50 80 24.70Q80 20.80 77.55 19.20Q75.10 17.60 70.50 17.60L63.60 17.60L63.60 32.20M70.70 33.10L72.50 33L82.20 49.20L80.30 49.20L70.70 33.10ZM90 49.20L90 16.20L92.30 16.20L98.50 32.20L100.70 38.10L100.90 38.10L103 32.20L109.30 16.20L111.60 16.20L111.60 49.20L110 49.20L110 24.45Q110 23.70 110.03 22.88Q110.05 22.05 110.08 21.25Q110.10 20.45 110.13 19.63Q110.15 18.80 110.15 18.05L110 18.05L108.20 23.40L101.45 40.40L100.10 40.40L93.40 23.40L91.45 18.05L91.35 18.05Q91.35 18.80 91.40 19.63Q91.45 20.45 91.47 21.25Q91.50 22.05 91.52 22.88Q91.55 23.70 91.55 24.45L91.55 49.20L90 49.20ZM152 49.20L152 16.20L161.10 16.20Q164.35 16.20 166.73 17.05Q169.10 17.90 170.40 19.78Q171.70 21.65 171.70 24.70Q171.70 27.60 170.40 29.58Q169.10 31.55 166.73 32.58Q164.35 33.60 161.10 33.60L153.60 33.60L153.60 49.20L152 49.20M153.60 32.20L160.50 32.20Q165.10 32.20 167.55 30.35Q170 28.50 170 24.70Q170 20.80 167.55 19.20Q165.10 17.60 160.50 17.60L153.60 17.60L153.60 32.20M160.70 33.10L162.50 33L172.20 49.20L170.30 49.20L160.70 33.10ZM192.30 49.80Q188.90 49.80 186.05 48.30Q183.20 46.80 181.50 44Q179.80 41.20 179.80 37.30Q179.80 33.40 181.48 30.58Q183.15 27.75 185.88 26.23Q188.60 24.70 191.70 24.70Q194.90 24.70 197.28 26Q199.65 27.30 200.98 29.78Q202.30 32.25 202.30 35.70Q202.30 36.05 202.30 36.45Q202.30 36.85 202.20 37.30L180.30 37.30L180.30 35.90L201.80 35.90L200.90 36.70Q200.90 31.30 198.33 28.70Q195.75 26.10 191.70 26.10Q189.10 26.10 186.75 27.43Q184.40 28.75 182.90 31.23Q181.40 33.70 181.40 37.20Q181.40 40.80 182.93 43.30Q184.45 45.80 186.93 47.10Q189.40 48.40 192.30 48.40Q194.85 48.40 196.88 47.65Q198.90 46.90 200.60 45.70L201.30 47Q199.75 48 197.65 48.90Q195.55 49.80 192.30 49.80ZM221.10 49.80Q219.25 49.80 217 48.90Q214.75 48 212.90 46.50L212.80 46.50L212.60 49.20L211.30 49.20L211.30 13.10L212.80 13.10L212.80 23.80L212.70 29.10L212.80 29.10Q214.70 27.15 217.20 25.93Q219.70 24.70 222.10 24.70Q225.55 24.70 227.80 26.20Q230.05 27.70 231.18 30.45Q232.30 33.20 232.30 36.90Q232.30 40.95 230.75 43.83Q229.20 46.70 226.65 48.25Q224.10 49.80 221.10 49.80M221.20 48.40Q223.95 48.40 226.10 46.93Q228.25 45.45 229.48 42.85Q230.70 40.25 230.70 36.90Q230.70 33.85 229.83 31.40Q228.95 28.95 227.05 27.53Q225.15 26.10 222 26.10Q220 26.10 217.60 27.25Q215.20 28.40 212.80 30.80L212.80 44.60Q215.05 46.65 217.35 47.53Q219.65 48.40 221.20 48.40ZM250.80 49.80Q247.65 49.80 245.05 48.33Q242.45 46.85 240.88 44.05Q239.30 41.25 239.30 37.30Q239.30 33.30 240.88 30.48Q242.45 27.65 245.05 26.18Q247.65 24.70 250.80 24.70Q253.95 24.70 256.55 26.18Q259.15 27.65 260.73 30.48Q262.30 33.30 262.30 37.30Q262.30 41.25 260.73 44.05Q259.15 46.85 256.55 48.33Q253.95 49.80 250.80 49.80M250.80 48.40Q253.65 48.40 255.90 47Q258.15 45.60 259.43 43.10Q260.70 40.60 260.70 37.30Q260.70 34 259.43 31.48Q258.15 28.95 255.90 27.53Q253.65 26.10 250.80 26.10Q247.95 26.10 245.73 27.53Q243.50 28.95 242.20 31.48Q240.90 34 240.90 37.30Q240.90 40.60 242.20 43.10Q243.50 45.60 245.73 47Q247.95 48.40 250.80 48.40ZM274.30 49.20L274.30 25.30L275.60 25.30L275.80 31.30L275.90 31.30Q277.85 28.45 280.78 26.58Q283.70 24.70 287.35 24.70Q288.50 24.70 289.78 24.90Q291.05 25.10 292.20 25.70L291.70 27.10Q290.35 26.60 289.48 26.40Q288.60 26.20 287.15 26.20Q284.05 26.20 281.25 27.95Q278.45 29.70 275.80 33.90L275.80 49.20L274.30 49.20ZM301.30 49.20L301.30 25.30L302.60 25.30L302.80 30.10L303 30.10Q305.15 27.75 307.43 26.23Q309.70 24.70 312.60 24.70Q317 24.70 319.15 27.05Q321.30 29.40 321.30 34.30L321.30 49.20L319.80 49.20L319.80 34.50Q319.80 30.25 318.10 28.18Q316.40 26.10 312.60 26.10Q309.90 26.10 307.75 27.53Q305.60 28.95 302.80 31.90L302.80 49.20L301.30 49.20Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

7
cdrm-frontend/dist/profile.svg vendored Normal file
View File

@ -0,0 +1,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#ffffff">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier"> <title>profile_round [#ffffff]</title> <desc>Created with Sketch.</desc> <defs> </defs> <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="Dribbble-Light-Preview" transform="translate(-140.000000, -2159.000000)" fill="#ffffff"> <g id="icons" transform="translate(56.000000, 160.000000)"> <path d="M100.562548,2016.99998 L87.4381713,2016.99998 C86.7317804,2016.99998 86.2101535,2016.30298 86.4765813,2015.66198 C87.7127655,2012.69798 90.6169306,2010.99998 93.9998492,2010.99998 C97.3837885,2010.99998 100.287954,2012.69798 101.524138,2015.66198 C101.790566,2016.30298 101.268939,2016.99998 100.562548,2016.99998 M89.9166645,2004.99998 C89.9166645,2002.79398 91.7489936,2000.99998 93.9998492,2000.99998 C96.2517256,2000.99998 98.0830339,2002.79398 98.0830339,2004.99998 C98.0830339,2007.20598 96.2517256,2008.99998 93.9998492,2008.99998 C91.7489936,2008.99998 89.9166645,2007.20598 89.9166645,2004.99998 M103.955674,2016.63598 C103.213556,2013.27698 100.892265,2010.79798 97.837022,2009.67298 C99.4560048,2008.39598 100.400241,2006.33098 100.053171,2004.06998 C99.6509769,2001.44698 97.4235996,1999.34798 94.7348224,1999.04198 C91.0232075,1998.61898 87.8750721,2001.44898 87.8750721,2004.99998 C87.8750721,2006.88998 88.7692896,2008.57398 90.1636971,2009.67298 C87.1074334,2010.79798 84.7871636,2013.27698 84.044024,2016.63598 C83.7745338,2017.85698 84.7789973,2018.99998 86.0539717,2018.99998 L101.945727,2018.99998 C103.221722,2018.99998 104.226185,2017.85698 103.955674,2016.63598" id="profile_round-[#ffffff]"> </path> </g> </g> </g> </g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

7
cdrm-frontend/dist/search.svg vendored Normal file
View File

@ -0,0 +1,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier"> <g id="Interface / Search_Magnifying_Glass"> <path id="Vector" d="M15 15L21 21M10 17C6.13401 17 3 13.866 3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10C17 13.866 13.866 17 10 17Z" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </g> </g>
</svg>

After

Width:  |  Height:  |  Size: 733 B

11
cdrm-frontend/dist/telegram.svg vendored Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="16" cy="16" r="14" fill="url(#paint0_linear_87_7225)"/>
<path d="M22.9866 10.2088C23.1112 9.40332 22.3454 8.76755 21.6292 9.082L7.36482 15.3448C6.85123 15.5703 6.8888 16.3483 7.42147 16.5179L10.3631 17.4547C10.9246 17.6335 11.5325 17.541 12.0228 17.2023L18.655 12.6203C18.855 12.4821 19.073 12.7665 18.9021 12.9426L14.1281 17.8646C13.665 18.3421 13.7569 19.1512 14.314 19.5005L19.659 22.8523C20.2585 23.2282 21.0297 22.8506 21.1418 22.1261L22.9866 10.2088Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_87_7225" x1="16" y1="2" x2="16" y2="30" gradientUnits="userSpaceOnUse">
<stop stop-color="#37BBFE"/>
<stop offset="1" stop-color="#007DBB"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 920 B

7
cdrm-frontend/dist/video.svg vendored Normal file
View File

@ -0,0 +1,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier"> <path fill-rule="evenodd" clip-rule="evenodd" d="M16 2H0V14H16V2ZM6.5 5V11H7.5L11 8L7.5 5H6.5Z" fill="#ffffff"/> </g>
</svg>

After

Width:  |  Height:  |  Size: 569 B

View File

@ -0,0 +1,38 @@
import js from '@eslint/js'
import globals from 'globals'
import react from 'eslint-plugin-react'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
export default [
{ ignores: ['dist'] },
{
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
settings: { react: { version: '18.3' } },
plugins: {
react,
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
...reactHooks.configs.recommended.rules,
'react/jsx-no-target-blank': 'off',
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
]

20
cdrm-frontend/index.html Normal file
View File

@ -0,0 +1,20 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favico.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Decrypt Widevine and PlayReady protected content." />
<meta name="keywords" content="CDRM, Widevine, PlayReady, DRM, Decrypt, CDM, CDM-Project, CDRM-Project, TPD94, Decryption" />
<meta property='og:title' content='CDRM-Project' />
<meta property='og:description' content='Decrypt Widevine & PlayReady Content' />
<meta property='og:image' content='https://cdrm-project.com/lockforog.png' />
<meta property='og:url' content='https://cdrm-project.com/' />
<meta property='og:locale' content='en_US' />
<title>CDRM-Project</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

5285
cdrm-frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,36 @@
{
"name": "cdrm",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.0.0",
"atob": "^2.1.2",
"helmet": "^8.1.0",
"proto.js": "^0.2.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-helmet": "^6.1.0",
"react-router-dom": "^7.1.3",
"shaka-player": "^4.13.0",
"tailwindcss": "^4.0.0"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.17.0",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0",
"vite": "^6.3.2"
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<circle cx="512" cy="512" r="512" style="fill:#5865f2"/>
<path d="M689.43 349a422.21 422.21 0 0 0-104.22-32.32 1.58 1.58 0 0 0-1.68.79 294.11 294.11 0 0 0-13 26.66 389.78 389.78 0 0 0-117.05 0 269.75 269.75 0 0 0-13.18-26.66 1.64 1.64 0 0 0-1.68-.79A421 421 0 0 0 334.44 349a1.49 1.49 0 0 0-.69.59c-66.37 99.17-84.55 195.9-75.63 291.41a1.76 1.76 0 0 0 .67 1.2 424.58 424.58 0 0 0 127.85 64.63 1.66 1.66 0 0 0 1.8-.59 303.45 303.45 0 0 0 26.15-42.54 1.62 1.62 0 0 0-.89-2.25 279.6 279.6 0 0 1-39.94-19 1.64 1.64 0 0 1-.16-2.72c2.68-2 5.37-4.1 7.93-6.22a1.58 1.58 0 0 1 1.65-.22c83.79 38.26 174.51 38.26 257.31 0a1.58 1.58 0 0 1 1.68.2c2.56 2.11 5.25 4.23 8 6.24a1.64 1.64 0 0 1-.14 2.72 262.37 262.37 0 0 1-40 19 1.63 1.63 0 0 0-.87 2.28 340.72 340.72 0 0 0 26.13 42.52 1.62 1.62 0 0 0 1.8.61 423.17 423.17 0 0 0 128-64.63 1.64 1.64 0 0 0 .67-1.18c10.68-110.44-17.88-206.38-75.7-291.42a1.3 1.3 0 0 0-.63-.63zM427.09 582.85c-25.23 0-46-23.16-46-51.6s20.38-51.6 46-51.6c25.83 0 46.42 23.36 46 51.6.02 28.44-20.37 51.6-46 51.6zm170.13 0c-25.23 0-46-23.16-46-51.6s20.38-51.6 46-51.6c25.83 0 46.42 23.36 46 51.6.01 28.44-20.17 51.6-46 51.6z" style="fill:#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,9 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 1024 1024" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="#000000">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier">
<path d="M877.685565 727.913127l-0.584863-0.365539a32.898541 32.898541 0 0 1-8.041866-46.423497 411.816631 411.816631 0 1 0-141.829267 145.777092c14.621574-8.992268 33.62962-5.117551 43.645398 8.772944l0.146216 0.073108a30.412874 30.412874 0 0 1-7.968758 43.206751l-6.141061 4.020933a475.201154 475.201154 0 1 1 163.615412-164.419599 29.974227 29.974227 0 0 1-42.841211 9.357807z m-537.342843-398.584106c7.164571-7.091463 24.71046-9.650239 33.26408 0 10.600641 11.185504 7.164571 29.462472 0 37.138798l-110.612207 107.468569L370.901811 576.14119c7.164571 7.091463 8.114974 27.342343 0 35.384209-9.796455 9.723347-29.828011 8.188081-36.480827 1.535265L208.309909 487.388236a18.423183 18.423183 0 0 1 0-25.953294l132.032813-132.032813z m343.314556 0l132.032813 132.032813a18.423183 18.423183 0 0 1 0 25.953294L689.652124 613.133772c-6.652816 6.579708-25.587754 10.746857-36.553935 0-10.30821-10.235102-7.091463-31.290168 0-38.381632l108.345863-100.669537-111.855041-108.638294c-7.164571-7.676326-9.504023-26.611265 0-36.04218 9.284699-9.138484 26.903696-7.091463 34.068267 0z m-135.54199-26.318833c3.582286-9.504023 21.347498-15.498868 32.679217-11.258612 10.819965 4.020933 17.180349 19.008046 14.256035 28.512069l-119.896906 329.716493c-3.509178 9.504023-20.616419 13.305632-30.193551 9.723347-10.161994-3.509178-21.201282-17.545889-17.545888-26.976804l120.627985-329.716493z" fill="#ffffff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>github [#142]</title>
<desc>Created with Sketch.</desc>
<defs>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Dribbble-Light-Preview" transform="translate(-140.000000, -7559.000000)" fill="#000000">
<g id="icons" transform="translate(56.000000, 160.000000)">
<path d="M94,7399 C99.523,7399 104,7403.59 104,7409.253 C104,7413.782 101.138,7417.624 97.167,7418.981 C96.66,7419.082 96.48,7418.762 96.48,7418.489 C96.48,7418.151 96.492,7417.047 96.492,7415.675 C96.492,7414.719 96.172,7414.095 95.813,7413.777 C98.04,7413.523 100.38,7412.656 100.38,7408.718 C100.38,7407.598 99.992,7406.684 99.35,7405.966 C99.454,7405.707 99.797,7404.664 99.252,7403.252 C99.252,7403.252 98.414,7402.977 96.505,7404.303 C95.706,7404.076 94.85,7403.962 94,7403.958 C93.15,7403.962 92.295,7404.076 91.497,7404.303 C89.586,7402.977 88.746,7403.252 88.746,7403.252 C88.203,7404.664 88.546,7405.707 88.649,7405.966 C88.01,7406.684 87.619,7407.598 87.619,7408.718 C87.619,7412.646 89.954,7413.526 92.175,7413.785 C91.889,7414.041 91.63,7414.493 91.54,7415.156 C90.97,7415.418 89.522,7415.871 88.63,7414.304 C88.63,7414.304 88.101,7413.319 87.097,7413.247 C87.097,7413.247 86.122,7413.234 87.029,7413.87 C87.029,7413.87 87.684,7414.185 88.139,7415.37 C88.139,7415.37 88.726,7417.2 91.508,7416.58 C91.513,7417.437 91.522,7418.245 91.522,7418.489 C91.522,7418.76 91.338,7419.077 90.839,7418.982 C86.865,7417.627 84,7413.783 84,7409.253 C84,7403.59 88.478,7399 94,7399" id="github-[#142]">
</path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0,0,256,256" width="50px" height="50px" fill-rule="nonzero"><g fill="#ffffff" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><g transform="scale(5.12,5.12)"><path d="M24.96289,1.05469c-0.20987,0.00724 -0.41214,0.08036 -0.57812,0.20898l-23,17.94727c-0.43579,0.33978 -0.51361,0.96851 -0.17383,1.4043c0.33978,0.43579 0.96851,0.51361 1.4043,0.17383l1.38477,-1.08008v26.29102c0.00006,0.55226 0.44774,0.99994 1,1h13.83203c0.10799,0.01785 0.21818,0.01785 0.32617,0h11.67383c0.10799,0.01785 0.21818,0.01785 0.32617,0h13.8418c0.55226,-0.00006 0.99994,-0.44774 1,-1v-26.29102l1.38477,1.08008c0.2819,0.21983 0.65967,0.27257 0.991,0.13833c0.33133,-0.13423 0.56586,-0.43504 0.61526,-0.7891c0.0494,-0.35406 -0.09386,-0.70757 -0.37579,-0.92736l-7.61523,-5.94141v-7.26953h-6v2.58594l-9.38477,-7.32227c-0.18607,-0.14428 -0.41707,-0.21828 -0.65234,-0.20898zM25,3.32227l19,14.82617v26.85156h-12v-19h-14v19h-12v-26.85156zM37,8h2v3.70898l-2,-1.5625zM20,28h10v17h-10z"></path></g></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.5 13.100000381469727 321.79998779296875 36.69999694824219" data-asc="0.984"><g fill="#ffffff"><g fill="#ffffff" transform="translate(0, 0)"><path d="M13.15 49.80Q10.10 49.80 7.60 48.58Q5.10 47.35 3.28 45.10Q1.45 42.85 0.47 39.68Q-0.50 36.50-0.50 32.60Q-0.50 27.40 1.25 23.58Q3 19.75 6.13 17.68Q9.25 15.60 13.45 15.60Q16.30 15.60 18.45 16.80Q20.60 18 21.85 19.50L20.85 20.60Q19.50 19 17.65 18.05Q15.80 17.10 13.45 17.10Q9.65 17.10 6.90 19Q4.15 20.90 2.67 24.38Q1.20 27.85 1.20 32.60Q1.20 37.35 2.67 40.88Q4.15 44.40 6.85 46.35Q9.55 48.30 13.25 48.30Q15.90 48.30 17.95 47.25Q20.00 46.20 21.85 44.10L22.85 45.10Q21.00 47.25 18.70 48.53Q16.40 49.80 13.15 49.80ZM30.80 49.20L30.80 16.20L38.20 16.20Q43.30 16.20 46.58 18.25Q49.85 20.30 51.42 24Q53 27.70 53 32.60Q53 37.50 51.42 41.25Q49.85 45 46.58 47.10Q43.30 49.20 38.20 49.20L30.80 49.20M32.40 47.80L38 47.80Q42.70 47.80 45.63 45.80Q48.55 43.80 49.92 40.38Q51.30 36.95 51.30 32.60Q51.30 28.25 49.92 24.85Q48.55 21.45 45.63 19.53Q42.70 17.60 38 17.60L32.40 17.60L32.40 47.80ZM62 49.20L62 16.20L71.10 16.20Q74.35 16.20 76.72 17.05Q79.10 17.90 80.40 19.78Q81.70 21.65 81.70 24.70Q81.70 27.60 80.40 29.58Q79.10 31.55 76.72 32.58Q74.35 33.60 71.10 33.60L63.60 33.60L63.60 49.20L62 49.20M63.60 32.20L70.50 32.20Q75.10 32.20 77.55 30.35Q80 28.50 80 24.70Q80 20.80 77.55 19.20Q75.10 17.60 70.50 17.60L63.60 17.60L63.60 32.20M70.70 33.10L72.50 33L82.20 49.20L80.30 49.20L70.70 33.10ZM90 49.20L90 16.20L92.30 16.20L98.50 32.20L100.70 38.10L100.90 38.10L103 32.20L109.30 16.20L111.60 16.20L111.60 49.20L110 49.20L110 24.45Q110 23.70 110.03 22.88Q110.05 22.05 110.08 21.25Q110.10 20.45 110.13 19.63Q110.15 18.80 110.15 18.05L110 18.05L108.20 23.40L101.45 40.40L100.10 40.40L93.40 23.40L91.45 18.05L91.35 18.05Q91.35 18.80 91.40 19.63Q91.45 20.45 91.47 21.25Q91.50 22.05 91.52 22.88Q91.55 23.70 91.55 24.45L91.55 49.20L90 49.20ZM152 49.20L152 16.20L161.10 16.20Q164.35 16.20 166.73 17.05Q169.10 17.90 170.40 19.78Q171.70 21.65 171.70 24.70Q171.70 27.60 170.40 29.58Q169.10 31.55 166.73 32.58Q164.35 33.60 161.10 33.60L153.60 33.60L153.60 49.20L152 49.20M153.60 32.20L160.50 32.20Q165.10 32.20 167.55 30.35Q170 28.50 170 24.70Q170 20.80 167.55 19.20Q165.10 17.60 160.50 17.60L153.60 17.60L153.60 32.20M160.70 33.10L162.50 33L172.20 49.20L170.30 49.20L160.70 33.10ZM192.30 49.80Q188.90 49.80 186.05 48.30Q183.20 46.80 181.50 44Q179.80 41.20 179.80 37.30Q179.80 33.40 181.48 30.58Q183.15 27.75 185.88 26.23Q188.60 24.70 191.70 24.70Q194.90 24.70 197.28 26Q199.65 27.30 200.98 29.78Q202.30 32.25 202.30 35.70Q202.30 36.05 202.30 36.45Q202.30 36.85 202.20 37.30L180.30 37.30L180.30 35.90L201.80 35.90L200.90 36.70Q200.90 31.30 198.33 28.70Q195.75 26.10 191.70 26.10Q189.10 26.10 186.75 27.43Q184.40 28.75 182.90 31.23Q181.40 33.70 181.40 37.20Q181.40 40.80 182.93 43.30Q184.45 45.80 186.93 47.10Q189.40 48.40 192.30 48.40Q194.85 48.40 196.88 47.65Q198.90 46.90 200.60 45.70L201.30 47Q199.75 48 197.65 48.90Q195.55 49.80 192.30 49.80ZM221.10 49.80Q219.25 49.80 217 48.90Q214.75 48 212.90 46.50L212.80 46.50L212.60 49.20L211.30 49.20L211.30 13.10L212.80 13.10L212.80 23.80L212.70 29.10L212.80 29.10Q214.70 27.15 217.20 25.93Q219.70 24.70 222.10 24.70Q225.55 24.70 227.80 26.20Q230.05 27.70 231.18 30.45Q232.30 33.20 232.30 36.90Q232.30 40.95 230.75 43.83Q229.20 46.70 226.65 48.25Q224.10 49.80 221.10 49.80M221.20 48.40Q223.95 48.40 226.10 46.93Q228.25 45.45 229.48 42.85Q230.70 40.25 230.70 36.90Q230.70 33.85 229.83 31.40Q228.95 28.95 227.05 27.53Q225.15 26.10 222 26.10Q220 26.10 217.60 27.25Q215.20 28.40 212.80 30.80L212.80 44.60Q215.05 46.65 217.35 47.53Q219.65 48.40 221.20 48.40ZM250.80 49.80Q247.65 49.80 245.05 48.33Q242.45 46.85 240.88 44.05Q239.30 41.25 239.30 37.30Q239.30 33.30 240.88 30.48Q242.45 27.65 245.05 26.18Q247.65 24.70 250.80 24.70Q253.95 24.70 256.55 26.18Q259.15 27.65 260.73 30.48Q262.30 33.30 262.30 37.30Q262.30 41.25 260.73 44.05Q259.15 46.85 256.55 48.33Q253.95 49.80 250.80 49.80M250.80 48.40Q253.65 48.40 255.90 47Q258.15 45.60 259.43 43.10Q260.70 40.60 260.70 37.30Q260.70 34 259.43 31.48Q258.15 28.95 255.90 27.53Q253.65 26.10 250.80 26.10Q247.95 26.10 245.73 27.53Q243.50 28.95 242.20 31.48Q240.90 34 240.90 37.30Q240.90 40.60 242.20 43.10Q243.50 45.60 245.73 47Q247.95 48.40 250.80 48.40ZM274.30 49.20L274.30 25.30L275.60 25.30L275.80 31.30L275.90 31.30Q277.85 28.45 280.78 26.58Q283.70 24.70 287.35 24.70Q288.50 24.70 289.78 24.90Q291.05 25.10 292.20 25.70L291.70 27.10Q290.35 26.60 289.48 26.40Q288.60 26.20 287.15 26.20Q284.05 26.20 281.25 27.95Q278.45 29.70 275.80 33.90L275.80 49.20L274.30 49.20ZM301.30 49.20L301.30 25.30L302.60 25.30L302.80 30.10L303 30.10Q305.15 27.75 307.43 26.23Q309.70 24.70 312.60 24.70Q317 24.70 319.15 27.05Q321.30 29.40 321.30 34.30L321.30 49.20L319.80 49.20L319.80 34.50Q319.80 30.25 318.10 28.18Q316.40 26.10 312.60 26.10Q309.90 26.10 307.75 27.53Q305.60 28.95 302.80 31.90L302.80 49.20L301.30 49.20Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -0,0 +1,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#ffffff">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier"> <title>profile_round [#ffffff]</title> <desc>Created with Sketch.</desc> <defs> </defs> <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="Dribbble-Light-Preview" transform="translate(-140.000000, -2159.000000)" fill="#ffffff"> <g id="icons" transform="translate(56.000000, 160.000000)"> <path d="M100.562548,2016.99998 L87.4381713,2016.99998 C86.7317804,2016.99998 86.2101535,2016.30298 86.4765813,2015.66198 C87.7127655,2012.69798 90.6169306,2010.99998 93.9998492,2010.99998 C97.3837885,2010.99998 100.287954,2012.69798 101.524138,2015.66198 C101.790566,2016.30298 101.268939,2016.99998 100.562548,2016.99998 M89.9166645,2004.99998 C89.9166645,2002.79398 91.7489936,2000.99998 93.9998492,2000.99998 C96.2517256,2000.99998 98.0830339,2002.79398 98.0830339,2004.99998 C98.0830339,2007.20598 96.2517256,2008.99998 93.9998492,2008.99998 C91.7489936,2008.99998 89.9166645,2007.20598 89.9166645,2004.99998 M103.955674,2016.63598 C103.213556,2013.27698 100.892265,2010.79798 97.837022,2009.67298 C99.4560048,2008.39598 100.400241,2006.33098 100.053171,2004.06998 C99.6509769,2001.44698 97.4235996,1999.34798 94.7348224,1999.04198 C91.0232075,1998.61898 87.8750721,2001.44898 87.8750721,2004.99998 C87.8750721,2006.88998 88.7692896,2008.57398 90.1636971,2009.67298 C87.1074334,2010.79798 84.7871636,2013.27698 84.044024,2016.63598 C83.7745338,2017.85698 84.7789973,2018.99998 86.0539717,2018.99998 L101.945727,2018.99998 C103.221722,2018.99998 104.226185,2017.85698 103.955674,2016.63598" id="profile_round-[#ffffff]"> </path> </g> </g> </g> </g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier"> <g id="Interface / Search_Magnifying_Glass"> <path id="Vector" d="M15 15L21 21M10 17C6.13401 17 3 13.866 3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10C17 13.866 13.866 17 10 17Z" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </g> </g>
</svg>

After

Width:  |  Height:  |  Size: 733 B

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="16" cy="16" r="14" fill="url(#paint0_linear_87_7225)"/>
<path d="M22.9866 10.2088C23.1112 9.40332 22.3454 8.76755 21.6292 9.082L7.36482 15.3448C6.85123 15.5703 6.8888 16.3483 7.42147 16.5179L10.3631 17.4547C10.9246 17.6335 11.5325 17.541 12.0228 17.2023L18.655 12.6203C18.855 12.4821 19.073 12.7665 18.9021 12.9426L14.1281 17.8646C13.665 18.3421 13.7569 19.1512 14.314 19.5005L19.659 22.8523C20.2585 23.2282 21.0297 22.8506 21.1418 22.1261L22.9866 10.2088Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_87_7225" x1="16" y1="2" x2="16" y2="30" gradientUnits="userSpaceOnUse">
<stop stop-color="#37BBFE"/>
<stop offset="1" stop-color="#007DBB"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 920 B

View File

@ -0,0 +1,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier"> <path fill-rule="evenodd" clip-rule="evenodd" d="M16 2H0V14H16V2ZM6.5 5V11H7.5L11 8L7.5 5H6.5Z" fill="#ffffff"/> </g>
</svg>

After

Width:  |  Height:  |  Size: 569 B

10
cdrm-frontend/src/App.css Normal file
View File

@ -0,0 +1,10 @@
@import "tailwindcss";
* {
box-sizing: border-box;
}
html, body, #root {
height: 100%;
width: 100%;
}

13
cdrm-frontend/src/App.jsx Normal file
View File

@ -0,0 +1,13 @@
import React from 'react'
import './App.css'
import AppContainer from './components/AppContainer/AppContainer.jsx'
function App() {
return (
<>
<AppContainer />
</>
)
}
export default App

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,29 @@
import React from 'react'
import Sidebar from '../Sidebar/Sidebar'
import Home from '../Pages/Home'
import API from '../Pages/API'
import Cache from '../Pages/Cache'
import TestPlayer from '../Pages/TestPlayer'
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
function AppContainer() {
return (
<Router>
<div className='flex flex-row'>
<div className='w-1/8 h-dvh overflow-y-auto'>
<Sidebar />
</div>
<div className='w-7/8 h-dvh overflow-y-auto scroll-smooth' id='main_content'>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/api" element={<API />} />
<Route path="/testplayer" element={<TestPlayer />} />
<Route path="/cache" element={<Cache />} />
</Routes>
</div>
</div>
</Router>
)
}
export default AppContainer

View File

@ -0,0 +1,173 @@
import "./protobuf.min.js";
import "./license_protocol.min.js";
const { SignedMessage, LicenseRequest } = protobuf.roots.default.license_protocol;
function uint8ArrayToBase64(uint8Array) {
const binaryString = Array.from(uint8Array)
.map(b => String.fromCharCode(b))
.join('');
return btoa(binaryString);
}
function parseFetch(fetchString) {
// Remove `await` if it exists in the string
fetchString = fetchString.replace(/^await\s+/, "");
// Use a more lenient regex to capture the fetch pattern (including complex bodies)
const fetchRegex = /fetch\(['"](.+?)['"],\s*(\{.+?\})\)/s; // Non-greedy match for JSON
const lines = fetchString.split('\n').map(line => line.trim()).filter(Boolean);
const result = {
method: 'UNDEFINED',
url: '',
headers: {},
body: null,
};
// Try matching the regex
const fetchMatch = fetchString.match(fetchRegex);
if (!fetchMatch) {
console.log(fetchString);
throw new Error("Invalid 'Copy as fetch' string.");
}
// Extract URL from the match
result.url = fetchMatch[1];
// Parse the options JSON from the match (this will include headers, body, etc.)
const optionsString = fetchMatch[2];
const options = JSON.parse(optionsString);
// Assign method, headers, and body if available
if (options.method) result.method = options.method;
if (options.headers) result.headers = options.headers;
if (options.body) result.body = options.body;
return result;
}
const WIDEVINE_SYSTEM_ID = new Uint8Array([0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, 0x4a, 0xce, 0xa3, 0xc8, 0x27, 0xdc, 0xd5, 0x1d, 0x21, 0xed]);
const PLAYREADY_SYSTEM_ID = new Uint8Array([0x9a, 0x04, 0xf0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xab, 0x92, 0xe6, 0x5b, 0xe0, 0x88, 0x5f, 0x95]);
const PSSH_MAGIC = new Uint8Array([0x70, 0x73, 0x73, 0x68]);
function intToUint8Array(num, endian) {
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint32(0, num, endian);
return new Uint8Array(buffer);
}
function shortToUint8Array(num, endian) {
const buffer = new ArrayBuffer(2);
const view = new DataView(buffer);
view.setUint16(0, num, endian);
return new Uint8Array(buffer);
}
function psshDataToPsshBoxB64(pssh_data, system_id) {
const dataLength = pssh_data.length;
const totalLength = dataLength + 32;
const pssh = new Uint8Array([
...intToUint8Array(totalLength, false),
...PSSH_MAGIC,
...new Uint8Array(4),
...system_id,
...intToUint8Array(dataLength, false),
...pssh_data
]);
return uint8ArrayToBase64(pssh);
}
function wrmHeaderToPlayReadyHeader(wrm_header){
const playready_object = new Uint8Array([
...shortToUint8Array(1, true),
...shortToUint8Array(wrm_header.length, true),
...wrm_header
]);
return new Uint8Array([
...intToUint8Array(playready_object.length + 2 + 4, true),
...shortToUint8Array(1, true),
...playready_object
]);
}
function encodeUtf16LE(str) {
const buffer = new Uint8Array(str.length * 2);
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i);
buffer[i * 2] = code & 0xff;
buffer[i * 2 + 1] = code >> 8;
}
return buffer;
}
function stringToUint8Array(string) {
return Uint8Array.from(string.split("").map(x => x.charCodeAt()));
}
export async function readTextFromClipboard() {
try {
// Request text from the clipboard
const clipboardText = await navigator.clipboard.readText();
const result = parseFetch(clipboardText);
let pssh_data_string;
let payload_string;
if (result.body.startsWith("<")) {
// If body starts with "<", process it as PlayReady content
payload_string = result.body;
const wrmHeaderMatch = payload_string.match(/.*(<WRMHEADER.*<\/WRMHEADER>).*/);
const wrmHeader = wrmHeaderMatch ? wrmHeaderMatch[1] : null;
const encodedWrmHeader = encodeUtf16LE(wrmHeader);
const playreadyHeader = wrmHeaderToPlayReadyHeader(encodedWrmHeader);
pssh_data_string = psshDataToPsshBoxB64(playreadyHeader, PLAYREADY_SYSTEM_ID);
} else {
// If body is in a different format, process as Widevine content
const uint8Array = stringToUint8Array(result.body);
let signed_message;
let license_request;
try {
signed_message = SignedMessage.decode(uint8Array);
license_request = LicenseRequest.decode(signed_message.msg);
} catch (decodeError) {
// If error occurs during decoding, return an empty pssh
console.error('Decoding failed, returning empty pssh', decodeError);
pssh_data_string = ''; // Empty pssh if decoding fails
}
if (license_request && license_request.contentId && license_request.contentId.widevinePsshData) {
const pssh_data = license_request.contentId.widevinePsshData.psshData[0];
pssh_data_string = psshDataToPsshBoxB64(pssh_data, WIDEVINE_SYSTEM_ID);
}
// Check if the body contains binary data (non-UTF-8 characters)
if (isBinary(uint8Array)) {
payload_string = uint8ArrayToBase64(uint8Array);
} else {
// If it's text, return it as is
payload_string = result.body;
}
}
// Output the result
document.getElementById("licurl").value = result.url;
document.getElementById("headers").value = JSON.stringify(result.headers);
document.getElementById("pssh").value = pssh_data_string;
document.getElementById("data").value = payload_string;
} catch (error) {
console.error('Failed to read clipboard contents:', error);
}
}
// Helper function to check if the data is binary
function isBinary(uint8Array) {
// Check for non-text (non-ASCII) bytes (basic heuristic)
return uint8Array.some(byte => byte > 127); // Non-ASCII byte indicates binary
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,176 @@
import React, { useState, useEffect } from 'react';
import { data } from 'react-router-dom';
const { protocol, hostname, port } = window.location;
let fullHost = `${protocol}//${hostname}`;
if (
(protocol === 'http:' && port !== '80') ||
(protocol === 'https:' && port !== '443' && port !== '')
) {
fullHost += `:${port}`;
}
function API() {
// Widevine device info
const [deviceInfo, setDeviceInfo] = useState({
device_type: '',
system_id: '',
security_level: '',
host: '',
secret: '',
device_name: ''
});
// PlayReady device info
const [prDeviceInfo, setPrDeviceInfo] = useState({
security_level: '',
host: '',
secret: '',
device_name: ''
});
useEffect(() => {
// Fetch Widevine info
fetch('/remotecdm/widevine/deviceinfo')
.then(response => response.json())
.then(data => {
setDeviceInfo({
device_type: data.device_type,
system_id: data.system_id,
security_level: data.security_level,
host: data.host,
secret: data.secret,
device_name: data.device_name
});
})
.catch(error => console.error('Error fetching Widevine info:', error));
// Fetch PlayReady info
fetch('/remotecdm/playready/deviceinfo')
.then(response => response.json())
.then(data => {
setPrDeviceInfo({
security_level: data.security_level,
host: data.host,
secret: data.secret,
device_name: data.device_name
});
})
.catch(error => console.error('Error fetching PlayReady info:', error));
}, []);
return (
<>
<div className='min-w-full w-full min-h-full overflow-x-auto bg-zinc-900 shadow-lg shadow-black flex flex-col flex-wrap p-10 justify-around'>
{/* Decryption Request Section */}
<details open className='p-5 mb-5 border shadow-lg shadow-black overflow-y-auto'>
<summary className='bg-[rgba(0,0,0,0.2)] p-2 rounded text-white flex shadow-purple-900 shadow-sm'>
Sending a decryption request | {`(Python)`}
</summary>
<br />
<div className='h-9/10 bg-[rgba(0,0,0,0.2)] p-2 rounded text-white shadow-sm shadow-purple-900 w-full overflow-x-auto overflow-y-auto'>
<pre className='p-2'>
{`import requests
print(requests.post(
url='${fullHost}/api/decrypt',
headers={
'Content-Type': 'application/json',
},
json={
'pssh': 'AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==',
'licurl': 'https://cwip-shaka-proxy.appspot.com/no_auth',
'headers': str({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0',
'Accept': '*/*',
'Accept-Language': 'en-US,en;q=0.5',
})
}
).json()['message'])`}
</pre>
</div>
</details>
{/* Search Request Section */}
<details open className=' p-5 mb-5 shadow-lg shadow-black border overflow-y-auto' >
<summary className='bg-[rgba(0,0,0,0.2)] p-2 rounded text-white flex shadow-sm shadow-purple-900'>
Sending a search request | {`(Python)`}
</summary>
<br />
<div className='h-9/10 bg-[rgba(0,0,0,0.2)] p-2 rounded text-white shadow-sm shadow-purple-900'>
<pre className='p-2'>{`import requests
print(requests.post(
url='${fullHost}/api/cache/search',
json={
'input': 'AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA=='
}
).json())`}</pre>
</div>
</details>
{/* Remote CDM Configuration Section */}
<details open className=' p-5 mb-5 shadow-lg shadow-black border overflow-y-auto'>
<summary className='bg-[rgba(0,0,0,0.2)] p-2 rounded text-white flex shadow-sm shadow-purple-900'>
Remote CDM configuration (Widevine) | {`(For PyWidevine / Devine / VineTrimmer / Extensions)`}
</summary>
<br />
<div className='h-9/10 bg-[rgba(0,0,0,0.2)] p-2 rounded text-white shadow-sm shadow-purple-900'>
<p className='p-2'>
device_type: <span id='wv_device_type'>"{deviceInfo.device_type}"</span>
<br></br>
system_id: <span id='wv_system_id'>{deviceInfo.system_id}</span>
<br></br>
security_level: <span id='wv_security_level'>{deviceInfo.security_level}</span>
<br></br>
host: <span id='wv_host'>"{fullHost}/remotecdm/widevine"</span>
<br></br>
secret: <span id='wv_secret'>"{deviceInfo.secret}"</span>
<br></br>
device_name: <span id='wv_device_name'>{deviceInfo.device_name}</span>
</p>
</div>
</details>
{/* Remote CDM Configuration Section */}
<details open className=' p-5 mb-5 shadow-lg shadow-black border overflow-y-auto'>
<summary className='bg-[rgba(0,0,0,0.2)] p-2 rounded text-white flex shadow-sm shadow-purple-900'>
Remote CDM configuration (PlayReady) | {`(For PyPlayReady / Extensions / PlayReady Proxy)`}
</summary>
<br />
<div className='h-9/10 bg-[rgba(0,0,0,0.2)] p-2 rounded text-white shadow-sm shadow-purple-900'>
<p className='p-2'>
device_name: <span id='pr_device_name'>{prDeviceInfo.device_name}</span>
<br></br>
security_level: <span id='pr_security_level'>{prDeviceInfo.security_level}</span>
<br></br>
host: <span id='pr_host'>"{fullHost}/remotecdm/playready"</span>
<br></br>
secret: <span id='pr_secret'>"{prDeviceInfo.secret}"</span>
<br></br>
</p>
</div>
</details>
{/* Webvault Configuration Section */}
<details open className='p-5 mb-5 shadow-lg shadow-black border overflow-y-auto'>
<summary className='bg-[rgba(0,0,0,0.2)] p-2 rounded text-white flex shadow-sm shadow-purple-900 transition-all transition-300 ease-in'>
Webvault configuration | {`(For Devine / VineTrimmer)`}
</summary>
<br />
<div className='h-9/10 bg-[rgba(0,0,0,0.2)] p-2 rounded text-white shadow-sm shadow-purple-900'>
<pre className='p-2'>{`key_vaults:
- type: API
name: "Online Vault"
uri: "${fullHost}/api/cache"
token: "${deviceInfo.secret}"`}</pre>
</div>
</details>
</div>
</>
);
}
export default API;

View File

@ -0,0 +1,118 @@
import React, { useState, useEffect, useRef } from 'react';
function Cache() {
const [searchQuery, setSearchQuery] = useState('');
const [cacheData, setCacheData] = useState([]);
const [keyCount, setKeyCount] = useState(0);
const debounceTimeout = useRef(null); // store the timeout ID
const handleInputChange = (event) => {
const query = event.target.value;
setSearchQuery(query);
// Clear the previous timeout
if (debounceTimeout.current) {
clearTimeout(debounceTimeout.current);
}
// Set a new timeout
debounceTimeout.current = setTimeout(() => {
if (query.trim() !== '') {
sendApiCall(query);
} else {
setCacheData([]); // optional: clear results on empty query
}
}, 1000); // 2 seconds
};
const fetchKeyCount = () => {
fetch('/api/cache/keycount')
.then((response) => response.json())
.then((data) => {
setKeyCount(data.count);
})
.catch((error) => {
console.error('Error fetching key count:', error);
});
};
useEffect(() => {
const fetchInterval = setInterval(fetchKeyCount, 10000);
fetchKeyCount();
return () => clearInterval(fetchInterval);
}, []);
const sendApiCall = (text) => {
fetch('/api/cache/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input: text }),
})
.then((response) => response.json())
.then((data) => setCacheData(data))
.catch((error) => console.error('Error:', error));
};
return (
<>
<div className='w-full h-full bg-zinc-900 flex flex-col p-0'>
<div className='flex flex-row w-full'>
<form className='flex flex-row w-8/10 p-10 h-full rounded-xl self-start'>
<input
type='text'
id='search'
name='search'
value={searchQuery}
onChange={handleInputChange}
placeholder='Search by PSSH/KID'
className='w-full text-white bg-[rgba(0,0,0,0.2)] focus:outline-none rounded focus:shadow-sm focus:shadow-green-700/50 transition-shadow duration-300 ease-in-out p-2'
/>
</form>
<p className='text-white w-2/10 p-10 rounded-xl self-start flex flex-col h-full'>
<span className='text-white w-1/1 text-center'>
Cached Keys: {keyCount} {/* Display the count of cached keys */}
</span>
<a href='/api/cache/download'>
<button className=' self-start w-1/1 bg-green-700 rounded-md mt-1 active:transform active:scale-95 cursor-pointer hover:bg-green-600/50 pt-1 pb-1'>
Download
</button>
</a>
</p>
</div>
<div className='h-full w-full p-10 overflow-y-auto'>
<div className="overflow-x-auto border p-10 rounded-2xl bg-[rgba(0,0,0,0.2)] shadow-md shadow-green-700 min-h-full overflow-y-auto">
<table className='min-w-full text-white'>
<thead>
<tr>
<th className='p-2 border border-black'>PSSH</th>
<th className='p-2 border border-black'>KID</th>
<th className='p-2 border border-black'>Key</th>
</tr>
</thead>
<tbody>
{cacheData.length > 0 ? (
cacheData.map((item, index) => (
<tr key={index}>
<td className='p-2 border border-black'>{item.PSSH}</td>
<td className='p-2 border border-black'>{item.KID}</td>
<td className='p-2 border border-black'>{item.Key}</td>
</tr>
))
) : (
<tr>
<td colSpan="3" className='p-2 border border-black text-center'>
No data found
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</div>
</>
);
}
export default Cache;

View File

@ -0,0 +1,124 @@
import React, { useState, useEffect } from 'react';
import { Helmet } from 'react-helmet';
import { readTextFromClipboard } from '../functions/ParseChallenge';
function Home() {
const [message, setMessage] = useState('');
const [isVisible, setIsVisible] = useState(false);
const scrollToTop = () => {
let divToScroll = document.getElementById('main_content');
divToScroll.scrollTop = 0; // Scroll to the top of the div
};
const copyToClipboard = (event) => {
let message = document.getElementById('messageresults').innerHTML;
// Replace <br> tags with newline characters
message = message.replace(/<br\s*\/?>/gi, '\n');
// Use textContent to get the actual text without HTML tags
navigator.clipboard.writeText(message); // Copy the message to the clipboard
console.log(message);
};
// Handlers for form submission and reset
const handleSubmit = (event) => {
event.preventDefault();
};
const handleSubmitButton = (event) => {
let pssh = document.getElementById('pssh').value;
let licurl = document.getElementById('licurl').value;
let headers = document.getElementById('headers').value;
let cookies = document.getElementById('cookies').value;
let data = document.getElementById('data').value;
fetch('/api/decrypt', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
pssh: pssh,
licurl: licurl,
headers: headers,
cookies: cookies,
data: data
}),
})
.then(response => response.json())
.then(data => {
const resultMessage = data['message'].replace(/\n/g, '<br />'); // Format the message as HTML
setMessage(resultMessage);
setIsVisible(true);
})
.catch((error) => {
console.error('Error during decryption request:', error);
setMessage('Error: Unable to process request.');
setIsVisible(true);
});
};
const handleResetButton = (event) => {
let pssh = document.getElementById('pssh');
let licurl = document.getElementById('licurl');
let headers = document.getElementById('headers');
let cookies = document.getElementById('cookies');
let data = document.getElementById('data');
pssh.value = '';
licurl.value = '';
headers.value = '';
cookies.value = '';
data.value = '';
setMessage('');
setIsVisible(false);
};
useEffect(() => {
if (isVisible) {
let divToScroll = document.getElementById('main_content');
divToScroll.scrollTop = divToScroll.scrollHeight;
}
}, [message, isVisible]);
return (
<>
<div className='w-full min-h-full bg-zinc-900 flex flex-col items-center justify-center'>
<form className='flex flex-col w-8/10 min-h-8/10 bg-[rgba(0,0,0,0.2)] p-10 border-black border-1 rounded-xl shadow-lg shadow-cyan-500/50 overflow-y-auto' onSubmit={handleSubmit}>
<label htmlFor='pssh' className='text-white mb-1'>PSSH:</label>
<input type='text' id='pssh' name='pssh' className='text-white bg-[rgba(0,0,0,0.2)] focus:outline-none rounded focus:shadow-sm focus:shadow-cyan-500/50 transition-shadow duration-300 ease-in-out p-2' />
<label htmlFor='licurl' className='text-white mb-1 mt-1'>License URL:</label>
<input type='text' id='licurl' name='licurl' className='text-white bg-[rgba(0,0,0,0.2)] focus:outline-none rounded focus:shadow-sm focus:shadow-cyan-500/50 transition-shadow duration-300 ease-in-out p-2' />
<label htmlFor='headers' className='text-white mb-1 mt-1'>Headers:</label>
<textarea id='headers' name='headers' className='text-white bg-[rgba(0,0,0,0.2)] h-24 focus:h-92 focus:outline-none rounded focus:shadow-sm focus:shadow-cyan-500/50 transition-all duration-300 ease-in-out p-2 resize-none' />
<label htmlFor='cookies' className='text-white mb-1 mt-1'>Cookies:</label>
<textarea id='cookies' name='cookies' className='text-white bg-[rgba(0,0,0,0.2)] h-24 focus:h-92 focus:outline-none rounded focus:shadow-sm focus:shadow-cyan-500/50 transition-all duration-300 ease-in-out p-2 resize-none' />
<label htmlFor='data' className='text-white mb-1 mt-1'>Data:</label>
<textarea id='data' name='data' className='text-white bg-[rgba(0,0,0,0.2)] h-24 focus:h-92 focus:outline-none rounded focus:shadow-sm focus:shadow-cyan-500/50 transition-all duration-300 ease-in-out p-2 resize-none' />
<div className='flex flex-row w-full justify-evenly mt-5 mb-5'>
<button type='button' onClick={handleSubmitButton} className='bg-cyan-500 text-white rounded p-2 hover:bg-cyan-600 transition-colors duration-300 ease-in-out w-1/6 cursor-pointer active:transform active:scale-95 overflow-x-hidden overflow-y-hidden'>Submit</button>
<button type='button' onClick={readTextFromClipboard} className='bg-yellow-500 text-white rounded p-2 hover:bg-yellow-600 transition-colors duration-300 ease-in-out w-1/6 cursor-pointer active:transform active:scale-95 overflow-x-hidden overflow-y-hidden'>Paste from fetch</button>
<button type='button' onClick={handleResetButton} className='bg-red-500 text-white rounded p-2 hover:bg-red-600 transition-colors duration-300 ease-in-out w-1/6 cursor-pointer active:transform active:scale-95 overflow-x-hidden overflow-y-hidden'>Reset</button>
</div>
</form>
</div>
{isVisible && (
<div className='w-full min-h-full h-full bg-zinc-900 flex flex-col items-center justify-center'>
<div className='w-8/10 min-h-8/10 flex flex-col bg-[rgba(0,0,0,0.2)] items-center p-10 border-black border-1 rounded-xl shadow-lg shadow-cyan-500/50 overflow-y-auto'>
<p className='w-full text-center text-white text-2xl overflow-hidden mb-10 pb-2 border-b'>Results:</p>
<p id='messageresults' className='w-8/10 h-6/10 text-center text-white text-2xl overflow-hidden bg-[rgba(0,0,0,0.2)] rounded-xl p-5' dangerouslySetInnerHTML={{ __html: message }}></p>
<div className='w-full h-1/10 flex justify-evenly mt-5 , mb-5'>
<button type='button' onClick={copyToClipboard} className='bg-green-500 text-white rounded p-2 hover:bg-green-600 transition-colors duration-300 ease-in-out min-w-1/6 w-1/6 min-h-4/6 h-4/6 cursor-pointer active:transform active:scale-95 overflow-x-hidden overflow-y-hidden'>Copy</button>
<button type='button' onClick={scrollToTop} className='bg-yellow-600 text-white rounded p-2 hover:bg-yellow-700 transition-colors duration-300 ease-in-out min-w-1/6 w-1/6 min-h-4/6 h-4/6 cursor-pointer active:transform active:scale-95 overflow-x-hidden overflow-y-hidden'>Back to top</button>
</div>
</div>
</div>
)}
</>
);
}
export default Home;

View File

@ -0,0 +1,106 @@
import React, { useState, useRef } from "react";
import shaka from "shaka-player"; // Import the Shaka Player library
function TestPlayer() {
const [mpdUrl, setMpdUrl] = useState("");
const [kid, setKid] = useState("");
const [key, setKey] = useState("");
const videoRef = useRef(null); // Ref to the video element
const handleSubmit = () => {
if (mpdUrl && kid && key) {
// Split the KIDs and Keys by new lines
const kidsArray = kid.split("\n").map((k) => k.trim());
const keysArray = key.split("\n").map((k) => k.trim());
if (kidsArray.length !== keysArray.length) {
console.error("The number of KIDs and Keys must be the same.");
return;
}
// Initialize Shaka Player only when the submit button is pressed
const player = new shaka.Player(videoRef.current);
// Widevine DRM configuration with the provided KIDs and Keys
const config = {
drm: {
clearKeys: {},
},
};
// Map KIDs to Keys
kidsArray.forEach((kid, index) => {
config.drm.clearKeys[kid] = keysArray[index];
});
console.log("Configuring player with the following DRM config:", config);
player.configure(config);
const manifestUri = mpdUrl; // Get the MPD URL from state
player.load(manifestUri)
.then(() => {
console.log("Shaka Player loaded successfully.");
// Start playback once the stream is loaded successfully
videoRef.current.play();
})
.catch((error) => {
console.error("Error loading the stream:", error);
});
}
};
return (
<div className="min-w-full w-full min-h-full h-full bg-zinc-900 shadow-lg shadow-black flex flex-row overflow-y-auto pt-5 pl-5 pr-5 items-center justify-around">
<div className="min-8/10 w-8/10 min-h-8/10 h-8/10 flex flex-row overflow-y-auto items-center justify-around border shadow-lg shadow-red-700 rounded-2xl">
<div className="w-7/10 h-7/10 border border-black rounded-2xl p-5 bg-[rgba(0,0,0,0.2)] shadow-lg shadow-black">
<video
ref={videoRef}
className="w-full h-full"
controls
// Removed autoPlay so it doesn't start until submit is pressed
/>
</div>
<div className="w-2/10 h-7/10 flex flex-col border border-black rounded-2xl p-5 bg-[rgba(0,0,0,0.2)] shadow-lg shadow-black focus-within:w-3/10 transition-all duration-300 ease-in-out">
<form className="h-full flex flex-col gap-2">
<input
type="text"
placeholder="MPD URL"
id="player_mpd"
name="player_mpd"
value={mpdUrl}
onChange={(e) => setMpdUrl(e.target.value)}
className="text-white bg-[rgba(0,0,0,0.2)] focus:outline-none rounded focus:shadow-sm focus:shadow-red-700/50 transition-all duration-300 ease-in-out p-2"
/>
<textarea
placeholder="KIDs, separated by new lines"
id="player_kid"
name="player_kid"
value={kid}
onChange={(e) => setKid(e.target.value)}
className="text-white bg-[rgba(0,0,0,0.2)] focus:outline-none rounded h-1/4 focus:h-1/3 overflow-y-auto focus:shadow-sm focus:shadow-red-700/50 transition-all duration-300 ease-in-out p-2 resize-none"
/>
<textarea
placeholder="Keys, separated by new lines"
id="player_key"
name="player_key"
value={key}
onChange={(e) => setKey(e.target.value)}
className="text-white bg-[rgba(0,0,0,0.2)] focus:outline-none rounded h-1/4 focus:h-1/3 focus:shadow-sm focus:shadow-red-700/50 transition-all duration-300 ease-in-out p-2 resize-none"
/>
<button
type="button"
onClick={handleSubmit}
className="mt-auto bg-white text-black rounded p-2 hover:bg-slate-200 transition-colors duration-300 ease-in-out w-full cursor-pointer active:transform active:scale-95 overflow-x-hidden overflow-y-hidden"
>
Submit
</button>
</form>
</div>
</div>
</div>
);
}
export default TestPlayer;

View File

@ -0,0 +1,19 @@
import React from 'react'
function Footer() {
return (
<div className='flex flex-row w-full h-full items-center justify-around overflow-hidden'>
<a href='https://discord.cdrm-project.com' className='w-1/6 h-1/6 hover:animate-bounce'>
<img src='/discord.svg' alt='Discord Logo'/>
</a>
<a href='https://telegram.cdrm-project.com' className='w-1/6 h-1/6 hover:animate-bounce'>
<img src='/telegram.svg' alt='Telegram Logo' />
</a>
<a href='https://github.com/tpd94' className='w-1/6 h-1/6 hover:animate-bounce'>
<img src='/github.svg' alt='Github Logo' />
</a>
</div>
)
}
export default Footer

View File

@ -0,0 +1,13 @@
import React from 'react'
function NavHead() {
return (
<div className='flex flex-row w-full h-full shadow-lg shadow-black items-center justify-center overflow-hidden'>
<a href='/' className='w-2/3 h-2/3 flex'>
<img src='/logo.svg' alt='CDRM Logo'/>
</a>
</div>
)
}
export default NavHead

View File

@ -0,0 +1,12 @@
import React from 'react'
import NavItems from './NavItems'
function MainNav() {
return (
<div className='flex flex-col w-full h-full overflow-y-auto shadow-lg shadow-black'>
< NavItems />
</div>
)
}
export default MainNav

View File

@ -0,0 +1,37 @@
import React from 'react'
import { Link, useLocation } from 'react-router-dom';
function NavItems() {
const location = useLocation();
return (
<>
<div className={`w-full h-1/10 flex flex-col items-center ${location.pathname === '/' ? 'bg-black/50' : 'hover:bg-black/35'} transition-all duration-300 ease-in-out overflow-hidden mt-5`}>
<Link to='/' className='flex w-8/10 items-center'>
<img src='/home.svg' alt='Home' className='flex h-5/10 w-3/10'/>
<p className='w-7/10 h-full flex items-center text-xl text-white'>Home</p>
</Link>
</div>
<div className={`w-full h-1/10 flex flex-col items-center ${location.pathname === '/Cache' ? 'bg-black/50' : 'hover:bg-black/35'} transition-all duration-300 ease-in-out overflow-hidden`}>
<Link to='/cache' className='flex w-8/10 items-center'>
<img src='/search.svg' alt='Search' className='flex h-5/10 w-3/10'/>
<p className='w-7/10 h-full flex items-center text-xl text-white'>Cache</p>
</Link>
</div>
<div className={`w-full h-1/10 flex flex-col items-center ${location.pathname === '/TestPlayer' ? 'bg-black/50' : 'hover:bg-black/35'} transition-all duration-300 ease-in-out overflow-hidden`}>
<Link to='/testplayer' className='flex w-8/10 items-center'>
<img src='/video.svg' alt='Test Player' className='flex h-5/10 w-3/10'/>
<p className='w-7/10 h-full flex items-center text-xl text-white'>Test Player</p>
</Link>
</div>
<div className={`w-full h-1/10 flex flex-col items-center ${location.pathname === '/API' ? 'bg-black/50' : 'hover:bg-black/35'} transition-all duration-300 ease-in-out overflow-hidden`}>
<Link to='/api' className='flex w-8/10 items-center'>
<img src='/docu_logo.svg' alt='API' className='flex h-5/10 w-3/10'/>
<p className='w-7/10 h-full flex items-center text-xl text-white'>API</p>
</Link>
</div>
</>
)
}
export default NavItems

View File

@ -0,0 +1,22 @@
import React from 'react'
import MainNav from './MainNav/MainNav'
import Footer from './Footer/Footer'
import NavHead from './Header/Header'
function Sidebar() {
return (
<div className='flex flex-col w-full h-full bg-zinc-900 border-r-1'>
<div className='w-full h-1/10'>
<NavHead />
</div>
<div className='w-full h-8/10'>
<MainNav />
</div>
<div className='w-full h-1/10'>
<Footer />
</div>
</div>
)
}
export default Sidebar

View File

@ -0,0 +1,9 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@ -0,0 +1,21 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [react(), tailwindcss(),],
server: {
proxy: {
'/api': {
target: 'http://127.0.0.1:5000',
changeOrigin: true,
secure: false,
},
'/remotecdm': {
target: 'http://127.0.0.1:5000',
changeOrigin: true,
secure: false,
},
},
},
})

View File

@ -0,0 +1,126 @@
import os
import yaml
import mysql.connector
from mysql.connector import Error
def get_db_config():
# Configure your MariaDB connection
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
config = yaml.safe_load(file)
db_config = {
'host': f'{config["mariadb"]["host"]}',
'user': f'{config["mariadb"]["user"]}',
'password': f'{config["mariadb"]["password"]}',
'database': f'{config["mariadb"]["database"]}'
}
return db_config
def create_database():
try:
with mysql.connector.connect(**get_db_config()) as conn:
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS licenses (
SERVICE VARCHAR(255),
PSSH TEXT,
KID VARCHAR(255) PRIMARY KEY,
`Key` TEXT,
License_URL TEXT,
Headers TEXT,
Cookies TEXT,
Data BLOB
)
''')
conn.commit()
except Error as e:
print(f"Error: {e}")
def cache_to_db(service=None, pssh=None, kid=None, key=None, license_url=None, headers=None, cookies=None, data=None):
try:
with mysql.connector.connect(**get_db_config()) as conn:
cursor = conn.cursor()
cursor.execute('SELECT 1 FROM licenses WHERE KID = %s', (kid,))
existing_record = cursor.fetchone()
cursor.execute('''
INSERT INTO licenses (SERVICE, PSSH, KID, `Key`, License_URL, Headers, Cookies, Data)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
SERVICE = VALUES(SERVICE),
PSSH = VALUES(PSSH),
`Key` = VALUES(`Key`),
License_URL = VALUES(License_URL),
Headers = VALUES(Headers),
Cookies = VALUES(Cookies),
Data = VALUES(Data)
''', (service, pssh, kid, key, license_url, headers, cookies, data))
conn.commit()
return True if existing_record else False
except Error as e:
print(f"Error: {e}")
return False
def search_by_pssh_or_kid(search_filter):
results = set()
try:
with mysql.connector.connect(**get_db_config()) as conn:
cursor = conn.cursor()
like_filter = f"%{search_filter}%"
cursor.execute('SELECT PSSH, KID, `Key` FROM licenses WHERE PSSH LIKE %s', (like_filter,))
results.update(cursor.fetchall())
cursor.execute('SELECT PSSH, KID, `Key` FROM licenses WHERE KID LIKE %s', (like_filter,))
results.update(cursor.fetchall())
final_results = [{'PSSH': row[0], 'KID': row[1], 'Key': row[2]} for row in results]
return final_results[:20]
except Error as e:
print(f"Error: {e}")
return []
def get_key_by_kid_and_service(kid, service):
try:
with mysql.connector.connect(**get_db_config()) as conn:
cursor = conn.cursor()
cursor.execute('SELECT `Key` FROM licenses WHERE KID = %s AND SERVICE = %s', (kid, service))
result = cursor.fetchone()
return result[0] if result else None
except Error as e:
print(f"Error: {e}")
return None
def get_kid_key_dict(service_name):
try:
with mysql.connector.connect(**get_db_config()) as conn:
cursor = conn.cursor()
cursor.execute('SELECT KID, `Key` FROM licenses WHERE SERVICE = %s', (service_name,))
return {row[0]: row[1] for row in cursor.fetchall()}
except Error as e:
print(f"Error: {e}")
return {}
def get_unique_services():
try:
with mysql.connector.connect(**get_db_config()) as conn:
cursor = conn.cursor()
cursor.execute('SELECT DISTINCT SERVICE FROM licenses')
return [row[0] for row in cursor.fetchall()]
except Error as e:
print(f"Error: {e}")
return []
def key_count():
try:
with mysql.connector.connect(**get_db_config()) as conn:
cursor = conn.cursor()
cursor.execute('SELECT COUNT(KID) FROM licenses')
return cursor.fetchone()[0]
except Error as e:
print(f"Error: {e}")
return 0

View File

@ -0,0 +1,123 @@
import sqlite3
import os
def create_database():
# Using with statement to manage the connection and cursor
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS licenses (
SERVICE TEXT,
PSSH TEXT,
KID TEXT PRIMARY KEY,
Key TEXT,
License_URL TEXT,
Headers TEXT,
Cookies TEXT,
Data TEXT
)
''')
def cache_to_db(service: str = None, pssh: str = None, kid: str = None, key: str = None, license_url: str = None, headers: str = None, cookies: str = None, data: str = None):
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
cursor = conn.cursor()
# Check if the record with the given KID already exists
cursor.execute('''SELECT 1 FROM licenses WHERE KID = ?''', (kid,))
existing_record = cursor.fetchone()
# Insert or replace the record
cursor.execute('''
INSERT OR REPLACE INTO licenses (SERVICE, PSSH, KID, Key, License_URL, Headers, Cookies, Data)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''', (service, pssh, kid, key, license_url, headers, cookies, data))
# If the record was existing and updated, return True (updated), else return False (added)
return True if existing_record else False
def search_by_pssh_or_kid(search_filter):
# Using with statement to automatically close the connection
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
cursor = conn.cursor()
# Initialize a set to store unique matching records
results = set()
# Search for records where PSSH contains the search_filter
cursor.execute('''
SELECT * FROM licenses WHERE PSSH LIKE ?
''', ('%' + search_filter + '%',))
rows = cursor.fetchall()
for row in rows:
results.add((row[1], row[2], row[3])) # (PSSH, KID, Key)
# Search for records where KID contains the search_filter
cursor.execute('''
SELECT * FROM licenses WHERE KID LIKE ?
''', ('%' + search_filter + '%',))
rows = cursor.fetchall()
for row in rows:
results.add((row[1], row[2], row[3])) # (PSSH, KID, Key)
# Convert the set of results to a list of dictionaries for output
final_results = [{'PSSH': result[0], 'KID': result[1], 'Key': result[2]} for result in results]
return final_results[:20]
def get_key_by_kid_and_service(kid, service):
# Using 'with' to automatically close the connection when done
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
cursor = conn.cursor()
# Query to search by KID and SERVICE
cursor.execute('''
SELECT Key FROM licenses WHERE KID = ? AND SERVICE = ?
''', (kid, service))
# Fetch the result
result = cursor.fetchone()
# Check if a result was found
return result[0] if result else None # The 'Key' is the first (and only) column returned in the result
def get_kid_key_dict(service_name):
# Using with statement to automatically manage the connection and cursor
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
cursor = conn.cursor()
# Query to fetch KID and Key for the selected service
cursor.execute('''
SELECT KID, Key FROM licenses WHERE SERVICE = ?
''', (service_name,))
# Fetch all results and create the dictionary
kid_key_dict = {row[0]: row[1] for row in cursor.fetchall()}
return kid_key_dict
def get_unique_services():
# Using with statement to automatically manage the connection and cursor
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
cursor = conn.cursor()
# Query to get distinct services from the 'licenses' table
cursor.execute('SELECT DISTINCT SERVICE FROM licenses')
# Fetch all results and extract the unique services
services = cursor.fetchall()
# Extract the service names from the tuple list
unique_services = [service[0] for service in services]
return unique_services
def key_count():
# Using with statement to automatically manage the connection and cursor
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
cursor = conn.cursor()
# Count the number of KID entries in the licenses table
cursor.execute('SELECT COUNT(KID) FROM licenses')
count = cursor.fetchone()[0] # Fetch the result and get the count
return count

View File

@ -0,0 +1,375 @@
from pywidevine.cdm import Cdm as widevineCdm
from pywidevine.device import Device as widevineDevice
from pywidevine.pssh import PSSH as widevinePSSH
from pyplayready.cdm import Cdm as playreadyCdm
from pyplayready.device import Device as playreadyDevice
from pyplayready.system.pssh import PSSH as playreadyPSSH
import requests
import base64
import ast
import glob
import os
import yaml
def find_license_key(data, keywords=None):
if keywords is None:
keywords = ["license", "licenseData", "widevine2License"] # Default list of keywords to search for
# If the data is a dictionary, check each key
if isinstance(data, dict):
for key, value in data.items():
if any(keyword in key.lower() for keyword in
keywords): # Check if any keyword is in the key (case-insensitive)
return value.replace("-", "+").replace("_", "/") # Return the value immediately when found
# Recursively check if the value is a dictionary or list
if isinstance(value, (dict, list)):
result = find_license_key(value, keywords) # Recursively search
if result: # If a value is found, return it
return result.replace("-", "+").replace("_", "/")
# If the data is a list, iterate through each item
elif isinstance(data, list):
for item in data:
result = find_license_key(item, keywords) # Recursively search
if result: # If a value is found, return it
return result.replace("-", "+").replace("_", "/")
return None # Return None if no matching key is found
def find_license_challenge(data, keywords=None, new_value=None):
if keywords is None:
keywords = ["license", "licenseData", "widevine2License", "licenseRequest"] # Default list of keywords to search for
# If the data is a dictionary, check each key
if isinstance(data, dict):
for key, value in data.items():
if any(keyword in key.lower() for keyword in keywords): # Check if any keyword is in the key (case-insensitive)
data[key] = new_value # Modify the value in-place
# Recursively check if the value is a dictionary or list
elif isinstance(value, (dict, list)):
find_license_challenge(value, keywords, new_value) # Recursively modify in place
# If the data is a list, iterate through each item
elif isinstance(data, list):
for i, item in enumerate(data):
result = find_license_challenge(item, keywords, new_value) # Recursively modify in place
return data # Return the modified original data (no new structure is created)
def is_base64(string):
try:
# Try decoding the string
decoded_data = base64.b64decode(string)
# Check if the decoded data, when re-encoded, matches the original string
return base64.b64encode(decoded_data).decode('utf-8') == string
except Exception:
# If decoding or encoding fails, it's not Base64
return False
def api_decrypt(pssh:str = None, license_url: str = None, headers: str = None, cookies: str = None, json_data: str = None):
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
config = yaml.safe_load(file)
if config['database_type'].lower() == 'sqlite':
from custom_functions.database.cache_to_db_sqlite import cache_to_db
elif config['database_type'].lower() == 'mariadb':
from custom_functions.database.cache_to_db_mariadb import cache_to_db
if pssh is None:
return {
'status': 'error',
'message': 'No PSSH provided'
}
try:
if "</WRMHEADER>".encode("utf-16-le") in base64.b64decode(pssh): # PR
try:
pr_pssh = playreadyPSSH(pssh)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred processing PSSH\n\n{error}'
}
try:
base_name = config["default_pr_cdm"]
if not base_name.endswith(".prd"):
base_name += ".prd"
prd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/PR/{base_name}')
else:
prd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/PR/{base_name}')
if prd_files:
pr_device = playreadyDevice.load(prd_files[0])
else:
return {
'status': 'error',
'message': 'No default .prd file found'
}
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred location PlayReady CDM file\n\n{error}'
}
try:
pr_cdm = playreadyCdm.from_device(pr_device)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred loading PlayReady CDM\n\n{error}'
}
try:
pr_session_id = pr_cdm.open()
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred opening a CDM session\n\n{error}'
}
try:
pr_challenge = pr_cdm.get_license_challenge(pr_session_id, pr_pssh.wrm_headers[0])
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting license challenge\n\n{error}'
}
try:
if headers:
format_headers = ast.literal_eval(headers)
else:
format_headers = None
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting headers\n\n{error}'
}
try:
if cookies:
format_cookies = ast.literal_eval(cookies)
else:
format_cookies = None
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting cookies\n\n{error}'
}
try:
if json_data and not is_base64(json_data):
format_json_data = ast.literal_eval(json_data)
else:
format_json_data = None
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting json_data\n\n{error}'
}
licence = None
try:
licence = requests.post(
url=license_url,
headers=format_headers,
cookies=format_cookies,
json=format_json_data if format_json_data is not None else None,
data=pr_challenge if format_json_data is None else None
)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred sending license reqeust\n\n{error}\n\n{licence.content}'
}
try:
pr_cdm.parse_license(pr_session_id, licence.text)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred parsing license content\n\n{error}\n\n{licence.content}'
}
returned_keys = ""
try:
keys = list(pr_cdm.get_keys(pr_session_id))
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting keys\n\n{error}'
}
try:
for index, key in enumerate(keys):
if key.key_type != 'SIGNING':
cache_to_db(pssh=pssh, license_url=license_url, headers=headers, cookies=cookies,
data=pr_challenge if json_data is None else json_data, kid=key.key_id.hex,
key=key.key.hex())
if index != len(keys) - 1:
returned_keys += f"{key.key_id.hex}:{key.key.hex()}\n"
else:
returned_keys += f"{key.key_id.hex}:{key.key.hex()}"
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred formatting keys\n\n{error}'
}
try:
pr_cdm.close(pr_session_id)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred closing session\n\n{error}'
}
try:
return {
'status': 'success',
'message': returned_keys
}
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting returned_keys\n\n{error}'
}
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred processing PSSH\n\n{error}'
}
else:
try:
wv_pssh = widevinePSSH(pssh)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred processing PSSH\n\n{error}'
}
try:
base_name = config["default_wv_cdm"]
if not base_name.endswith(".wvd"):
base_name += ".wvd"
wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/WV/{base_name}')
else:
wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/WV/{base_name}')
if wvd_files:
wv_device = widevineDevice.load(wvd_files[0])
else:
return {
'status': 'error',
'message': 'No default .wvd file found'
}
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred location Widevine CDM file\n\n{error}'
}
try:
wv_cdm = widevineCdm.from_device(wv_device)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred loading Widevine CDM\n\n{error}'
}
try:
wv_session_id = wv_cdm.open()
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred opening a CDM session\n\n{error}'
}
try:
wv_challenge = wv_cdm.get_license_challenge(wv_session_id, wv_pssh)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting license challenge\n\n{error}'
}
try:
if headers:
format_headers = ast.literal_eval(headers)
else:
format_headers = None
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting headers\n\n{error}'
}
try:
if cookies:
format_cookies = ast.literal_eval(cookies)
else:
format_cookies = None
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting cookies\n\n{error}'
}
try:
if json_data and not is_base64(json_data):
format_json_data = ast.literal_eval(json_data)
format_json_data = find_license_challenge(data=format_json_data, new_value=base64.b64encode(wv_challenge).decode())
else:
format_json_data = None
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting json_data\n\n{error}'
}
licence = None
try:
licence = requests.post(
url=license_url,
headers=format_headers,
cookies=format_cookies,
json=format_json_data if format_json_data is not None else None,
data=wv_challenge if format_json_data is None else None
)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred sending license reqeust\n\n{error}\n\n{licence.content}'
}
try:
wv_cdm.parse_license(wv_session_id, licence.content)
except:
try:
license_json = licence.json()
license_value = find_license_key(license_json)
wv_cdm.parse_license(wv_session_id, license_value)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred parsing license content\n\n{error}\n\n{licence.content}'
}
returned_keys = ""
try:
keys = list(wv_cdm.get_keys(wv_session_id))
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting keys\n\n{error}'
}
try:
for index, key in enumerate(keys):
if key.type != 'SIGNING':
cache_to_db(pssh=pssh, license_url=license_url, headers=headers, cookies=cookies, data=wv_challenge if json_data is None else json_data, kid=key.kid.hex, key=key.key.hex())
if index != len(keys) - 1:
returned_keys += f"{key.kid.hex}:{key.key.hex()}\n"
else:
returned_keys += f"{key.kid.hex}:{key.key.hex()}"
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred formatting keys\n\n{error}'
}
try:
wv_cdm.close(wv_session_id)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred closing session\n\n{error}'
}
try:
return {
'status': 'success',
'message': returned_keys
}
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting returned_keys\n\n{error}'
}

View File

@ -0,0 +1,68 @@
import os
import yaml
import requests
def check_for_wvd_cdm():
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
config = yaml.safe_load(file)
if config['default_wv_cdm'] == '':
answer = ' '
while answer[0].upper() != 'Y' and answer[0].upper() != 'N':
answer = input('No default Widevine CDM specified, would you like to download one from The CDM Project? (Y)es/(N)o: ')
if answer[0].upper() == 'Y':
response = requests.get(url='https://cdm-project.com/CDRM-Team/CDMs/raw/branch/main/Widevine/L3/public.wvd')
if response.status_code == 200:
with open(f'{os.getcwd()}/configs/CDMs/WV/public.wvd', 'wb') as file:
file.write(response.content)
config['default_wv_cdm'] = 'public'
with open(f'{os.getcwd()}/configs/config.yaml', 'w') as file:
yaml.dump(config, file)
print("Successfully downloaded Widevine CDM")
else:
exit(f"Download failed, please try again or place a .wvd file in {os.getcwd()}/configs/CDMs/WV and specify the name in {os.getcwd()}/configs/config.yaml")
if answer[0].upper() == 'N':
exit(f"Place a .wvd file in {os.getcwd()}/configs/CDMs/WV and specify the name in {os.getcwd()}/configs/config.yaml")
else:
base_name = config["default_wv_cdm"]
if not base_name.endswith(".wvd"):
base_name += ".wvd"
if os.path.exists(f'{os.getcwd()}/configs/CDMs/WV/{base_name}'):
return
else:
exit(f"Widevine CDM {base_name} does not exist in {os.getcwd()}/configs/CDMs/WV")
def check_for_prd_cdm():
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
config = yaml.safe_load(file)
if config['default_pr_cdm'] == '':
answer = ' '
while answer[0].upper() != 'Y' and answer[0].upper() != 'N':
answer = input('No default PlayReady CDM specified, would you like to download one from The CDM Project? (Y)es/(N)o: ')
if answer[0].upper() == 'Y':
response = requests.get(url='https://cdm-project.com/CDRM-Team/CDMs/raw/branch/main/Playready/SL2000/public.prd')
if response.status_code == 200:
with open(f'{os.getcwd()}/configs/CDMs/PR/public.prd', 'wb') as file:
file.write(response.content)
config['default_pr_cdm'] = 'public'
with open(f'{os.getcwd()}/configs/config.yaml', 'w') as file:
yaml.dump(config, file)
print("Successfully downloaded PlayReady CDM")
else:
exit(f"Download failed, please try again or place a .prd file in {os.getcwd()}/configs/CDMs/PR and specify the name in {os.getcwd()}/configs/config.yaml")
if answer[0].upper() == 'N':
exit(f"Place a .prd file in {os.getcwd()}/configs/CDMs/PR and specify the name in {os.getcwd()}/configs/config.yaml")
else:
base_name = config["default_pr_cdm"]
if not base_name.endswith(".prd"):
base_name += ".prd"
if os.path.exists(f'{os.getcwd()}/configs/CDMs/PR/{base_name}'):
return
else:
exit(f"PlayReady CDM {base_name} does not exist in {os.getcwd()}/configs/CDMs/WV")
def check_for_cdms():
check_for_wvd_cdm()
check_for_prd_cdm()

View File

@ -0,0 +1,25 @@
import os
def check_for_config_file():
if os.path.exists(f'{os.getcwd()}/configs/config.yaml'):
return
else:
default_config = """\
default_wv_cdm: ''
default_pr_cdm: ''
# change the type to mariadb to use mariadb below
database_type: 'sqlite'
fqdn: ''
remote_cdm_secret: ''
# uncomment all the lines below to use mariadb and fill out the information
#mariadb:
# user: ''
# password: ''
# host: ''
# port: ''
# database: ''
"""
with open(f'{os.getcwd()}/configs/config.yaml', 'w') as f:
f.write(default_config)
return

View File

@ -0,0 +1,29 @@
import os
import yaml
def check_for_sqlite_database():
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
config = yaml.safe_load(file)
if os.path.exists(f'{os.getcwd()}/databases/key_cache.db'):
return
else:
if config['database_type'].lower() != 'mariadb':
from custom_functions.database.cache_to_db_sqlite import create_database
create_database()
return
else:
return
def check_for_mariadb_database():
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
config = yaml.safe_load(file)
if config['database_type'].lower() == 'mariadb':
from custom_functions.database.cache_to_db_mariadb import create_database
create_database()
return
else:
return
def check_for_sql_database():
check_for_sqlite_database()
check_for_mariadb_database()

View File

@ -0,0 +1,44 @@
import os
def check_for_config_folder():
if os.path.isdir(f'{os.getcwd()}/configs'):
return
else:
os.mkdir(f'{os.getcwd()}/configs')
return
def check_for_database_folder():
if os.path.isdir(f'{os.getcwd()}/databases'):
return
else:
os.mkdir(f'{os.getcwd()}/databases')
os.mkdir(f'{os.getcwd()}/databases/sql')
return
def check_for_cdm_folder():
if os.path.isdir(f'{os.getcwd()}/configs/CDMs'):
return
else:
os.mkdir(f'{os.getcwd()}/configs/CDMs')
return
def check_for_wv_cdm_folder():
if os.path.isdir(f'{os.getcwd()}/configs/CDMs/WV'):
return
else:
os.mkdir(f'{os.getcwd()}/configs/CDMs/WV')
return
def check_for_cdm_pr_folder():
if os.path.isdir(f'{os.getcwd()}/configs/CDMs/PR'):
return
else:
os.mkdir(f'{os.getcwd()}/configs/CDMs/PR')
return
def folder_checks():
check_for_config_folder()
check_for_database_folder()
check_for_cdm_folder()
check_for_wv_cdm_folder()
check_for_cdm_pr_folder()

View File

@ -0,0 +1,11 @@
from custom_functions.prechecks.folder_checks import folder_checks
from custom_functions.prechecks.config_file_checks import check_for_config_file
from custom_functions.prechecks.database_checks import check_for_sql_database
from custom_functions.prechecks.cdm_checks import check_for_cdms
def run_precheck():
folder_checks()
check_for_config_file()
check_for_cdms()
check_for_sql_database()
return

13
examples/config.yaml Normal file
View File

@ -0,0 +1,13 @@
default_wv_cdm: 'public'
default_pr_cdm: 'public'
# change the type to mariadb to use mariadb below
database_type: 'mariadb'
fqdn: 'cdrm-project.com'
remote_cdm_secret: 'CDRM'
# uncomment all the lines below to use mariadb and fill out the information
mariadb:
user: 'cdrm'
password: 'password'
host: '127.0.0.1'
database: 'cdrm_project'

21
main.py Normal file
View File

@ -0,0 +1,21 @@
from custom_functions.prechecks.precheck import run_precheck
run_precheck()
from flask import Flask
from flask_cors import CORS
from routes.react import react_bp
from routes.api import api_bp
from routes.remote_device_wv import remotecdm_wv_bp
from routes.remote_device_pr import remotecdm_pr_bp
app = Flask(__name__)
CORS(app)
# Register the blueprint
app.register_blueprint(react_bp)
app.register_blueprint(api_bp)
app.register_blueprint(remotecdm_wv_bp)
app.register_blueprint(remotecdm_pr_bp)
if __name__ == '__main__':
app.run(debug=True)

8
requirements.txt Normal file
View File

@ -0,0 +1,8 @@
Flask~=3.1.0
Flask-Cors
pywidevine~=1.8.0
pyplayready~=0.6.0
requests~=2.32.3
protobuf~=4.25.6
PyYAML~=6.0.2
mysql-connector-python

229
routes/api.py Normal file
View File

@ -0,0 +1,229 @@
import os
import sqlite3
from flask import Blueprint, jsonify, request, send_file
import json
from custom_functions.decrypt.api_decrypt import api_decrypt
import shutil
import math
import yaml
import mysql.connector
from io import StringIO
import tempfile
import time
api_bp = Blueprint('api', __name__)
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
config = yaml.safe_load(file)
if config['database_type'].lower() != 'mariadb':
from custom_functions.database.cache_to_db_sqlite import search_by_pssh_or_kid, cache_to_db, \
get_key_by_kid_and_service, get_unique_services, get_kid_key_dict, key_count
elif config['database_type'].lower() == 'mariadb':
from custom_functions.database.cache_to_db_mariadb import search_by_pssh_or_kid, cache_to_db, \
get_key_by_kid_and_service, get_unique_services, get_kid_key_dict, key_count
def get_db_config():
# Configure your MariaDB connection
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
config = yaml.safe_load(file)
db_config = {
'host': f'{config["mariadb"]["host"]}',
'user': f'{config["mariadb"]["user"]}',
'password': f'{config["mariadb"]["password"]}',
'database': f'{config["mariadb"]["database"]}'
}
return db_config
@api_bp.route('/api/cache/search', methods=['POST'])
def get_data():
search_argument = json.loads(request.data)['input']
results = search_by_pssh_or_kid(search_filter=search_argument)
return jsonify(results)
@api_bp.route('/api/cache/<service>/<kid>', methods=['GET'])
def get_single_key_service(service, kid):
result = get_key_by_kid_and_service(kid=kid, service=service)
return jsonify({
'code': 0,
'content_key': result,
})
@api_bp.route('/api/cache/<service>', methods=['GET'])
def get_multiple_key_service(service):
result = get_kid_key_dict(service_name=service)
pages = math.ceil(len(result) / 10)
return jsonify({
'code': 0,
'content_keys': result,
'pages': pages
})
@api_bp.route('/api/cache/<service>/<kid>', methods=['POST'])
def add_single_key_service(service, kid):
body = request.get_json()
content_key = body['content_key']
result = cache_to_db(service=service, kid=kid, key=content_key)
if result:
return jsonify({
'code': 0,
'updated': True,
})
elif result is False:
return jsonify({
'code': 0,
'updated': True,
})
@api_bp.route('/api/cache/<service>', methods=['POST'])
def add_multiple_key_service(service):
body = request.get_json()
keys_added = 0
keys_updated = 0
for kid, key in body['content_keys'].items():
result = cache_to_db(service=service, kid=kid, key=key)
if result is True:
keys_updated += 1
elif result is False:
keys_added += 1
return jsonify({
'code': 0,
'added': str(keys_added),
'updated': str(keys_updated),
})
@api_bp.route('/api/cache', methods=['POST'])
def unique_service():
services = get_unique_services()
return jsonify({
'code': 0,
'service_list': services,
})
@api_bp.route('/api/cache/download', methods=['GET'])
def download_database():
if config['database_type'].lower() != 'mariadb':
original_database_path = f'{os.getcwd()}/databases/sql/key_cache.db'
# Make a copy of the original database (without locking the original)
modified_database_path = f'{os.getcwd()}/databases/sql/key_cache_modified.db'
# Using shutil.copy2 to preserve metadata (timestamps, etc.)
shutil.copy2(original_database_path, modified_database_path)
# Open the copied database for modification using 'with' statement to avoid locks
with sqlite3.connect(modified_database_path) as conn:
cursor = conn.cursor()
# Update all rows to remove Headers and Cookies (set them to NULL or empty strings)
cursor.execute('''
UPDATE licenses
SET Headers = NULL,
Cookies = NULL
''')
# No need for explicit commit, it's done automatically with the 'with' block
# The connection will automatically be committed and closed when the block ends
# Send the modified database as an attachment
return send_file(modified_database_path, as_attachment=True, download_name='key_cache.db')
if config['database_type'].lower() == 'mariadb':
try:
# Connect to MariaDB
conn = mysql.connector.connect(**get_db_config())
cursor = conn.cursor()
# Update sensitive data (this updates the live DB, you may want to duplicate rows instead)
cursor.execute('''
UPDATE licenses
SET Headers = NULL,
Cookies = NULL
''')
conn.commit()
# Now export the table
cursor.execute('SELECT * FROM licenses')
rows = cursor.fetchall()
column_names = [desc[0] for desc in cursor.description]
# Dump to SQL-like format
output = StringIO()
output.write(f"-- Dump of `licenses` table\n")
for row in rows:
values = ', '.join(f"'{str(v).replace('\'', '\\\'')}'" if v is not None else 'NULL' for v in row)
output.write(f"INSERT INTO licenses ({', '.join(column_names)}) VALUES ({values});\n")
# Write to a temp file for download
temp_dir = tempfile.gettempdir()
temp_path = os.path.join(temp_dir, 'key_cache.sql')
with open(temp_path, 'w', encoding='utf-8') as f:
f.write(output.getvalue())
return send_file(temp_path, as_attachment=True, download_name='licenses_dump.sql')
except mysql.connector.Error as err:
return {"error": str(err)}, 500
_keycount_cache = {
'count': None,
'timestamp': 0
}
@api_bp.route('/api/cache/keycount', methods=['GET'])
def get_count():
now = time.time()
if now - _keycount_cache['timestamp'] > 10 or _keycount_cache['count'] is None:
_keycount_cache['count'] = key_count()
_keycount_cache['timestamp'] = now
return jsonify({
'count': _keycount_cache['count']
})
@api_bp.route('/api/decrypt', methods=['POST'])
def decrypt_data():
api_request_data = json.loads(request.data)
if 'pssh' in api_request_data:
if api_request_data['pssh'] == '':
api_request_pssh = None
else:
api_request_pssh = api_request_data['pssh']
else:
api_request_pssh = None
if 'licurl' in api_request_data:
if api_request_data['licurl'] == '':
api_request_licurl = None
else:
api_request_licurl = api_request_data['licurl']
else:
api_request_licurl = None
if 'headers' in api_request_data:
if api_request_data['headers'] == '':
api_request_headers = None
else:
api_request_headers = api_request_data['headers']
else:
api_request_headers = None
if 'cookies' in api_request_data:
if api_request_data['cookies'] == '':
api_request_cookies = None
else:
api_request_cookies = api_request_data['cookies']
else:
api_request_cookies = None
if 'data' in api_request_data:
if api_request_data['data'] == '':
api_request_data = None
else:
api_request_data = api_request_data['data']
else:
api_request_data = None
result = api_decrypt(pssh=api_request_pssh, license_url=api_request_licurl, headers=api_request_headers, cookies=api_request_cookies, json_data=api_request_data)
if result['status'] == 'success':
return jsonify({
'status': 'success',
'message': result['message']
})
else:
return jsonify({
'status': 'fail',
'message': result['message']
})

16
routes/react.py Normal file
View File

@ -0,0 +1,16 @@
from flask import Blueprint, send_from_directory, request
import os
react_bp = Blueprint('react_bp', __name__, static_folder=f'{os.getcwd()}/cdrm-frontend/dist', static_url_path='/')
@react_bp.route('/', methods=['GET'])
@react_bp.route('/<path:path>', methods=["GET"])
@react_bp.route('/<path>', methods=["GET"])
def index(path=''):
if request.method == 'GET':
if path != "" and os.path.exists(react_bp.static_folder + '/' + path):
return send_from_directory(react_bp.static_folder, path)
else:
return send_from_directory(react_bp.static_folder, 'index.html')
else:
return

200
routes/remote_device_pr.py Normal file
View File

@ -0,0 +1,200 @@
from flask import Blueprint, jsonify, request, current_app, Response
import os
import yaml
from pyplayready.device import Device as PlayReadyDevice
from pyplayready.cdm import Cdm as PlayReadyCDM
from pyplayready import PSSH as PlayReadyPSSH
from pyplayready.exceptions import (InvalidSession, TooManySessions, InvalidLicense, InvalidPssh)
remotecdm_pr_bp = Blueprint('remotecdm_pr', __name__)
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
config = yaml.safe_load(file)
@remotecdm_pr_bp.route('/remotecdm/playready', methods=['GET', 'HEAD'])
def remote_cdm_playready():
if request.method == 'GET':
return jsonify({
'message': 'OK'
})
if request.method == 'HEAD':
response = Response(status=200)
response.headers['Server'] = 'playready serve'
return response
@remotecdm_pr_bp.route('/remotecdm/playready/deviceinfo', methods=['GET'])
def remote_cdm_playready_deviceinfo():
base_name = config["default_pr_cdm"]
if not base_name.endswith(".prd"):
full_file_name = (base_name + ".prd")
device = PlayReadyDevice.load(f'{os.getcwd()}/configs/CDMs/PR/{full_file_name}')
cdm = PlayReadyCDM.from_device(device)
return jsonify({
'security_level': cdm.security_level,
'host': f'{config["fqdn"]}/remotecdm/playready',
'secret': f'{config["remote_cdm_secret"]}',
'device_name': f'{base_name}'
})
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/open', methods=['GET'])
def remote_cdm_playready_open(device):
if str(device).lower() == config['default_pr_cdm'].lower():
pr_device = PlayReadyDevice.load(f'{os.getcwd()}/configs/CDMs/PR/{config["default_pr_cdm"]}.prd')
cdm = current_app.config['CDM'] = PlayReadyCDM.from_device(pr_device)
session_id = cdm.open()
return jsonify({
'message': 'Success',
'data': {
'session_id': session_id.hex(),
'device': {
'security_level': cdm.security_level
}
}
})
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/close/<session_id>', methods=['GET'])
def remote_cdm_playready_close(device, session_id):
if str(device).lower() == config['default_pr_cdm'].lower():
session_id = bytes.fromhex(session_id)
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'status': 400,
'message': f'No CDM for "{device}" has been opened yet. No session to close'
})
try:
cdm.close(session_id)
except InvalidSession:
return jsonify({
'status': 400,
'message': f'Invalid session ID "{session_id.hex()}", it may have expired'
})
return jsonify({
'status': 200,
'message': f'Successfully closed Session "{session_id.hex()}".',
})
else:
return jsonify({
'status': 400,
'message': f'Unauthorized'
})
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/get_license_challenge', methods=['POST'])
def remote_cdm_playready_get_license_challenge(device):
if str(device).lower() == config['default_pr_cdm'].lower():
body = request.get_json()
for required_field in ("session_id", "init_data"):
if not body.get(required_field):
return jsonify({
'status': 400,
'message': f'Missing required field "{required_field}" in JSON body'
})
cdm = current_app.config["CDM"]
session_id = bytes.fromhex(body["session_id"])
init_data = body["init_data"]
if not init_data.startswith("<WRMHEADER"):
try:
pssh = PlayReadyPSSH(init_data)
if pssh.wrm_headers:
init_data = pssh.wrm_headers[0]
except InvalidPssh as e:
return jsonify({
'message': f'Unable to parse base64 PSSH, {e}'
})
try:
license_request = cdm.get_license_challenge(
session_id=session_id,
wrm_header=init_data
)
except InvalidSession:
return jsonify({
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
})
except Exception as e:
return jsonify({
'message': f'Error, {e}'
})
return jsonify({
'message': 'success',
'data': {
'challenge': license_request
}
})
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/parse_license', methods=['POST'])
def remote_cdm_playready_parse_license(device):
if str(device).lower() == config['default_pr_cdm'].lower():
body = request.get_json()
for required_field in ("license_message", "session_id"):
if not body.get(required_field):
return jsonify({
'message': f'Missing required field "{required_field}" in JSON body'
})
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'message': f"No Cdm session for {device} has been opened yet. No session to use."
})
session_id = bytes.fromhex(body["session_id"])
license_message = body["license_message"]
try:
cdm.parse_license(session_id, license_message)
except InvalidSession:
return jsonify({
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
})
except InvalidLicense as e:
return jsonify({
'message': f"Invalid License, {e}"
})
except Exception as e:
return jsonify({
'message': f"Error, {e}"
})
return jsonify({
'message': 'Successfully parsed and loaded the Keys from the License message'
})
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/get_keys', methods=['POST'])
def remote_cdm_playready_get_keys(device):
if str(device).lower() == config['default_pr_cdm'].lower():
body = request.get_json()
for required_field in ("session_id",):
if not body.get(required_field):
return jsonify({
'message': f'Missing required field "{required_field}" in JSON body'
})
session_id = bytes.fromhex(body["session_id"])
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'message': f"Missing required field '{required_field}' in JSON body."
})
try:
keys = cdm.get_keys(session_id)
except InvalidSession:
return jsonify({
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
})
except Exception as e:
return jsonify({
'message': f"Error, {e}"
})
keys_json = [
{
"key_id": key.key_id.hex,
"key": key.key.hex(),
"type": key.key_type.value,
"cipher_type": key.cipher_type.value,
"key_length": key.key_length,
}
for key in keys
]
return jsonify({
'message': 'success',
'data': {
'keys': keys_json
}
})

369
routes/remote_device_wv.py Normal file
View File

@ -0,0 +1,369 @@
import os
from flask import Blueprint, jsonify, request, current_app, Response
import base64
from typing import Any, Optional, Union
from google.protobuf.message import DecodeError
from pywidevine.pssh import PSSH as widevinePSSH
from pywidevine import __version__
from pywidevine.cdm import Cdm as widevineCDM
from pywidevine.device import Device as widevineDevice
from pywidevine.exceptions import (InvalidContext, InvalidInitData, InvalidLicenseMessage, InvalidLicenseType,
InvalidSession, SignatureMismatch, TooManySessions)
import yaml
remotecdm_wv_bp = Blueprint('remotecdm_wv', __name__)
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
config = yaml.safe_load(file)
@remotecdm_wv_bp.route('/remotecdm/widevine', methods=['GET', 'HEAD'])
def remote_cdm_widevine():
if request.method == 'GET':
return jsonify({
'status': 200,
'message': f"{config['fqdn'].upper()} Remote Widevine CDM."
})
if request.method == 'HEAD':
response = Response(status=200)
response.headers['Server'] = f'https://github.com/devine-dl/pywidevine serve v{__version__}'
return response
@remotecdm_wv_bp.route('/remotecdm/widevine/deviceinfo', methods=['GET'])
def remote_cdm_widevine_deviceinfo():
if request.method == 'GET':
base_name = config["default_wv_cdm"]
if not base_name.endswith(".wvd"):
full_file_name = (base_name + ".wvd")
device = widevineDevice.load(f'{os.getcwd()}/configs/CDMs/WV/{full_file_name}')
cdm = widevineCDM.from_device(device)
return jsonify({
'device_type': cdm.device_type.name,
'system_id': cdm.system_id,
'security_level': cdm.security_level,
'host': f'{config["fqdn"]}/remotecdm/widevine',
'secret': f'{config["remote_cdm_secret"]}',
'device_name': f'{base_name}'
})
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/open', methods=['GET'])
def remote_cdm_widevine_open(device):
if str(device).lower() == config['default_wv_cdm'].lower():
wv_device = widevineDevice.load(f'{os.getcwd()}/configs/CDMs/WV/{config["default_wv_cdm"]}.wvd')
cdm = current_app.config["CDM"] = widevineCDM.from_device(wv_device)
session_id = cdm.open()
return jsonify({
'status': 200,
'message': 'Success',
'data': {
'session_id': session_id.hex(),
'device': {
'system_id': cdm.system_id,
'security_level': cdm.security_level,
}
}
})
else:
return jsonify({
'status': 400,
'message': 'Unauthorized'
})
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/close/<session_id>', methods=['GET'])
def remote_cdm_widevine_close(device, session_id):
if str(device).lower() == config['default_wv_cdm'].lower():
session_id = bytes.fromhex(session_id)
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'status': 400,
'message': f'No CDM for "{device}" has been opened yet. No session to close'
})
try:
cdm.close(session_id)
except InvalidSession:
return jsonify({
'status': 400,
'message': f'Invalid session ID "{session_id.hex()}", it may have expired'
})
return jsonify({
'status': 200,
'message': f'Successfully closed Session "{session_id.hex()}".',
})
else:
return jsonify({
'status': 400,
'message': f'Unauthorized'
})
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/set_service_certificate', methods=['POST'])
def remote_cdm_widevine_set_service_certificate(device):
if str(device).lower() == config['default_wv_cdm'].lower():
body = request.get_json()
for required_field in ("session_id", "certificate"):
if required_field == "certificate":
has_field = required_field in body # it needs the key, but can be empty/null
else:
has_field = body.get(required_field)
if not has_field:
return jsonify({
'status': 400,
'message': f'Missing required field "{required_field}" in JSON body'
})
session_id = bytes.fromhex(body["session_id"])
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'status': 400,
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
})
certificate = body["certificate"]
try:
provider_id = cdm.set_service_certificate(session_id, certificate)
except InvalidSession:
return jsonify({
'status': 400,
'message': f'Invalid session id: "{session_id.hex()}", it may have expired'
})
except DecodeError as error:
return jsonify({
'status': 400,
'message': f'Invalid Service Certificate, {error}'
})
except SignatureMismatch:
return jsonify({
'status': 400,
'message': 'Signature Validation failed on the Service Certificate, rejecting'
})
return jsonify({
'status': 200,
'message': f"Successfully {['set', 'unset'][not certificate]} the Service Certificate.",
'data': {
'provider_id': provider_id,
}
})
else:
return jsonify({
'status': 400,
'message': f'Unauthorized'
})
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_service_certificate', methods=['POST'])
def remote_cdm_widevine_get_service_certificate(device):
if str(device).lower() == config['default_wv_cdm'].lower():
body = request.get_json()
for required_field in ("session_id",):
if not body.get(required_field):
return jsonify({
'status': 400,
'message': f'Missing required field "{required_field}" in JSON body'
})
session_id = bytes.fromhex(body["session_id"])
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'status': 400,
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
})
try:
service_certificate = cdm.get_service_certificate(session_id)
except InvalidSession:
return jsonify({
'status': 400,
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
})
if service_certificate:
service_certificate_b64 = base64.b64encode(service_certificate.SerializeToString()).decode()
else:
service_certificate_b64 = None
return jsonify({
'status': 200,
'message': 'Successfully got the Service Certificate',
'data': {
'service_certificate': service_certificate_b64,
}
})
else:
return jsonify({
'status': 400,
'message': f'Unauthorized'
})
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_license_challenge/<license_type>', methods=['POST'])
def remote_cdm_widevine_get_license_challenge(device, license_type):
if str(device).lower() == config['default_wv_cdm'].lower():
body = request.get_json()
for required_field in ("session_id", "init_data"):
if not body.get(required_field):
return jsonify({
'status': 400,
'message': f'Missing required field "{required_field}" in JSON body'
})
session_id = bytes.fromhex(body["session_id"])
privacy_mode = body.get("privacy_mode", True)
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'status': 400,
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
})
if current_app.config.get("force_privacy_mode"):
privacy_mode = True
if not cdm.get_service_certificate(session_id):
return jsonify({
'status': 403,
'message': 'No Service Certificate set but Privacy Mode is Enforced.'
})
current_app.config['pssh'] = body['init_data']
init_data = widevinePSSH(body['init_data'])
try:
license_request = cdm.get_license_challenge(
session_id=session_id,
pssh=init_data,
license_type=license_type,
privacy_mode=privacy_mode
)
except InvalidSession:
return jsonify({
'status': 400,
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
})
except InvalidInitData as error:
return jsonify({
'status': 400,
'message': f'Invalid Init Data, {error}'
})
except InvalidLicenseType:
return jsonify({
'status': 400,
'message': f'Invalid License Type {license_type}'
})
return jsonify({
'status': 200,
'message': 'Success',
'data': {
'challenge_b64': base64.b64encode(license_request).decode()
}
})
else:
return jsonify({
'status': 400,
'message': f'Unauthorized'
})
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/parse_license', methods=['POST'])
def remote_cdm_widevine_parse_license(device):
if str(device).lower() == config['default_wv_cdm'].lower():
body = request.get_json()
for required_field in ("session_id", "license_message"):
if not body.get(required_field):
return jsonify({
'status': 400,
'message': f'Missing required field "{required_field}" in JSON body'
})
session_id = bytes.fromhex(body["session_id"])
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'status': 400,
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
})
try:
cdm.parse_license(session_id, body['license_message'])
except InvalidLicenseMessage as error:
return jsonify({
'status': 400,
'message': f'Invalid License Message, {error}'
})
except InvalidContext as error:
return jsonify({
'status': 400,
'message': f'Invalid Context, {error}'
})
except InvalidSession:
return jsonify({
'status': 400,
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
})
except SignatureMismatch:
return jsonify({
'status': 400,
'message': f'Signature Validation failed on the License Message, rejecting.'
})
return jsonify({
'status': 200,
'message': 'Successfully parsed and loaded the Keys from the License message.',
})
else:
return jsonify({
'status': 400,
'message': 'Unauthorized'
})
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_keys/<key_type>', methods=['POST'])
def remote_cdm_widevine_get_keys(device, key_type):
if str(device).lower() == config['default_wv_cdm'].lower():
body = request.get_json()
for required_field in ("session_id",):
if not body.get(required_field):
return jsonify({
'status': 400,
'message': f'Missing required field "{required_field}" in JSON body'
})
session_id = bytes.fromhex(body["session_id"])
key_type: Optional[str] = key_type
if key_type == 'ALL':
key_type = None
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'status': 400,
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
})
try:
keys = cdm.get_keys(session_id, key_type)
except InvalidSession:
return jsonify({
'status': 400,
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
})
except ValueError as error:
return jsonify({
'status': 400,
'message': f'The Key Type value "{key_type}" is invalid, {error}'
})
keys_json = [
{
"key_id": key.kid.hex,
"key": key.key.hex(),
"type": key.type,
"permissions": key.permissions
}
for key in keys
if not key_type or key.type == key_type
]
for entry in keys_json:
if config['database_type'].lower() != 'mariadb':
from custom_functions.database.cache_to_db_sqlite import cache_to_db
elif config['database_type'].lower() == 'mariadb':
from custom_functions.database.cache_to_db_mariadb import cache_to_db
if entry['type'] != 'SIGNING':
cache_to_db(pssh=str(current_app.config['pssh']), kid=entry['key_id'], key=entry['key'])
return jsonify({
'status': 200,
'message': 'Success',
'data': {
'keys': keys_json
}
})