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

decrypt license voucher with example #22

Merged
merged 5 commits into from
Oct 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion examples/download_books_aax.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ def download_file(url):

auth = audible.FileAuthenticator(
filename="FILENAME",
encryption="json",
password=password
)
client = audible.Client(auth)
Expand Down
58 changes: 37 additions & 21 deletions examples/download_books_aaxc.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import json
import pathlib
import shutil

import audible
import httpx
from audible.aescipher import decrypt_voucher_from_licenserequest


# 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",
Expand All @@ -22,16 +24,21 @@ 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 get_download_link(license_response):
return license_response["content_license"]["content_metadata"]["content_url"]["offline_url"]


def download_file(url, filename):
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


Expand All @@ -40,24 +47,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"]

for book in books:
asin = book['asin']
title = book['title'] + f"( {asin}).aaxc"
dl_link = _get_download_link(asin, quality="Extreme")
if dl_link:
"response_groups": "product_attrs",
"num_results": "999"
}
)

for book in books["items"]:
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_from_licenserequest(auth, lr)
voucher_file.write_text(json.dumps(decrypted_voucher, indent=4))
print(f"saved voucher to: {voucher_file}")
49 changes: 49 additions & 0 deletions src/audible/aescipher.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,52 @@ 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)


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
)
4 changes: 2 additions & 2 deletions src/audible/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'')

Expand Down