* Add proper support for server certificates (only for local WV devices)

* Fix issues with some services that append extra data, which is not a PR WRM initData, to the WV PSSH
This commit is contained in:
Ingan121
2025-07-21 20:39:24 +09:00
parent fd3f1b7b3d
commit 0ec34da0bc
3 changed files with 34 additions and 21 deletions

View File

@@ -80,7 +80,7 @@ async function parseClearKey(body, sendResponse, tab_url) {
sendResponse(JSON.stringify({pssh: pssh_data, keys : formatted_keys}));
}
async function generateChallenge(body, sendResponse) {
async function generateChallenge(body, sendResponse, serverCert) {
const pssh_data = getWvPsshFromConcatPssh(body);
if (!pssh_data) {
@@ -107,6 +107,10 @@ async function generateChallenge(body, sendResponse) {
pssh_data
);
if (serverCert) {
session.setServiceCertificate(base64toUint8Array(serverCert));
}
const [challenge, request_id] = session.createLicenseRequest(LicenseType.STREAMING, widevine_device.type === 2);
sessions.set(uint8ArrayToBase64(request_id), session);
@@ -355,11 +359,12 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
return;
} catch {
if (message.body) {
const split = message.body.split(":");
if (message.body.startsWith("lookup:")) {
const split = message.body.split(":");
const [ _, sessionId, kidHex, serverCert ] = split;
// Find first log that contains the requested KID
const log = logs.find(log =>
log.keys.some(k => k.kid.toLowerCase() === split[2].toLowerCase())
log.keys.some(k => k.kid.toLowerCase() === kidHex.toLowerCase())
);
if (!log) {
console.warn("[Vineless] Lookup failed: no log found for KID", kidHex);
@@ -378,7 +383,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
const device_type = await SettingsManager.getSelectedDeviceType();
switch (device_type) {
case "WVD":
await generateChallenge(log.pssh_data, sendResponse);
await generateChallenge(log.pssh_data, sendResponse, serverCert);
break;
case "REMOTE":
await generateChallengeRemote(log.pssh_data, sendResponse);
@@ -386,7 +391,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
}
break;
case "PLAYREADY": // UNTESTED
await generatePRChallenge(log.pssh_data, sendResponse, split[1]);
await generatePRChallenge(log.pssh_data, sendResponse, sessionId);
break;
}
} else if (message.body.startsWith("pr:")) {
@@ -395,21 +400,22 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
manifests.clear();
return;
}
const split = message.body.split(':');
await generatePRChallenge(split[2], sendResponse, split[1]);
const [ _, sessionId, wrmHeader ] = split;
await generatePRChallenge(wrmHeader, sendResponse, sessionId);
} else {
if (!await SettingsManager.getWVEnabled()) {
sendResponse();
manifests.clear();
return;
}
const [ pssh, serverCert ] = split;
const device_type = await SettingsManager.getSelectedDeviceType();
switch (device_type) {
case "WVD":
await generateChallenge(message.body, sendResponse);
await generateChallenge(pssh, sendResponse, serverCert);
break;
case "REMOTE":
await generateChallengeRemote(message.body, sendResponse);
await generateChallengeRemote(pssh, sendResponse); // No serverCert support for remote yet
break;
}
}

View File

@@ -535,10 +535,11 @@
if (!await getEnabledForKeySystem(keySystem) || _this._ck) {
return await _target.apply(_this, _args);
}
// Server certificates are not supported yet
// Chrome returns false when this is called on a CK MediaKeys, while Firefox raises an exception when done so
// Let's just return false for now to prevent some sites from entirely breaking
// Server certificate support is planned for the future, for services that truly need it for playback/higher quality content
if (keySystem.startsWith("com.widevine.alpha")) {
_this._emeShim.serverCert = uint8ArrayToBase64(new Uint8Array(_args[0]));
return true;
}
// Server certificates are not supported on ClearKey or PlayReady
return false;
});
@@ -561,7 +562,16 @@
if (_args[0].toLowerCase() === "webm") {
const kid = uint8ArrayToHex(_args[1]);
const base64Pssh = await emitAndWaitForResponse("REQUEST", `lookup:${_this.sessionId}:${kid}`);
let data = `lookup:${_this.sessionId}:${kid}`;
if (_this._mediaKeys._emeShim.serverCert) {
data += `:${_this._mediaKeys._emeShim.serverCert}`;
}
const base64Pssh = await emitAndWaitForResponse("REQUEST", data);
if (!base64Pssh) {
const error = new Error("[Vineless] No PSSH received for WebM request");
console.error(error);
throw error;
}
const evt = new MediaKeyMessageEvent("message", {
message: base64toUint8Array(base64Pssh).buffer,
messageType: "license-request"
@@ -571,7 +581,10 @@
}
const base64Pssh = uint8ArrayToBase64(new Uint8Array(_args[1]));
const data = keySystem.startsWith("com.microsoft.playready") ? `pr:${_this.sessionId}:${base64Pssh}` : base64Pssh;
let data = keySystem.startsWith("com.microsoft.playready") ? `pr:${_this.sessionId}:${base64Pssh}` : base64Pssh;
if (_this._mediaKeys._emeShim.serverCert) {
data += `:${_this._mediaKeys._emeShim.serverCert}`;
}
const challenge = await emitAndWaitForResponse("REQUEST", data);
const challengeBytes = base64toUint8Array(challenge);

View File

@@ -507,12 +507,6 @@ export function stringToHex(string){
export function getWvPsshFromConcatPssh(psshBase64) {
const raw = base64toUint8Array(psshBase64);
// Detect PlayReady PSSH by presence of "WRMHEADER" in UTF-16LE
const text = new TextDecoder('utf-16le').decode(raw);
if (!text.includes('WRMHEADER')) {
return psshBase64; // Keep as-is if PlayReady not mixed in
}
let offset = 0;
while (offset + 8 <= raw.length) {
const size = new DataView(raw.buffer, raw.byteOffset + offset).getUint32(0);