Skip to content
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

Open
CoolJoe72 opened this issue Aug 18, 2023 · 70 comments
Open

Atmos Download (Future Feature Request… if possible) #155

CoolJoe72 opened this issue Aug 18, 2023 · 70 comments
Labels
enhancement New feature or request help wanted Extra attention is needed

Comments

@CoolJoe72
Copy link

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 stereo available_codecs

"asset_details": [ "is_spatial": true,"name": "Dolby"]

This is all new to me and I'm not even sure what I'm looking for.

@mkb79
Copy link
Owner

mkb79 commented Aug 19, 2023

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 POST request to https://api.audible.de/1.0/content/B004UVB7KC/licenserequest. B004UVB7KC must be replaced with your ASIN.

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 Streaming request but it download and saves the audio file.

Edit:
The iOS Audible app makes a download request. It looks so

{
  "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:
In both cases a m3u8 playlist file is provided for downloading using the FairPlay format. After that, the client made a request to https://api.audible.de/1.0/content/B004UVB7KC/drmlicense with a licenseChallenge body to receive the license. I does not know how to decrypt FairPlay DRM.

@CoolJoe72
Copy link
Author

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.

@devnoname120
Copy link
Sponsor Contributor

devnoname120 commented Sep 3, 2023

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.

@mkb79
Copy link
Owner

mkb79 commented Sep 4, 2023

@devnoname120

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 drm_types are Mpeg, PlayReady, Hls, Dash, FairPlay, Widevine, HlsCmaf, Adrm. And known codecs are ec+3, ac-4, mp4a.40.42, mp4a.40.2. Maybe these helps a little bit?!

@mkb79
Copy link
Owner

mkb79 commented Sep 4, 2023

If you remove FairPlay from supported drm_types it will download the book in AAXC (Adrm mode)! I does not know if it using Dolby Atmos. But it uses the mp4a.40.2 codec in booth cases. Tried it with asin 3844535306 and german marketplace .

Edit:
Oh it seams these audiobook is not available in Dolby Atmos.

Edit:
Tried it again with an Atmos title. It will only download with ec+3 and only as FairPlay protected title. Maybe you have more luck.

@devnoname120
Copy link
Sponsor Contributor

@mkb79 I can't test right now. Did you try with ac-4 and WideVine? This seems to me to be the combo that will most likely work for Android content.

@mkb79
Copy link
Owner

mkb79 commented Sep 4, 2023

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: Unable to retrieve asset details from Sable(AssetInfos), for marketplaceId:AN7V1F1VY261K, asin:B0BGYDYQ38, acr:null, skuLite:OR_ORIG_002267, version:LATEST, aaaClientId:ApolloEnv:AudibleApiExternalRouterService/EU/Prod. Maybe B0BGYDYQ38 is not available on Android devices in Dolby Atmos or they are using some other codecs/drm_types there.

@mkb79
Copy link
Owner

mkb79 commented Sep 4, 2023

I've documented some new API endpoints related to the FairPlay DRM.

Steps to download Dolby Atmos titles:

  1. The client makes a licenserequest.
  2. It receives the response containing a URI to a m3u8 file
  3. The client makes multiple GET requests to this URI with the User-Agent: AppleCoreMedia/1.0.0.20G75 (iPhone; U; CPU OS 16_6 like Mac OS X; de_de) and receives content from type `application/vnd.apple.mpegurl
  4. Client requests the FairPlay certificate.
  5. Client makes a GET request to dpm.demdex.net and receives JSON content (Edit: these step may be not iOS App related)
  6. Client make a POST request to drmlicense endpoint
  7. The client request build a new URI from the URI from 2 and the filename taken from the m3u8 file and receives the audio/mp4 file.

Edit:
I can’t replay step 6 currently. I does not know how to build the license challenge. If I follow the other steps from above, leaving step 6 out, I receive a 403 status error. So step 6 must be the import part. Maybe the challenge is build using the cert from point 4 and other things?

@mkb79
Copy link
Owner

mkb79 commented Sep 4, 2023

@devnoname120
I’ve found out how I can extract the uri from the m3u8 file and download the Dolby Atmos mp4 file. Now I've to find out how to decrypt these file.

@devnoname120
Copy link
Sponsor Contributor

devnoname120 commented Sep 4, 2023

@mkb79 What's the output of file dolby_atmos_file.mp4?

Note: step 5 is probably irrelevant as dpm.demdex.net is an endpoint used by Adobe Experience Platform Identity Service.

@mkb79
Copy link
Owner

mkb79 commented Sep 4, 2023

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 Range header specify the byte range like bytes=0-1368458706 for asin B0BGYDYQ38. These audiobook has a size of 1368458706 bytes. If you forgot the Range header I've got an 403 error. So this header is mandatory.

After downloading you have a mp4 file which is encrypted via SAMPLE-AES. Now you need the key for decryption.

@devnoname120
Copy link
Sponsor Contributor

Here is the full licenserequest JSON body definition on Android:

{
  "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.

@mkb79
Copy link
Owner

mkb79 commented Sep 4, 2023

Thank you very much. That seams a licenserequest for streaming purposes. use_adaptive_bit_rate is typically for streaming requests. acr is the amazon content reference. This value is unique for each book/asin/codec?/version combination.

Important for me is the response for a download license request for an Dolby Atmos book.

@devnoname120
Copy link
Sponsor Contributor

Unfortunately I don't have an Android device that supports Dolby Atmos… Neither do I own a Dolby Atmos book.

@devnoname120
Copy link
Sponsor Contributor

@mkb79 Can you retry with this user agent?

Audible, Android, 3.58.0, samsung, SM-S906B, g0sxeea, 13, 1.0, WIFI

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.

@mkb79
Copy link
Owner

mkb79 commented Sep 4, 2023

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

@devnoname120
Copy link
Sponsor Contributor

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

@devnoname120
Copy link
Sponsor Contributor

For my future reference here is the list of available Dolby Atmos audiobooks: https://www.audible.com/public-collections/1998b1ba-07e8-470f-8581-f97365772fe0

@Mbucari
Copy link

Mbucari commented Oct 16, 2023

@devnoname120 @mkb79

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 Body Change 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:

  1. The client makes a licenserequest.
    a. Request URL: https://api.audible.com/1.0/content/B07K4VYQ5X/licenserequest
    b. Request Body

    {
      "supported_media_features": {
        "drm_types": [
          "Widevine",
          "Adrm",
          "Mpeg"
        ],
        "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"
    }
  2. It receives the response containing a URI to a Dash MPD file

    Example MPD File Zero-G MPD File
    <?xml version='1.0' encoding='utf-8'?>
    <MPD minBufferTime='PT20S' type='static' mediaPresentationDuration='PT4H8M52.693S' profiles='urn:mpeg:dash:profile:isoff-main:2011'
    xmlns='urn:mpeg:dash:schema:mpd:2011'
    xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:schemaLocation='urn:mpeg:DASH:schema:MPD:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd'>
    <Period id='0' duration='PT4H8M52.693S'>
    <AdaptationSet id='0' contentType='audio' lang='und' subsegmentAlignment='true'>
    <ContentProtection xmlns:cenc='urn:mpeg:cenc:2013' cenc:default_KID='27bde860-e27a-902c-9fda-9aa043c4fc11' schemeIdUri='urn:mpeg:dash:mp4protection:2011' value='cenc'/>
    <ContentProtection xmlns:cenc='urn:mpeg:cenc:2013' schemeIdUri='urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed'>
    <cenc:pssh>
    AAAAsXBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAJESECe96GDiepAsn9qaoEPE/BESEFvn1UjnJquHS2LOtGVV88cSEBwnhnUCfvJndrBf6z7afIsaB0F1ZGlibGUiUGNpZDoNCko3M29ZT0o2a0N5ZjJwcWdROFQ4RVE9PSxXK2ZWU09jbXE0ZExZczYwWlZYenh3PT0sSENlR2RRSis4bWQyc0YvclB0cDhpdz09
    </cenc:pssh>
    </ContentProtection>
    <ContentProtection xmlns:cenc='urn:mpeg:cenc:2013' schemeIdUri='urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95'>
    <cenc:pssh>
    AAAAVHBzc2gBAAAAmgTweZhAQoarkuZb4IhflQAAAAMnvehg4nqQLJ/amqBDxPwRW+fVSOcmq4dLYs60ZVXzxxwnhnUCfvJndrBf6z7afIsAAAAA
    </cenc:pssh>
    </ContentProtection>
    <Representation id='0' mimeType='audio/mp4' codecs='ac-4.02.02.00' bandwidth='324598' audioSamplingRate='48000'>
    <AudioChannelConfiguration schemeIdUri='urn:mpeg:mpegB:cicp:ChannelConfiguration' value='2'/>
    <SupplementalProperty schemeIdUri='tag:dolby.com,2016:dash:virtualized_content:2016' value='1'/>
    <BaseURL>../../../../base/or_orig_000434/47096871/cenc/g1/or_orig_000434_48_320-ac4.mp4?ss_sec=20&amp;use_token_based_signing=true</BaseURL>
    <SegmentBase timescale='48000' indexRange='1118-10281' indexRangeExact='true'>
    <Initialization range='0-1117'/>
    </SegmentBase>
    </Representation>
    </AdaptationSet>
    </Period>
    </MPD>
  3. Client makes a GET request for the first Initialization bytes of the file. (Found in the MPD file. Value is range='0-1117' in the example.) These bytes are the mp4 file's ftyp and moov boxes.

  4. Client make a POST request to drmlicense endpoint
    Body:

    {
      "consumption_type": "Download",
      "drm_type": "Widevine",
      "licenseChallenge": "..."
    }

    licenseChallenge is the Base64 encoded bytes returned from ExoMediaDrm.KeyRequest.getData()

  5. Client downloads the rest of the mp4 file.

I don't have any widevine experience, but a good place to start reverse engineering Audible's implementation is in com.audible.widevinecdm.WidevineL3CdmFactory.

@mkb79 mkb79 pinned this issue Oct 16, 2023
@mkb79
Copy link
Owner

mkb79 commented Oct 16, 2023

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

@devnoname120
Copy link
Sponsor Contributor

devnoname120 commented Oct 16, 2023 via email

@Mbucari
Copy link

Mbucari commented Oct 17, 2023

Hey @Mbucari and thank you for the payloads.

You're welcome @devnoname120. FYI, my patched apk removed cert pinning so you can use Http Toolkit to get all https traffic.

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 forgot to mention this. On Android the private key is formatted like so (Asn.1 values)

PrivateKeyInfo SEQUENCE (3 elem)
  version Version INTEGER 0
  privateKeyAlgorithm AlgorithmIdentifier SEQUENCE (2 elem)
    algorithm OBJECT IDENTIFIER 1.2.840.113549.1.1.1 rsaEncryption (PKCS #1)
    parameters ANY NULL
  privateKey PrivateKey OCTET STRING
    SEQUENCE (9 elem)
      INTEGER 0
      INTEGER (RSA Modulus)
      INTEGER (RSA Public Exponent)
      INTEGER (RSA D)
      INTEGER (RSA P)
      INTEGER (RSA Q)
      INTEGER (RSA DP)
      INTEGER (RSA DQ)
      INTEGER (RSA InverseQ)

The PrivateKey octet string is equivalent to the PEM delivered by iPhone registration.

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.

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.

@Mbucari
Copy link

Mbucari commented Oct 17, 2023

@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

@devnoname120
Copy link
Sponsor Contributor

devnoname120 commented Oct 30, 2023

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.

@Mbucari
Copy link

Mbucari commented Nov 6, 2023

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

@devnoname120
Copy link
Sponsor Contributor

@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

Can you explain what these keys are?

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.

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?

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

@devnoname120
Copy link
Sponsor Contributor

@Mbucari Do you have any updates on the Widevine CDM dumps? 🙏

@Mbucari
Copy link

Mbucari commented Dec 4, 2023

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
Copy link
Sponsor Contributor

devnoname120 commented Apr 17, 2024

@szescxz My bad about the decryption keys then. Btw I found this as an alternative to widevine.com diagnostics: https://emarsden.github.io/pssh-box-wasm/decode/

Edit: my memory is shady but aren't those just to set up Widevine? Then the decryption key(s) are obtained from it, no?

Too bad that https://tools.axinom.com is registration-walled + requires a subscription now (but a trial is available iirc) because it has by far the best and most comprehensive tools to decode MPD/PSSH/DASH/Widevine stuff.

@devnoname120
Copy link
Sponsor Contributor

devnoname120 commented Apr 17, 2024

@mkb79 I fixed this either by converting the key to the right format or changing the code of audible-cli so that it supports it (I don't remember which I did eventually). I will dig up and give you the code once I'm at my computer, probably tomorrow.

@mkb79
Copy link
Owner

mkb79 commented Apr 17, 2024

@mkb79 I fixed this either by converting the key to the right format or changing the code of audible-cli so that it supports it (I don't remember which I did eventually). I will dig up and give you the code once I'm at my computer, probably tomorrow.

That's great, thank you. In the meantime I'll try it myself.

@devnoname120
Copy link
Sponsor Contributor

@mkb79 Just looked it up quickly before going to work and I have that:

import base64
from cryptography.hazmat.primitives import serialization

def base64_der_to_pkcs1(base64_key):
    der_private_key = base64.b64decode(base64_key)
    private_key = serialization.load_der_private_key(der_private_key, password=None)

    pkcs1_private_key = private_key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.TraditionalOpenSSL,
        encryption_algorithm=serialization.NoEncryption()
    )

    return pkcs1_private_key.decode('utf-8')#.replace('\n', '\\n')


base64_der_key = "MII[REDACTED VERY VERY LONG BASE64 STRING]="

# Convert to PEM format
pem_private_key = base64_der_to_pkcs1(base64_der_key)

# Print or save the PEM private key
print(pem_private_key)

@devnoname120
Copy link
Sponsor Contributor

@mkb79 I will send the rest (authentication, etc.) tomorrow because I need to clean up personal data and right now I have to hop off to work.

@szescxz
Copy link

szescxz commented Apr 17, 2024

my memory is shady but aren't those just to set up Widevine?

@devnoname120
To put it in simple words, PSSH (which includes the key IDs) is included in the license request generated by the CDM, presented to license server as an identifier of the requested content. It's not encrypted in the license request, so you can see them after decoding.

As for the decryption part, quoting from https://w3c.github.io/encrypted-media/format-registry/stream/mp4.html#stream-format:

Each key is identified by a key ID and each encrypted sample is associated with the key ID of the key needed to decrypt it.

and

Streams may contain a mixture of encrypted and unencrypted samples.

@devnoname120
Copy link
Sponsor Contributor

devnoname120 commented Apr 17, 2024

@szescxz OK just for info I found a screenshot of a real license request (from a device with hardware support for Dolby Atmos) that I decoded. Don't think it's useful for this part but so we have it in that thread for posterity:

image


For the decryption iirc when I looked into this I was using mp4decrypt (https://github.com/truedread/Bento4). I think it supports multiple keys but my memory is hazy (again) on that.

@devnoname120
Copy link
Sponsor Contributor

devnoname120 commented Apr 17, 2024

@mkb79 OK so here is a dump of code I have. It should be helpful for your implementation.

I don't remember exactly what works/what doesn't and I don't have time to test but I'm pretty sure that something does run up to the point of initializing Widevine, sending back the challenge to Audible, and Audible complaining that the provisioned certificate inside Widevine is wrong (it's not signed with the Audible cert, which should be extracted as discussed above with @szescxz).

diff --git a/src/audible/client.py b/src/audible/client.py
index 369435e..a4991de 100644
--- a/src/audible/client.py
+++ b/src/audible/client.py
@@ -7,6 +7,22 @@ import httpx
 from httpx import URL
 from httpx._models import Headers, HeaderTypes  # noqa: F401
 
+LEVEL = logging.DEBUG
+
+logger_httpx = logging.getLogger("httpx")
+logger_httpx.setLevel(LEVEL)
+#logger_httpx.setConsoleLogger(LEVEL)
+# log_helper.set_file_logger(FILENAME, LEVEL)
+#logger_httpx.captureWarnings()
+
+
+
+logging.basicConfig(
+    format="%(levelname)s [%(asctime)s] %(name)s - %(message)s",
+    datefmt="%Y-%m-%d %H:%M:%S",
+    level=logging.DEBUG
+)
+
 from .auth import Authenticator
 from .exceptions import (
     BadRequest,
@@ -88,7 +104,8 @@ class Client:
             default_headers.update(headers)
 
         self.session = self._SESSION(
-            headers=default_headers, timeout=timeout, auth=auth, **session_kwargs
+            headers=default_headers, timeout=timeout, auth=auth, verify=False, **session_kwargs
+            #headers=default_headers, timeout=timeout, auth=auth, proxies="http://localhost:6617", verify=False, **session_kwargs
         )
 
         if response_callback is None:
diff --git a/src/audible/login.py b/src/audible/login.py
index 90692da..f39b607 100644
--- a/src/audible/login.py
+++ b/src/audible/login.py
@@ -19,11 +19,10 @@ from .metadata import encrypt_metadata, meta_audible_app
 logger = logging.getLogger("audible.login")
 
 USER_AGENT = (
-    "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) "
-    "AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"
+     "[REDACTED user agent of Android device that supports Audible Atmos]"
 )
 
-
 def default_captcha_callback(captcha_url: str) -> str:
     """Helper function for handling captcha."""
     captcha = httpx.get(captcha_url).content
@@ -202,8 +201,8 @@ def build_oauth_url(
     if with_username:
         base_url = f"https://www.audible.{domain}/ap/signin"
         return_to = f"https://www.audible.{domain}/ap/maplanding"
-        assoc_handle = f"amzn_audible_ios_lap_{country_code}"
-        page_id = "amzn_audible_ios_privatepool"
+        assoc_handle = f"amzn_audible_android_aui_{country_code}"
+        page_id = f"amzn_audible_android_aui_{country_code}"
 
     oauth_params = {
         "openid.oa2.response_type": "code",
diff --git a/src/audible/register.py b/src/audible/register.py
index 27a1abf..f29b9a9 100644
--- a/src/audible/register.py
+++ b/src/audible/register.py
@@ -1,10 +1,26 @@
+import base64
+from cryptography.hazmat.primitives import serialization
+
 from datetime import datetime, timedelta
 from typing import Any, Dict, Optional
 
+import json
 import httpx
 
 from .login import build_client_id
 
+def base64_der_to_pkcs1(base64_key):
+    der_private_key = base64.b64decode(base64_key)
+    private_key = serialization.load_der_private_key(der_private_key, password=None)
+
+    pkcs1_private_key = private_key.private_bytes(
+        encoding=serialization.Encoding.PEM,
+        format=serialization.PrivateFormat.TraditionalOpenSSL,
+        encryption_algorithm=serialization.NoEncryption()
+    )
+
+    return pkcs1_private_key.decode('utf-8')#.replace('\n', '\\n')
+
 
 def register(
     authorization_code: str,
@@ -39,19 +55,30 @@ def register(
             "store_authentication_cookie",
         ],
         "cookies": {"website_cookies": [], "domain": f".amazon.{domain}"},
+        "device_metadata": {
+            "device_os_family": "android",
+            "device_serial": "[REDACTED]",
+            "device_type": "A10KISP2GWF0E4",
+            "manufacturer": "[REDACTED]",
+            "model": "[REDACTED]",
+            "os_version": "25",
+            "product": "[REDACTED]",
+        },
         "registration_data": {
-            "domain": "Device",
-            "app_version": "3.56.2",
-            "device_serial": serial,
-            "device_type": "A2CZJZGLK2JJVM",
+            "domain": "DeviceLegacy",
+            # "app_version": "3.59.0",
+            "app_version": "139018",
+            "device_serial": "[REDACTED]",
+            "device_type": "A10KISP2GWF0E4",
             "device_name": (
-                "%FIRST_NAME%%FIRST_NAME_POSSESSIVE_STRING%%DUPE_"
-                "STRATEGY_1ST%Audible for iPhone"
+                "%FIRST_NAME%%FIRST_NAME_POSSESSIVE_STRING%%DUPE_STRATEGY_1ST%Audible for Android"
             ),
-            "os_version": "15.0.0",
-            "software_version": "35602678",
-            "device_model": "iPhone",
-            "app_name": "Audible",
+            "os_version": "[REDACTED an Android release-keys string]",
+            # "software_version": "35900789",
+            "software_version": "130050002",
+            "device_model": "[REDACTED]",
+            "app_name": "com.audible.application",
         },
         "auth_data": {
             "client_id": build_client_id(serial),
@@ -71,11 +98,15 @@ def register(
     if resp.status_code != 200:
         raise Exception(resp_json)
 
+    print(json.dumps(resp_json))
+
     success_response = resp_json["response"]["success"]
 
     tokens = success_response["tokens"]
     adp_token = tokens["mac_dms"]["adp_token"]
-    device_private_key = tokens["mac_dms"]["device_private_key"]
+    device_private_key = base64_der_to_pkcs1(tokens["mac_dms"]["device_private_key"])
+    print(device_private_key)
+    # device_private_key = "-----BEGIN RSA PRIVATE KEY-----\n" + device_private_key + "-----END RSA PRIVATE KEY-----\n"
     store_authentication_cookie = tokens["store_authentication_cookie"]
     access_token = tokens["bearer"]["access_token"]
     refresh_token = tokens["bearer"]["refresh_token"]

device_register.py:

from audible import Authenticator, Client, log_helper

def prompt_captcha_callback(captcha_url: str) -> str:
    """Helper function for handling captcha."""

    print("Captcha found")
    print(captcha_url)

    guess = input("Answer for CAPTCHA")
    return str(guess).strip().lower()


def prompt_otp_callback() -> str:
    """Helper function for handling 2-factor authentication."""

    print("2FA is activated for this account.")
    guess = input("Please enter OTP Code")
    return str(guess).strip().lower()


auth = Authenticator.from_login(
    username="redacted@example.com",
    password="[REDACTED]",
    locale="fr",
    captcha_callback=prompt_captcha_callback,
    otp_callback=prompt_otp_callback)

device_name = auth.device_info["device_name"]
print(f"Successfully registered {device_name}.")

ab = auth.get_activation_bytes()
print(ab)

auth.to_file(filename="/Users/paul/.audible/android.json")

ec3-download.py:

#!/usr/bin/env python3

# widevine imports
import base64, requests, sys, xmltodict
import logging
import json
# from urllib.parse import urlparse

from pywidevine.cdm import Cdm
from pywidevine.device import Device
from pywidevine.pssh import PSSH

# from cdm import cdm, deviceconfig
from getPSSH import get_pssh
# from wvdecryptcustom import WvDecrypt
# from cdm.formats import wv_proto2_pb2 as wv_proto2
from base64 import b64encode


# import json

from audible import Authenticator, Client, log_helper
# import logging
from http.client import HTTPConnection

LEVEL = "debug"
HTTPConnection.debuglevel = 1

log_helper.set_level(LEVEL)
log_helper.set_console_logger(LEVEL)
# log_helper.set_file_logger(FILENAME, LEVEL)
log_helper.capture_warnings()
requests_log = logging.getLogger("requests.packages.urllib3")
requests_log.setLevel(logging.DEBUG)
requests_log.propagate = True


# pywidevine stuff
device = Device.load("/Users/paul/dev/Audible/my_audible_l3.wvd")
cdm = Cdm.from_device(device)
session_id = cdm.open()

# Audible stuff
auth_file_path = "/Users/paul/.audible/atmos-test-3"  # FILL OUT
asin = "[REDACTED]"  # FILL OUT

auth = Authenticator.from_file(auth_file_path)


with Client(auth) as client:

    body = {
        "consumption_type": "Download",
        "quality": "High",
        "response_groups": "content_reference,chapter_info,pdf_url,ad_insertion",
        "spatial": "true",
        "supported_media_features": {
            "chapter_titles_type": "Tree",
            "codecs": [
                "mp4a.40.2",
                "ec+3"
            ],
            "drm_types": [
                "Widevine",
                "Adrm",
                "Mpeg"
            ]
        }
    }
    #   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,
        headers={"user-agent": "[REDACTED audible user agent of Android device with Dolby Atmos that has hardware support]"}
    )
    print(json.dumps(lr, indent=4))

    mpd_url = lr['content_license']['license_response']
    print('mpd_url: ' + mpd_url)



#    mpd_url = 'https://[REDACTED].cloudfront.net/[REDACTED]-ec3_v8.master.mpd?ss_sec=10&iss_sec=10&isc=1&use_token_based_signing=true'

    #lic_url = 'https://api.audible.fr/1.0/content/[REDACTED]/drmlicense'
    pssh_raw = get_pssh(mpd_url)
    # pssh = '[REDACTED base64]'
    # params from mpd_url:
    # ottsession=[REDACTED hexadecimal]&
    # puid=[REDACTED integer]&
    # video_content_id=[REDACTED hexadecimal]&

    print(f'{chr(10)}PSSH obtained:\n{pssh_raw}')

    pssh = PSSH(pssh_raw)
    challenge = cdm.get_license_challenge(session_id, pssh, license_type="OFFLINE", privacy_mode=False)
    challenge_b64 = b64encode(challenge)
    print(f'Got challenge:\n{challenge_b64}')
    response = client.post(
        f"content/{asin}/drmlicense",
        body={
            "consumption_type": "Download",
            "drm_type": "Widevine",
            "licenseChallenge": str(challenge_b64, "utf-8" ),
        },
        headers={"user-agent": "[REDACTED audible user agent of Android device with Dolby Atmos that has hardware support]"}
    )

    print('response.license: ' + response['license'])
    with open("widevine_license.txt", "w") as lic_file:
        lic_file.write(response.license)

    cdm.parse_license(session_id, response['license'])
    for key in cdm.get_keys(session_id):
        print(f"[{key.type}] {key.kid.hex}:{key.key.hex()}")

    # close session, disposes of session data
    cdm.close(session_id)

    def widevine_get_keys(pssh, cert_b64=None):
        """main func, emulates license request and then decrypt obtained license
        fileds that changes every new request is signature, expirationTimestamp, watchSessionId, puid, and rawLicenseRequestBase64 """
        wvdecrypt = WvDecrypt(init_data_b64=pssh, cert_data_b64=cert_b64, device=deviceconfig.device_android_generic)
        raw_request = wvdecrypt.get_challenge()
        request = b64encode(raw_request)
        signature = cdm.hash_object

        # audible
        response = client.post(
            f"content/{asin}/drmlicense",
            body={
                "consumption_type": "download",
                "drm_type": "widevine",
                "licensechallenge": str(request, "utf-8" ),
            },
            headers={"user-agent": "[REDACTED audible user agent of Android device with Dolby Atmos that has hardware support]"}
        )

        print('response.license: ' + response['license'])

        with open("widevine_license.txt", "w") as lic_file:
            lic_file.write(response.license)

        license_b64 = response.license

    #
    #    license_b64 = "[REDACTED very long base64 string]"

        wvdecrypt.update_license(license_b64)
        Correct, keyswvdecrypt = wvdecrypt.start_process()
        if Correct:
            return Correct, keyswvdecrypt

    # correct, keys = widevine_get_keys(pssh)

    # print('is correct? ' + correct)
    # for key in keys:
    #     print('KID:KEY -> ' + key)

Then some other files (I'm not sure anymore which is used/is useful/works but at least you have them at hand):

wvdecryptcustom.py:

# uncompyle6 version 3.7.3
# Python bytecode 3.6 (3379)
# Decompiled from: Python 3.7.8 (tags/v3.7.8:4b47a5b6ba, Jun 28 2020, 08:53:46) [MSC v.1916 64 bit (AMD64)]
# Embedded file name: pywidevine\decrypt\wvdecryptcustom.py
import logging, subprocess, re, base64
from cdm import cdm, deviceconfig

class WvDecrypt(object):
    WV_SYSTEM_ID = [
     237, 239, 139, 169, 121, 214, 74, 206, 163, 200, 39, 220, 213, 29, 33, 237]

    def __init__(self, init_data_b64, cert_data_b64, device):
        self.init_data_b64 = init_data_b64
        self.cert_data_b64 = cert_data_b64
        self.device = device
        self.cdm = cdm.Cdm()

        def check_pssh(pssh_b64):
            pssh = base64.b64decode(pssh_b64)
            if not pssh[12:28] == bytes(self.WV_SYSTEM_ID):
                new_pssh = bytearray([0, 0, 0])
                new_pssh.append(32 + len(pssh))
                new_pssh[4:] = bytearray(b'pssh')
                new_pssh[8:] = [0, 0, 0, 0]
                new_pssh[13:] = self.WV_SYSTEM_ID
                new_pssh[29:] = [0, 0, 0, 0]
                new_pssh[31] = len(pssh)
                new_pssh[32:] = pssh
                return base64.b64encode(new_pssh)
            else:
                return pssh_b64

        self.session = self.cdm.open_session(check_pssh(self.init_data_b64), deviceconfig.DeviceConfig(self.device), None, True)
        if self.cert_data_b64:
            self.cdm.set_service_certificate(self.session, self.cert_data_b64)

    def log_message(self, msg):
        return '{}'.format(msg)

    def start_process(self):
        keyswvdecrypt = []
        try:
            for key in self.cdm.get_keys(self.session):
                if key.type == 'CONTENT':
                    keyswvdecrypt.append(self.log_message('{}:{}'.format(key.kid.hex(), key.key.hex())))

        except Exception:
            return (
             False, keyswvdecrypt)
        else:
            return (
             True, keyswvdecrypt)

    def get_challenge(self):
        return self.cdm.get_license_request(self.session)

    def update_license(self, license_b64):
        self.cdm.provide_license(self.session, license_b64)
        return True

getPSSH.py

import requests, xmltodict

def get_pssh(mpd_url):
    # pssh = '[REDACTED]'
    pssh = ''
    try:
        r = requests.get(url=mpd_url)
        r.raise_for_status()
        xml = xmltodict.parse(r.text)
        mpd = xml
        periods = mpd['MPD']['Period']
    except Exception as e:
        pssh = input(f'\nUnable to find PSSH in MPD: {e}. \nEdit getPSSH.py or enter PSSH manually: ')
        return pssh
    if isinstance(periods, list):
        print('Period is a list')
        for idx, period in enumerate(periods):
            if isinstance(period['AdaptationSet'], list):
                print('AdaptationSet is a list')
                for ad_set in period['AdaptationSet']:
                    if True or ad_set['@mimeType'] in ['video/mp4', 'audio/mp4']:
                        print('Found appropriate mime')
                        # try:
                        for t in ad_set['ContentProtection']:
                            print(t['@schemeIdUri'].lower())
                            if t['@schemeIdUri'].lower() == "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed":
                                print('Found appropriate scheme id')
                                pssh = t["cenc:pssh"]
                                print('Found pssh: ' + pssh)
                        # except Exception:
                        #    pass
            else:
                print('AdaptationSet is a single item')
                if True or period['AdaptationSet']['@mimeType'] in ['video/mp4', 'audio/mp4']:
                    print('Found appropriate mime')
                    # try:
                    for t in period['AdaptationSet']['ContentProtection']:
                        print(t['@schemeIdUri'].lower())
                        if t['@schemeIdUri'].lower() == "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed":
                            print('Found appropriate scheme id')
                            pssh = t["cenc:pssh"]
                            print('Found pssh: ' + pssh)
                    # except Exception:
                        # pass
    else:
        print('Period is a single item')
        for ad_set in periods['AdaptationSet']:
                if True or ad_set['@mimeType'] in ['video/mp4', 'audio/mp4']:
                    print('Found appropriate mime')
                    # try:
                    for t in ad_set['ContentProtection']:
                        print(t['@schemeIdUri'].lower())
                        if t['@schemeIdUri'].lower() == "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed":
                            print('Found appropriate scheme id')
                            pssh = t["cenc:pssh"]
                            print('Found pssh: ' + pssh)
                    # except Exception:
                    #     pass
    if pssh == '':
        pssh = input('Unable to find PSSH in mpd. Edit getPSSH.py or enter PSSH manually: ')
    return pssh

13.py:

# -*- coding: utf-8 -*-
# Module: widevine_keys
# Created on: 10.12.2021
# Authors: medvm
# Version: 2.1.0

import base64, requests, sys, xmltodict
# import headers
# import cookies
import json
from cdm import cdm, deviceconfig
from base64 import b64encode
from getPSSH import get_pssh
from wvdecryptcustom import WvDecrypt
from cdm.formats import wv_proto2_pb2 as wv_proto2
from urllib.parse import urlparse
import logging
# logging.basicConfig(level=logging.DEBUG)
# MDP_URL = input('\nInput MPD URL: ')
MDP_URL = 'https://[REDACTED].cloudfront.net/[REDACTED]-ec3_v8.master.mpd?ss_sec=10&iss_sec=10&isc=1&use_token_based_signing=true'

lic_url = 'https://api.audible.fr/1.0/content/[REDACTED]/drmlicense'
responses = []
license_b64 = ''
pssh = get_pssh(MDP_URL)
params = None
params = urlparse(lic_url).query
# pssh = '[REDACTED]'
# params from mdp_url:
# ottsession=[REDACTED hexadecimal number]&
# puid=[REDACTED integer number]&
# video_content_id=[REDACTED hexadecimal number]&

print(f'{chr(10)}PSSH obtained.\n{pssh}')

def widevine_get_keys(pssh, lic_url, cert_b64=None):
    """main func, emulates license request and then decrypt obtained license
    fileds that changes every new request is signature, expirationTimestamp, watchSessionId, puid, and rawLicenseRequestBase64 """
    wvdecrypt = WvDecrypt(init_data_b64=pssh, cert_data_b64=cert_b64, device=deviceconfig.device_android_generic)
    raw_request = wvdecrypt.get_challenge()
    request = b64encode(raw_request)
    signature = cdm.hash_object

    # audible
    response = requests.post(url=lic_url, headers=headers.headers, params=params,
        json={
            "consumption_type": "Download",
            "drm_type": "Widevine",
            "licenseChallenge": str(request, "utf-8" ),
        }
    ))

    try:
        str(response.content, "utf-8")
    except UnicodeDecodeError:
        widevine_license = response
        print(f'{chr(10)}license response status: {widevine_license}{chr(10)}')
        break
    else:
        if len(str(response.content, "utf-8")) > 500:
            widevine_license = response
            print(f'{chr(10)}license response status: {widevine_license}{chr(10)}')
            break
    if idx == len(responses) - 1:
        print(f'{chr(10)}license response status: {response}')
        print(f'server reports: {str(response.content, "utf-8")}')
        print(f'server did not issue license, make sure you have correctly pasted all the required headers in the headers.py. Also check json/raw params of POST request.{chr(10)}')
        exit()

    lic_field_names = ['license', 'payload', 'getWidevineLicenseResponse']
    lic_field_names2 = ['license']

    open('license_content.bin', 'wb').write(widevine_license.content)

    try:
        if str(widevine_license.content, 'utf-8').find(':'):
            for key in lic_field_names:
                try:
                    license_b64 = json.loads(widevine_license.content.decode())[key]
                except:
                    pass
                else:
                    for key2 in lic_field_names2:
                        try:
                            license_b64 = json.loads(widevine_license.content.decode())[key][key2]
                        except:
                            pass
        else:
            license_b64 = widevine_license.content
    except:
        license_b64 = b64encode(widevine_license.content)
#
#    license_b64 = "[REDACTED]"
    wvdecrypt.update_license(license_b64)
    Correct, keyswvdecrypt = wvdecrypt.start_process()
    if Correct:
        return Correct, keyswvdecrypt

correct, keys = widevine_get_keys(pssh, lic_url)

print('is correct? ' + correct)
for key in keys:
    print('KID:KEY -> ' + key)

@mkb79
Copy link
Owner

mkb79 commented Apr 17, 2024

@devnoname120
Thank you very much. This helps a lot. I could convert the private key to the right format now. But when I try to make a licenserequest to an Atmos title, I've received an 404er error. Which user-agent do you use?

Update:
Maybe the reason for my issue is, that I only changed the body of the registration request to an Android device. These seams to be not enough or the device does not support Atmos natively. I'll change the login form to an Android device now and then I'll see, if this works.

@mkb79
Copy link
Owner

mkb79 commented Apr 18, 2024

@devnoname120

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

@devnoname120
Copy link
Sponsor Contributor

devnoname120 commented Apr 18, 2024

@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 USER_AGENT would look like something like that:

Mozilla/5.0 (Linux; Android 11; ONEPLUS IN2013 Build/NMF26F; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.96 Mobile Safari/537.36

@szescxz
Copy link

szescxz commented Apr 18, 2024

Also note that I was able to do licenserequest on an unsupported device with the Frida hook above.
As long as supported_media_features.drm_types includes Widevine, supported_media_features.codecs includes ec+3 and/or ac-4 (although I never found any players supporting ac-4 playback), and spatial is set to true, this should be sufficient to request for the spatial audio manifest (compared to normal ones).

BTW, I think the User-Agent string on my side is something like

Dalvik/2.1.0 (Linux; U; Android 11; REDACTED_DEVICE_MODEL_NAME Build/REDACTED_BUILD_NUMBER); com.audible.application 3.73.0 b:154017

@devnoname120
Copy link
Sponsor Contributor

devnoname120 commented Apr 18, 2024

@mkb79 And the release-keys can be found in the build.props of the device (here OnePlus 8):

OnePlus/OnePlus8/OnePlus8:11/RP1A.201005.001/2110102308:user/release-keys

And with the rest of the information from the build.props of that device the registration body would look like:

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 device_serial of 20 characters.


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

@devnoname120
Copy link
Sponsor Contributor

@szescxz ac-4 is used for iPhones AFAIK

@szescxz
Copy link

szescxz commented Apr 18, 2024

@szescxz ac-4 is used for iPhones AFAIK

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.
ec+3 works fine.

@mkb79
Copy link
Owner

mkb79 commented Apr 18, 2024

@devnoname120

@mkb79 And the release-keys can be found in the build.props of the device (here OnePlus 8):

OnePlus/OnePlus8/OnePlus8:11/RP1A.201005.001/2110102308:user/release-keys

And with the rest of the information from the build.props of that device the registration body would look like:

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 device_serial of 20 characters.

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

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.

@mkb79
Copy link
Owner

mkb79 commented Apr 18, 2024

@devnoname120
I've changed the login part now. But it does not work. Maybe the registration data are incorrect so Audible does not know that this is an Atmos compatible device.

@devnoname120
Copy link
Sponsor Contributor

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

@devnoname120
Copy link
Sponsor Contributor

@mkb79 Can you give me your code diff so that I can double-check?

@mkb79
Copy link
Owner

mkb79 commented Apr 18, 2024

@devnoname120
I've created this branch with my rework. This includes a new registration request body and a conversion of the private key.

@devnoname120
Copy link
Sponsor Contributor

devnoname120 commented Apr 18, 2024

@mkb79 I don't see any changes to src/audible/login.py in your branch. Neither do I see any changes to the f"content/{asin}/licenserequest request.

@mkb79
Copy link
Owner

mkb79 commented Apr 19, 2024

@devnoname120
I've merged my changes to login.py to the same branch now.

device_register.py

from audible import Authenticator


r = Authenticator.from_login(
    "[REDACTED]",
    "[REDACTED]",
    "de"
)
r.to_file("credentials-android.json")

licenserequest.py

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 FairPlay content. With my Android profile it is ADRM

Edit:
I've registered a new device on US market to make sure, it's not marketplace related. It makes no difference.

@szescxz
Copy link

szescxz commented Apr 19, 2024

Can reproduce on my end.
But if changing consumption_type to Streaming (as well as changing other related parameters) then the server will return Widevine content.

Credential from a real device is still able to request Widevine content with consumption_type set to Download, though.

@mkb79
Copy link
Owner

mkb79 commented Apr 19, 2024

@szescxz
If I set the body in my licenserequest.py to

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

Credential from a real device is still able to request Widevine content with consumption_type set to Download, though.

Maybe the correct body of the registration request makes the difference. Can you provide your body (without the serial?

@mkb79
Copy link
Owner

mkb79 commented Apr 19, 2024

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.

@mkb79 mkb79 added enhancement New feature or request help wanted Extra attention is needed labels Apr 23, 2024
@devnoname120
Copy link
Sponsor Contributor

Any news?

@mkb79
Copy link
Owner

mkb79 commented Apr 28, 2024

Last status on my part:
I'm able to register an Android Audible device. But I can only stream Widevine/AC-4 content. Streaming EC3 content or downloading AC-4/EC3 content is not possible. But this is still okay. I can get the full content with a streaming request. But don’t in EC3.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

5 participants