Release v1.0.7
This commit is contained in:
parent
32e62a375d
commit
5de30690b3
|
@ -4,12 +4,18 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0.7] - 2024-05-12
|
||||
|
||||
### Added
|
||||
|
||||
- Added a new function specific to VENDOR 15 based on insights from [videohelp](https://forum.videohelp.com/threads/414104-Impossible-situation-dumping-keys-using-virtual-Android#post2730673).
|
||||
- Included a detailed process for extracting keys in offline mode.
|
||||
|
||||
## [1.0.6] - 2024-04-26
|
||||
|
||||
### Added
|
||||
|
||||
- Added `mksrc` script to manually improve Android shell interaction.
|
||||
- Added `editor` script for a text editor within the Android shell.
|
||||
|
||||
### Changed
|
||||
|
||||
|
@ -94,6 +100,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||
|
||||
- Initial release of the project, laying the foundation for future enhancements and features.
|
||||
|
||||
[1.0.7]: https://github.com/hyugogirubato/KeyDive/releases/tag/v1.0.7
|
||||
[1.0.6]: https://github.com/hyugogirubato/KeyDive/releases/tag/v1.0.6
|
||||
[1.0.5]: https://github.com/hyugogirubato/KeyDive/releases/tag/v1.0.5
|
||||
[1.0.4]: https://github.com/hyugogirubato/KeyDive/releases/tag/v1.0.4
|
||||
|
|
12
README.md
12
README.md
|
@ -42,6 +42,18 @@ Follow these steps to set up KeyDive:
|
|||
|
||||
This sequence ensures that the DRM-protected content is active and ready for key extraction by the time the KeyDive script is initiated, optimizing the extraction process.
|
||||
|
||||
### Offline Extraction Process
|
||||
|
||||
For situations where internet access is limited or unavailable, KeyDive supports an offline extraction mode. This mode allows for the extraction of DRM keys without an active internet connection. Follow these steps to prepare:
|
||||
|
||||
1. **Prepare the Android Device:**
|
||||
- Install all necessary dependencies and tools while connected to the internet. Ensure that all software and libraries required by KeyDive are properly configured on the device. This includes making sure the device is fully prepared to handle DRM extraction in an offline environment.
|
||||
|
||||
2. **Execute KeyDive in Offline Mode:**
|
||||
- Once all the preparations are complete and the device is disconnected from the internet, run the KeyDive script to extract the Widevine L3 keys. Ensure that the DRM-protected content is ready and available on the device for extraction.
|
||||
|
||||
For a detailed step-by-step guide on setting up and executing KeyDive without internet access, please refer to our dedicated document: [Offline Mode Detailed Guide](./docs/Axinom/OFFLINE.md).
|
||||
|
||||
### Command-Line Options
|
||||
|
||||
```shell
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
# Offline DRM Key Extraction for Axinom Content
|
||||
|
||||
This project focuses on extracting DRM keys offline from streams protected by Axinom DRM. It involves patching an open-source application to customize stream definitions, disabling network connectivity checks, and optionally bypassing SSL pinning. Additionally, it includes steps for handling device provisioning requests using a fake server.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Android Studio SDK to modify and build the APK.
|
||||
- Frida setup on your device/emulator for runtime instrumentation.
|
||||
- HTTP Toolkit for intercepting and modifying network traffic.
|
||||
- Basic understanding of Android development and network protocols.
|
||||
|
||||
## Setup
|
||||
|
||||
### Step 1: Patch the APK
|
||||
|
||||
1. Clone the repository of the open-source app you intend to patch.
|
||||
2. Modify the app’s source code to:
|
||||
- Define the correct stream URL.
|
||||
- Disable any network connectivity checks in the app. For example, bypass methods that check for active internet connections.
|
||||
```shell
|
||||
python3 builder_mobile.py
|
||||
```
|
||||
3. Build the modified APK and install it on your Android device or emulator.
|
||||
```shell
|
||||
python3 patcher.py
|
||||
```
|
||||
|
||||
### Step 2: Bypass SSL Pinning (if necessary)
|
||||
|
||||
If the app implements SSL pinning, follow the steps below to bypass it using Frida:
|
||||
|
||||
1. Ensure Frida is installed on your device or emulator.
|
||||
2. Use the script provided in the [Frida-CodeShare repository](https://github.com/hyugogirubato/Frida-CodeShare/tree/main/scripts/android-pinning) to intercept SSL pinning methods dynamically.
|
||||
3. Run the script using the command:
|
||||
```
|
||||
frida -D "DEVICE_ID" -l "pinning.js" -f "PACKAGE_NAME"
|
||||
```
|
||||
Replace `"DEVICE_ID"` with your device or emulator ID and `"PACKAGE_NAME"` with the package name of your patched app.
|
||||
|
||||
### Step 3: Setup Fake Server for Provisioning
|
||||
|
||||
1. Setup a Python server to mimic the license server. This server should always respond with a 302 redirect loop, essentially providing an infinite timeout.
|
||||
```shell
|
||||
python3 app.py
|
||||
```
|
||||
2. Implement the fake server with endpoints required for the DRM license and provisioning requests.
|
||||
|
||||
### Step 4: Use HTTP Toolkit
|
||||
|
||||
1. Install and set up HTTP Toolkit on your PC.
|
||||
2. Import predefined rules to simulate the static responses needed for the app, like `manifest.mpd`.
|
||||
3. Direct your app traffic through HTTP Toolkit to manipulate the responses as needed.
|
||||
|
||||
## Running the App
|
||||
|
||||
Launch the patched app on your device. Since the network checks are disabled, and the app is configured to use the fake server responses, it should function without real internet access, allowing for offline DRM key extraction.
|
|
@ -0,0 +1,47 @@
|
|||
.method private isNetworkAvailable()Z
|
||||
.registers 2
|
||||
|
||||
# const-string v0, "connectivity"
|
||||
|
||||
# .line 139
|
||||
# invoke-virtual {p0, v0}, Lcom/axinom/drm/sample/activity/SampleChooserActivity;->getSystemService(Ljava/lang/String;)Ljava/lang/Object;
|
||||
|
||||
# move-result-object v0
|
||||
|
||||
# check-cast v0, Landroid/net/ConnectivityManager;
|
||||
|
||||
# if-eqz v0, :cond_f
|
||||
|
||||
# .line 142
|
||||
# invoke-virtual {v0}, Landroid/net/ConnectivityManager;->getActiveNetworkInfo()Landroid/net/NetworkInfo;
|
||||
|
||||
# move-result-object v0
|
||||
|
||||
# goto :goto_10
|
||||
|
||||
# :cond_f
|
||||
# const/4 v0, 0x0
|
||||
|
||||
# :goto_10
|
||||
# if-eqz v0, :cond_1a
|
||||
|
||||
# .line 144
|
||||
# invoke-virtual {v0}, Landroid/net/NetworkInfo;->isConnected()Z
|
||||
|
||||
# move-result v0
|
||||
|
||||
# if-eqz v0, :cond_1a
|
||||
|
||||
# const/4 v0, 0x1
|
||||
|
||||
# goto :goto_1b
|
||||
|
||||
# :cond_1a
|
||||
# const/4 v0, 0x0
|
||||
|
||||
# :goto_1b
|
||||
|
||||
const/4 v0, 0x1
|
||||
|
||||
return v0
|
||||
.end method
|
Binary file not shown.
|
@ -0,0 +1,47 @@
|
|||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
# @Info: values to be patch
|
||||
PATCH = {
|
||||
"assets/samplelist.json": [
|
||||
[None, "samplelist.json"]
|
||||
],
|
||||
"com/axinom/drm/sample/activity/SampleChooserActivity.smali": [
|
||||
["SampleChooserActivity.smali", None]
|
||||
]
|
||||
}
|
||||
|
||||
# @Info: Keystore to sign the application
|
||||
KEYSTORE = {
|
||||
"algo": "RSA",
|
||||
"size": 2048,
|
||||
"sign": "SHA-256",
|
||||
"validity": 365 * 25,
|
||||
"password": "Axinom_PASSWORD",
|
||||
"alias": "Axinom_DRM_DEMO",
|
||||
"meta": {
|
||||
"common_name": "Axinom",
|
||||
"organizational_unit": "Front-End",
|
||||
"organization": "Axinom",
|
||||
"locality": "Tartu",
|
||||
"state": "Tartumaa",
|
||||
"country": "EE",
|
||||
}
|
||||
}
|
||||
|
||||
# @Info: Info about application
|
||||
METADATA = {
|
||||
# "name": "Axinom DRM Sample Player",
|
||||
"version": "202211021",
|
||||
"source": "https://github.com/Axinom/drm-sample-player-android",
|
||||
"input": "axinom.apk",
|
||||
"output": "axinom_signed.apk",
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
Path("config.yaml").write_text(yaml.dump({
|
||||
"metadata": METADATA,
|
||||
"keystore": KEYSTORE,
|
||||
"patch": PATCH
|
||||
}))
|
|
@ -0,0 +1,260 @@
|
|||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import xmltodict
|
||||
import yaml
|
||||
|
||||
|
||||
def any2str(data: any) -> str:
|
||||
if isinstance(data, (bytes, bytearray)):
|
||||
data = data.decode("utf-8")
|
||||
|
||||
if isinstance(data, (dict, list)):
|
||||
data = json.dumps(data, indent=2, separators=(",", ":"))
|
||||
|
||||
return str(data)
|
||||
|
||||
|
||||
class Keystore:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
algo: str = "RSA",
|
||||
size: int = 2048,
|
||||
sign: str = "SHA-256",
|
||||
validity: int = 365,
|
||||
password: str = None,
|
||||
alias: str = None,
|
||||
meta: dict = None,
|
||||
path: Path = Path("..")
|
||||
):
|
||||
assert path.is_dir(), "Invalid Dir Path"
|
||||
assert algo in ["RSA", "EC", "DSA"], "Invalid Algorithm"
|
||||
assert sign in ["MD5", "SHA-1", "SHA-256", "SHA-512"], "Invalid Signature"
|
||||
|
||||
if algo == "RSA":
|
||||
assert size in [1024, 2048, 3072, 4096], "Invalid RSA Size"
|
||||
assert sign in ["MD5", "SHA-1", "SHA-256", "SHA-512"], "Invalid RSA Signature"
|
||||
elif algo == "EC":
|
||||
assert size in [192, 224, 256, 384, 521], "Invalid EC Size"
|
||||
assert sign in ["SHA-256", "SHA-512"], "Invalid EC Signature"
|
||||
elif algo == "DSA":
|
||||
assert size in [1024], "Invalid DSA Size"
|
||||
assert sign in ["SHA-1"], "Invalid DSA Signature"
|
||||
|
||||
self.algorithm = algo
|
||||
self.size = size
|
||||
self.signature = "{}with{}".format(
|
||||
sign.replace("-", ""),
|
||||
"ECDSA" if algo == "EC" else algo
|
||||
)
|
||||
self.digest = sign
|
||||
self.validity = validity
|
||||
meta = meta if meta else {}
|
||||
self.metadata = {
|
||||
"common_name": meta.get("common_name", "Unknown"),
|
||||
"organizational_unit": meta.get("organizational_unit", "Unknown"),
|
||||
"organization": meta.get("organization", "Unknown"),
|
||||
"locality": meta.get("locality", "Unknown"),
|
||||
"state": meta.get("state", "Unknown"),
|
||||
"country": meta.get("country", "Unknown"),
|
||||
}
|
||||
|
||||
match = re.search(r'[\s:]?([a-zA-Z]+)', self.metadata["common_name"])
|
||||
name = re.sub(r'[^A-Za-z0-9]', "", match.group(1)).lower() if match else "keystore"
|
||||
self.path = path / f"{name}_{algo.lower()}.p12"
|
||||
self.password = password or f"{name}_password"
|
||||
self.alias = alias or f"{name}_alias"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return json.dumps({
|
||||
"path": str(self.path),
|
||||
"algorithm": self.algorithm,
|
||||
"size": self.size,
|
||||
"signature": self.signature,
|
||||
"digest": self.digest,
|
||||
"validity": self.validity,
|
||||
"password": self.password,
|
||||
"alias": self.alias,
|
||||
"metadata": self.metadata
|
||||
}, indent=2)
|
||||
|
||||
def sign(self, path: Path) -> None:
|
||||
assert path.is_file() and path.suffix == ".apk", "Invalid APK Path"
|
||||
|
||||
if not self.path.is_file():
|
||||
tmp = Path("keystore.jks")
|
||||
os.system(
|
||||
'keytool -genkeypair -keystore "{}" -alias "{}" -keyalg "{}" -keysize "{}" -sigalg "{}" -validity "{}" -storepass "{}" -keypass "{}" -dname "CN=\\"{}\\", OU=\\"{}\\", O=\\"{}\\", L=\\"{}\\", ST=\\"{}\\", C=\\"{}\\"" -noprompt'.format(
|
||||
tmp, self.alias, self.algorithm, self.size, self.signature,
|
||||
self.validity, self.password, self.password, self.metadata["common_name"],
|
||||
self.metadata["organizational_unit"], self.metadata["organization"],
|
||||
self.metadata["locality"], self.metadata["state"], self.metadata["country"]
|
||||
))
|
||||
|
||||
os.system(
|
||||
'keytool -importkeystore -srckeystore "{}" -srcstorepass "{}" -destkeystore "{}" -deststoretype "PKCS12" -deststorepass "{}" -destkeypass "{}" -srcalias "{}"'.format(
|
||||
tmp, self.password, self.path, self.password, self.password, self.alias
|
||||
))
|
||||
tmp.unlink(missing_ok=True)
|
||||
os.system('apksigner sign --ks "{}" --ks-key-alias "{}" --ks-pass "pass:{}" --key-pass "pass:{}" "{}"'.format(
|
||||
self.path, self.alias, self.password, self.password, path
|
||||
))
|
||||
Path(str(path) + ".idsig").unlink(missing_ok=True)
|
||||
|
||||
def info(self, path: Path) -> None:
|
||||
assert path.is_file() and path.suffix == ".apk", "Invalid APK Path"
|
||||
os.system(f'apksigner verify --print-certs "{path}"')
|
||||
|
||||
|
||||
class ApkTool:
|
||||
|
||||
def __init__(self, instance: Path = Path(".apktool")):
|
||||
self.instance = instance
|
||||
|
||||
def decompile(self, path: Path) -> None:
|
||||
assert path.is_file() and path.suffix == ".apk", "Invalid APK Path"
|
||||
if not self.instance.is_dir():
|
||||
os.system(f'apktool d "{path}" -o "{self.instance}" -f --no-crunch --only-main-classes')
|
||||
|
||||
def compile(self, path: Path) -> None:
|
||||
assert path.suffix == ".apk", "Invalid APK Path"
|
||||
if not path.is_file():
|
||||
assert self.instance.is_dir(), "Invalid ApkTool Path"
|
||||
tmp = Path("unaligned.apk")
|
||||
|
||||
os.system(f'apktool b "{self.instance}" -o "{tmp}" -f --no-crunch')
|
||||
if tmp.is_file(): os.system(f'zipalign -f -p "4" "{tmp}" "{path}"')
|
||||
if path.is_file(): shutil.rmtree(self.instance, ignore_errors=True)
|
||||
tmp.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def rename_app(parent: Path, name: str) -> None:
|
||||
manifest_path = parent / "AndroidManifest.xml"
|
||||
if not manifest_path.is_file():
|
||||
raise FileNotFoundError(manifest_path)
|
||||
|
||||
manifest_dict = xmltodict.parse(manifest_path.read_bytes(), encoding="utf-8")
|
||||
value = str(manifest_dict["manifest"]["application"]["@android:label"])
|
||||
if value.startswith("@string/"):
|
||||
key = value.split("@string/")[1]
|
||||
|
||||
source = None
|
||||
for path in (parent / "res").iterdir():
|
||||
strings_path = path / "strings.xml"
|
||||
if "values" in str(path) and strings_path.is_file():
|
||||
strings_dict = xmltodict.parse(strings_path.read_bytes(), encoding="utf-8")
|
||||
|
||||
for item in strings_dict["resources"]["string"]:
|
||||
if isinstance(item, dict) and item["@name"] == key:
|
||||
source = item["#text"]
|
||||
print(f"I: Patching {strings_path.name} ({strings_path.parent})")
|
||||
if source != name:
|
||||
item["#text"] = name
|
||||
strings_path.write_bytes(
|
||||
xmltodict.unparse(strings_dict, encoding="utf-8", pretty=True).encode("utf-8"))
|
||||
break
|
||||
if not source:
|
||||
raise ImportError(value)
|
||||
else:
|
||||
manifest_dict["manifest"]["application"]["@android:label"] = name
|
||||
manifest_path.write_bytes(xmltodict.unparse(manifest_dict, encoding="utf-8", pretty=True).encode("utf-8"))
|
||||
print(f"I: Patching {manifest_path.name} ({manifest_path.parent})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
config = Path("config.yaml")
|
||||
if not config.is_file():
|
||||
config = Path(input("Config Path: "))
|
||||
if not config.is_file():
|
||||
raise FileNotFoundError(config)
|
||||
|
||||
content = yaml.safe_load(config.read_text())
|
||||
|
||||
apktool = ApkTool()
|
||||
jks = Keystore(**content["keystore"])
|
||||
src = Path(content["metadata"]["input"])
|
||||
opt = Path(content["metadata"]["output"])
|
||||
|
||||
for key, value in content["metadata"].items():
|
||||
print(f"I: {key.capitalize()}: {value}")
|
||||
|
||||
if not opt.is_file():
|
||||
apktool.decompile(src)
|
||||
|
||||
# @Info: Patch apk
|
||||
for key, value in content["patch"].items(): # {Path: list[tuple]}
|
||||
|
||||
path = apktool.instance / key
|
||||
if not path.is_file():
|
||||
exist = False
|
||||
for subp in apktool.instance.iterdir():
|
||||
path = subp / key
|
||||
if path.is_file():
|
||||
exist = True
|
||||
break
|
||||
|
||||
if not exist:
|
||||
raise FileNotFoundError(key)
|
||||
|
||||
src_data = path.read_text()
|
||||
for v in value:
|
||||
if v[0] is None:
|
||||
if isinstance(v[1], str):
|
||||
# @Info: Replace complet file using [None, Path]
|
||||
v[1] = Path(v[1])
|
||||
if not v[1].is_file():
|
||||
raise FileNotFoundError(v[1])
|
||||
src_data = v[1].read_text()
|
||||
elif v[1] is None:
|
||||
# @Info: Replace with empty file
|
||||
src_data = ""
|
||||
else:
|
||||
# @Info: Replace with custom char
|
||||
src_data = any2str(v[1])
|
||||
elif v[1] is None:
|
||||
# @Info: Replace functon using [Path, None]
|
||||
if not isinstance(v[0], str):
|
||||
raise ImportError(v[0])
|
||||
|
||||
v[0] = Path(v[0])
|
||||
if not v[0].is_file():
|
||||
raise FileNotFoundError(v[0])
|
||||
|
||||
opt_data = v[0].read_text()
|
||||
if opt_data not in src_data:
|
||||
try:
|
||||
keys = opt_data.split("\n")
|
||||
start = next(filter(None, keys), None)
|
||||
stop = next(filter(None, reversed(keys)), None)
|
||||
start_index = src_data.index(start)
|
||||
stop_index = src_data.index(stop, start_index) + len(stop)
|
||||
src_data = src_data.replace(src_data[start_index:stop_index], opt_data)
|
||||
except Exception as e:
|
||||
raise ValueError(v[0])
|
||||
else:
|
||||
# @Info: Replace char using [str, str]
|
||||
if not v[0] in src_data and not v[1] in src_data:
|
||||
raise ImportError(v[0])
|
||||
src_data = src_data.replace(*v)
|
||||
|
||||
path.write_text(src_data)
|
||||
print(f"I: Patching {path.name} ({path.parent})")
|
||||
|
||||
# @Info: Rename apk
|
||||
name = content["metadata"].get("name")
|
||||
if name:
|
||||
rename_app(apktool.instance, name)
|
||||
|
||||
apktool.compile(opt)
|
||||
print(f"I: Keystore: {jks.path}")
|
||||
print(f"I: Validity: {jks.validity}")
|
||||
jks.sign(opt)
|
||||
|
||||
jks.info(opt)
|
||||
|
||||
print(f'I: MD5: {hashlib.md5(opt.read_bytes()).hexdigest()}')
|
|
@ -0,0 +1,9 @@
|
|||
[
|
||||
{
|
||||
"title": "Axinom demo video - single key (DASH; cenc)",
|
||||
"videoUrl": "https://media.axprod.net/VTB/DrmQuickStart/AxinomDemoVideo-SingleKey/Encrypted_Cenc/Manifest.mpd",
|
||||
"drmScheme": "widevine",
|
||||
"licenseServer": "https://drm-widevine-licensing.axtest.net/AcquireLicense",
|
||||
"licenseToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiNjllNTQwODgtZTllMC00NTMwLThjMWEtMWViNmRjZDBkMTRlIiwibWVzc2FnZSI6eyJ2ZXJzaW9uIjoyLCJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImxpY2Vuc2UiOnsiYWxsb3dfcGVyc2lzdGVuY2UiOnRydWV9LCJjb250ZW50X2tleXNfc291cmNlIjp7ImlubGluZSI6W3siaWQiOiIyMTFhYzFkYy1jOGEyLTQ1NzUtYmFmNy1mYTRiYTU2YzM4YWMiLCJ1c2FnZV9wb2xpY3kiOiJUaGVPbmVQb2xpY3kifV19LCJjb250ZW50X2tleV91c2FnZV9wb2xpY2llcyI6W3sibmFtZSI6IlRoZU9uZVBvbGljeSIsInBsYXlyZWFkeSI6eyJwbGF5X2VuYWJsZXJzIjpbIjc4NjYyN0Q4LUMyQTYtNDRCRS04Rjg4LTA4QUUyNTVCMDFBNyJdfX1dfX0.D9FM9sbTFxBmcCOC8yMHrEtTwm0zy6ejZUCrlJbHz_U"
|
||||
}
|
||||
]
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,44 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" minBufferTime="PT1.500S" type="static" mediaPresentationDuration="PT0H0M52.209S" maxSegmentDuration="PT0H0M4.011S" profiles="urn:mpeg:dash:profile:isoff-live:2011,http://dashif.org/guidelines/dash264" xmlns:cenc="urn:mpeg:cenc:2013">
|
||||
<Period duration="PT0H0M52.209S">
|
||||
<!--Axinom Makemedia v4.1.2-004677-9bc38e0 targeting General Purpose Media Format specification v10
|
||||
ffmpeg version N-90069-gdd8351b118-sherpya Copyright (c) 2000-2018 the FFmpeg developers
|
||||
x265 [info]: HEVC encoder version 2.4+14-bc0e9bd7c08f5ddc
|
||||
x264 0.150.2833 df79067
|
||||
MP4Box - GPAC version 0.7.2-DEV-rev539-gff59dfa0-master
|
||||
MediaInfoLib - v0.7.96
|
||||
-->
|
||||
<AdaptationSet segmentAlignment="true" maxWidth="1920" maxHeight="1080" maxFrameRate="24" par="16:9" lang="und" group="1" selectionPriority="0">
|
||||
<ContentProtection schemeIdUri="urn:mpeg:dash:mp4protection:2011" value="cenc" cenc:default_KID="211ac1dc-c8a2-4575-baf7-fa4ba56c38ac" />
|
||||
<ContentProtection value="MSPR 2.0" schemeIdUri="urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95">
|
||||
<cenc:pssh>AAAB5HBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAAcTEAQAAAQABALoBPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4AcwA9ACIAaAB0AHQAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANwAvADAAMwAvAFAAbABhAHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0AC4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsARQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQAUgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgAzAE0ARQBhAEkAYQBMAEkAZABVAFcANgA5AC8AcABMAHAAVwB3ADQAcgBBAD0APQA8AC8ASwBJAEQAPgA8AC8ARABBAFQAQQA+ADwALwBXAFIATQBIAEUAQQBEAEUAUgA+AA==</cenc:pssh>
|
||||
<pro xmlns="urn:microsoft:playready">xAEAAAEAAQC6ATwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4AMwBNAEUAYQBJAGEATABJAGQAVQBXADYAOQAvAHAATABwAFcAdwA0AHIAQQA9AD0APAAvAEsASQBEAD4APAAvAEQAQQBUAEEAPgA8AC8AVwBSAE0ASABFAEEARABFAFIAPgA=</pro>
|
||||
</ContentProtection>
|
||||
<ContentProtection value="Widevine" schemeIdUri="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed">
|
||||
<cenc:pssh>AAAANHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAABQIARIQIRrB3MiiRXW69/pLpWw4rA==</cenc:pssh>
|
||||
</ContentProtection>
|
||||
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="main" />
|
||||
<SegmentTemplate media="$RepresentationID$/$Number%04d$.m4s" timescale="24" startNumber="1" duration="96" initialization="$RepresentationID$/init.mp4" />
|
||||
<Representation id="1" mimeType="video/mp4" codecs="avc1.640015" width="512" height="288" frameRate="24" sar="1:1" startWithSAP="1" bandwidth="358733"></Representation>
|
||||
<Representation id="2" mimeType="video/mp4" codecs="avc1.64001E" width="640" height="360" frameRate="24" sar="1:1" startWithSAP="1" bandwidth="685122"></Representation>
|
||||
<Representation id="3" mimeType="video/mp4" codecs="avc1.64001E" width="852" height="480" frameRate="24" sar="640:639" startWithSAP="1" bandwidth="1015285"></Representation>
|
||||
<Representation id="4" mimeType="video/mp4" codecs="avc1.64001F" width="1280" height="720" frameRate="24" sar="1:1" startWithSAP="1" bandwidth="1743704"></Representation>
|
||||
<Representation id="5" mimeType="video/mp4" codecs="avc1.640028" width="1920" height="1080" frameRate="24" sar="1:1" startWithSAP="1" bandwidth="2423111"></Representation>
|
||||
</AdaptationSet>
|
||||
<AdaptationSet segmentAlignment="true" lang="und" group="2" selectionPriority="0">
|
||||
<ContentProtection schemeIdUri="urn:mpeg:dash:mp4protection:2011" value="cenc" cenc:default_KID="211ac1dc-c8a2-4575-baf7-fa4ba56c38ac" />
|
||||
<ContentProtection value="MSPR 2.0" schemeIdUri="urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95">
|
||||
<cenc:pssh>AAAB5HBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAAcTEAQAAAQABALoBPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4AcwA9ACIAaAB0AHQAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANwAvADAAMwAvAFAAbABhAHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0AC4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsARQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQAUgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgAzAE0ARQBhAEkAYQBMAEkAZABVAFcANgA5AC8AcABMAHAAVwB3ADQAcgBBAD0APQA8AC8ASwBJAEQAPgA8AC8ARABBAFQAQQA+ADwALwBXAFIATQBIAEUAQQBEAEUAUgA+AA==</cenc:pssh>
|
||||
<pro xmlns="urn:microsoft:playready">xAEAAAEAAQC6ATwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4AMwBNAEUAYQBJAGEATABJAGQAVQBXADYAOQAvAHAATABwAFcAdwA0AHIAQQA9AD0APAAvAEsASQBEAD4APAAvAEQAQQBUAEEAPgA8AC8AVwBSAE0ASABFAEEARABFAFIAPgA=</pro>
|
||||
</ContentProtection>
|
||||
<ContentProtection value="Widevine" schemeIdUri="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed">
|
||||
<cenc:pssh>AAAANHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAABQIARIQIRrB3MiiRXW69/pLpWw4rA==</cenc:pssh>
|
||||
</ContentProtection>
|
||||
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="main" />
|
||||
<SegmentTemplate media="$RepresentationID$/$Number%04d$.m4s" timescale="48000" startNumber="1" duration="192000" initialization="$RepresentationID$/init.mp4" />
|
||||
<Representation id="6" mimeType="audio/mp4" codecs="mp4a.40.5" audioSamplingRate="48000" startWithSAP="1" bandwidth="67041">
|
||||
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2" />
|
||||
</Representation>
|
||||
</AdaptationSet>
|
||||
</Period>
|
||||
</MPD>
|
|
@ -0,0 +1,30 @@
|
|||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Flask, Response, redirect
|
||||
|
||||
RESPONSE_PATH = Path() / 'response.json'
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
def read_file() -> bytes:
|
||||
return RESPONSE_PATH.read_bytes() if RESPONSE_PATH.is_file() else b''
|
||||
|
||||
|
||||
@app.route('/', defaults={'path': ''})
|
||||
@app.route('/<path:path>', methods=['GET', 'POST'])
|
||||
def catch_all(path):
|
||||
count = 0
|
||||
while count < 50:
|
||||
content = read_file()
|
||||
if content:
|
||||
return Response(status=200, content_type='application/json', response=content)
|
||||
time.sleep(1)
|
||||
count += 1
|
||||
|
||||
return redirect('https://www.googleapis.com/certificateprovisioning/v1/devicecertificates/create', code=302)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=9090, debug=True)
|
|
@ -0,0 +1 @@
|
|||
|
Loading…
Reference in New Issue