Created
June 2, 2022 09:11
-
-
Save mkb79/2a3f0acb3a75d3cf82d0c85a29497d22 to your computer and use it in GitHub Desktop.
Goodreads: Get access token for Goodreads API (proof-of-concept)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import base64 | |
import gzip | |
import hashlib | |
import hmac | |
import json | |
import secrets | |
import uuid | |
from datetime import datetime | |
from io import BytesIO | |
from functools import partialmethod | |
from typing import Tuple, Union | |
import httpx | |
from pyaes import AESModeOfOperationCBC, Encrypter, Decrypter | |
USER_AGENT = "AmazonWebView/GoodreadsForIOS App/4.0.1/iOS/15.4.1/iPhone" | |
FRC_SIG_SALT: bytes = b"HmacSHA256" | |
FRC_AES_SALT: bytes = b"AES/CBC/PKCS7Padding" | |
class FrcCookieHelper: | |
def __init__(self, password: str) -> None: | |
self.password = password.encode() | |
def _get_key(self, salt: bytes) -> bytes: | |
return hashlib.pbkdf2_hmac("sha1", self.password, salt, 1000, 16) | |
get_signature_key = partialmethod(_get_key, FRC_SIG_SALT) | |
get_aes_key = partialmethod(_get_key, FRC_AES_SALT) | |
@staticmethod | |
def unpack(frc: str) -> Tuple[bytes, bytes, bytes]: | |
pad = (4 - len(frc) % 4) * "=" | |
frc = BytesIO(base64.b64decode(frc+pad)) | |
frc.seek(1) # the first byte is always 0, skip them | |
return frc.read(8), frc.read(16), frc.read() # sig, iv, data | |
@staticmethod | |
def pack(sig: bytes, iv: bytes, data: bytes) -> str: | |
frc = b"\x00" + sig[:8] + iv[:16] + data | |
frc = base64.b64encode(frc).strip(b"=") | |
return frc.decode() | |
def verify_signature(self, frc: str) -> bool: | |
key = self.get_signature_key() | |
sig, iv, data = self.unpack(frc) | |
new_signature = hmac.new(key, iv + data, hashlib.sha256).digest() | |
return sig == new_signature[:len(sig)] | |
def decrypt(self, frc: str, verify_signature: bool = True) -> bytes: | |
if verify_signature: | |
self.verify_signature(frc) | |
key = self.get_aes_key() | |
sig, iv, data = self.unpack(frc) | |
decrypter = Decrypter(AESModeOfOperationCBC(key, iv)) | |
decrypted = decrypter.feed(data) + decrypter.feed() | |
decompressed = gzip.decompress(decrypted) | |
return decompressed | |
def encrypt(self, data: Union[str, dict]) -> str: | |
if isinstance(data, dict): | |
data = json.dumps(data, indent=2, separators=(",", " : ")).replace("/", "\\/").encode() | |
compressed = BytesIO() | |
with gzip.GzipFile(fileobj=compressed, mode="wb", mtime=False) as f: | |
f.write(data) | |
compressed.seek(8) | |
compressed.write(b"\x00\x13") | |
compressed = compressed.getvalue() | |
key = self.get_aes_key() | |
iv = secrets.token_bytes(16) | |
encrypter = Encrypter(AESModeOfOperationCBC(key, iv)) | |
encrypted = encrypter.feed(compressed) + encrypter.feed() | |
key = self.get_signature_key() | |
signature = hmac.new(key, iv + encrypted, hashlib.sha256).digest() | |
packed = self.pack(signature, iv, encrypted) | |
return packed + len(packed) % 4 * "=" | |
def register(username, password): | |
url = "https://api.amazon.com/auth/register" | |
device_serial = secrets.token_hex(16).upper() | |
frc = { | |
"ApplicationVersion": "4.1", | |
"DeviceOSVersion": "iOS/15.5", | |
"ScreenWidthPixels": "428", | |
"TimeZone": "+02:00", | |
"ScreenHeightPixels": "926", | |
"ApplicationName": "Goodreads", | |
"DeviceJailbroken": False, | |
"DeviceLanguage": "en-DE", | |
"DeviceFingerprintTimestamp": round(datetime.utcnow().timestamp()) * 1000, | |
"ThirdPartyDeviceId": str(uuid.uuid4()).upper(), | |
"DeviceName": "iPhone", | |
"Carrier": "Vodafone.de" | |
} | |
frc = FrcCookieHelper(device_serial).encrypt(frc) | |
headers = { | |
"x-amzn-identity-auth-domain": "goodreads.com", | |
"User-Agent": USER_AGENT, | |
"Accept-Encoding": "gzip", | |
"Accept": "application/json", | |
"Accept-Language": "en-DE", | |
"Accept-Charset": "utf-8" | |
} | |
json_body = { | |
"requested_extensions": [ | |
"device_info", | |
"customer_info" | |
], | |
"cookies": { | |
"website_cookies": [], | |
"domain": ".goodreads.com" | |
}, | |
"registration_data": { | |
"domain": "Device", | |
"app_version": "4.1", | |
"device_type": "A3NWHXTQ4EBCZS", | |
"os_version": "15.5", | |
"device_serial": device_serial, | |
"device_model": "iPhone", | |
"app_name": "GoodreadsForIOS App", | |
"software_version": "1" | |
}, | |
"auth_data": { | |
"user_id_password": { | |
"user_id": username, | |
"password": password | |
} | |
}, | |
"user_context_map": { | |
"frc": frc | |
}, | |
"requested_token_type": [ | |
"bearer", | |
"mac_dms", | |
"website_cookies" | |
] | |
} | |
r = httpx.post(url, headers=headers, json=json_body) | |
return r | |
def deregister(access_token): | |
url = "https://api.amazon.com/auth/deregister" | |
json_body = {"deregister_all_existing_accounts": True} | |
headers = {"Authorization": f"Bearer {access_token}"} | |
r = httpx.post( | |
url, | |
json=json_body, | |
headers=headers | |
) | |
return r | |
def refresh_access_token(refresh_token): | |
url = "https://api.amazon.com/auth/token" | |
headers = { | |
"x-amzn-identity-auth-domain": "goodreads.com", | |
"User-Agent": USER_AGENT, | |
"Accept-Encoding": "gzip" | |
} | |
body = { | |
"app_name": "GoodreadsForIOS App", | |
"app_version": "4.0.1", | |
"di.sdk.version": "6.12.1", | |
"source_token": refresh_token, | |
"package_name": "com.goodreads.Goodreads", | |
"di.hw.version": "iPhone", | |
"platform": "iOS", | |
"requested_token_type": "access_token", | |
"source_token_type": "refresh_token", | |
"di.os.name": "iOS", | |
"di.os.version": "15.4.1", | |
"current_version": "6.12.1" | |
} | |
r = httpx.post(url, data=body, headers=headers) | |
return r | |
def exchange_cookies(refresh_token): | |
url = "https://api.amazon.com/ap/exchangetoken/cookies" | |
headers = { | |
"x-amzn-identity-auth-domain": "goodreads.com", | |
"User-Agent": USER_AGENT, | |
"Accept-Encoding": "gzip" | |
} | |
body = { | |
"openid.assoc_handle": "amzn_goodreads_web_na", | |
"app_name": "GoodreadsForIOS App", | |
"app_version": "4.0.1", | |
"di.sdk.version": "6.12.1", | |
"domain": ".goodreads.com", | |
"source_token": refresh_token, | |
"di.hw.version": "iPhone", | |
"cookies": "eyJjb29raWVzIjp7Ii5nb29kcmVhZHMuY29tIjpbXX19", | |
"requested_token_type": "auth_cookies", | |
"source_token_type": "refresh_token", | |
"di.os.name": "iOS", | |
"di.os.version": "15.4.1" | |
} | |
r = httpx.post(url, data=body, headers=headers) | |
return r |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment