Skip to content

Instantly share code, notes, and snippets.

@mkb79
Created June 2, 2022 09:11
Show Gist options
  • Save mkb79/2a3f0acb3a75d3cf82d0c85a29497d22 to your computer and use it in GitHub Desktop.
Save mkb79/2a3f0acb3a75d3cf82d0c85a29497d22 to your computer and use it in GitHub Desktop.
Goodreads: Get access token for Goodreads API (proof-of-concept)
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