okova
v0.14.0
Published
Advanced DRM inspection toolkit
Downloads
1,044
Maintainers
Readme
okova
Okova is a toolkit (browser extension, command-line tool, and JavaScript library) for inspecting DRM-protected media and working with Widevine, PlayReady, and ClearKey.
Okova is under active development. APIs and behavior may change before version 1.0.
Features
- Client credentials support: bring your WVD, PRD, raw credential files, or remote JSON config and import them into browser extension
- Playback of DRM-protected content using client credentials (including browsers that do not support DRM like Zen or Helium)
- Logging details from EME events in DevTools console
- Network-independent interception via browser extension, so it doesn't matter if license request has one-time tokens or a custom request/response body format
- Remote instance to handle API requests and TypeScript SDK for managing sessions on the client
- Runtime agnostic core: works in Node.js, Bun, Deno, browsers and more
- Encrypted Media Extensions API compatibility via
requestMediaKeySystemAccess()method
Browser Extension
Installing Chrome extension
- Download archive from latest release
- Go to
chrome://extensions/page - Ensure Developer Mode enabled and then drag and drop downloaded zip file to this page
Installing Firefox extension
- Download archive from latest release
- Go to
about:debugging#/runtime/this-firefoxpage - Click
Load Temporary Add-onbutton and choose downloaded zip file
Temporary add-on is not persistent and will be removed after browser restart
Command-line tool
Installation
Command-line tool installation requires a pre-installed JavaScript runtime, such as Node.js 24.5.0 or later.
npm install -g okovaUsage
See help for all possible arguments and options:
okova --help
okova client and okova creds are aliases for okova credentials.
Pack client credential files ./drm-files/device_client_id_blob and ./drm-files/device_private_key into a single WVD file:
okova credentials pack ./drm-files ./unknown_android-sdk-built-for-x86.wvdOutput example:
Credentials packed: /Users/.../unknown_android-sdk-built-for-x86.wvdShow client credential information:
okova credentials info ./unknown_android-sdk-built-for-x86.wvdOutput example:
application_name: org.chromium.webview_shell
company_name: unknown
model_name: Android SDK built for x86
architecture_name: x86
device_name: generic_x86
product_name: sdk_phone_x86
build_info: Android/sdk_phone_x86/generic_x86:10/RSR1.410600.002.B3/1792159:userdebug/dev-keys
widevine_cdm_version: 16.0.0
oem_crypto_security_patch_level: 0
oem_crypto_build_information: OEMCrypto Level3 Code 8162 May 9 2018 14:01:12JavaScript library
Library installation requires a pre-installed JavaScript runtime, such as Node.js 24.5.0 or later.
Installation
npm install okovaUsage
See examples for more.
Obtain a Widevine license for Bitmovin's video:
import { readFile } from 'node:fs/promises';
import { fromBase64, Widevine, WidevineClientCredentials } from 'okova';
async function main() {
// Prepare init data (PSSH)
const initData = fromBase64(
'AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==',
).toBuffer();
// Load client credentials
const clientCredentials = await WidevineClientCredentials.from({
wvd: await readFile('client.wvd'),
});
const widevine = new Widevine({ clientCredentials });
const session = widevine.createSession();
// Handle generated license challenge (or other session messages like individualization request)
session.onmessage = async (event) => {
const { message } = event.detail;
// Send license request
const licenseUrl = 'https://cwip-shaka-proxy.appspot.com/no_auth';
const response = await fetch(licenseUrl, { body: message, method: 'POST' })
.then((r) => r.arrayBuffer())
.then((buffer) => new Uint8Array(buffer));
// Update session with license response
await session.update(response);
};
// Generate license challenge
await session.generateRequest(initData);
// Wait for keys
const keys = await session.waitForKeys();
for (const [keyId, key] of keys) {
console.log(`${keyId}:${key}`);
}
await session.close(); // Close session to delete of any license(s) and key(s) that have not been explicitly stored.
await session.remove(); // Destroy the license(s) and/or key(s) associated with the session whether they are in memory, persistent store or both.
}fetchDecryptionKeys handles license exchanges with a 30-second default timeout.
Use timeoutMs to change the deadline and signal to cancel. It throws
LicenseHttpError for unsuccessful HTTP responses and NoContentKeysError when
an exchange finishes without content keys. License requests are not automatically retried.
Saving and resuming native sessions
Widevine and PlayReady can serialize an open session with pause(). This takes
a snapshot; the original session stays open. Close it before restoring into the
same engine, since resumeSession() rejects an already-open session ID:
const state = session.pause();
await session.close();
const resumed = engine.resumeSession(state);Resume with the same client credentials. Native Widevine sessions do not support
persistent-license storage through load().
PlayReady custom challenge data
Pass application-specific data as a string when creating the engine:
const engine = new PlayReady({ clientCredentials, customData: applicationData });
const keys = await fetchDecryptionKeys({ cdm: engine, pssh, server: licenseUrl });Pass the original text without XML escaping. Remote clients accept the same
customData option; REST clients include it in the POST /sessions body.
Remote sessions
Start a local API with your client credentials and a secret of your choice:
okova serve --credentials client.wvd --secret 'replace-with-your-secret'The API uses the host and port from okova.config.json, defaulting to
http://127.0.0.1:4000. Create a session using your server address:
curl http://127.0.0.1:4000/sessions \
-H 'Content-Type: application/json' \
-H 'x-secret-key: replace-with-your-secret' \
-d '{"keySystem":"com.widevine.alpha"}'For anonymous access, replace --secret ... with --public and remove the
x-secret-key header from the curl command.
See the remote client example to request licenses and keys.
Inspect, edit, and convert PSSH boxes
okova pssh inspect "$PSSH" --json
okova pssh kids "$PSSH"
okova pssh convert "$PSSH" --target playready --la-url https://example.com/licensePass base64 directly or use - to read base64 text from stdin.
Inspect and KID extraction process all boxes;
use --box <index> to select one by zero-based index, required for conversion
of multi-box input. Raw DRM headers and media files are not accepted.
inspect prints box metadata and KIDs; kids prints one KID per line; convert
writes base64. All support --json. Inspection JSON includes a KID status of
available, unsupported, or invalid, distinguishing empty KIDs from errors.
Conversion warnings go to stderr and also appear in conversion JSON.
import {
PSSH_SYSTEM_IDS,
createPsshBox,
parsePsshBoxes,
getPsshKeyIds,
setPsshKeyIds,
convertPsshBox,
psshBoxToBase64,
} from 'okova';
const widevine = setPsshKeyIds(createPsshBox({ systemId: PSSH_SYSTEM_IDS.widevine }), [
'00112233-4455-6677-8899-aabbccddeeff',
]);
const playready = convertPsshBox(widevine, 'playready', {
laUrl: 'https://example.com/license',
});
const [box] = parsePsshBoxes(psshBoxToBase64(playready));
console.log(box.systemId, box.version, getPsshKeyIds(box));
const restored = convertPsshBox(box, 'widevine');parsePsshBoxes accepts bytes or base64 containing full PSSH boxes.
serializePsshBox returns bytes; psshBoxToBase64 returns base64.
IDs accept 16-byte arrays, UUID strings, or 32 hex digits.
Use getPsshKeyIds to inspect KIDs and setPsshKeyIds to replace Widevine KIDs.
PlayReady KID replacement is not supported. Editing and conversion return new
boxes without changing their inputs.
Conversion preserves KIDs and encryption signaling but discards other metadata,
including license URLs and checksums. Supply laUrl when converting to PlayReady
if needed. Converted headers may not meet every license server's requirements.
Disclaimer
- This project does not condone piracy or any action against the terms of the DRM systems.
- All efforts in this project have been the result of Reverse-Engineering, Publicly available research, and Trial & Error.
- Do not use this program to decrypt or access any content for which you do not have the legal rights or explicit permission.
- Unauthorized decryption or distribution of copyrighted materials is a violation of applicable laws and intellectual property rights.
- This tool must not be used for any illegal activities, including but not limited to piracy, circumventing digital rights management (DRM), or unauthorized access to protected content.
- The developers, contributors, and maintainers of this program are not responsible for any misuse or illegal activities performed using this software.
- By using this program, you agree to comply with all applicable laws and regulations governing digital rights and copyright protections.
