Release v1.0.7

This commit is contained in:
hyugogirubato 2024-05-12 15:34:11 +02:00
parent 32e62a375d
commit 5de30690b3
12 changed files with 515 additions and 1 deletions

View File

@ -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

View File

@ -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

56
docs/Axinom/OFFLINE.md Normal file
View File

@ -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 apps 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.

View File

@ -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.

View File

@ -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
}))

View File

@ -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()}')

View File

@ -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

View File

@ -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>

30
docs/Axinom/server/app.py Normal file
View File

@ -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)

View File

@ -0,0 +1 @@