137 lines
5.2 KiB
HTML
137 lines
5.2 KiB
HTML
<!DOCTYPE html>
|
|||
|
|
<html>
|
||
|
|
<head>
|
||
|
|
<meta charset="utf-8">
|
||
|
|
<title>Widevine CDM Trigger</title>
|
||
|
|
</head>
|
||
|
|
<body style="background:#000;color:#fff;font-family:monospace;padding:20px">
|
||
|
|
<h2>Widevine CDM Direct Trigger</h2>
|
||
|
|
<p style="color:#aaa">No video needed — directly invokes the CDM to generate a license challenge.</p>
|
||
|
|
<button id="btn" style="font-size:20px;padding:12px 30px;cursor:pointer">
|
||
|
|
▶ Trigger Widevine (generates RSA_sign)
|
||
|
|
</button>
|
||
|
|
<pre id="log" style="margin-top:16px;font-size:13px;line-height:1.5"></pre>
|
||
|
|
<script>
|
||
|
|
var log = document.getElementById('log');
|
||
|
|
function L(s) { log.textContent += s + '\n'; console.log(s); }
|
||
|
|
|
||
|
|
// TVP content PSSH
|
||
|
|
var PSSH_B64 = 'AAAAOnBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAABoIARIQ1DG5TL5NQ/2zoimqi0ixiUjj3JWbBg==';
|
||
|
|
var LICENSE = 'https://vod.tvp.pl/api/products/2993670/drm/widevine/external?platform=BROWSER&type=MOVIE';
|
||
|
|
|
||
|
|
function b64ToBytes(b64) {
|
||
|
|
var s = atob(b64), a = new Uint8Array(s.length);
|
||
|
|
for (var i = 0; i < s.length; i++) a[i] = s.charCodeAt(i);
|
||
|
|
return a;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Auto-trigger 1.5 s after page loads (no button click needed)
|
||
|
|
window.addEventListener('load', function() {
|
||
|
|
setTimeout(function() { document.getElementById('btn').click(); }, 1500);
|
||
|
|
});
|
||
|
|
|
||
|
|
document.getElementById('btn').onclick = async function() {
|
||
|
|
this.disabled = true;
|
||
|
|
L('--- Starting ---');
|
||
|
|
try {
|
||
|
|
var cfg = [{
|
||
|
|
initDataTypes: ['cenc'],
|
||
|
|
videoCapabilities: [
|
||
|
|
{ contentType: 'video/mp4; codecs="avc1.42E01E"', robustness: 'SW_SECURE_CRYPTO' },
|
||
|
|
{ contentType: 'video/mp4; codecs="avc1.42E01E"', robustness: '' }
|
||
|
|
],
|
||
|
|
audioCapabilities: [
|
||
|
|
{ contentType: 'audio/mp4; codecs="mp4a.40.2"', robustness: 'SW_SECURE_CRYPTO' },
|
||
|
|
{ contentType: 'audio/mp4; codecs="mp4a.40.2"', robustness: '' }
|
||
|
|
]
|
||
|
|
}];
|
||
|
|
|
||
|
|
L('Requesting com.widevine.alpha access...');
|
||
|
|
var access = await navigator.requestMediaKeySystemAccess('com.widevine.alpha', cfg);
|
||
|
|
L('OK: got MediaKeySystemAccess');
|
||
|
|
|
||
|
|
var mediaKeys = await access.createMediaKeys();
|
||
|
|
L('OK: created MediaKeys');
|
||
|
|
|
||
|
|
var session = mediaKeys.createSession('temporary');
|
||
|
|
L('OK: session created');
|
||
|
|
|
||
|
|
session.addEventListener('message', async function(e) {
|
||
|
|
L('');
|
||
|
|
L('Message type: ' + e.messageType + ' size: ' + e.message.byteLength + ' bytes');
|
||
|
|
|
||
|
|
if (e.messageType === 'individualization-request') {
|
||
|
|
L('Service cert request — setting EMPTY cert to force plaintext client_id...');
|
||
|
|
try {
|
||
|
|
// Setting empty cert disables encryption of client_id
|
||
|
|
await mediaKeys.setServerCertificate(new Uint8Array([0]));
|
||
|
|
L('Empty service cert set.');
|
||
|
|
} catch(err) {
|
||
|
|
L('setServerCertificate error: ' + err.message);
|
||
|
|
}
|
||
|
|
// Generate request again — should now produce plaintext client_id
|
||
|
|
try {
|
||
|
|
var pssh2 = b64ToBytes(PSSH_B64);
|
||
|
|
await session.generateRequest('cenc', pssh2);
|
||
|
|
L('generateRequest called again.');
|
||
|
|
} catch(e2) {
|
||
|
|
L('generateRequest error: ' + e2.message);
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (e.messageType !== 'license-request' && e.messageType !== 'license-renewal') {
|
||
|
|
L('Unhandled message type: ' + e.messageType);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
L('*** LICENSE CHALLENGE READY (' + e.message.byteLength + ' bytes) ***');
|
||
|
|
L('*** RSA_sign fired — Frida should have captured the key! ***');
|
||
|
|
L('');
|
||
|
|
|
||
|
|
// Save challenge to local capture server for client_id extraction
|
||
|
|
try {
|
||
|
|
L('Saving challenge to localhost:8765/capture ...');
|
||
|
|
await fetch('http://localhost:8765/capture', {
|
||
|
|
method: 'POST', body: e.message,
|
||
|
|
headers: { 'Content-Type': 'application/octet-stream' }
|
||
|
|
});
|
||
|
|
L('Challenge saved to localhost capture server.');
|
||
|
|
} catch(err) {
|
||
|
|
L('Capture error: ' + err.message);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Send to license server via local proxy
|
||
|
|
try {
|
||
|
|
L('Sending to license server via proxy...');
|
||
|
|
var r = await fetch('http://localhost:8765/proxy', {
|
||
|
|
method: 'POST', body: e.message,
|
||
|
|
headers: { 'Content-Type': 'application/octet-stream' }
|
||
|
|
});
|
||
|
|
L('License server HTTP ' + r.status);
|
||
|
|
if (r.ok) {
|
||
|
|
var lic = await r.arrayBuffer();
|
||
|
|
await session.update(new Uint8Array(lic));
|
||
|
|
L('License installed! Full DRM session active.');
|
||
|
|
}
|
||
|
|
} catch(err) {
|
||
|
|
L('License fetch error: ' + err.message);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
var pssh = b64ToBytes(PSSH_B64);
|
||
|
|
L('Calling generateRequest with PSSH (' + pssh.length + ' bytes)...');
|
||
|
|
await session.generateRequest('cenc', pssh);
|
||
|
|
L('generateRequest returned — challenge was built.');
|
||
|
|
|
||
|
|
} catch(e) {
|
||
|
|
L('ERROR: ' + e.name + ': ' + e.message);
|
||
|
|
document.getElementById('btn').disabled = false;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
L('Ready. Press the button.');
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|