DRM & CDM Configuration¶
This document covers Digital Rights Management (DRM) and Content Decryption Module (CDM) configuration options.
cdm (dict)¶
Pre-define which Widevine or PlayReady device to use for each Service by Service Tag as Key (case-insensitive).
The value should be a WVD or PRD filename without the file extension. When
loading the device, unshackle will look in both the WVDs and PRDs directories
for a matching file.
For example,
You may also specify this device based on the profile used.
For example,
EXAMPLE: chromecdm_903_l3
EXAMPLE2: nexus_6_l1
EXAMPLE3:
john_sd: chromecdm_903_l3
jane_uhd: nexus_5_l1
You can also specify a fallback value to predefine if a match was not made.
This can be done using default key. This can help reduce redundancy in your specifications.
For example, the following has the same result as the previous example, as well as all other
services and profiles being pre-defined to use chromecdm_903_l3.
You can also select CDMs based on video resolution using comparison operators (>=, >, <=, <)
or exact match on the resolution height.
For example,
EXAMPLE:
"<=1080": generic_android_l3 # Use L3 for 1080p and below
">1080": nexus_5_l1 # Use L1 for above 1080p (1440p, 2160p)
default: generic_android_l3 # Fallback if no quality match
You can mix profiles and quality thresholds in the same service:
EXAMPLE:
john: example_l3_profile # Profile-based selection
"<=720": example_mobile_l3 # Quality-based selection
"1080": example_standard_l3 # Exact match for 1080p
">=1440": example_premium_l1 # Quality-based selection
default: example_standard_l3 # Fallback
remote_cdm (list[dict])¶
Configure remote CDM (Content Decryption Module) APIs to use for decrypting DRM-protected content. Remote CDMs allow you to use high-security CDMs (L1/L2 for Widevine, SL2000/SL3000 for PlayReady) without having the physical device files locally.
unshackle supports multiple types of remote CDM providers:
- DecryptLabs CDM - Official DecryptLabs KeyXtractor API with intelligent caching
- Custom API CDM - Highly configurable adapter for any third-party CDM API
- Legacy PyWidevine Serve - Standard pywidevine serve-compliant APIs
The name of each defined remote CDM can be referenced in the cdm configuration as if it was a local device file.
DecryptLabs Remote CDM¶
DecryptLabs provides a professional CDM API service with support for multiple device types and intelligent key caching.
Supported Devices:
- Widevine: ChromeCDM (L3), L1 (Security Level 1), L2 (Security Level 2)
- PlayReady: SL2 (SL2000), SL3 (SL3000)
Configuration:
remote_cdm:
# Widevine L1 Device
- name: decrypt_labs_l1
type: decrypt_labs # Required: identifies as DecryptLabs CDM
device_name: L1 # Required: must match exactly (L1, L2, ChromeCDM, SL2, SL3)
host: https://keyxtractor.decryptlabs.com
secret: YOUR_API_KEY # Your DecryptLabs API key
# Widevine L2 Device
- name: decrypt_labs_l2
type: decrypt_labs
device_name: L2
host: https://keyxtractor.decryptlabs.com
secret: YOUR_API_KEY
# Chrome CDM (L3)
- name: decrypt_labs_chrome
type: decrypt_labs
device_name: ChromeCDM
host: https://keyxtractor.decryptlabs.com
secret: YOUR_API_KEY
# PlayReady SL2000
- name: decrypt_labs_playready_sl2
type: decrypt_labs
device_name: SL2
device_type: PLAYREADY # Required for PlayReady
host: https://keyxtractor.decryptlabs.com
secret: YOUR_API_KEY
# PlayReady SL3000
- name: decrypt_labs_playready_sl3
type: decrypt_labs
device_name: SL3
device_type: PLAYREADY
host: https://keyxtractor.decryptlabs.com
secret: YOUR_API_KEY
Features: - Intelligent key caching system (reduces API calls) - Automatic integration with unshackle's vault system - Support for both Widevine and PlayReady - Multiple security levels (L1, L2, L3, SL2000, SL3000)
Note
The device_type field determines whether the CDM operates in PlayReady or Widevine mode.
Setting device_type: PLAYREADY (or using device_name: SL2 / SL3) activates PlayReady mode.
The security_level field is auto-computed from device_name when not specified (e.g., SL2 defaults
to 2000, SL3 to 3000, and Widevine devices default to 3). You can override these if needed.
Custom API Remote CDM¶
A highly configurable CDM adapter that can work with virtually any third-party CDM API through YAML configuration. This allows you to integrate custom CDM services without writing code.
Basic Example:
remote_cdm:
- name: custom_chrome_cdm
type: custom_api # Required: identifies as Custom API CDM
host: https://your-cdm-api.com
timeout: 30 # Optional: request timeout in seconds
device:
name: ChromeCDM
type: CHROME # CHROME, ANDROID, PLAYREADY
system_id: 27175
security_level: 3
auth:
type: bearer # bearer, header, basic, body
key: YOUR_API_TOKEN
endpoints:
get_request:
path: /get-challenge
method: POST
decrypt_response:
path: /get-keys
method: POST
caching:
enabled: true # Enable key caching
use_vaults: true # Integrate with vault system
Full field reference:
remote_cdm:
- name: advanced_custom_api
type: custom_api
host: https://api.example.com
timeout: 30 # Default request timeout (seconds); overridden per-endpoint
device:
name: L1 # Passed as 'scheme' in base request params
type: ANDROID # CHROME, ANDROID, or PLAYREADY
system_id: 26830 # Widevine system ID (informational; not sent)
security_level: 1 # 1–3 for Widevine; 2000/3000 for PlayReady
# Authentication — applied to every request via session headers or Basic auth
auth:
type: header # bearer | header | basic | body | query
header_name: X-API-Key # used when type=header (default: Authorization)
key: YOUR_SECRET_KEY # API key / token / password
# bearer_token: TOKEN # explicit token for type=bearer (falls back to 'key')
# username: user # required for type=basic
# password: pass # required for type=basic
custom_headers: # merged into every request
User-Agent: Unshackle/3.1.0
X-Client-Version: "1.0"
# Endpoints — both keys are required
endpoints:
get_request: # first call: obtain challenge or cached keys
path: /v2/challenge
method: POST
timeout: 30 # overrides top-level timeout for this endpoint
decrypt_response: # second call: exchange license response for keys
path: /v2/decrypt
method: POST
timeout: 30
# request_mapping — transform outgoing parameters before each request
# Base params sent to get_request: scheme, init_data, [service], [service_certificate]
# Base params sent to decrypt_response: scheme, session_id, init_data, license_request, license_response
request_mapping:
get_request:
param_names: # rename base param keys
init_data: pssh
scheme: device_type
static_params: # always-added fixed values
api_version: "2.0"
conditional_params: # added only when condition is true
- condition: "is_playready == True"
params:
drm_type: playready
transforms: # applied after renaming; type values listed below
- param: pssh
type: base64_encode
nested_params: # group named params under a parent key
payload:
- pssh
- device_type
exclude_params: # remove unwanted params before sending
- service_certificate
decrypt_response:
param_names:
license_request: challenge
license_response: license
# response_mapping — extract and validate fields from API responses
response_mapping:
get_request:
fields: # map standard names to dot-path locations in response JSON
challenge: data.challenge
session_id: session.id
message: status.message
response_types: # classify response so CDM can choose the right path
- condition: "message_type == 'license-request'"
type: license_request # CDM expects challenge + session_id
- condition: "message_type == 'cached-keys'"
type: cached_keys # CDM skips license round-trip
success_conditions: # all must be true; failure raises RequestException
- "message == 'success'"
error_fields: # inspected when success_conditions fail (default: error, message, details)
- error
- details
transforms: # applied to extracted fields
- field: challenge
type: base64_decode
decrypt_response:
fields:
keys: data.keys
message: status.message
key_fields: # map key-object property names to standard names
kid: key_id # default: kid
key: content_key # default: key
type: key_type # default: type (value used as-is; CONTENT treated as content key)
success_conditions:
- "message == 'success'"
# caching — vault and API-level key caching
caching:
enabled: true # master switch (default: true)
use_vaults: true # check/write unshackle key vaults (default: true)
check_cached_first: true # check vaults before making any API call (default: true)
Supported transforms types (for both request_mapping and response_mapping):
base64_encode, base64_decode, hex_encode, hex_decode, json_stringify, json_parse, parse_key_string
Supported authentication types:
- bearer — Authorization: Bearer <key> (or bearer_token field)
- header — arbitrary header; set header_name (default Authorization)
- basic — HTTP Basic auth via username / password
- body — credentials injected into request body (handled by request_mapping)
- query — credentials appended to query string (handled by request_mapping)
PyPlayReady Remote CDM¶
Connects to a running unshackle serve instance for PlayReady licensing. Uses
pyplayready.remote.remotecdm.RemoteCdm under the hood. No type field is needed —
the entry is routed to PlayReady when device_type: PLAYREADY is set (same branching
logic as the legacy Widevine path).
remote_cdm:
- name: playready_remote
device_name: my_prd_device # Device name registered on the serve instance
device_type: PLAYREADY # Required to select the PlayReady code path
system_id: 0 # Not used by pyplayready RemoteCdm; set to 0
security_level: 3000 # 2000 = SL2000, 3000 = SL3000
host: http://127.0.0.1:8786/playready # Must include the /playready path suffix
secret: your-api-secret-key # X-Secret-Key sent to the serve instance
Note
The /playready path suffix in host is required — pyplayready.RemoteCdm
appends its own endpoint paths on top of this base, so omitting the suffix will result
in 404 errors.
Legacy PyWidevine Serve Format¶
Standard pywidevine serve-compliant remote CDM configuration (backwards compatibility).
remote_cdm:
- name: legacy_chrome_cdm
device_name: chrome
device_type: CHROME
system_id: 27175
security_level: 3
host: https://domain.com/api
secret: secret_key
Note
If the type field is not specified, the entry is treated as a legacy pywidevine serve CDM.
decrypt_labs_api_key (str)¶
API key for DecryptLabs CDM service integration.
When set, enables the use of DecryptLabs remote CDM services in your remote_cdm configuration.
This is used specifically for type: "decrypt_labs" entries in the remote CDM list.
For example,
Note
This is different from the per-CDM secret field in remote_cdm entries. This provides a global
API key that can be referenced across multiple DecryptLabs CDM configurations. If a remote_cdm entry with
type: "decrypt_labs" does not have a secret field specified, the global decrypt_labs_api_key will be
used as a fallback.
decryption (str|dict)¶
Configure which decryption tool to use for DRM-protected content. Default: shaka.
Supported values:
- shaka - Shaka Packager (default)
- mp4decrypt - Bento4 mp4decrypt
You can specify a single decrypter for all services:
Or configure per-service with a DEFAULT fallback:
Service keys are case-insensitive (normalized to uppercase internally).
MonaLisa DRM¶
MonaLisa is a WASM-based DRM system that uses local key extraction and two-stage segment decryption. Unlike Widevine and PlayReady, MonaLisa does not use a challenge/response flow with a license server. Instead, the PSSH value (ticket) is provided directly by the service API, and keys are extracted locally via a WASM module.
Requirements¶
- ML-Worker binary: Must be available on your system
PATH(discovered viabinaries.ML_Worker). This is the binary that performs stage-1 decryption.
Decryption stages¶
- ML-Worker binary: Removes MonaLisa encryption layer (bbts -> ents). The key is passed via command-line argument.
- AES-ECB decryption: Final decryption with service-provided key.
MonaLisa uses per-segment decryption during download (not post-download like Widevine/PlayReady), so segments are decrypted as they are downloaded.
Note
MonaLisa is configured per-service rather than through global config options. Services that use MonaLisa handle ticket/key retrieval and CDM initialization internally.
ClearKey DRM¶
Two distinct ClearKey mechanisms are supported; neither needs a CDM device or any DRM config:
HLS AES-128 ClearKey¶
The key is fetched from (or near) the M3U8 EXT-X-KEY URI and segments are decrypted with
pure-Python AES-CBC. Fully automatic — nothing to configure.
DASH ClearKey (org.w3.clearkey)¶
W3C EME ClearKey for DASH CENC content. The DASH parser recognises the clearkey
ContentProtection scheme (urn:uuid:e2719d58-a985-b3c9-781a-b030af78d30e), takes the KID from
cenc:default_KID, and reads the license server URL from the manifest's <Laurl> element when
present.
License flow: the W3C JSON license request ({"kids": [...], "type": "temporary"}) is POSTed to
the license server, which returns the content key as a JWK Set. Keys land in the same vault and
--export paths as Widevine/PlayReady, and decryption uses the same shaka-packager/mp4decrypt
CENC backends (decryption config option applies).
Service integration (simplest first):
1. Manifest carries a <Laurl> — works with zero service code.
2. Custom endpoint/headers — service overrides get_clearkey_license.
3. Bespoke key delivery — service pre-populates the DRM object's keys in get_tracks.
key_vaults (list[dict])¶
Key Vaults store your obtained Content Encryption Keys (CEKs) and Key IDs per-service.
This can help reduce unnecessary License calls even during the first download. This is because a Service may provide the same Key ID and CEK for both Video and Audio, as well as for multiple resolutions or bitrates.
You can have as many Key Vaults as you would like. It's nice to share Key Vaults or use a unified Vault on Teams as sharing CEKs immediately can help reduce License calls drastically.
Four types of Vaults are in the Core codebase: API, SQLite, MySQL, and HTTP. API and HTTP make HTTP requests to a RESTful API, whereas SQLite and MySQL directly connect to an SQLite or MySQL Database.
Note: SQLite and MySQL vaults have to connect directly to the Host/IP. It cannot be in front of a PHP API or such. Beware that some Hosting Providers do not let you access the MySQL server outside their intranet and may not be accessible outside their hosting platform.
Additional behavior:
no_push(bool): Optional per-vault flag. Whentrue, the vault will not receive pushed keys (writes) but will still be queried and can provide keys for lookups. Useful for read-only/backup vaults.timeout(float): Optional per-vault network timeout in seconds for the remoteAPIandHTTPvaults. Defaults to the globalvault_timeout(10 seconds) so an unreachable vault host fails fast instead of hanging the download. Set it on a vault to override the global, e.g.timeout: 5.0. Has no effect on SQLite (local) or MySQL (uses its driver's own connect timeout).
Set a global default for all remote vaults at the top level of the config:
Using an API Vault¶
API vaults use a specific HTTP request format, therefore API or HTTP Key Vault APIs from other projects or services may not work in unshackle. The API format can be seen in the API Vault Code.
- type: API
name: "John#0001's Vault" # arbitrary vault name
uri: "https://key-vault.example.com" # api base uri (can also be an IP or IP:Port)
# uri: "127.0.0.1:80/key-vault"
# uri: "https://api.example.com/key-vault"
token: "random secret key" # authorization token
# no_push: true # optional; make this API vault read-only (lookups only)
# timeout: 5.0 # optional; per-vault network timeout (seconds), overrides vault_timeout
Using a MySQL Vault¶
MySQL vaults can be either MySQL or MariaDB servers. I recommend MariaDB. A MySQL Vault can be on a local or remote network, but I recommend SQLite for local Vaults.
- type: MySQL
name: "John#0001's Vault" # arbitrary vault name
host: "127.0.0.1" # host/ip
# port: 3306 # port (defaults to 3306)
database: vault # database used for unshackle
username: jane11
password: Doe123
# no_push: false # optional; defaults to false
I recommend giving only a trustable user (or yourself) CREATE permission and then use unshackle to cache at least one CEK per Service to have it create the tables. If you don't give any user permissions to create tables, you will need to make tables yourself.
- Use a password on all user accounts.
- Never use the root account with unshackle (even if it's you).
- Do not give multiple users the same username and/or password.
- Only give users access to the database used for unshackle.
- You may give trusted users CREATE permission so unshackle can create tables if needed.
- Other uses should only be given SELECT and INSERT permissions.
Using an SQLite Vault¶
SQLite Vaults are usually only used for locally stored vaults. This vault may be stored on a mounted Cloud storage drive, but I recommend using SQLite exclusively as an offline-only vault. Effectively this is your backup vault in case something happens to your MySQL Vault.
- type: SQLite
name: "My Local Vault" # arbitrary vault name
path: "C:/Users/Jane11/Documents/unshackle/data/key_vault.db"
# no_push: true # optional; commonly true for local backup vaults
Note
You do not need to create the file at the specified path.
SQLite will create a new SQLite database at that path if one does not exist.
Try not to accidentally move the db file once created without reflecting the change in the config, or you will end
up with multiple databases.
If you work on a Team I recommend every team member having their own SQLite Vault even if you all use a MySQL vault together.
Using an HTTP Vault¶
HTTP Vaults provide flexible HTTP-based key storage with support for multiple API modes. This vault type is useful for integrating with various third-party key vault APIs.
- type: HTTP
name: "My HTTP Vault"
host: "https://vault-api.example.com"
api_key: "your_api_key" # or use 'password' field
api_mode: "json" # query, json, or decrypt_labs
# username: "user" # required for query mode only
# no_push: false # optional; defaults to false
# timeout: 5.0 # optional; per-vault network timeout (seconds), overrides vault_timeout
Supported API Modes:
query- Uses GET requests with query parameters. Requiresusernamefield.json- Uses POST requests with JSON payloads. Token-based authentication.decrypt_labs- DecryptLabs API format. Read-only mode (no_pushis forced totrue).
Example configurations:
# Query mode (requires username)
- type: HTTP
name: "Query Vault"
host: "https://api.example.com/keys"
username: "myuser"
password: "mypassword"
api_mode: "query"
# JSON mode
- type: HTTP
name: "JSON Vault"
host: "https://api.example.com/vault"
api_key: "secret_token"
api_mode: "json"
# DecryptLabs mode (read-only)
- type: HTTP
name: "DecryptLabs Cache"
host: "https://keyxtractor.decryptlabs.com/cache"
api_key: "your_decrypt_labs_api_key"
api_mode: "decrypt_labs"
Note
The decrypt_labs mode is always read-only and cannot receive pushed keys.