Skip to content

Commit

Permalink
First attempt at pyplayready support for conversion functions
Browse files Browse the repository at this point in the history
  • Loading branch information
emarsden committed Nov 25, 2024
1 parent 4dc4e27 commit fa6790a
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 13 deletions.
33 changes: 33 additions & 0 deletions www-zola/content/convert.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,39 @@ pywidevine library compiled to WebAssembly, running fully inside your browser.
</article>
</form>


## Playready support

This provides experimental interface to the [pyplayready](https://github.com/ready-dl/pyplayready)
library for Playready support in Python.

<form>
<article>
<header><h3>Blobs to Playready device (.prd)</h3></header>
<label data-tooltip="group_cert.bin">Group certificate
<input id="prgroupcert" type="file" required aria-invalid="true"/>
</label>
<label data-tooltip="group_key.pem">Group key
<input id="prgroupkey" type="file" required aria-invalid="true"/>
</label>
<button id="to_playready_device">Convert</button>
</article>
</form>

<hr>

<form>
<article>
<header><h3>PRD to blobs</h3></header>
<label>Playready device in .prd format
<input id="prdevice" type="file" required aria-invalid="true"/>
</label>
<button id="export_playready_device">Convert</button>
</article>
</form>



**Privacy**: this tool is implemented in WebAssembly and runs locally in your web browser. Once
loaded, it will run fully offline.

Expand Down
111 changes: 98 additions & 13 deletions www-zola/static/js/convert.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,21 @@ const myPackages = [
"requests",
"/pssh-box-wasm/pyodide/construct-2.8.8-py2.py3-none-any.whl",
"https://files.pythonhosted.org/packages/41/9f/60f8a4c8e7767a8c34f5c42428662e03fa3e38ad18ba41fcc5370ee43263/pywidevine-1.8.0-py3-none-any.whl",
"https://files.pythonhosted.org/packages/aa/a2/27fea39af627c0ce5dbf6108bf969ea8f5fc9376d29f11282a80e3426f1d/pymp4-1.4.0-py3-none-any.whl"
"https://files.pythonhosted.org/packages/aa/a2/27fea39af627c0ce5dbf6108bf969ea8f5fc9376d29f11282a80e3426f1d/pymp4-1.4.0-py3-none-any.whl",
"pyplayready"
]
let pyodide = await loadPyodide({ packages: myPackages });
console.log("Pyodide + pywidevine loaded");
document.getElementById("loading").style.display = "none";
pyodide.setDebug(true)

const toWVD=`
const to_WVD=`
import js
from base64 import b64encode, b64decode
from pywidevine.device import Device, DeviceTypes
cid=b64decode(js.cid.encode())
prk=b64decode(js.prk.encode())
cid = b64decode(js.cid.encode())
prk = b64decode(js.prk.encode())
device = Device(client_id=cid,
private_key=prk,
type_=DeviceTypes['ANDROID'],
Expand All @@ -41,13 +42,13 @@ device = Device(client_id=cid,
b64encode(device).decode()
`

const fromWVD=`
const from_WVD=`
import js
from zipfile import ZipFile
from base64 import b64encode, b64decode
from pywidevine.device import Device
wvd=b64decode(js.wvd.encode())
wvd = b64decode(js.wvd.encode())
device = Device.loads(wvd)
with ZipFile('device_blobs.zip', 'w') as zf:
Expand All @@ -68,8 +69,9 @@ const b64 = {
encode: b => btoa(String.fromCharCode(...new Uint8Array(b)))
};

function downloadResult(ret, name){
let blob =new Blob([b64.decode(ret)], {type: "octet/stream"});
function downloadResult(ret, name) {
// let blob = new Blob([b64.decode(ret)], {type: "octet/stream"});
let blob = new Blob([b64.decode(ret)], {type: "application/octet-stream"});
let blobLink = URL.createObjectURL(blob);
let a = document.createElement('a');
a.download = name;
Expand All @@ -83,22 +85,105 @@ function downloadResult(ret, name){
document.getElementById("toWVDGo").addEventListener("click", async function(e) {
e.preventDefault();
e.target.style.cursor = "wait";
window.cid=b64.encode(
window.cid = b64.encode(
(await document.getElementById("cid").files[0].arrayBuffer())
);
window.prk=b64.encode(
window.prk = b64.encode(
(await document.getElementById("prk").files[0].arrayBuffer())
);
let result=await pyodide.runPythonAsync(toWVD);
let result = await pyodide.runPythonAsync(to_WVD);
downloadResult(result, "device.wvd")
});

document.getElementById("fromWVDGo").addEventListener("click", async function(e) {
e.preventDefault();
e.target.style.cursor = "wait";
window.wvd=b64.encode(
window.wvd = b64.encode(
(await document.getElementById("wvd").files[0].arrayBuffer())
)
let result=await pyodide.runPythonAsync(fromWVD);
let result = await pyodide.runPythonAsync(from_WVD);
downloadResult(result, "device_blobs.zip")
});



// === Playready functions ====

// Create a Playready Device (.prd) file from an ECC private group key and group certificate chain.
//
// See https://github.com/ready-dl/pyplayready/blob/main/pyplayready/main.py#L130
const to_playready_device=`
import js
from base64 import b64encode, b64decode
from Crypto.Random import get_random_bytes
from pyplayready.ecc_key import ECCKey
from pyplayready.bcert import CertificateChain, Certificate
group_certificate = b64decode(js.prgroupcert.encode())
group_key = b64decode(js.prgroupkey.encode())
encryption_key = ECCKey.generate()
signing_key = ECCKey.generate()
certificate_chain = CertificateChain.loads(group_certificate)
group_key = ECCKey.loads(group_key)
new_certificate = Certificate.new_leaf_cert(
cert_id = get_random_bytes(16),
security_level = certificate_chain.get_security_level(),
client_id = get_random_bytes(16),
signing_key = signing_key,
encryption_key = encryption_key,
group_key = group_key,
parent = certificate_chain
)
certificate_chain.prepend(new_certificate)
device = Device(
group_certificate=certificate_chain.dumps(),
encryption_key=encryption_key.dumps(),
signing_key=signing_key.dumps()
)
prd_bin = device.dumps()
b64encode(prd_bin).decode()
`

const export_playready_device=`
import js
from zipfile import ZipFile
from base64 import b64encode, b64decode
prd_bin = b64decode(js.prdevice.encode())
device = Device.loads(prd_bin)
log(device.get_name())
with ZipFile('device_blobs.zip', 'w') as zf:
with zf.open('bgroupcert.dat', 'w') as f:
f.write(device.group_certificate.dumps())
with zf.open('zprivencr.dat', 'w') as f:
f.write(device.encryption_key.dumps())
with zf.open('zprivsig.dat', 'w') as f:
f.write(device.signing_key.dumps())
b64encode(open('device_blobs.zip', 'rb').read()).decode()
`

document.getElementById("to_playready_device").addEventListener("click", async function(e) {
e.preventDefault();
e.target.style.cursor = "wait";
window.prgroupcert = b64.encode(
(await document.getElementById("prgroupcert").files[0].arrayBuffer())
);
window.prgroupkey = b64.encode(
(await document.getElementById("prgroupkey").files[0].arrayBuffer())
);
let result = await pyodide.runPythonAsync(to_playready_device);
downloadResult(result, "playready_device.prd")
});

document.getElementById("export_playready_device").addEventListener("click", async function(e) {
e.preventDefault();
e.target.style.cursor = "wait";
window.prdevice = b64.encode(
(await document.getElementById("prdevice").files[0].arrayBuffer())
)
let result = await pyodide.runPythonAsync(export_playready_device);
downloadResult(result, "device_blobs.zip")
});

0 comments on commit fa6790a

Please sign in to comment.