Skip to content

Commit 53e6101

Browse files
authored
Merge pull request #22 from mkb79/issue-3
decrypt license voucher with example
2 parents bd7f020 + e835a2a commit 53e6101

File tree

4 files changed

+88
-24
lines changed

4 files changed

+88
-24
lines changed

examples/download_books_aax.py

-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ def download_file(url):
5858

5959
auth = audible.FileAuthenticator(
6060
filename="FILENAME",
61-
encryption="json",
6261
password=password
6362
)
6463
client = audible.Client(auth)

examples/download_books_aaxc.py

+37-21
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1+
import json
12
import pathlib
2-
import shutil
33

44
import audible
55
import httpx
6+
from audible.aescipher import decrypt_voucher_from_licenserequest
67

78

8-
# files downloaded via this script can't be converted at this moment
9+
# files downloaded via this script can be converted
910
# audible uses a new format (aaxc instead of aax)
1011
# more informations and workaround here:
1112
# https://github.com/mkb79/Audible/issues/3
13+
# especially: https://github.com/mkb79/Audible/issues/3#issuecomment-705262614
1214

1315

14-
# get download link(s) for book
15-
def _get_download_link(asin, quality):
16+
# get license response for book
17+
def get_license_response(client, asin, quality):
1618
try:
1719
response = client.post(
1820
f"content/{asin}/licenserequest",
@@ -22,16 +24,21 @@ def _get_download_link(asin, quality):
2224
"quality": quality
2325
}
2426
)
25-
return response['content_license']['content_metadata']['content_url']['offline_url']
27+
return response
2628
except Exception as e:
2729
print(f"Error: {e}")
2830
return
2931

3032

33+
def get_download_link(license_response):
34+
return license_response["content_license"]["content_metadata"]["content_url"]["offline_url"]
35+
36+
3137
def download_file(url, filename):
32-
r = httpx.get(url)
33-
with open(filename, 'wb') as f:
34-
shutil.copyfileobj(r.iter_raw, f)
38+
with httpx.stream("GET", url) as r:
39+
with open(filename, 'wb') as f:
40+
for chunck in r.iter_bytes():
41+
f.write(chunck)
3542
return filename
3643

3744

@@ -40,24 +47,33 @@ def download_file(url, filename):
4047

4148
auth = audible.FileAuthenticator(
4249
filename="FILENAME",
43-
encryption="json",
4450
password=password
4551
)
46-
client = audible.AudibleAPI(auth)
52+
client = audible.Client(auth)
4753

4854
books = client.get(
49-
path="0.0/library/books",
55+
path="library",
5056
params={
51-
"purchaseAfterDate": "01/01/1970"
52-
}
53-
)["books"]["book"]
54-
55-
for book in books:
56-
asin = book['asin']
57-
title = book['title'] + f"( {asin}).aaxc"
58-
dl_link = _get_download_link(asin, quality="Extreme")
59-
if dl_link:
57+
"response_groups": "product_attrs",
58+
"num_results": "999"
59+
}
60+
)
61+
62+
for book in books["items"]:
63+
asin = book["asin"]
64+
title = book["title"] + f"( {asin}).aaxc"
65+
lr = get_license_response(client, asin, quality="Extreme")
66+
67+
if lr:
68+
# download book
69+
dl_link = get_download_link(lr)
6070
filename = pathlib.Path.cwd() / "audiobooks" / title
6171
print(f"download link now: {dl_link}")
6272
status = download_file(dl_link, filename)
63-
print(f"downloaded file: {status}")
73+
print(f"downloaded file: {status} to {filename}")
74+
75+
# save voucher
76+
voucher_file = filename.with_suffix(".json")
77+
decrypted_voucher = decrypt_voucher_from_licenserequest(auth, lr)
78+
voucher_file.write_text(json.dumps(decrypted_voucher, indent=4))
79+
print(f"saved voucher to: {voucher_file}")

src/audible/aescipher.py

+49
Original file line numberDiff line numberDiff line change
@@ -231,3 +231,52 @@ def remove_file_encryption(source, target, password, **kwargs):
231231
crypter = AESCipher(password, **kwargs)
232232
decrypted = crypter.from_file(source_file, encryption=encryption)
233233
pathlib.Path(target).write_text(decrypted)
234+
235+
236+
def _decrypt_voucher(device_serial_number, customer_id, device_type, asin, voucher):
237+
# https://github.com/mkb79/Audible/issues/3#issuecomment-705262614
238+
buf = (device_type + device_serial_number + customer_id + asin).encode("ascii")
239+
digest = sha256(buf).digest()
240+
key = digest[0:16]
241+
iv = digest[16:]
242+
243+
# decrypt "voucher" using AES in CBC mode with no padding
244+
plaintext = aes_cbc_decrypt(key, iv, voucher).rstrip("\x00")
245+
return json.loads(plaintext)
246+
247+
248+
def decrypt_voucher_from_licenserequest(auth, license_response):
249+
"""Decrypt the voucher from licenserequest response
250+
251+
:param auth: An instance of an `Authenticator` clsss
252+
:type auth: audible.FileAuthenticator, audible.LoginAuthenticator
253+
:param license_response: The response content from a successful http
254+
`POST` request to api endpoint /1.0/content/{asin}/licenserequest
255+
:type license_response: dict
256+
:returns: The decryptes license voucher with needed key and iv
257+
:rtype: dict
258+
259+
.. note::
260+
A device registration is needed to use the auth instance for a
261+
license request and to obtain the needed device data
262+
263+
"""
264+
# device data
265+
device_info = auth.device_info
266+
device_serial_number = device_info["device_serial_number"]
267+
device_type = device_info["device_type"]
268+
269+
# user data
270+
customer_id = auth.customer_info["user_id"]
271+
272+
# book specific data
273+
asin = license_response["content_license"]["asin"]
274+
encrypted_voucher = base64.b64decode(license_response["content_license"]["license_response"])
275+
276+
return _decrypt_voucher(
277+
device_serial_number=device_serial_number,
278+
customer_id=customer_id,
279+
device_type=device_type,
280+
asin=asin,
281+
voucher=encrypted_voucher
282+
)

src/audible/auth.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ def refresh_website_cookies(
6565

6666
raw_cookies = resp_dict["response"]["tokens"]["cookies"]
6767
website_cookies = dict()
68-
for domain in raw_cookies:
69-
for cookie in raw_cookies[domain]:
68+
for cookies_domain in raw_cookies:
69+
for cookie in raw_cookies[cookies_domain]:
7070
website_cookies[cookie["Name"]] = cookie["Value"].replace(r'"',
7171
r'')
7272

0 commit comments

Comments
 (0)