-
-
Notifications
You must be signed in to change notification settings - Fork 47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Atmos Download (Future Feature Request… if possible) #155
Comments
I'm absolutely low on time this week. But you can try out yourself if you can download and decrypt the atmos audio. You have to make a The body for the request above is {
"use_adaptive_bit_rate" : true,
"supported_media_features" : {
"codecs" : [
"mp4a.40.2",
"mp4a.40.42",
"ec+3"
],
"drm_types" : [
"Mpeg",
"Hls",
"HlsCmaf",
"FairPlay"
]
},
"response_groups" : "content_reference,chapter_info,last_position_heard,pdf_url,certificate",
"consumption_type" : "Streaming",
"spatial" : true
} I does not know why the iOS Audible app makes a Edit: {
"quality" : "High",
"response_groups" : "chapter_info,content_reference,last_position_heard,pdf_url",
"consumption_type" : "Download",
"supported_media_features" : {
"codecs" : [
"mp4a.40.2",
"mp4a.40.42",
"ec+3"
],
"drm_types" : [
"Mpeg",
"Adrm",
"FairPlay"
]
},
"spatial" : true
} Edit: |
Well it was worth a shot, and thank you all your time you put into it. I wasn't expecting a response other than yeah that would be neat and maybe marked as a future option once it was figured out in like 6 months or more. Looks like I'll have to do some research now I know what direction to go in, when I get some free time. Thank you. |
Audible for Android also supports Dolby Atmos. Does it also download a FairPlay format? Because to the best of my knowledge FairPlay is only available on Apple devices so it may be using another (weaker?) DRM. |
The iOS and Android Audible apps are requesting the same API to make a license request. Therefore, you just have to find out which request body the Android app uses. For iOS this body is sent: {
"quality" : "High",
"response_groups" : "chapter_info,content_reference,last_position_heard,pdf_url",
"consumption_type" : "Download",
"supported_media_features" : {
"codecs" : [
"mp4a.40.2",
"mp4a.40.42",
"ec+3"
],
"drm_types" : [
"Mpeg",
"Adrm",
"FairPlay"
]
},
"spatial" : true
} I do not have an Android device to decrypt the HTTPS traffic sent from the Audible app to the API. So you can using the script below to play around to make a licenserequest and test some codecs and drm_types and check which response the API will give: import json
from audible import Authenticator, Client
auth_file_path = "..." # FILL OUT
asin = "..." # FILL OUT
auth = Authenticator.from_file(auth_file_path)
with Client(auth) as client:
body = {
"quality": "High",
"response_groups": "chapter_info,content_reference,last_position_heard,pdf_url",
"consumption_type": "Download",
"supported_media_features":
{
"codecs": [
"mp4a.40.2",
"mp4a.40.42",
"ec+3"
],
"drm_types": [
"Mpeg",
"Adrm",
"FairPlay"
]
},
"spatial": True
}
lr = client.post(
f"content/{asin}/licenserequest",
body=body,
)
print(json.dumps(lr, indent=4)) Known |
If you remove Edit: Edit: |
@mkb79 I can't test right now. Did you try with |
I've tried it with the codecs ec+3 and ac-4. Drm_types where set to Mpeg, PlayReady, Hls, Dash, Widevine, HlsCmaf, Adrm (only removing FairPlay). This will give me a HTTP 404 error with the message: |
I've documented some new API endpoints related to the FairPlay DRM. Steps to download Dolby Atmos titles:
Edit: |
@devnoname120 |
@mkb79 What's the output of Note: step 5 is probably irrelevant as |
The m3u8 playlist contains the mp4 file location. With these information you can build the correct URI to the mp4 file. These file must be downloaded using a special After downloading you have a mp4 file which is encrypted via SAMPLE-AES. Now you need the key for decryption. |
Here is the full {
"supported_media_features" : {
"drm_types": [
"adrm",
"hls",
"play_ready",
"mpeg",
"dash",
"widevine"
],
"codecs": [
"mp4a.40.2", // AAC_LC
"mp4a.40.42", // XHE_AAC
"ec+3", // EC_PLUS_3
"ac-4", // AC_4
],
"chapter_titles_type": "flat/tree",
},
"spatial" : true, // true/false,
"consumption_type" : "streaming/download",
"rights_validation": "ownership/radio/aycl",
"quality" : "low/normal/high/extreme",
"version": "[version]",
"acr": "CR![id_of_28_characters]", // Some kind of id for license?
"use_adaptive_bit_rate": true, // true/false
"playback_start_ms": 123,
"playback_end_ms": 456,
"response_groups" : "content_reference,chapter_info,pdf_url,last_position_heard,ad_insertion",
"file_version": "[version]"
} Not sure whether it differs from the one you see on iOS or not. Just posting this for my future reference I'll dig more into the Atmos stuff on Android. |
Thank you very much. That seams a licenserequest for streaming purposes. Important for me is the response for a download license request for an Dolby Atmos book. |
Unfortunately I don't have an Android device that supports Dolby Atmos… Neither do I own a Dolby Atmos book. |
@mkb79 Can you retry with this user agent?
This corresponds to a Samsung Galaxy S22 (it has native Dolby Atmos support). I cobbled up this user agent by looking at how the Audible app constructs it and filling it with the information I was able to find on the internet. I'm not 100% sure I didn't make any mistakes while building it though. |
@devnoname120 Specifying an User-Agent makes no different. But I'm checked the downloaded book B0BGYDYQ38 with MediaInfo General
Complete name : audio.mp4
Format : MPEG-4
Format profile : Base Media / Version 1
Codec ID : mp41 (iso8/isom/mp41/dash/cmfc)
File size : 1.27 GiB
Duration : 3 h 57 min
Overall bit rate mode : Constant
Overall bit rate : 769 kb/s
Encoded date : 2023-03-22 12:22:54 UTC
Tagged date : 2023-03-22 12:22:54 UTC
Audio
ID : 1
Format : E-AC-3
Format/Info : Enhanced AC-3
Commercial name : Dolby Digital Plus
Codec ID : enca / ec-3
Duration : 3 h 57 min
Bit rate mode : Constant
Channel(s) : 6 channels
Channel layout : L R C LFE Ls Rs
Sampling rate : 48.0 kHz
Compression mode : Lossy
Service kind : Complete Main
Encoded date : 2023-03-22 12:22:54 UTC
Tagged date : 2023-03-22 12:22:54 UTC
Encryption : Encrypted So downloading Dolby Atmos titles is no problem. But decrypting FPS is the next step. I know how I can receive the FairPlay cert. The drmlicense request will then receive the license which can be used for decryption. |
@mkb79 I'm not familiar with FairPlay but it's a tough nut to crack. I'll try to trick the Audible app into thinking that my phone supports Dolby Atmos and dump the resulting HTTP requests. That will be for another time though! |
For my future reference here is the list of available Dolby Atmos audiobooks: https://www.audible.com/public-collections/1998b1ba-07e8-470f-8581-f97365772fe0 |
Widevine is only available on android devices. For you to be able to request Widevine DRM, your audible client must be registered as an android device. Currently, audible-cli only registers as an iPhone. To register as an Android device, use the following registration body: Android Registration BodyChange the registration body to the following:body = {
"requested_token_type": [
"bearer",
"mac_dms",
"website_cookies",
"store_authentication_cookie",
],
"cookies": {"website_cookies": [], "domain": f".amazon.{domain}"},
"registration_data": {
"domain": "DeviceLegacy",
"app_version": "141028",
"device_serial": serial,
"device_type": "A10KISP2GWF0E4",
"device_name": (
"%FIRST_NAME%%FIRST_NAME_POSSESSIVE_STRING%%DUPE_"
"STRATEGY_1ST%Audible for Android"
),
"os_version": "google/sdk_gphone64_x86_64/emu64xa:14/UPB5.230623.003/10615560:userdebug/dev-keys",
"software_version": "130050002",
"device_model": "sdk_gphone64_x86_64",
"app_name": "com.audible.application",
},
"auth_data": {
"use_global_authentication": "true",
"client_id": build_client_id(serial),
"authorization_code": authorization_code,
"code_verifier": code_verifier.decode(),
"code_algorithm": "SHA-256",
"client_domain": "DeviceLegacy",
},
"requested_extensions": ["device_info", "customer_info"],
} I don't own an android Device that supports Dolby Atmos, but I was able to modify the Audible apk to allow downloading Atmos files. You can download it here. Steps to download Dolby Atmos titles using Zero-G as an example:
I don't have any widevine experience, but a good place to start reverse engineering Audible's implementation is in |
Thank you very much for sharing your findings. Since I don't have an Android device, I unfortunately couldn't find out the exact registration body. This will help a lot. Now we have two possible approaches (Widevine or FairPlay) to decrypt Atmos titles. Maybe some of them is successful. |
Hey @Mbucari and thank you for the payloads. I didn't update you guys in this thread but in the meantime I did a pretty thorough reverse engineering of the Audible Android app. I was also able to defeat the Amazon MAP certificate pinning and insert my own mitmproxy CA inside their hardcoded base64 + gzipped BKS bundle of certificates.
I updated my local fork of audible-cli in order to support Android auth, but (at least on my side) they use a different format of certificates (not PEM) for payload signatures and I haven't fixed that part entirely yet. I additionally dumped a L3 Widevine CDM so I should be ready to decrypt the actual resulting Dolby Atmos payload — unless there are more protections on top of Widevine.
I wanted to do a PR or at least a complete PoC before updating you, but to avoid duplicate work it I could maybe release my findings before doing a PoC when I get the chance to.
…On Oct 16, 2023, 23:52 +0200, mkb79 ***@***.***>, wrote:
@Mbucari
Thank you very much for sharing your findings. Since I don't have an Android device, I unfortunately couldn't find out the exact registration body. This will help a lot.
Now we have two possible approaches (Widevine or FairPlay) to decrypt Atmos titles. Maybe some of them is successful.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you were mentioned.Message ID: ***@***.***>
|
You're welcome @devnoname120. FYI, my patched apk removed cert pinning so you can use Http Toolkit to get all https traffic.
I forgot to mention this. On Android the private key is formatted like so (Asn.1 values)
The
At the moment I have no idea what any of that means, but it sounds very impressive! Do you have some links to widevine documentation that could help explain this? My searches only yielded high-level info which seems pretty useless for reversing. |
@devnoname120 I really wish I could be more helpful with the encryption key, but I don't have python code for it. You can use this JavaScript parser to decode the base64 and get the integer values: https://lapo.it/asn1js/ And you can see my c# Asn.1 decoder here: https://github.com/rmcrackan/AudibleApi/blob/a630d6f04b2840d68b532a782eab3f46ec14aac0/AudibleApi/Cryptography/PrivateKey.cs#L54C1-L54C1 |
Quick update: I made good progress. I have a PoC that authenticates, gets the Atmos content license object, extracts the pssh from the MPD, sets up a new Widevine L3 session with my dumped CDM keys, gets a new challenge from the CDM, and sends a Widevine L3 license request to Audible with that challenge. Currently Audible refuses to grant my L3 license request. I double-checked my license request and it looks correct — I think that the CDM keys that I extracted are just not approved by Audible. Next step is getting my hand on a rooted physical Android physical in order to extract new Widevine CDM keys and move on to the next step. |
@devnoname120 I have a couple of old android devices that I could root and, with your instruction, dump CDM keys. Hell, I'd be willing to gift one of them to you if you'd like. Can you explain what these keys are? If you dumped a working CDM and we use them in our audible decryptors, won't they just be revoked? And when they revoked, would we have to buy a new device to get a new, valid CDM? |
@Mbucari That would be great, thanks! Getting my hand on CDM keys would be enough. These keys would only be for my personal use. In order to get the keys you would need to follow these instructions: https://github.com/lollolong/dumper
They are used to simulate a Widevine L3 device. From that simulated device we create a challenge that is sent to Audible's server, which in turn returns personalized decryption keys that the simulator deciphers and then returns back to us. I'm not in my sharpest mental state right now so I hope it makes sense.
You're right, if we included dumped keys they would be banned pretty fast. Users would have to dump their own keys or use a Widevine proxy service such as https://getwvkeys.cc/ or one based on pywidevine's remote CDM feature. I don't know any good proxies yet |
@Mbucari Do you have any updates on the Widevine CDM dumps? 🙏 |
Not yet, sorry. Thanks for the reminder. I'll get to it this week. |
@devnoname120 Update: |
I'm writing my packages using Pythonista on my iOS device most of the time. On Pythonista I can't use the cryptography package. So I rewrote your code to convert the Android private key. If you are interested, the code can be found below. def base64_der_to_pkcs1(base64_key):
import base64
import rsa
from pyasn1.codec.der import decoder
from pyasn1.type import univ, namedtype
class PrivateKeyAlgorithm(univ.Sequence):
componentType = namedtype.NamedTypes(
namedtype.NamedType("algorithm", univ.ObjectIdentifier()),
namedtype.NamedType("parameters", univ.Any()),
)
class PrivateKeyInfo(univ.Sequence):
componentType = namedtype.NamedTypes(
namedtype.NamedType("version", univ.Integer()),
namedtype.NamedType("pkalgo", PrivateKeyAlgorithm()),
namedtype.NamedType("key", univ.OctetString()),
)
der_pk = base64.b64decode(base64_key)
(priv, _) = decoder.decode(der_pk, asn1Spec=PrivateKeyInfo())
key = rsa.PrivateKey.load_pkcs1(priv["key"], format="DER")
return key.save_pkcs1().decode("utf-8") |
@mkb79 Here is a list of Android devices that support true Dolby Atmos (≠ just has the Dolby Atmos equalizer app). For example for the OnePlus 8 the
|
Also note that I was able to do BTW, I think the
|
@mkb79 And the
And with the rest of the information from the body = {
"requested_token_type": [
"bearer",
"mac_dms",
"website_cookies",
"store_authentication_cookie",
],
"cookies": {"website_cookies": [], "domain": f".amazon.{domain}"},
"device_metadata": {
"device_os_family": "android",
"device_serial": "9f25e8f5e3d8ed7b9415",
"device_type": "A10KISP2GWF0E4",
"manufacturer": "OnePlus",
"model": "IN2013",
"os_version": "30",
"product": "OnePlus8",
},
"registration_data": {
"domain": "DeviceLegacy",
"app_version": "139018",
"device_serial": "9f25e8f5e3d8ed7b9415",
"device_type": "A10KISP2GWF0E4",
"device_name": (
"%FIRST_NAME%%FIRST_NAME_POSSESSIVE_STRING%%DUPE_STRATEGY_1ST%Audible for Android"
),
"os_version": "OnePlus/OnePlus8/OnePlus8:11/RP1A.201005.001/2110102308:user/release-keys",
"software_version": "110090009",
"device_model": "IN2013",
"app_name": "com.audible.application",
},
"auth_data": {
"client_id": build_client_id(serial),
"authorization_code": authorization_code,
"code_verifier": code_verifier.decode(),
"code_algorithm": "SHA-256",
"client_domain": "DeviceLegacy",
},
"requested_extensions": ["device_info", "customer_info"],
} Note: I generated a random @mkb79 Also, it may have changed but the code I gave you is all I needed to make registration/login/download work. So you may want to double-check your changes to see if you missed something. |
@szescxz |
Well, server did issue a Widevine license for me to decrypt the file using Android tokens/identifiers. No way to verify the result since I don't have a supported decoder, though. |
I'm registered a new device and only changed the body with your suggestion, but I can’t download Widevine content. It still uses Adrm. Maybe changing the registration body is not enough. I'll try again by changing the login part too and will report back. |
@devnoname120 |
@szescxz But how did you get the decryption key? This is the part where I'm confused. Did you just hook the right functions to grab it from the fangs of Widevine? |
@mkb79 Can you give me your code diff so that I can double-check? |
@devnoname120 |
@mkb79 I don't see any changes to |
@devnoname120
from audible import Authenticator
r = Authenticator.from_login(
"[REDACTED]",
"[REDACTED]",
"de"
)
r.to_file("credentials-android.json")
import json
import audible
auth_file_android = "credentials-android.json"
auth_file_iphone = "credentials-iphone.json"
auth = audible.Authenticator.from_file(auth_file_android)
with audible.Client(auth=auth, country_code='us') as client:
asin = "B0C66LN3JW"
body = {
"supported_media_features": {
"drm_types": [
"Widevine",
"Adrm",
"Mpeg",
"FairPlay"
],
"codecs": [
"mp4a.40.2",
"mp4a.40.42",
"ec+3",
"ac-4"
],
"chapter_titles_type": "Tree"
},
"spatial": True,
"consumption_type": "Download",
"quality": "High",
"response_groups": "content_reference,chapter_info,pdf_url,ad_insertion"
}
r = client.post(f'content/{asin}/licenserequest', body=body)
# print(json.dumps(r, indent=4))
drm_type = r["content_license"]["drm_type"]
cr = r["content_license"]["content_metadata"]["content_reference"]
codec = cr["codec"]
content_format = cr["content_format"]
print(f"DRM TYPE: {drm_type}")
print(f"CODEC {codec}")
print(f"FORMAT: {content_format}") When I run the licenserequest with my iPhone profile, i've got Edit: |
Can reproduce on my end. Credential from a real device is still able to request |
@szescxz body = {
"supported_media_features": {
"drm_types": [
"Widevine",
"Mpeg",
"FairPlay",
#"Adrm",
],
"codecs": [
"mp4a.40.2",
"mp4a.40.42",
"ec+3",
"ac-4"
],
"chapter_titles_type": "Tree"
},
"spatial": True,
"consumption_type": "Streaming",
"quality": "High",
"response_groups": "content_reference,chapter_info,pdf_url,ad_insertion"
} it results in DRM TYPE: Widevine
CODEC ac-4
FORMAT: M4A_AC4 When I uncomment "Adrm" the result is Bad Request (400): Only Dash, HlsCmaf, Hls, and Mpeg can be requested with Streaming consumption_type
Maybe the correct body of the registration request makes the difference. Can you provide your body (without the serial? |
If I use my iPhone credentials and the streaming request, the result is DRM TYPE: FairPlay
CODEC ec+3
FORMAT: M4A_EC3 The codec and format differ between the android and iPhone credentials. |
Any news? |
Last status on my part: |
I was wondering if there was a way to piggy-back, off of this implementation, that allows for the downloading of EC-3 files, from Apple Music. https://github.com/alacleaker/apple-music-alac-downloader I've been able to use this script, and was able to acquire some DRM free, Dolby Atmos files. |
They are using a virtual machine and "patch" the system via an agent on the go to handle Apples FPS. If you find out, how Android or Apple build and encrypt the license challenge request, then you where able to download and decrypt FPS content. The FairPlay cert can be requested using this endpoint. |
So are you saying this could work? I'm wondering if there is a way to route the downloaded mp4 EC-3 files, through the script? Here's another version of the same process, but this one uses .go files, to accomplish the task. |
Sorry for the wait, here goes my full script of (roughly) the entire process: import base64
import hashlib
import json
import os
import secrets
import subprocess
import uuid
from datetime import datetime, timedelta, timezone
from email.utils import parsedate_to_datetime
from urllib.parse import parse_qs, urlencode, urlparse
import xml.etree.ElementTree as ET
# pip install requests
import requests
# pip install cryptography
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
# pip install pywidevine
from pywidevine.cdm import Cdm
from pywidevine.device import Device
from pywidevine.pssh import PSSH
class Audible:
APP_NAME = "com.audible.application"
APK_VERSION_NAME = "3.79.0"
APK_VERSION_CODE = "160008"
APP_VERSION = "130050002"
MAP_VERSION = "20240412N"
REGION_CONFIGS = {
"US": {
"domain": "amazon.com",
"region": "NA",
"base_url": "https://www.amazon.com/ap/signin",
"return_to": "https://www.audible.com/ap/maplanding",
"assoc_handle": "amzn_audible_android_experiment_us",
"register_url": "https://api.audible.com/auth/register"
}
}
@staticmethod
def generate_request_id():
return str(uuid.uuid4())
@staticmethod
def sign_request(adp_token, device_private_key, method, url, params={}, data=""):
request_date = datetime.now(timezone.utc)
parsed_url = urlparse(url)
query_string = urlencode(params)
payload = f"{method}\n{parsed_url.path if parsed_url.path != '' else '/'}{'?' + query_string if query_string != '' else ''}\n{request_date.strftime('%Y-%m-%dT%H:%M:%SZ')}\n{data}\n{adp_token}"
signature = base64.b64encode(
device_private_key.sign(
payload.encode(),
padding.PKCS1v15(),
hashes.SHA256()
)
).decode()
return {
"x-adp-token": adp_token,
"x-adp-alg": "SHA256WithRSA:1.0",
"x-adp-signature": f"{signature}:{request_date.strftime('%Y-%m-%dT%H:%M:%SZ')}"
}
def __init__(self, region, device_properties):
self.device_properties = device_properties
self.region = region
self.user_agent = "Dalvik/2.1.0 (Linux; U; Android {release}; {model} Build/{build_id})".format_map({
"release": device_properties["ro.build.version.release"],
"model": device_properties["ro.product.model"],
"build_id": device_properties["ro.build.id"]
})
self.device_metadata = {
"device_os_family": "android",
"device_type": "A10KISP2GWF0E4",
"device_serial": None,
"manufacturer": device_properties["ro.product.manufacturer"],
"model": device_properties["ro.product.model"],
"os_version": device_properties["ro.product.build.version.sdk"],
"product": device_properties["ro.product.name"]
}
self.registration_data = {
"domain": "DeviceLegacy",
"device_type": self.device_type_id,
"device_name": "%FIRST_NAME%%FIRST_NAME_POSSESSIVE_STRING%%DUPE_STRATEGY_1ST%Audible for Android",
"app_name": self.APP_NAME,
"app_version": self.APK_VERSION_CODE,
"device_model": self.device_properties["ro.product.model"],
"os_version": self.device_properties["ro.build.fingerprint"],
"software_version": "0"
}
self.refresh_token = None
self.access_token = None
self.access_token_expiry = None
self.session = requests.Session()
# uncomment for debugging with mitmproxy
#self.session.proxies.update({"https": "http://localhost:8080"})
#self.session.verify = False
@property
def device_type_id(self):
return self.device_metadata["device_type"]
@property
def device_serial(self):
return self.device_metadata["device_serial"]
def start_login(self):
def build_device_serial() -> str:
return uuid.uuid4().hex.lower()[:20]
def create_s256_code_challenge(verifier: bytes) -> bytes:
m = hashlib.sha256(verifier)
return base64.urlsafe_b64encode(m.digest()).rstrip(b"=")
def build_client_id(serial: str) -> str:
client_id = serial.encode() + b"#" + self.device_type_id.encode()
return client_id.hex()
def create_code_verifier(length: int = 32) -> bytes:
verifier = secrets.token_bytes(length)
return base64.urlsafe_b64encode(verifier).rstrip(b"=")
self.device_metadata["device_serial"] = build_device_serial()
self.registration_data["device_serial"] = self.device_serial
client_id = build_client_id(self.device_serial)
code_verifier = create_code_verifier()
code_challenge = create_s256_code_challenge(code_verifier)
base_url = self.REGION_CONFIGS[self.region]["base_url"]
return_to = self.REGION_CONFIGS[self.region]["return_to"]
assoc_handle = self.REGION_CONFIGS[self.region]["assoc_handle"]
page_id = "amzn_audible_android_aui_v2_dark_us"
oauth_params = {
"openid.oa2.response_type": "code",
"openid.oa2.code_challenge_method": "S256",
"openid.oa2.code_challenge": code_challenge,
"openid.return_to": return_to,
"openid.assoc_handle": assoc_handle,
"openid.identity": "http://specs.openid.net/auth/2.0/" "identifier_select",
"pageId": page_id,
"accountStatusPolicy": "P1",
"openid.claimed_id": "http://specs.openid.net/auth/2.0/" "identifier_select",
"openid.mode": "checkid_setup",
"openid.ns.oa2": "http://www.amazon.com/ap/ext/oauth/2",
"openid.oa2.client_id": f"device:{client_id}",
"openid.ns.pape": "http://specs.openid.net/extensions/pape/1.0",
"openid.oa2.scope": "device_auth_access",
"openid.ns": "http://specs.openid.net/auth/2.0",
"openid.pape.max_auth_age": "0",
}
self.auth_data = {
"client_domain": self.registration_data["domain"],
"client_id": client_id,
"code_algorithm": "SHA-256",
"code_verifier": code_verifier.decode(),
"use_global_authentication": "true"
}
return f"{base_url}?{urlencode(oauth_params)}"
def load_credentials(self, credentials):
self.device_metadata["device_type"] = credentials["extensions"]["device_info"]["device_type"]
self.device_metadata["device_serial"] = credentials["extensions"]["device_info"]["device_serial_number"]
self.adp_token = credentials["tokens"]["mac_dms"]["adp_token"]
self.device_private_key = serialization.load_der_private_key(base64.b64decode(credentials["tokens"]["mac_dms"]["device_private_key"]), password=None)
self.refresh_access_token()
def register_device(self, response_url):
self.auth_data.update({
"authorization_code": parse_qs(urlparse(response_url).query)["openid.oa2.authorization_code"][0]
})
resp = self.session.post(
self.REGION_CONFIGS[self.region]["register_url"],
json={
"auth_data": self.auth_data,
"cookies": {
"domain": f'www.{self.REGION_CONFIGS[self.region]["domain"]}',
"website_cookies": []
},
"device_metadata": self.device_metadata,
"registration_data": self.registration_data,
"requested_extensions": [
"device_info",
"customer_info"
],
"requested_token_type": [
"bearer",
"mac_dms",
"store_authentication_cookie",
"website_cookies"
]
},
headers={
"User-Agent": self.user_agent,
"x-amzn-identity-auth-domain": self.REGION_CONFIGS[self.region]["register_url"].split("/")[2],
"X-Amzn-RequestId": self.generate_request_id()
}
)
resp.raise_for_status()
credentials = resp.json()["response"]["success"]
self.load_credentials(credentials)
return credentials
def audibleapi_request(self, method: str, uri, params={}, data={}):
method = method.upper()
headers = {
"x-amzn-identity-auth-domain": f"www.audible.com",
"X-Amzn-RequestId": self.generate_request_id()
}
if data:
data = json.dumps(data, separators=(",", ":"), ensure_ascii=False)
headers["Content-Type"] = "application/json"
else:
data = ""
url = f"https://api.audible.com{uri}"
if self.refresh_token:
headers.update({
"User-Agent": f'AmazonWebView/MAPClientLib/{self.APP_VERSION}/Android/{self.device_properties["ro.build.version.release"]}/{self.device_metadata["model"]}',
"Authorization": f"Bearer {self.access_token}"
})
else:
headers["User-Agent"] = self.user_agent
headers.update(self.sign_request(
self.adp_token,
self.device_private_key,
method,
url,
data=data
))
return self.session.request(
method,
url,
params=params,
data=data.encode("utf-8"),
headers=headers
)
def refresh_access_token(self):
data = {
"app_name": self.APP_NAME,
"app_version": self.APP_VERSION,
"device_metadata": self.device_metadata,
"map_version": {
"client_metrics_integrated": True,
"current_version": self.MAP_VERSION,
"package_name": self.APP_NAME,
"platform": "Android"
}
}
if self.refresh_token:
data.update({
"requested_token_type": "access_token",
"source_token": self.refresh_token,
"source_token_type": "refresh_token"
})
else:
data.update({
"requested_token_type": "refresh_token",
"source_token": "source_token",
"source_token_type": "dms_token"
})
resp = self.audibleapi_request(
"POST",
"/auth/token",
data=data
)
if not resp.ok:
print(resp.json()["error_description"])
resp.raise_for_status()
now = parsedate_to_datetime(resp.headers["X-Amz-Date"])
resp = resp.json()
self.refresh_token = resp.get("refresh_token", self.refresh_token)
self.access_token = resp["access_token"]
self.access_token_expiry = now + timedelta(seconds=resp["expires_in"])
@property
def is_access_token_expired(self):
return datetime.now(timezone.utc) >= self.access_token_expiry
def licenserequest(self, asin, consumption_type="Download", spatial=False):
assert consumption_type in ["Download", "Streaming"]
codecs = [
"mp4a.40.2"
]
if spatial:
codecs += [
"ec+3",
"ac-4"
]
resp = self.audibleapi_request(
"POST",
f"/1.0/content/{asin}/licenserequest",
data={
"supported_media_features": {
"drm_types": [
"Widevine",
"Adrm",
"Mpeg"
],
"codecs": codecs,
"chapter_titles_type": "Tree",
"previews": False,
"catalog_samples": False
},
"spatial": spatial,
"consumption_type": consumption_type,
"quality": "High",
"response_groups": "content_reference,chapter_info,pdf_url,ad_insertion,narration_speed"
}
)
resp.raise_for_status()
return resp.json()
def drmlicense_widevine(self, asin: str, challenge: bytes, consumption_type: str="Download") -> bytes:
assert consumption_type in ["Download", "Streaming"]
resp = self.audibleapi_request(
"POST",
f"/1.0/content/{asin}/drmlicense",
data={
"consumption_type": consumption_type,
"drm_type": "Widevine",
"licenseChallenge": base64.b64encode(challenge).decode()
}
)
resp.raise_for_status()
return base64.b64decode(resp.json()["license"])
def extract_widevine_pssh(client: Audible, manifest_url):
user_agent = f'com.audible.playersdk.player/{client.APK_VERSION_NAME} (Linux;Android {client.device_properties["ro.build.version.release"]}) AndroidXMedia3/1.3.0'
nsmap = {"mpd": "urn:mpeg:dash:schema:mpd:2011", "cenc": "urn:mpeg:cenc:2013"}
resp = client.session.get(manifest_url, headers={"User-Agent": user_agent})
resp.raise_for_status()
manifest = ET.fromstring(resp.content)
widevine_scheme_id_uri = Cdm.urn
widevine_psshs = manifest.findall(f".//mpd:ContentProtection[@schemeIdUri='{widevine_scheme_id_uri}']/cenc:pssh", namespaces=nsmap)
widevine_psshs = set([i.text.strip() for i in widevine_psshs])
return widevine_psshs
if __name__ == "__main__":
from pathlib import Path
# adb shell getprop or whatever
device_props = {
"ro.build.fingerprint": "OnePlus/OnePlus8/OnePlus8:11/RP1A.201005.001/2110102308:user/release-keys",
"ro.build.id": "RP1A.201005.001",
"ro.build.version.release": "11",
"ro.product.build.version.sdk": "30",
"ro.product.manufacturer": "OnePlus",
"ro.product.model": "IN2013",
"ro.product.name": "OnePlus8"
}
region = "US"
asin = "B0CY635C64"
consumption_type = "Download"
client = Audible(region, device_props)
session_file_name = os.path.join(os.path.dirname(os.path.realpath(__file__)), f"{Path(__file__).stem}_{region.lower()}.session")
# register a new device if necessary
try:
with open(session_file_name, "r") as f:
session_data = json.load(f)
client.load_credentials(session_data["audible"]["credentials"])
except FileNotFoundError:
login_url = client.start_login()
credentials = client.register_device(
input(
f'Visit the following URL:\n{login_url}\nPaste the final URL after finishing sign in flow: '
)
)
with open(session_file_name, "w") as f:
json.dump({
"audible": {
"credentials": credentials
}
}, f)
resp = client.licenserequest(asin, consumption_type=consumption_type, spatial=True)
content_license = resp["content_license"]
codec = content_license["content_metadata"]["content_reference"]["codec"]
manifest_url = content_license["license_response"]
print(f'Manifest URL: {manifest_url}')
assert content_license["drm_type"] == "Widevine"
widevine_psshs = extract_widevine_pssh(client, manifest_url)
# You will need an accepted CDM to run the code below
cdm = Cdm.from_device(Device.load("audible.wvd"))
assert cdm.system_id == 22435 # app does not seem to use L1 even if the device supports it
session_id = cdm.open()
challenge = cdm.get_license_challenge(
session_id=session_id,
pssh=PSSH(widevine_psshs.pop()),
license_type="OFFLINE" if consumption_type == "Download" else "STREAMING",
privacy_mode=False # matches with the actual behavior
)
print("Requesting decryption keys")
license = client.drmlicense_widevine(asin, challenge, consumption_type=consumption_type)
cdm.parse_license(session_id, license)
keys = cdm.get_keys(session_id)
keys = [key for key in keys if key.type == "CONTENT"]
if keys:
print("Keys:")
print("\n".join([f"{key.kid.hex}:{key.key.hex()}" for key in keys]))
# Usage of N_m3u8DL-RE is for demonstration only
# consider implement the download + merge part in Python with multi-threaded download support
# and use shaka-packager for decryption
# FFmpeg does not support AC4 plus it cannot disable DASH probing (since the server requires the Range header but rejects "Range: bytes=0-")
# so not recommended here
print(f"Downloading and decrypting with N_m3u8DL-RE")
subprocess.run(" ".join([
"N_m3u8DL-RE", # Download from https://github.com/nilaoda/N_m3u8DL-RE/releases/latest and put it in PATH
"--header",
'"Range: bytes"', # HTTP 403 hackaround
"--save-name",
f"{asin}.{codec}",
"--use-shaka-packager" # Download from https://github.com/shaka-project/shaka-packager/releases/latest and put it in PATH
] + [f"--key {key.kid.hex}:{key.key.hex()}" for key in keys] + [f"'{manifest_url}'"]), shell=True).check_returncode() Due to the aforementioned reasons I'm not going to share the CDM in public (and by this design users are supposed to extract their own CDM; I prefer not to share any tutorials on this for now). |
@szescxz FYI: |
DASH manifests given to the Android side does not seem to contain FairPlay's UUID. The only available ones are Widevine and PlayReady (see https://dashif.org/identifiers/content_protection/ for a list of UUIDs), so I believe FairPlay DRM is restricted to HLS manifests only which means everything is within Apple's ecosystem. The concept should be somewhat similar, though, if your goal is to refactor the codebase to support various DRM systems via external modules/libraries. I picked Widevine because
Therefore I'm unable to offer help on other DRM systems. |
I've used an application, such as Downie, to download the full mp4 file, but they are all encrypted. I'm not sure if this helps, or make anything easier. |
So it would be awesome to enable the Audible Dolby Atmos option for downloading
I'm not sure if something would need to be added to the api or even what codec format they are using but it seems it can be downloaded in the iOS app and some select android devices.
Digging around the api found this in
/1.0/library/B0C66LN3JW
but it showed the standard stereoavailable_codecs
This is all new to me and I'm not even sure what I'm looking for.
The text was updated successfully, but these errors were encountered: