From da9b5a5a0626033c08b8a2aa32c67d20e57025c5 Mon Sep 17 00:00:00 2001 From: mkb79 Date: Thu, 8 Oct 2020 09:55:47 +0200 Subject: [PATCH 1/5] decrypt license voucher and example --- examples/download_books_aax.py | 1 - examples/download_books_aaxc.py | 68 +++++++++++++++++++++++++-------- src/audible/aescipher.py | 12 ++++++ 3 files changed, 65 insertions(+), 16 deletions(-) diff --git a/examples/download_books_aax.py b/examples/download_books_aax.py index 1191548c..4880cb60 100644 --- a/examples/download_books_aax.py +++ b/examples/download_books_aax.py @@ -58,7 +58,6 @@ def download_file(url): auth = audible.FileAuthenticator( filename="FILENAME", - encryption="json", password=password ) client = audible.Client(auth) diff --git a/examples/download_books_aaxc.py b/examples/download_books_aaxc.py index 332d03b9..68aac753 100644 --- a/examples/download_books_aaxc.py +++ b/examples/download_books_aaxc.py @@ -1,18 +1,22 @@ +import base64 +import json import pathlib import shutil import audible import httpx +from audible.aescipher import decrypt_voucher as dv -# files downloaded via this script can't be converted at this moment +# files downloaded via this script can be converted # audible uses a new format (aaxc instead of aax) # more informations and workaround here: # https://github.com/mkb79/Audible/issues/3 +# especially: https://github.com/mkb79/Audible/issues/3#issuecomment-705262614 -# get download link(s) for book -def _get_download_link(asin, quality): +# get license response for book +def get_license_response(client, asin, quality): try: response = client.post( f"content/{asin}/licenserequest", @@ -22,13 +26,38 @@ def _get_download_link(asin, quality): "quality": quality } ) - return response['content_license']['content_metadata']['content_url']['offline_url'] + return response except Exception as e: print(f"Error: {e}") return +def decrypt_voucher(auth, license_response): + # device data + device_info = auth.device_info + device_serial_number = device_info["device_serial_number"] + device_type = device_info["device_type"] + + # user data + customer_id = auth.customer_info["user_id"] + + # book specific data + asin = license_response["content_license"]["asin"] + encrypted_voucher = base64.b64decode(license_response["content_license"]["license_response"]) + + return dv(device_serial_number=device_serial_number, + customer_id=customer_id, + device_type=device_type, + asin=asin, + voucher=encrypted_voucher) + + +def get_download_link(license_response): + return license_response["content_license"]["content_metadata"]["content_url"]["offline_url"] + + def download_file(url, filename): + # example download function r = httpx.get(url) with open(filename, 'wb') as f: shutil.copyfileobj(r.iter_raw, f) @@ -40,24 +69,33 @@ def download_file(url, filename): auth = audible.FileAuthenticator( filename="FILENAME", - encryption="json", password=password ) - client = audible.AudibleAPI(auth) + client = audible.Client(auth) books = client.get( - path="0.0/library/books", + path="library", params={ - "purchaseAfterDate": "01/01/1970" - } - )["books"]["book"] + "response_groups": "product_attrs", + "num_results": "999" + } + ) for book in books: - asin = book['asin'] - title = book['title'] + f"( {asin}).aaxc" - dl_link = _get_download_link(asin, quality="Extreme") - if dl_link: + asin = book["asin"] + title = book["title"] + f"( {asin}).aaxc" + lr = get_license_response(client, asin, quality="Extreme") + + if lr: + # download book + dl_link = get_download_link(lr) filename = pathlib.Path.cwd() / "audiobooks" / title print(f"download link now: {dl_link}") status = download_file(dl_link, filename) - print(f"downloaded file: {status}") + print(f"downloaded file: {status} to {filename}") + + # save voucher + voucher_file = filename.with_suffix(".json") + decrypted_voucher = decrypt_voucher(auth, lr) + voucher_file.write_text(json.dumps(decrypt_voucher, indent=4)) + print(f"saved voucher to: {voucher_file}") diff --git a/src/audible/aescipher.py b/src/audible/aescipher.py index cb48fee8..bb23c56b 100644 --- a/src/audible/aescipher.py +++ b/src/audible/aescipher.py @@ -231,3 +231,15 @@ def remove_file_encryption(source, target, password, **kwargs): crypter = AESCipher(password, **kwargs) decrypted = crypter.from_file(source_file, encryption=encryption) pathlib.Path(target).write_text(decrypted) + + +def decrypt_voucher(device_serial_number, customer_id, device_type, asin, voucher): + # https://github.com/mkb79/Audible/issues/3#issuecomment-705262614 + buf = (device_type + device_serial_number + customer_id + asin).encode("ascii") + digest = sha256(buf).digest() + key = digest[0:16] + iv = digest[16:] + + # decrypt "voucher" using AES in CBC mode with no padding + plaintext = aes_cbc_decrypt(key, iv, voucher).rstrip("\x00") + return json.loads(plaintext) From 2a18ac1015ba1ae543575ad1f8430b91aec20ba4 Mon Sep 17 00:00:00 2001 From: mkb79 Date: Thu, 8 Oct 2020 16:17:06 +0200 Subject: [PATCH 2/5] bugfix download_books_aaxc --- examples/download_books_aaxc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/download_books_aaxc.py b/examples/download_books_aaxc.py index 68aac753..b481d334 100644 --- a/examples/download_books_aaxc.py +++ b/examples/download_books_aaxc.py @@ -81,7 +81,7 @@ def download_file(url, filename): } ) - for book in books: + for book in books["items"]: asin = book["asin"] title = book["title"] + f"( {asin}).aaxc" lr = get_license_response(client, asin, quality="Extreme") From c76ae73e9d276db3ab14e0abba43923c32a34b9f Mon Sep 17 00:00:00 2001 From: mkb79 Date: Thu, 8 Oct 2020 17:54:26 +0200 Subject: [PATCH 3/5] bugfix download function for download books aaxc example --- examples/download_books_aaxc.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/examples/download_books_aaxc.py b/examples/download_books_aaxc.py index b481d334..56ad49f8 100644 --- a/examples/download_books_aaxc.py +++ b/examples/download_books_aaxc.py @@ -1,7 +1,6 @@ import base64 import json import pathlib -import shutil import audible import httpx @@ -57,10 +56,10 @@ def get_download_link(license_response): def download_file(url, filename): - # example download function - r = httpx.get(url) - with open(filename, 'wb') as f: - shutil.copyfileobj(r.iter_raw, f) + with httpx.stream("GET", url) as r: + with open(filename, 'wb') as f: + for chunck in r.iter_bytes(): + f.write(chunck) return filename From 145bd7260eb15e397ef089509cf17d5237d79c9d Mon Sep 17 00:00:00 2001 From: mkb79 Date: Tue, 13 Oct 2020 09:52:26 +0200 Subject: [PATCH 4/5] some changes to decrypt voucher function and example --- examples/download_books_aaxc.py | 27 +++-------------------- src/audible/aescipher.py | 39 ++++++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/examples/download_books_aaxc.py b/examples/download_books_aaxc.py index 56ad49f8..b09b6891 100644 --- a/examples/download_books_aaxc.py +++ b/examples/download_books_aaxc.py @@ -1,10 +1,9 @@ -import base64 import json import pathlib import audible import httpx -from audible.aescipher import decrypt_voucher as dv +from audible.aescipher import decrypt_voucher_from_licenserequest # files downloaded via this script can be converted @@ -31,26 +30,6 @@ def get_license_response(client, asin, quality): return -def decrypt_voucher(auth, license_response): - # device data - device_info = auth.device_info - device_serial_number = device_info["device_serial_number"] - device_type = device_info["device_type"] - - # user data - customer_id = auth.customer_info["user_id"] - - # book specific data - asin = license_response["content_license"]["asin"] - encrypted_voucher = base64.b64decode(license_response["content_license"]["license_response"]) - - return dv(device_serial_number=device_serial_number, - customer_id=customer_id, - device_type=device_type, - asin=asin, - voucher=encrypted_voucher) - - def get_download_link(license_response): return license_response["content_license"]["content_metadata"]["content_url"]["offline_url"] @@ -95,6 +74,6 @@ def download_file(url, filename): # save voucher voucher_file = filename.with_suffix(".json") - decrypted_voucher = decrypt_voucher(auth, lr) - voucher_file.write_text(json.dumps(decrypt_voucher, indent=4)) + decrypted_voucher = decrypt_voucher_from_licenserequest(auth, lr) + voucher_file.write_text(json.dumps(decrypted_voucher, indent=4)) print(f"saved voucher to: {voucher_file}") diff --git a/src/audible/aescipher.py b/src/audible/aescipher.py index bb23c56b..4f01c927 100644 --- a/src/audible/aescipher.py +++ b/src/audible/aescipher.py @@ -233,7 +233,7 @@ def remove_file_encryption(source, target, password, **kwargs): pathlib.Path(target).write_text(decrypted) -def decrypt_voucher(device_serial_number, customer_id, device_type, asin, voucher): +def _decrypt_voucher(device_serial_number, customer_id, device_type, asin, voucher): # https://github.com/mkb79/Audible/issues/3#issuecomment-705262614 buf = (device_type + device_serial_number + customer_id + asin).encode("ascii") digest = sha256(buf).digest() @@ -243,3 +243,40 @@ def decrypt_voucher(device_serial_number, customer_id, device_type, asin, vouche # decrypt "voucher" using AES in CBC mode with no padding plaintext = aes_cbc_decrypt(key, iv, voucher).rstrip("\x00") return json.loads(plaintext) + + +def decrypt_voucher_from_licenserequest(auth, license_response): + """Decrypt the voucher from licenserequest response + + :param auth: An instance of an `Authenticator` clsss + :type auth: audible.FileAuthenticator, audible.LoginAuthenticator + :param license_response: The response content from a successful http + `POST` request to api endpoint /1.0/content/{asin}/licenserequest + :type license_response: dict + :returns: The decryptes license voucher with needed key and iv + :rtype: dict + + .. note:: + A device registration is needed to use the auth instance for a + license request and to obtain the needed device data + + """ + # device data + device_info = auth.device_info + device_serial_number = device_info["device_serial_number"] + device_type = device_info["device_type"] + + # user data + customer_id = auth.customer_info["user_id"] + + # book specific data + asin = license_response["content_license"]["asin"] + encrypted_voucher = base64.b64decode(license_response["content_license"]["license_response"]) + + return _decrypt_voucher( + device_serial_number=device_serial_number, + customer_id=customer_id, + device_type=device_type, + asin=asin, + voucher=encrypted_voucher + ) From e835a2acc8c70ac78eb8d4e41677b4bfeb30e1fe Mon Sep 17 00:00:00 2001 From: mkb79 Date: Thu, 15 Oct 2020 13:21:08 +0200 Subject: [PATCH 5/5] Fix: redefining `domain` argument in auth.py --- src/audible/auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/audible/auth.py b/src/audible/auth.py index 8c1ca29f..b647ce97 100644 --- a/src/audible/auth.py +++ b/src/audible/auth.py @@ -65,8 +65,8 @@ def refresh_website_cookies( raw_cookies = resp_dict["response"]["tokens"]["cookies"] website_cookies = dict() - for domain in raw_cookies: - for cookie in raw_cookies[domain]: + for cookies_domain in raw_cookies: + for cookie in raw_cookies[cookies_domain]: website_cookies[cookie["Name"]] = cookie["Value"].replace(r'"', r'')