Add files via upload
This commit is contained in:
parent
11e1c4b5ba
commit
cb5526a735
7 changed files with 3154 additions and 0 deletions
118
setup/customRSA.py
Normal file
118
setup/customRSA.py
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
'''
|
||||||
|
Use my own small RSA code so we don't have to include the huge
|
||||||
|
python3-rsa just for these small bits.
|
||||||
|
The original code used blinding and this one doesn't,
|
||||||
|
but we don't really care about side-channel attacks ...
|
||||||
|
'''
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
from Cryptodome.PublicKey import RSA
|
||||||
|
except ImportError:
|
||||||
|
# Some distros still ship this as Crypto
|
||||||
|
from Crypto.PublicKey import RSA
|
||||||
|
|
||||||
|
class CustomRSA:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def encrypt_for_adobe_signature(signing_key, message):
|
||||||
|
key = RSA.importKey(signing_key)
|
||||||
|
keylen = CustomRSA.byte_size(key.n)
|
||||||
|
padded = CustomRSA.pad_message(message, keylen)
|
||||||
|
payload = CustomRSA.transform_bytes2int(padded)
|
||||||
|
encrypted = CustomRSA.normal_encrypt(key, payload)
|
||||||
|
block = CustomRSA.transform_int2bytes(encrypted, keylen)
|
||||||
|
return bytearray(block)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def byte_size(number):
|
||||||
|
# type: (int) -> int
|
||||||
|
return (number.bit_length() + 7) // 8
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def pad_message(message, target_len):
|
||||||
|
# type: (bytes, int) -> bytes
|
||||||
|
|
||||||
|
# Padding always uses 0xFF
|
||||||
|
# Returns: 00 01 PADDING 00 MESSAGE
|
||||||
|
|
||||||
|
max_message_length = target_len - 11
|
||||||
|
message_length = len(message)
|
||||||
|
|
||||||
|
if message_length > max_message_length:
|
||||||
|
raise OverflowError("Message too long, has %d bytes but only space for %d" % (message_length, max_message_length))
|
||||||
|
|
||||||
|
padding_len = target_len - message_length - 3
|
||||||
|
|
||||||
|
ret = bytearray(b"".join([b"\x00\x01", padding_len * b"\xff", b"\x00"]))
|
||||||
|
ret.extend(bytes(message))
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def normal_encrypt(key, message):
|
||||||
|
|
||||||
|
if message < 0 or message > key.n:
|
||||||
|
raise ValueError("Invalid message")
|
||||||
|
|
||||||
|
encrypted = pow(message, key.d, key.n)
|
||||||
|
return encrypted
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def py2_int_to_bytes(value, length, big_endian = True):
|
||||||
|
result = []
|
||||||
|
|
||||||
|
for i in range(0, length):
|
||||||
|
result.append(value >> (i * 8) & 0xff)
|
||||||
|
|
||||||
|
if big_endian:
|
||||||
|
result.reverse()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def py2_bytes_to_int(bytes, big_endian = True):
|
||||||
|
# type: (bytes, bool) -> int
|
||||||
|
|
||||||
|
my_bytes = bytes
|
||||||
|
if not big_endian:
|
||||||
|
my_bytes.reverse()
|
||||||
|
|
||||||
|
result = 0
|
||||||
|
for b in my_bytes:
|
||||||
|
result = result * 256 + int(b)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def transform_bytes2int(raw_bytes):
|
||||||
|
# type: (bytes) -> int
|
||||||
|
|
||||||
|
if sys.version_info[0] >= 3:
|
||||||
|
return int.from_bytes(raw_bytes, "big", signed=False)
|
||||||
|
|
||||||
|
return CustomRSA.py2_bytes_to_int(raw_bytes, True)
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def transform_int2bytes(number, fill_size = 0):
|
||||||
|
# type: (int, int) -> bytes
|
||||||
|
|
||||||
|
if number < 0:
|
||||||
|
raise ValueError("Negative number")
|
||||||
|
|
||||||
|
size = None
|
||||||
|
|
||||||
|
if fill_size > 0:
|
||||||
|
size = fill_size
|
||||||
|
else:
|
||||||
|
size = max(1, CustomRSA.byte_size(number))
|
||||||
|
|
||||||
|
if sys.version_info[0] >= 3:
|
||||||
|
return number.to_bytes(size, "big")
|
||||||
|
|
||||||
|
return CustomRSA.py2_int_to_bytes(number, size, True)
|
130
setup/fulfill.py
Normal file
130
setup/fulfill.py
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
'''
|
||||||
|
This is an experimental Python version of libgourou.
|
||||||
|
'''
|
||||||
|
|
||||||
|
# pyright: reportUndefinedVariable=false
|
||||||
|
|
||||||
|
import os, time, shutil
|
||||||
|
|
||||||
|
import zipfile
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
from setup.libadobe import sendHTTPRequest_DL2FILE
|
||||||
|
from setup.libadobeFulfill import buildRights, fulfill
|
||||||
|
from setup.libpdf import patch_drm_into_pdf
|
||||||
|
|
||||||
|
FILE_DEVICEKEY = "devicesalt"
|
||||||
|
FILE_DEVICEXML = "device.xml"
|
||||||
|
FILE_ACTIVATIONXML = "activation.xml"
|
||||||
|
|
||||||
|
#######################################################################
|
||||||
|
|
||||||
|
|
||||||
|
def download(replyData):
|
||||||
|
# replyData: str
|
||||||
|
adobe_fulfill_response = etree.fromstring(replyData)
|
||||||
|
NSMAP = { "adept" : "http://ns.adobe.com/adept" }
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
adDC = lambda tag: '{%s}%s' % ('http://purl.org/dc/elements/1.1/', tag)
|
||||||
|
|
||||||
|
# print (replyData)
|
||||||
|
|
||||||
|
download_url = adobe_fulfill_response.find("./%s/%s/%s" % (adNS("fulfillmentResult"), adNS("resourceItemInfo"), adNS("src"))).text
|
||||||
|
resource_id = adobe_fulfill_response.find("./%s/%s/%s" % (adNS("fulfillmentResult"), adNS("resourceItemInfo"), adNS("resource"))).text
|
||||||
|
license_token_node = adobe_fulfill_response.find("./%s/%s/%s" % (adNS("fulfillmentResult"), adNS("resourceItemInfo"), adNS("licenseToken")))
|
||||||
|
|
||||||
|
rights_xml_str = buildRights(license_token_node)
|
||||||
|
|
||||||
|
if (rights_xml_str is None):
|
||||||
|
print("Building rights.xml failed!")
|
||||||
|
return False
|
||||||
|
|
||||||
|
book_name = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
metadata_node = adobe_fulfill_response.find("./%s/%s/%s" % (adNS("fulfillmentResult"), adNS("resourceItemInfo"), adNS("metadata")))
|
||||||
|
book_name = metadata_node.find("./%s" % (adDC("title"))).text
|
||||||
|
except:
|
||||||
|
book_name = "Book"
|
||||||
|
|
||||||
|
|
||||||
|
# Download eBook:
|
||||||
|
|
||||||
|
print(download_url)
|
||||||
|
filename_tmp = book_name + ".tmp"
|
||||||
|
|
||||||
|
dl_start_time = int(time.time() * 1000)
|
||||||
|
ret = sendHTTPRequest_DL2FILE(download_url, filename_tmp)
|
||||||
|
dl_end_time = int(time.time() * 1000)
|
||||||
|
print("Download took %d milliseconds" % (dl_end_time - dl_start_time))
|
||||||
|
|
||||||
|
if (ret != 200):
|
||||||
|
print("Download failed with error %d" % (ret))
|
||||||
|
return False
|
||||||
|
|
||||||
|
with open(filename_tmp, "rb") as f:
|
||||||
|
book_content = f.read(10)
|
||||||
|
|
||||||
|
filetype = ".bin"
|
||||||
|
|
||||||
|
if (book_content.startswith(b"PK")):
|
||||||
|
print("That's a ZIP file -> EPUB")
|
||||||
|
filetype = ".epub"
|
||||||
|
elif (book_content.startswith(b"%PDF")):
|
||||||
|
print("That's a PDF file")
|
||||||
|
filetype = ".pdf"
|
||||||
|
|
||||||
|
filename = book_name + filetype
|
||||||
|
shutil.move(filename_tmp, filename)
|
||||||
|
|
||||||
|
if filetype == ".epub":
|
||||||
|
# Store EPUB rights / encryption stuff
|
||||||
|
zf = zipfile.ZipFile(filename, "a")
|
||||||
|
zf.writestr("META-INF/rights.xml", rights_xml_str)
|
||||||
|
zf.close()
|
||||||
|
|
||||||
|
print("File successfully fulfilled")
|
||||||
|
return filename
|
||||||
|
|
||||||
|
elif filetype == ".pdf":
|
||||||
|
print("Successfully downloaded PDF, patching encryption ...")
|
||||||
|
|
||||||
|
adobe_fulfill_response = etree.fromstring(rights_xml_str)
|
||||||
|
NSMAP = { "adept" : "http://ns.adobe.com/adept" }
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
resource = adobe_fulfill_response.find("./%s/%s" % (adNS("licenseToken"), adNS("resource"))).text
|
||||||
|
|
||||||
|
os.rename(filename, "tmp_" + filename)
|
||||||
|
ret = patch_drm_into_pdf("tmp_" + filename, rights_xml_str, filename, resource)
|
||||||
|
os.remove("tmp_" + filename)
|
||||||
|
if (ret):
|
||||||
|
print("File successfully fulfilled")
|
||||||
|
return filename
|
||||||
|
else:
|
||||||
|
print("Errors occurred while patching " + filename)
|
||||||
|
return False
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("Error: Weird filetype")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def downloadFile(file="URLLink.acsm"):
|
||||||
|
|
||||||
|
print("Fulfilling book '" + file + "' ...")
|
||||||
|
success, replyData = fulfill(file)
|
||||||
|
if (success is False):
|
||||||
|
print("Hey, that didn't work!")
|
||||||
|
print(replyData)
|
||||||
|
else:
|
||||||
|
print("Downloading book '" + file + "' ...")
|
||||||
|
success = download(replyData)
|
||||||
|
if (success is False):
|
||||||
|
print("That didn't work!")
|
||||||
|
else:
|
||||||
|
return success
|
||||||
|
|
||||||
|
|
654
setup/libadobe.py
Normal file
654
setup/libadobe.py
Normal file
|
@ -0,0 +1,654 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
'''
|
||||||
|
Helper library with code needed for Adobe stuff.
|
||||||
|
'''
|
||||||
|
|
||||||
|
from uuid import getnode
|
||||||
|
import sys, os, hashlib, base64
|
||||||
|
import ssl
|
||||||
|
|
||||||
|
import urllib.request as ulib
|
||||||
|
import urllib.error as uliberror
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
try:
|
||||||
|
from Cryptodome import Random
|
||||||
|
from Cryptodome.Cipher import AES
|
||||||
|
from Cryptodome.Hash import SHA
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
# Some distros still ship Crypto
|
||||||
|
from Crypto import Random
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
from Crypto.Hash import SHA
|
||||||
|
|
||||||
|
|
||||||
|
#@@CALIBRE_COMPAT_CODE@@
|
||||||
|
|
||||||
|
|
||||||
|
from setup.customRSA import CustomRSA
|
||||||
|
|
||||||
|
from oscrypto import keys
|
||||||
|
from oscrypto.asymmetric import dump_certificate, dump_private_key
|
||||||
|
|
||||||
|
|
||||||
|
VAR_ACS_SERVER_HTTP = "http://adeactivate.adobe.com/adept"
|
||||||
|
VAR_ACS_SERVER_HTTPS = "https://adeactivate.adobe.com/adept"
|
||||||
|
|
||||||
|
FILE_DEVICEKEY = "devicesalt"
|
||||||
|
FILE_DEVICEXML = "device.xml"
|
||||||
|
FILE_ACTIVATIONXML = "activation.xml"
|
||||||
|
|
||||||
|
|
||||||
|
# Lists of different ADE "versions" we know about
|
||||||
|
VAR_VER_SUPP_CONFIG_NAMES = [ "ADE 1.7.2", "ADE 2.0.1", "ADE 3.0.1", "ADE 4.0.3", "ADE 4.5.10", "ADE 4.5.11" ]
|
||||||
|
VAR_VER_SUPP_VERSIONS = [ "ADE WIN 9,0,1131,27", "2.0.1.78765", "3.0.1.91394", "4.0.3.123281",
|
||||||
|
"com.adobe.adobedigitaleditions.exe v4.5.10.186048",
|
||||||
|
"com.adobe.adobedigitaleditions.exe v4.5.11.187303" ]
|
||||||
|
VAR_VER_HOBBES_VERSIONS = [ "9.0.1131.27", "9.3.58046", "10.0.85385", "12.0.123217", "12.5.4.186049", "12.5.4.187298" ]
|
||||||
|
VAR_VER_OS_IDENTIFIERS = [ "Windows Vista", "Windows Vista", "Windows 8", "Windows 8", "Windows 8", "Windows 8" ]
|
||||||
|
|
||||||
|
|
||||||
|
# "Missing" versions:
|
||||||
|
# 1.7.1, 2.0, 3.0, 4.0, 4.0.1, 4.0.2, 4.5 to 4.5.9
|
||||||
|
# 4.5.7.179634
|
||||||
|
|
||||||
|
# This is a list of ALL versions we know (and can potentially use if present in a config file).
|
||||||
|
# Must have the same length / size as the four lists above.
|
||||||
|
VAR_VER_BUILD_IDS = [ 1131, 78765, 91394, 123281, 186048, 187303 ]
|
||||||
|
# Build ID 185749 also exists, that's a different (older) variant of 4.5.10.
|
||||||
|
|
||||||
|
# This is a list of versions that can be used for new authorizations:
|
||||||
|
VAR_VER_ALLOWED_BUILD_IDS_AUTHORIZE = [ 78765, 91394, 123281, 187303 ]
|
||||||
|
|
||||||
|
# This is a list of versions to be displayed in the version changer.
|
||||||
|
VAR_VER_ALLOWED_BUILD_IDS_SWITCH_TO = [ 1131, 78765, 91394, 123281, 187303 ]
|
||||||
|
|
||||||
|
# Versions >= this one are using HTTPS
|
||||||
|
# According to changelogs, this is implemented as of ADE 4.0.1 - no idea what build ID that is.
|
||||||
|
VAR_VER_NEED_HTTPS_BUILD_ID_LIMIT = 123281
|
||||||
|
|
||||||
|
# Versions >= this are using a different order for the XML elements in a FulfillmentNotification.
|
||||||
|
# This doesn't matter for fulfillment at all, but I want to emulate ADE as accurately as possible.
|
||||||
|
# Implemented as of ADE 4.0.0, no idea what exact build number that is.
|
||||||
|
VAR_VER_USE_DIFFERENT_NOTIFICATION_XML_ORDER = 123281
|
||||||
|
|
||||||
|
# Default build ID to use - ADE 2.0.1
|
||||||
|
VAR_VER_DEFAULT_BUILD_ID = 78765
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def are_ade_version_lists_valid():
|
||||||
|
# These five lists MUST all have the same amount of elements.
|
||||||
|
# Otherwise that will cause all kinds of issues.
|
||||||
|
|
||||||
|
fail = False
|
||||||
|
if len(VAR_VER_SUPP_CONFIG_NAMES) != len(VAR_VER_SUPP_VERSIONS):
|
||||||
|
fail = True
|
||||||
|
if len(VAR_VER_SUPP_CONFIG_NAMES) != len(VAR_VER_HOBBES_VERSIONS):
|
||||||
|
fail = True
|
||||||
|
if len(VAR_VER_SUPP_CONFIG_NAMES) != len(VAR_VER_OS_IDENTIFIERS):
|
||||||
|
fail = True
|
||||||
|
if len(VAR_VER_SUPP_CONFIG_NAMES) != len(VAR_VER_BUILD_IDS):
|
||||||
|
fail = True
|
||||||
|
|
||||||
|
if fail:
|
||||||
|
print("Internal error in ACSM Input: Mismatched version list lenghts.")
|
||||||
|
print("This should never happen, please open a bug report.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
devkey_bytes = None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def get_devkey_path():
|
||||||
|
global FILE_DEVICEKEY
|
||||||
|
return FILE_DEVICEKEY
|
||||||
|
def get_device_path():
|
||||||
|
global FILE_DEVICEXML
|
||||||
|
return FILE_DEVICEXML
|
||||||
|
def get_activation_xml_path():
|
||||||
|
global FILE_ACTIVATIONXML
|
||||||
|
return FILE_ACTIVATIONXML
|
||||||
|
|
||||||
|
|
||||||
|
def update_account_path(folder_path):
|
||||||
|
# type: (str) -> None
|
||||||
|
|
||||||
|
global FILE_DEVICEKEY, FILE_DEVICEXML, FILE_ACTIVATIONXML
|
||||||
|
|
||||||
|
FILE_DEVICEKEY = os.path.join(folder_path, "devicesalt")
|
||||||
|
FILE_DEVICEXML = os.path.join(folder_path, "device.xml")
|
||||||
|
FILE_ACTIVATIONXML = os.path.join(folder_path, "activation.xml")
|
||||||
|
|
||||||
|
|
||||||
|
def createDeviceKeyFile():
|
||||||
|
# Original implementation: Device::createDeviceKeyFile()
|
||||||
|
|
||||||
|
DEVICE_KEY_SIZE = 16
|
||||||
|
global devkey_bytes
|
||||||
|
devkey_bytes = Random.get_random_bytes(DEVICE_KEY_SIZE)
|
||||||
|
|
||||||
|
f = open(FILE_DEVICEKEY, "wb")
|
||||||
|
f.write(devkey_bytes)
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
def int_to_bytes(value, length, big_endian = True):
|
||||||
|
# Helper function for Python2 only (big endian)
|
||||||
|
# Python3 uses int.to_bytes()
|
||||||
|
result = []
|
||||||
|
|
||||||
|
for i in range(0, length):
|
||||||
|
result.append(value >> (i * 8) & 0xff)
|
||||||
|
|
||||||
|
if big_endian:
|
||||||
|
result.reverse()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_mac_address():
|
||||||
|
mac1 = getnode()
|
||||||
|
mac2 = getnode()
|
||||||
|
if (mac1 != mac2) or ((mac1 >> 40) % 2):
|
||||||
|
if sys.version_info[0] >= 3:
|
||||||
|
return bytes([1, 2, 3, 4, 5, 0])
|
||||||
|
else:
|
||||||
|
return bytearray([1, 2, 3, 4, 5, 0])
|
||||||
|
|
||||||
|
if sys.version_info[0] >= 3:
|
||||||
|
return mac1.to_bytes(6, byteorder='big')
|
||||||
|
|
||||||
|
return int_to_bytes(mac1, 6)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def makeSerial(random):
|
||||||
|
# type: (bool) -> str
|
||||||
|
|
||||||
|
# Original implementation: std::string Device::makeSerial(bool random)
|
||||||
|
|
||||||
|
# It doesn't look like this implementation results in the same fingerprint Adobe is using in ADE.
|
||||||
|
# Given that Adobe only ever sees the SHA1 hash of this value, that probably doesn't matter.
|
||||||
|
|
||||||
|
sha_out = None
|
||||||
|
|
||||||
|
if not random:
|
||||||
|
try:
|
||||||
|
# Linux
|
||||||
|
uid = os.getuid()
|
||||||
|
import pwd
|
||||||
|
username = pwd.getpwuid(uid).pw_name.encode("utf-8").decode("latin-1")
|
||||||
|
except:
|
||||||
|
# Windows
|
||||||
|
uid = 1000
|
||||||
|
try:
|
||||||
|
username = os.getlogin().encode("utf-8").decode("latin-1")
|
||||||
|
except:
|
||||||
|
import getpass
|
||||||
|
username = getpass.getuser().encode("utf-8").decode("latin-1")
|
||||||
|
|
||||||
|
mac_address = get_mac_address()
|
||||||
|
|
||||||
|
dataToHash = "%d:%s:%02x:%02x:%02x:%02x:%02x:%02x\x00" % (uid, username,
|
||||||
|
mac_address[0], mac_address[1], mac_address[2],
|
||||||
|
mac_address[3], mac_address[4], mac_address[5])
|
||||||
|
|
||||||
|
sha_out = hashlib.sha1(dataToHash.encode('latin-1')).hexdigest().lower()
|
||||||
|
else:
|
||||||
|
import binascii
|
||||||
|
sha_out = binascii.hexlify(Random.get_random_bytes(20)).lower()
|
||||||
|
|
||||||
|
return sha_out
|
||||||
|
|
||||||
|
def makeFingerprint(serial):
|
||||||
|
# type: (str) -> str
|
||||||
|
|
||||||
|
# Original implementation: std::string Device::makeFingerprint(const std::string& serial)
|
||||||
|
# base64(sha1(serial + privateKey))
|
||||||
|
# Fingerprint must be 20 bytes or less.
|
||||||
|
|
||||||
|
global devkey_bytes
|
||||||
|
if devkey_bytes is None:
|
||||||
|
f = open(FILE_DEVICEKEY, "rb")
|
||||||
|
devkey_bytes = f.read()
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
str_to_hash = serial.decode('latin-1') + devkey_bytes.decode('latin-1')
|
||||||
|
hashed_str = hashlib.sha1(str_to_hash.encode('latin-1')).digest()
|
||||||
|
b64str = base64.b64encode(hashed_str)
|
||||||
|
|
||||||
|
return b64str
|
||||||
|
|
||||||
|
|
||||||
|
############################################## HTTP stuff:
|
||||||
|
|
||||||
|
def sendHTTPRequest_DL2FILE(URL, outputfile):
|
||||||
|
# type: (str, str) -> int
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Accept": "*/*",
|
||||||
|
"User-Agent": "book2png",
|
||||||
|
# MacOS uses different User-Agent. Good thing we're emulating a Windows client.
|
||||||
|
}
|
||||||
|
req = ulib.Request(url=URL, headers=headers)
|
||||||
|
handler = ulib.urlopen(req)
|
||||||
|
|
||||||
|
chunksize = 16 * 1024
|
||||||
|
|
||||||
|
ret_code = handler.getcode()
|
||||||
|
|
||||||
|
|
||||||
|
loc = None
|
||||||
|
try:
|
||||||
|
loc = req.headers.get("Location")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if loc is not None:
|
||||||
|
return sendHTTPRequest_DL2FILE(loc)
|
||||||
|
|
||||||
|
if ret_code != 200:
|
||||||
|
return ret_code
|
||||||
|
|
||||||
|
with open(outputfile, "wb") as f:
|
||||||
|
while True:
|
||||||
|
chunk = handler.read(chunksize)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
f.write(chunk)
|
||||||
|
|
||||||
|
return 200
|
||||||
|
|
||||||
|
def sendHTTPRequest_getSimple(URL):
|
||||||
|
# type: (str) -> str
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Accept": "*/*",
|
||||||
|
"User-Agent": "book2png",
|
||||||
|
# MacOS uses different User-Agent. Good thing we're emulating a Windows client.
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ignore SSL:
|
||||||
|
# It appears as if lots of book distributors have either invalid or expired certs ...
|
||||||
|
# No idea how Adobe handles that (pinning?), but we can just ignore SSL errors and continue anyways.
|
||||||
|
# Not the best solution, but it works.
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
ctx.check_hostname = False
|
||||||
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
req = ulib.Request(url=URL, headers=headers)
|
||||||
|
handler = ulib.urlopen(req, context=ctx)
|
||||||
|
|
||||||
|
content = handler.read()
|
||||||
|
|
||||||
|
loc = None
|
||||||
|
try:
|
||||||
|
loc = req.headers.get("Location")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if loc is not None:
|
||||||
|
return sendHTTPRequest_getSimple(loc)
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
def sendPOSTHTTPRequest(URL, document, type, returnRC = False):
|
||||||
|
# type: (str, bytes, str, bool) -> str
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Accept": "*/*",
|
||||||
|
"User-Agent": "book2png",
|
||||||
|
# MacOS uses different User-Agent. Good thing we're emulating a Windows client.
|
||||||
|
"Content-Type": type
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ignore SSL:
|
||||||
|
# It appears as if lots of book distributors have either invalid or expired certs ...
|
||||||
|
# No idea how Adobe handles that (pinning?), but we can just ignore SSL errors and continue anyways.
|
||||||
|
# Not the best solution, but it works.
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
ctx.check_hostname = False
|
||||||
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
# Make sure URL has a protocol
|
||||||
|
# Some vendors (see issue #22) apparently don't include "http://" in some of their URLs.
|
||||||
|
# Python returns an error when it encounters such a URL, so just add that prefix if it's not present.
|
||||||
|
|
||||||
|
if not "://" in URL:
|
||||||
|
print("Provider is using malformed URL %s, fixing." % (URL))
|
||||||
|
URL = "http://" + URL
|
||||||
|
|
||||||
|
req = ulib.Request(url=URL, headers=headers, data=document)
|
||||||
|
try:
|
||||||
|
handler = ulib.urlopen(req, context=ctx)
|
||||||
|
except uliberror.HTTPError as err:
|
||||||
|
# This happens with HTTP 500 and related errors.
|
||||||
|
print("Post request caused HTTPError %d" % (err.code))
|
||||||
|
if returnRC:
|
||||||
|
return err.code, "Post request caused HTTPException"
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
except uliberror.URLError as err:
|
||||||
|
# This happens if the hostname cannot be resolved.
|
||||||
|
print("Post request failed with URLError")
|
||||||
|
if returnRC:
|
||||||
|
return 900, "Post request failed with URLError"
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
ret_code = handler.getcode()
|
||||||
|
if (ret_code == 204 and returnRC):
|
||||||
|
return 204, ""
|
||||||
|
if (ret_code != 200):
|
||||||
|
print("Post request returned something other than 200 - returned %d" % (ret_code))
|
||||||
|
|
||||||
|
content = handler.read()
|
||||||
|
|
||||||
|
loc = None
|
||||||
|
try:
|
||||||
|
loc = req.headers.get("Location")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if loc is not None:
|
||||||
|
return sendPOSTHTTPRequest(loc, document, type, returnRC)
|
||||||
|
|
||||||
|
if returnRC:
|
||||||
|
return ret_code, content
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def sendHTTPRequest(URL):
|
||||||
|
# type: (str) -> str
|
||||||
|
return sendHTTPRequest_getSimple(URL)
|
||||||
|
|
||||||
|
|
||||||
|
def sendRequestDocu(document, URL):
|
||||||
|
# type: (str, str) -> str
|
||||||
|
return sendPOSTHTTPRequest(URL, document.encode("utf-8"), "application/vnd.adobe.adept+xml", False)
|
||||||
|
|
||||||
|
def sendRequestDocuRC(document, URL):
|
||||||
|
# type: (str, str) -> str
|
||||||
|
return sendPOSTHTTPRequest(URL, document.encode("utf-8"), "application/vnd.adobe.adept+xml", True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
######### Encryption and signing ###################
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_with_device_key(data):
|
||||||
|
|
||||||
|
data = bytearray(data)
|
||||||
|
|
||||||
|
global devkey_bytes
|
||||||
|
if devkey_bytes is None:
|
||||||
|
f = open(FILE_DEVICEKEY, "rb")
|
||||||
|
devkey_bytes = f.read()
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
remain = 16
|
||||||
|
if (len(data) % 16):
|
||||||
|
remain = 16 - (len(data) % 16)
|
||||||
|
|
||||||
|
for _ in range(remain):
|
||||||
|
data.append(remain)
|
||||||
|
|
||||||
|
data = bytes(data)
|
||||||
|
|
||||||
|
|
||||||
|
iv = Random.get_random_bytes(16)
|
||||||
|
cip = AES.new(devkey_bytes, AES.MODE_CBC, iv)
|
||||||
|
encrypted = cip.encrypt(data)
|
||||||
|
|
||||||
|
res = iv + encrypted
|
||||||
|
return res
|
||||||
|
|
||||||
|
def decrypt_with_device_key(data):
|
||||||
|
|
||||||
|
if isinstance(data, str):
|
||||||
|
# Python2
|
||||||
|
data = bytes(data)
|
||||||
|
|
||||||
|
global devkey_bytes
|
||||||
|
if devkey_bytes is None:
|
||||||
|
f = open(FILE_DEVICEKEY, "rb")
|
||||||
|
devkey_bytes = f.read()
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
cip = AES.new(devkey_bytes, AES.MODE_CBC, data[:16])
|
||||||
|
decrypted = bytearray(cip.decrypt(data[16:]))
|
||||||
|
|
||||||
|
# Remove padding
|
||||||
|
decrypted = decrypted[:-decrypted[-1]]
|
||||||
|
|
||||||
|
return decrypted
|
||||||
|
|
||||||
|
|
||||||
|
def addNonce():
|
||||||
|
|
||||||
|
# TODO: Update nonce calculation
|
||||||
|
# Currently, the plugin always uses the current time, and the counter (tmp) is always 0.
|
||||||
|
# What Adobe does instead is save the current time on program start, then increase tmp
|
||||||
|
# every time a Nonce is needed.
|
||||||
|
|
||||||
|
dt = datetime.utcnow()
|
||||||
|
sec = (dt - datetime(1970,1,1)).total_seconds()
|
||||||
|
Ntime = int(sec * 1000)
|
||||||
|
# Ntime is now milliseconds since 1970
|
||||||
|
|
||||||
|
# Unixtime to gregorian timestamp
|
||||||
|
Ntime += 62167219200000
|
||||||
|
|
||||||
|
# Something is fishy with this tmp value. It usually is 0 in ADE, but not always.
|
||||||
|
# I haven't yet figured out what it means ...
|
||||||
|
tmp = 0
|
||||||
|
|
||||||
|
if sys.version_info[0] >= 3:
|
||||||
|
final = bytearray(Ntime.to_bytes(8, 'little'))
|
||||||
|
final.extend(tmp.to_bytes(4, 'little'))
|
||||||
|
else:
|
||||||
|
final = bytearray(int_to_bytes(Ntime, 8, False))
|
||||||
|
final.extend(int_to_bytes(tmp, 4, True))
|
||||||
|
|
||||||
|
|
||||||
|
ret = ""
|
||||||
|
|
||||||
|
ret += "<adept:nonce>%s</adept:nonce>" % (base64.b64encode(final).decode("utf-8"))
|
||||||
|
|
||||||
|
m10m = dt + timedelta(minutes=10)
|
||||||
|
m10m_str = m10m.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|
||||||
|
ret += "<adept:expiration>%s</adept:expiration>" % (m10m_str)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def get_cert_from_pkcs12(_pkcs12, _key):
|
||||||
|
|
||||||
|
_, cert, _ = keys.parse_pkcs12(_pkcs12, _key)
|
||||||
|
return dump_certificate(cert, encoding="der")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def sign_node(node):
|
||||||
|
|
||||||
|
sha_hash = hash_node(node)
|
||||||
|
sha_hash = sha_hash.digest()
|
||||||
|
|
||||||
|
# print("Hash is " + sha_hash.hex())
|
||||||
|
|
||||||
|
global devkey_bytes
|
||||||
|
global pkcs12
|
||||||
|
|
||||||
|
if devkey_bytes is None:
|
||||||
|
f = open(FILE_DEVICEKEY, "rb")
|
||||||
|
devkey_bytes = f.read()
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
# Get private key
|
||||||
|
|
||||||
|
try:
|
||||||
|
activationxml = etree.parse(FILE_ACTIVATIONXML)
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
pkcs12 = activationxml.find("./%s/%s" % (adNS("credentials"), adNS("pkcs12"))).text
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
my_pkcs12 = base64.b64decode(pkcs12)
|
||||||
|
my_priv_key, _, _ = keys.parse_pkcs12(my_pkcs12, base64.b64encode(devkey_bytes))
|
||||||
|
my_priv_key = dump_private_key(my_priv_key, None, "der")
|
||||||
|
|
||||||
|
# textbook RSA with that private key
|
||||||
|
|
||||||
|
block = CustomRSA.encrypt_for_adobe_signature(my_priv_key, sha_hash)
|
||||||
|
signature = base64.b64encode(block).decode()
|
||||||
|
|
||||||
|
# Debug
|
||||||
|
# print("sig is %s\n" % block.hex())
|
||||||
|
|
||||||
|
return signature
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def hash_node(node):
|
||||||
|
|
||||||
|
hash_ctx = SHA.new()
|
||||||
|
hash_node_ctx(node, hash_ctx)
|
||||||
|
return hash_ctx
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ASN_NONE = 0
|
||||||
|
ASN_NS_TAG = 1 # aka "BEGIN_ELEMENT"
|
||||||
|
ASN_CHILD = 2 # aka "END_ATTRIBUTES"
|
||||||
|
ASN_END_TAG = 3 # aka "END_ELEMENT"
|
||||||
|
ASN_TEXT = 4 # aka "TEXT_NODE"
|
||||||
|
ASN_ATTRIBUTE = 5 # aka "ATTRIBUTE"
|
||||||
|
|
||||||
|
debug = False
|
||||||
|
|
||||||
|
def hash_node_ctx(node, hash_ctx):
|
||||||
|
|
||||||
|
qtag = etree.QName(node.tag)
|
||||||
|
|
||||||
|
if (qtag.localname == "hmac" or qtag.localname == "signature"):
|
||||||
|
if (qtag.namespace == "http://ns.adobe.com/adept"):
|
||||||
|
# Adobe HMAC and signature are not hashed
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
print("Warning: Found hmac or signature node in unexpected namespace " + qtag.namespace)
|
||||||
|
|
||||||
|
hash_do_append_tag(hash_ctx, ASN_NS_TAG)
|
||||||
|
|
||||||
|
if qtag.namespace is None:
|
||||||
|
hash_do_append_string(hash_ctx, "")
|
||||||
|
else:
|
||||||
|
hash_do_append_string(hash_ctx, qtag.namespace)
|
||||||
|
hash_do_append_string(hash_ctx, qtag.localname)
|
||||||
|
|
||||||
|
|
||||||
|
attrKeys = node.keys()
|
||||||
|
|
||||||
|
# Attributes need to be sorted
|
||||||
|
attrKeys.sort()
|
||||||
|
# TODO Implement UTF-8 bytewise sorting:
|
||||||
|
# "Attributes are sorted first by their namespaces and
|
||||||
|
# then by their names; sorting is done bytewise on UTF-8
|
||||||
|
# representations."
|
||||||
|
|
||||||
|
for attribute in attrKeys:
|
||||||
|
# Hash all the attributes
|
||||||
|
hash_do_append_tag(hash_ctx, ASN_ATTRIBUTE)
|
||||||
|
|
||||||
|
# Check for element namespace and hash that, if present:
|
||||||
|
q_attribute = etree.QName(attribute)
|
||||||
|
|
||||||
|
# Hash element namespace (usually "")
|
||||||
|
# If namespace is none, use "". Else, use namespace.
|
||||||
|
hash_do_append_string(hash_ctx, "" if q_attribute.namespace is None else q_attribute.namespace)
|
||||||
|
|
||||||
|
# Hash (local) name and value
|
||||||
|
hash_do_append_string(hash_ctx, q_attribute.localname)
|
||||||
|
hash_do_append_string(hash_ctx, node.get(attribute))
|
||||||
|
|
||||||
|
hash_do_append_tag(hash_ctx, ASN_CHILD)
|
||||||
|
|
||||||
|
if (node.text is not None):
|
||||||
|
# If there's raw text, hash that.
|
||||||
|
|
||||||
|
# This code block used to just be the following:
|
||||||
|
# hash_do_append_tag(hash_ctx, ASN_TEXT)
|
||||||
|
# hash_do_append_string(hash_ctx, node.text.strip())
|
||||||
|
# though that only works with text nodes < 0x7fff.
|
||||||
|
# While I doubt we'll ever encounter text nodes larger than 32k in
|
||||||
|
# this application, I want to implement the spec correctly.
|
||||||
|
# So there's a loop going over the text, hashing 32k chunks.
|
||||||
|
|
||||||
|
text = node.text.strip()
|
||||||
|
textlen = len(text)
|
||||||
|
if textlen > 0:
|
||||||
|
done = 0
|
||||||
|
remaining = 0
|
||||||
|
while True:
|
||||||
|
remaining = textlen - done
|
||||||
|
if remaining > 0x7fff:
|
||||||
|
#print("Warning: Why are we hashing a node larger than 32k?")
|
||||||
|
remaining = 0x7fff
|
||||||
|
|
||||||
|
hash_do_append_tag(hash_ctx, ASN_TEXT)
|
||||||
|
hash_do_append_string(hash_ctx, text[done:done+remaining])
|
||||||
|
|
||||||
|
done += remaining
|
||||||
|
if done >= textlen:
|
||||||
|
break
|
||||||
|
|
||||||
|
for child in node:
|
||||||
|
# If there's child nodes, hash these as well.
|
||||||
|
hash_node_ctx(child, hash_ctx)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
hash_do_append_tag(hash_ctx, ASN_END_TAG)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def hash_do_append_string(hash_ctx, string):
|
||||||
|
# type: (SHA.SHA1Hash, str) -> None
|
||||||
|
|
||||||
|
if sys.version_info[0] >= 3:
|
||||||
|
str_bytes = bytes(string, encoding="utf-8")
|
||||||
|
else:
|
||||||
|
str_bytes = bytes(string)
|
||||||
|
|
||||||
|
length = len(str_bytes)
|
||||||
|
len_upper = int(length / 256)
|
||||||
|
len_lower = int(length & 0xFF)
|
||||||
|
|
||||||
|
hash_do_append_raw_bytes(hash_ctx, [len_upper, len_lower])
|
||||||
|
hash_do_append_raw_bytes(hash_ctx, str_bytes)
|
||||||
|
|
||||||
|
def hash_do_append_tag(hash_ctx, tag):
|
||||||
|
# type: (SHA.SHA1Hash, int) -> None
|
||||||
|
|
||||||
|
if (tag > 5):
|
||||||
|
return
|
||||||
|
|
||||||
|
hash_do_append_raw_bytes(hash_ctx, [tag])
|
||||||
|
|
||||||
|
def hash_do_append_raw_bytes(hash_ctx, data):
|
||||||
|
# type: (SHA.SHA1Hash, bytes) -> None
|
||||||
|
hash_ctx.update(bytearray(data))
|
910
setup/libadobeAccount.py
Normal file
910
setup/libadobeAccount.py
Normal file
|
@ -0,0 +1,910 @@
|
||||||
|
from lxml import etree
|
||||||
|
import base64
|
||||||
|
import locale, platform
|
||||||
|
|
||||||
|
try:
|
||||||
|
from Cryptodome.PublicKey import RSA
|
||||||
|
from Cryptodome.Util.asn1 import DerSequence
|
||||||
|
from Cryptodome.Cipher import PKCS1_v1_5
|
||||||
|
except ImportError:
|
||||||
|
# Some distros ship this as Crypto still.
|
||||||
|
from Crypto.PublicKey import RSA
|
||||||
|
from Crypto.Util.asn1 import DerSequence
|
||||||
|
from Crypto.Cipher import PKCS1_v1_5
|
||||||
|
|
||||||
|
#@@CALIBRE_COMPAT_CODE@@
|
||||||
|
|
||||||
|
|
||||||
|
from setup.libadobe import addNonce, sign_node, sendRequestDocu, sendHTTPRequest
|
||||||
|
from setup.libadobe import makeFingerprint, makeSerial, encrypt_with_device_key, decrypt_with_device_key
|
||||||
|
from setup.libadobe import get_devkey_path, get_device_path, get_activation_xml_path
|
||||||
|
from setup.libadobe import VAR_VER_SUPP_CONFIG_NAMES, VAR_VER_HOBBES_VERSIONS, VAR_VER_OS_IDENTIFIERS
|
||||||
|
from setup.libadobe import VAR_VER_ALLOWED_BUILD_IDS_SWITCH_TO, VAR_VER_SUPP_VERSIONS, VAR_ACS_SERVER_HTTP
|
||||||
|
from setup.libadobe import VAR_ACS_SERVER_HTTPS, VAR_VER_BUILD_IDS, VAR_VER_NEED_HTTPS_BUILD_ID_LIMIT, VAR_VER_ALLOWED_BUILD_IDS_AUTHORIZE
|
||||||
|
|
||||||
|
|
||||||
|
def createDeviceFile(randomSerial, useVersionIndex = 0):
|
||||||
|
# type: (bool, int) -> bool
|
||||||
|
|
||||||
|
# Original implementation: Device::createDeviceFile(const std::string& hobbes, bool randomSerial)
|
||||||
|
|
||||||
|
if useVersionIndex >= len(VAR_VER_SUPP_CONFIG_NAMES):
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
build_id = VAR_VER_BUILD_IDS[useVersionIndex]
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if build_id not in VAR_VER_ALLOWED_BUILD_IDS_AUTHORIZE:
|
||||||
|
# ADE 1.7.2 or another version that authorization is disabled for
|
||||||
|
return False
|
||||||
|
|
||||||
|
serial = makeSerial(randomSerial)
|
||||||
|
fingerprint = makeFingerprint(serial)
|
||||||
|
|
||||||
|
NSMAP = { "adept" : "http://ns.adobe.com/adept" }
|
||||||
|
etree.register_namespace("adept", NSMAP["adept"])
|
||||||
|
|
||||||
|
root = etree.Element(etree.QName(NSMAP["adept"], "deviceInfo"))
|
||||||
|
etree.SubElement(root, etree.QName(NSMAP["adept"], "deviceType")).text = "standalone"
|
||||||
|
|
||||||
|
# These three elements are not supposed to be sent to Adobe:
|
||||||
|
etree.SubElement(root, etree.QName(NSMAP["adept"], "deviceClass")).text = "Desktop"
|
||||||
|
etree.SubElement(root, etree.QName(NSMAP["adept"], "deviceSerial")).text = serial
|
||||||
|
etree.SubElement(root, etree.QName(NSMAP["adept"], "deviceName")).text = platform.uname()[1]
|
||||||
|
# ##
|
||||||
|
|
||||||
|
atr_ver = etree.SubElement(root, etree.QName(NSMAP["adept"], "version"))
|
||||||
|
atr_ver.set("name", "hobbes")
|
||||||
|
atr_ver.set("value", VAR_VER_HOBBES_VERSIONS[useVersionIndex])
|
||||||
|
|
||||||
|
atr_ver2 = etree.SubElement(root, etree.QName(NSMAP["adept"], "version"))
|
||||||
|
atr_ver2.set("name", "clientOS")
|
||||||
|
|
||||||
|
# This used to contain code to actually read the user's operating system.
|
||||||
|
# That's probably not a good idea because then Adobe sees a bunch of requests from "Linux"
|
||||||
|
#atr_ver2.set("value", platform.system() + " " + platform.release())
|
||||||
|
atr_ver2.set("value", VAR_VER_OS_IDENTIFIERS[useVersionIndex])
|
||||||
|
|
||||||
|
atr_ver3 = etree.SubElement(root, etree.QName(NSMAP["adept"], "version"))
|
||||||
|
atr_ver3.set("name", "clientLocale")
|
||||||
|
|
||||||
|
language = None
|
||||||
|
try:
|
||||||
|
language = locale.getdefaultlocale()[0].split('_')[0]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if language is None or language == "":
|
||||||
|
# Can sometimes happen on MacOS with default English language
|
||||||
|
language = "en"
|
||||||
|
|
||||||
|
atr_ver3.set("value", language)
|
||||||
|
|
||||||
|
etree.SubElement(root, etree.QName(NSMAP["adept"], "fingerprint")).text = fingerprint
|
||||||
|
|
||||||
|
f = open(get_device_path(), "w")
|
||||||
|
f.write("<?xml version=\"1.0\"?>\n")
|
||||||
|
f.write(etree.tostring(root, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("latin-1"))
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def getAuthMethodsAndCert():
|
||||||
|
# Queries the /AuthenticationServiceInfo endpoint to get a list
|
||||||
|
# of available ID providers.
|
||||||
|
# Returns a list of providers, and the login certificate.
|
||||||
|
|
||||||
|
# The login certificate stuff would usually be handled elsewhere,
|
||||||
|
# but that would require another request to Adobe's servers
|
||||||
|
# which is not what we want (as ADE only performs one request, too),
|
||||||
|
# so we need to store this cert.
|
||||||
|
|
||||||
|
# If you DO call this method before calling createUser,
|
||||||
|
# it is your responsibility to pass the authCert returned by this function
|
||||||
|
# to the createUser function call.
|
||||||
|
# Otherwise the plugin will not look 100% like ADE to Adobe.
|
||||||
|
|
||||||
|
authenticationURL = VAR_ACS_SERVER_HTTP + "/AuthenticationServiceInfo"
|
||||||
|
response2 = sendHTTPRequest(authenticationURL)
|
||||||
|
|
||||||
|
adobe_response_xml2 = etree.fromstring(response2)
|
||||||
|
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
|
||||||
|
try:
|
||||||
|
authCert = None
|
||||||
|
authCert = adobe_response_xml2.find("./%s" % (adNS("certificate"))).text
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Get sign-in methods.
|
||||||
|
sign_in_methods = adobe_response_xml2.findall("./%s/%s" % (adNS("signInMethods"), adNS("signInMethod")))
|
||||||
|
|
||||||
|
aid_ids = []
|
||||||
|
aid_names = []
|
||||||
|
|
||||||
|
for method in sign_in_methods:
|
||||||
|
mid = method.get("method", None)
|
||||||
|
txt = method.text
|
||||||
|
|
||||||
|
if mid != "anonymous":
|
||||||
|
aid_ids.append(mid)
|
||||||
|
aid_names.append(txt)
|
||||||
|
|
||||||
|
return [aid_ids, aid_names], authCert
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def createUser(useVersionIndex = 0, authCert = None):
|
||||||
|
|
||||||
|
if useVersionIndex >= len(VAR_VER_SUPP_CONFIG_NAMES):
|
||||||
|
return False, "Invalid Version index", [[], []]
|
||||||
|
|
||||||
|
NSMAP = { "adept" : "http://ns.adobe.com/adept" }
|
||||||
|
|
||||||
|
root = etree.Element("activationInfo")
|
||||||
|
root.set("xmlns", NSMAP["adept"])
|
||||||
|
|
||||||
|
etree.register_namespace("adept", NSMAP["adept"])
|
||||||
|
|
||||||
|
activationServiceInfo = etree.SubElement(root, etree.QName(NSMAP["adept"], "activationServiceInfo"))
|
||||||
|
|
||||||
|
useHTTPS = False
|
||||||
|
if VAR_VER_BUILD_IDS[useVersionIndex] >= VAR_VER_NEED_HTTPS_BUILD_ID_LIMIT:
|
||||||
|
useHTTPS = True
|
||||||
|
|
||||||
|
|
||||||
|
if useHTTPS:
|
||||||
|
# ADE 4.X uses HTTPS
|
||||||
|
activationURL = VAR_ACS_SERVER_HTTPS + "/ActivationServiceInfo"
|
||||||
|
else:
|
||||||
|
activationURL = VAR_ACS_SERVER_HTTP + "/ActivationServiceInfo"
|
||||||
|
|
||||||
|
response = sendHTTPRequest(activationURL)
|
||||||
|
|
||||||
|
#print("======================================================")
|
||||||
|
#print("Sending request to " + activationURL)
|
||||||
|
#print("got response:")
|
||||||
|
#print(response)
|
||||||
|
#print("======================================================")
|
||||||
|
|
||||||
|
adobe_response_xml = etree.fromstring(response)
|
||||||
|
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
|
||||||
|
authURL = adobe_response_xml.find("./%s" % (adNS("authURL"))).text
|
||||||
|
userInfoURL = adobe_response_xml.find("./%s" % (adNS("userInfoURL"))).text
|
||||||
|
certificate = adobe_response_xml.find("./%s" % (adNS("certificate"))).text
|
||||||
|
|
||||||
|
if (authURL is None or userInfoURL is None or certificate is None):
|
||||||
|
return False, "Error: Unexpected reply from Adobe.", [[], []]
|
||||||
|
|
||||||
|
etree.SubElement(activationServiceInfo, etree.QName(NSMAP["adept"], "authURL")).text = authURL
|
||||||
|
etree.SubElement(activationServiceInfo, etree.QName(NSMAP["adept"], "userInfoURL")).text = userInfoURL
|
||||||
|
if useHTTPS:
|
||||||
|
# ADE 4.X uses HTTPS
|
||||||
|
etree.SubElement(activationServiceInfo, etree.QName(NSMAP["adept"], "activationURL")).text = VAR_ACS_SERVER_HTTPS
|
||||||
|
else:
|
||||||
|
etree.SubElement(activationServiceInfo, etree.QName(NSMAP["adept"], "activationURL")).text = VAR_ACS_SERVER_HTTP
|
||||||
|
etree.SubElement(activationServiceInfo, etree.QName(NSMAP["adept"], "certificate")).text = certificate
|
||||||
|
|
||||||
|
|
||||||
|
if authCert is None:
|
||||||
|
# This is not supposed to happen, but if it does, then just query it again from Adobe.
|
||||||
|
authenticationURL = authURL + "/AuthenticationServiceInfo"
|
||||||
|
response2 = sendHTTPRequest(authenticationURL)
|
||||||
|
|
||||||
|
adobe_response_xml2 = etree.fromstring(response2)
|
||||||
|
authCert = adobe_response_xml2.find("./%s" % (adNS("certificate"))).text
|
||||||
|
|
||||||
|
|
||||||
|
etree.SubElement(activationServiceInfo, etree.QName(NSMAP["adept"], "authenticationCertificate")).text = authCert
|
||||||
|
|
||||||
|
|
||||||
|
f = open(get_activation_xml_path(), "w")
|
||||||
|
f.write("<?xml version=\"1.0\"?>\n")
|
||||||
|
f.write(etree.tostring(root, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("latin-1"))
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
return True, "Done"
|
||||||
|
|
||||||
|
def encryptLoginCredentials(username, password, authenticationCertificate):
|
||||||
|
# type: (str, str, str) -> bytes
|
||||||
|
|
||||||
|
from setup.libadobe import devkey_bytes as devkey_adobe
|
||||||
|
import struct
|
||||||
|
|
||||||
|
if devkey_adobe is not None:
|
||||||
|
devkey_bytes = devkey_adobe
|
||||||
|
else:
|
||||||
|
f = open(get_devkey_path(), "rb")
|
||||||
|
devkey_bytes = f.read()
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
_authenticationCertificate = base64.b64decode(authenticationCertificate)
|
||||||
|
|
||||||
|
# Build buffer <devkey_bytes> <len username> <username> <len password> <password>
|
||||||
|
|
||||||
|
ar = bytearray(devkey_bytes)
|
||||||
|
ar.extend(bytearray(struct.pack("B", len(username))))
|
||||||
|
ar.extend(bytearray(username.encode("latin-1")))
|
||||||
|
ar.extend(bytearray(struct.pack("B", len(password))))
|
||||||
|
ar.extend(bytearray(password.encode("latin-1")))
|
||||||
|
|
||||||
|
# Crypt code from https://stackoverflow.com/a/12921889/4991648
|
||||||
|
cert = DerSequence()
|
||||||
|
cert.decode(_authenticationCertificate)
|
||||||
|
tbsCertificate = DerSequence()
|
||||||
|
tbsCertificate.decode(cert[0])
|
||||||
|
subjectPublicKeyInfo = tbsCertificate[6]
|
||||||
|
|
||||||
|
rsakey = RSA.importKey(subjectPublicKeyInfo)
|
||||||
|
cipherAC = PKCS1_v1_5.new(rsakey)
|
||||||
|
crypted_msg = cipherAC.encrypt(bytes(ar))
|
||||||
|
|
||||||
|
return crypted_msg
|
||||||
|
|
||||||
|
|
||||||
|
def buildSignInRequestForAnonAuthConvert(username, password, authenticationCertificate):
|
||||||
|
# type: (str, str, str) -> str
|
||||||
|
|
||||||
|
NSMAP = { "adept" : "http://ns.adobe.com/adept" }
|
||||||
|
etree.register_namespace("adept", NSMAP["adept"])
|
||||||
|
|
||||||
|
root = etree.Element(etree.QName(NSMAP["adept"], "signIn"))
|
||||||
|
root.set("method", "AdobeID")
|
||||||
|
|
||||||
|
crypted_msg = encryptLoginCredentials(username, password, authenticationCertificate)
|
||||||
|
|
||||||
|
etree.SubElement(root, etree.QName(NSMAP["adept"], "signInData")).text = base64.b64encode(crypted_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
activationxml = etree.parse(get_activation_xml_path())
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
user_uuid = activationxml.find("./%s/%s" % (adNS("credentials"), adNS("user"))).text
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Note: I tried replacing the user_uuid with the UUID of another (anonymous) authorization
|
||||||
|
# to see if it was possible to take over another account, but that didn't work. That's the reason
|
||||||
|
# why this request has the signature node, the payload needs to be signed with the user certificate
|
||||||
|
# that matches the UUID in the <adept:user> tag.
|
||||||
|
|
||||||
|
etree.SubElement(root, etree.QName(NSMAP["adept"], "user")).text = user_uuid
|
||||||
|
signature = sign_node(root)
|
||||||
|
etree.SubElement(root, etree.QName(NSMAP["adept"], "signature")).text = signature
|
||||||
|
|
||||||
|
return "<?xml version=\"1.0\"?>\n" + etree.tostring(root, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("latin-1")
|
||||||
|
|
||||||
|
|
||||||
|
def buildSignInRequest(type, username, password, authenticationCertificate):
|
||||||
|
# type: (str, str, str, str) -> str
|
||||||
|
|
||||||
|
NSMAP = { "adept" : "http://ns.adobe.com/adept" }
|
||||||
|
etree.register_namespace("adept", NSMAP["adept"])
|
||||||
|
|
||||||
|
root = etree.Element(etree.QName(NSMAP["adept"], "signIn"))
|
||||||
|
root.set("method", type)
|
||||||
|
|
||||||
|
crypted_msg = encryptLoginCredentials(username, password, authenticationCertificate)
|
||||||
|
|
||||||
|
etree.SubElement(root, etree.QName(NSMAP["adept"], "signInData")).text = base64.b64encode(crypted_msg)
|
||||||
|
|
||||||
|
# Generate Auth key and License Key
|
||||||
|
authkey = RSA.generate(1024, e=65537)
|
||||||
|
licensekey = RSA.generate(1024, e=65537)
|
||||||
|
|
||||||
|
authkey_pub = authkey.publickey().exportKey("DER")
|
||||||
|
authkey_priv = authkey.exportKey("DER", pkcs=8)
|
||||||
|
authkey_priv_enc = encrypt_with_device_key(authkey_priv)
|
||||||
|
|
||||||
|
licensekey_pub = licensekey.publickey().exportKey("DER")
|
||||||
|
licensekey_priv = licensekey.exportKey("DER", pkcs=8)
|
||||||
|
licensekey_priv_enc = encrypt_with_device_key(licensekey_priv)
|
||||||
|
|
||||||
|
|
||||||
|
etree.SubElement(root, etree.QName(NSMAP["adept"], "publicAuthKey")).text = base64.b64encode(authkey_pub)
|
||||||
|
etree.SubElement(root, etree.QName(NSMAP["adept"], "encryptedPrivateAuthKey")).text = base64.b64encode(authkey_priv_enc)
|
||||||
|
|
||||||
|
etree.SubElement(root, etree.QName(NSMAP["adept"], "publicLicenseKey")).text = base64.b64encode(licensekey_pub)
|
||||||
|
etree.SubElement(root, etree.QName(NSMAP["adept"], "encryptedPrivateLicenseKey")).text = base64.b64encode(licensekey_priv_enc)
|
||||||
|
|
||||||
|
return "<?xml version=\"1.0\"?>\n" + etree.tostring(root, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("latin-1")
|
||||||
|
|
||||||
|
|
||||||
|
def convertAnonAuthToAccount(username, passwd):
|
||||||
|
|
||||||
|
# If you have an anonymous authorization, you can convert that to an AdobeID.
|
||||||
|
# Important: You can only do this ONCE for each AdobeID.
|
||||||
|
# The AdobeID you are using for this must not be connected to any ADE install.
|
||||||
|
|
||||||
|
# This is intended for cases where people install ADE, use an anonymous auth,
|
||||||
|
# buy a couple books, and then decide to get a fresh AdobeID.
|
||||||
|
|
||||||
|
# Get authenticationCertificate
|
||||||
|
try:
|
||||||
|
activationxml = etree.parse(get_activation_xml_path())
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
authenticationCertificate = activationxml.find("./%s/%s" % (adNS("activationServiceInfo"), adNS("authenticationCertificate"))).text
|
||||||
|
except:
|
||||||
|
return False, "Missing authenticationCertificate"
|
||||||
|
|
||||||
|
if authenticationCertificate == "":
|
||||||
|
return False, "Empty authenticationCertificate"
|
||||||
|
|
||||||
|
linkRequest = buildSignInRequestForAnonAuthConvert(username, passwd, authenticationCertificate)
|
||||||
|
signInURL = activationxml.find("./%s/%s" % (adNS("activationServiceInfo"), adNS("authURL"))).text + "/AddSignInDirect"
|
||||||
|
linkResponse = sendRequestDocu(linkRequest, signInURL)
|
||||||
|
|
||||||
|
try:
|
||||||
|
credentialsXML = etree.fromstring(linkResponse)
|
||||||
|
|
||||||
|
if (credentialsXML.tag == adNS("error")):
|
||||||
|
err = credentialsXML.get("data")
|
||||||
|
err_parts = err.split(' ')
|
||||||
|
if err_parts[0] == "E_AUTH_USER_ALREADY_REGISTERED":
|
||||||
|
# This error happens when you're not using a "fresh" AdobeID.
|
||||||
|
# The AdobeID already has an UUID and authentication data, thus
|
||||||
|
# it cannot be set up using the data from the anonymous authorization.
|
||||||
|
try:
|
||||||
|
return False, "Can't link anon auth " + err_parts[2] + " to account, account already has user ID " + err_parts[3]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif err_parts[0] == "E_AUTH_USERID_INUSE":
|
||||||
|
# This error happens when the UUID of the anonymous auth is already
|
||||||
|
# in use by a given AdobeID.
|
||||||
|
# This can happen if you have one anonymous auth, export that,
|
||||||
|
# then convert it to AdobeID A, then re-import the backed-up anonymous auth
|
||||||
|
# (or use another computer that has the identical cloned anonymous auth)
|
||||||
|
# and then try to link that auth to another AdobeID B.
|
||||||
|
# Adobe then notices that the anonymous authorization you're trying to link
|
||||||
|
# has already been linked to an Adobe account.
|
||||||
|
try:
|
||||||
|
return False, "Can't link anon auth: Anon auth " + err_parts[3] + " has already been linked to another AdobeID"
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return False, "Can't link anon auth to account: " + err
|
||||||
|
|
||||||
|
elif (credentialsXML.tag != adNS("success")):
|
||||||
|
return False, "Invalid main tag " + credentialsXML.tag
|
||||||
|
except:
|
||||||
|
return False, "Invalid response to login request"
|
||||||
|
|
||||||
|
|
||||||
|
# If we end up here, the account linking was successful. Now we just need to update the activation.xml accordingly.
|
||||||
|
|
||||||
|
activationxml = etree.parse(get_activation_xml_path())
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
cred_node = activationxml.find("./%s" % (adNS("credentials")))
|
||||||
|
|
||||||
|
|
||||||
|
NSMAP = { "adept" : "http://ns.adobe.com/adept" }
|
||||||
|
tmp_node = etree.SubElement(cred_node, etree.QName(NSMAP["adept"], "username"))
|
||||||
|
|
||||||
|
# Adobe / ADE only supports this account linking for AdobeID accounts, not for any Vendor IDs.
|
||||||
|
tmp_node.set("method", "AdobeID")
|
||||||
|
tmp_node.text = username
|
||||||
|
|
||||||
|
# Write to file
|
||||||
|
f = open(get_activation_xml_path(), "w")
|
||||||
|
f.write("<?xml version=\"1.0\"?>\n")
|
||||||
|
f.write(etree.tostring(activationxml, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("latin-1"))
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
|
||||||
|
return True, "Account linking successful"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def signIn(account_type, username, passwd):
|
||||||
|
|
||||||
|
|
||||||
|
# Get authenticationCertificate
|
||||||
|
activationxml = etree.parse(get_activation_xml_path())
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
authenticationCertificate = activationxml.find("./%s/%s" % (adNS("activationServiceInfo"), adNS("authenticationCertificate"))).text
|
||||||
|
|
||||||
|
|
||||||
|
# Type = "AdobeID" or "anonymous". For "anonymous", username and passwd need to be the empty string.
|
||||||
|
signInRequest = buildSignInRequest(account_type, username, passwd, authenticationCertificate)
|
||||||
|
|
||||||
|
signInURL = activationxml.find("./%s/%s" % (adNS("activationServiceInfo"), adNS("authURL"))).text + "/SignInDirect"
|
||||||
|
|
||||||
|
credentials = sendRequestDocu(signInRequest, signInURL)
|
||||||
|
|
||||||
|
|
||||||
|
#print("======================================================")
|
||||||
|
#print("Sending request to " + signInURL)
|
||||||
|
#print("Payload:")
|
||||||
|
#print(signInRequest)
|
||||||
|
#print("got response:")
|
||||||
|
#print(credentials)
|
||||||
|
#print("======================================================")
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
credentialsXML = etree.fromstring(credentials)
|
||||||
|
|
||||||
|
if (credentialsXML.tag == adNS("error")):
|
||||||
|
err = credentialsXML.get("data")
|
||||||
|
if ("E_AUTH_FAILED" in err and "CUS05051" in err):
|
||||||
|
return False, "Invalid username or password!"
|
||||||
|
elif ("E_AUTH_FAILED" in err and "LOGIN_FAILED" in err):
|
||||||
|
return False, "E_AUTH_FAILED/LOGIN_FAILED. If you have 2FA enabled, please disable that and try again."
|
||||||
|
else:
|
||||||
|
return False, "Unknown Adobe error:" + credentials
|
||||||
|
|
||||||
|
elif (credentialsXML.tag == adNS("credentials")):
|
||||||
|
pass
|
||||||
|
#print("Login successful")
|
||||||
|
else:
|
||||||
|
return False, "Invalid main tag " + credentialsXML.tag
|
||||||
|
|
||||||
|
|
||||||
|
except:
|
||||||
|
return False, "Invalid response to login request"
|
||||||
|
|
||||||
|
# Got correct credentials
|
||||||
|
|
||||||
|
private_key_data_encrypted = credentialsXML.find("./%s" % (adNS("encryptedPrivateLicenseKey"))).text
|
||||||
|
private_key_data_encrypted = base64.b64decode(private_key_data_encrypted)
|
||||||
|
private_key_data = decrypt_with_device_key(private_key_data_encrypted)
|
||||||
|
|
||||||
|
|
||||||
|
# Okay, now we got the credential response correct. Now "just" apply all these to the main activation.xml
|
||||||
|
|
||||||
|
f = open(get_activation_xml_path(), "w")
|
||||||
|
|
||||||
|
f.write("<?xml version=\"1.0\"?>\n")
|
||||||
|
f.write(etree.tostring(activationxml, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("latin-1").replace("</activationInfo>", ""))
|
||||||
|
|
||||||
|
# Yeah, that's ugly, but I didn't get etree to work with the different Namespaces ...
|
||||||
|
|
||||||
|
f.write("<adept:credentials xmlns:adept=\"http://ns.adobe.com/adept\">\n")
|
||||||
|
f.write("<adept:user>%s</adept:user>\n" % (credentialsXML.find("./%s" % (adNS("user"))).text))
|
||||||
|
if account_type != "anonymous":
|
||||||
|
f.write("<adept:username method=\"%s\">%s</adept:username>\n" % (credentialsXML.find("./%s" % (adNS("username"))).get("method", account_type), credentialsXML.find("./%s" % (adNS("username"))).text))
|
||||||
|
f.write("<adept:pkcs12>%s</adept:pkcs12>\n" % (credentialsXML.find("./%s" % (adNS("pkcs12"))).text))
|
||||||
|
f.write("<adept:licenseCertificate>%s</adept:licenseCertificate>\n" % (credentialsXML.find("./%s" % (adNS("licenseCertificate"))).text))
|
||||||
|
f.write("<adept:privateLicenseKey>%s</adept:privateLicenseKey>\n" % (base64.b64encode(private_key_data).decode("latin-1")))
|
||||||
|
f.write("<adept:authenticationCertificate>%s</adept:authenticationCertificate>\n" % (authenticationCertificate))
|
||||||
|
f.write("</adept:credentials>\n")
|
||||||
|
f.write("</activationInfo>\n")
|
||||||
|
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
return True, "Done"
|
||||||
|
|
||||||
|
def exportProxyAuth(act_xml_path, activationToken):
|
||||||
|
# This authorizes a tethered device.
|
||||||
|
# ret, data = exportProxyAuth(act_xml_path, data)
|
||||||
|
|
||||||
|
activationxml = etree.parse(get_activation_xml_path())
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
|
||||||
|
# At some point I should probably rewrite this, but I want to be sure the format is
|
||||||
|
# correct so I'm recreating the whole XML myself.
|
||||||
|
|
||||||
|
rt_si_authURL = activationxml.find("./%s/%s" % (adNS("activationServiceInfo"), adNS("authURL"))).text
|
||||||
|
rt_si_userInfoURL = activationxml.find("./%s/%s" % (adNS("activationServiceInfo"), adNS("userInfoURL"))).text
|
||||||
|
rt_si_activationURL = activationxml.find("./%s/%s" % (adNS("activationServiceInfo"), adNS("activationURL"))).text
|
||||||
|
rt_si_certificate = activationxml.find("./%s/%s" % (adNS("activationServiceInfo"), adNS("certificate"))).text
|
||||||
|
|
||||||
|
rt_c_user = activationxml.find("./%s/%s" % (adNS("credentials"), adNS("user"))).text
|
||||||
|
rt_c_licenseCertificate = activationxml.find("./%s/%s" % (adNS("credentials"), adNS("licenseCertificate"))).text
|
||||||
|
rt_c_privateLicenseKey = activationxml.find("./%s/%s" % (adNS("credentials"), adNS("privateLicenseKey"))).text
|
||||||
|
rt_c_authenticationCertificate = activationxml.find("./%s/%s" % (adNS("credentials"), adNS("authenticationCertificate"))).text
|
||||||
|
|
||||||
|
rt_c_username = None
|
||||||
|
rt_c_usernameMethod = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
rt_c_username = activationxml.find("./%s/%s" % (adNS("credentials"), adNS("username"))).text
|
||||||
|
rt_c_usernameMethod = activationxml.find("./%s/%s" % (adNS("credentials"), adNS("username"))).get("method", "AdobeID")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
ret = "<?xml version=\"1.0\"?>"
|
||||||
|
ret += "<activationInfo xmlns=\"http://ns.adobe.com/adept\">"
|
||||||
|
ret += "<adept:activationServiceInfo xmlns:adept=\"http://ns.adobe.com/adept\">"
|
||||||
|
ret += "<adept:authURL>%s</adept:authURL>" % (rt_si_authURL)
|
||||||
|
ret += "<adept:userInfoURL>%s</adept:userInfoURL>" % (rt_si_userInfoURL)
|
||||||
|
ret += "<adept:activationURL>%s</adept:activationURL>" % (rt_si_activationURL)
|
||||||
|
ret += "<adept:certificate>%s</adept:certificate>" % (rt_si_certificate)
|
||||||
|
ret += "</adept:activationServiceInfo>"
|
||||||
|
|
||||||
|
ret += "<adept:credentials xmlns:adept=\"http://ns.adobe.com/adept\">"
|
||||||
|
ret += "<adept:user>%s</adept:user>" % (rt_c_user)
|
||||||
|
ret += "<adept:licenseCertificate>%s</adept:licenseCertificate>" % (rt_c_licenseCertificate)
|
||||||
|
ret += "<adept:privateLicenseKey>%s</adept:privateLicenseKey>" % (rt_c_privateLicenseKey)
|
||||||
|
ret += "<adept:authenticationCertificate>%s</adept:authenticationCertificate>" % (rt_c_authenticationCertificate)
|
||||||
|
|
||||||
|
if rt_c_username is not None:
|
||||||
|
ret += "<adept:username method=\"%s\">%s</adept:username>" % (rt_c_usernameMethod, rt_c_username)
|
||||||
|
|
||||||
|
ret += "</adept:credentials>"
|
||||||
|
|
||||||
|
activationToken = activationToken.decode("latin-1")
|
||||||
|
# Yeah, terrible hack, but Adobe sends the token with namespace but exports it without.
|
||||||
|
activationToken = activationToken.replace(' xmlns="http://ns.adobe.com/adept"', '')
|
||||||
|
|
||||||
|
ret += activationToken
|
||||||
|
|
||||||
|
ret += "</activationInfo>"
|
||||||
|
|
||||||
|
# Okay, now we can finally write this to the device.
|
||||||
|
|
||||||
|
try:
|
||||||
|
f = open(act_xml_path, "w")
|
||||||
|
f.write(ret)
|
||||||
|
f.close()
|
||||||
|
except:
|
||||||
|
return False, "Can't write file"
|
||||||
|
|
||||||
|
return True, "Done"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def buildActivateReqProxy(useVersionIndex = 0, proxyData = None):
|
||||||
|
|
||||||
|
if proxyData is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if useVersionIndex >= len(VAR_VER_SUPP_CONFIG_NAMES):
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
build_id = VAR_VER_BUILD_IDS[useVersionIndex]
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if build_id not in VAR_VER_ALLOWED_BUILD_IDS_AUTHORIZE:
|
||||||
|
# ADE 1.7.2 or another version that authorization is disabled for
|
||||||
|
return False
|
||||||
|
|
||||||
|
local_device_xml = etree.parse(get_device_path())
|
||||||
|
local_activation_xml = etree.parse(get_activation_xml_path())
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
|
||||||
|
version = None
|
||||||
|
clientOS = None
|
||||||
|
clientLocale = None
|
||||||
|
|
||||||
|
ver = local_device_xml.findall("./%s" % (adNS("version")))
|
||||||
|
|
||||||
|
|
||||||
|
for f in ver:
|
||||||
|
if f.get("name") == "hobbes":
|
||||||
|
version = f.get("value")
|
||||||
|
elif f.get("name") == "clientOS":
|
||||||
|
clientOS = f.get("value")
|
||||||
|
elif f.get("name") == "clientLocale":
|
||||||
|
clientLocale = f.get("value")
|
||||||
|
|
||||||
|
if (version is None or clientOS is None or clientLocale is None):
|
||||||
|
return False, "Required version information missing"
|
||||||
|
|
||||||
|
|
||||||
|
ret = ""
|
||||||
|
|
||||||
|
ret += "<?xml version=\"1.0\"?>"
|
||||||
|
ret += "<adept:activate xmlns:adept=\"http://ns.adobe.com/adept\" requestType=\"initial\">"
|
||||||
|
ret += "<adept:fingerprint>%s</adept:fingerprint>" % (proxyData.find("./%s" % (adNS("fingerprint"))).text)
|
||||||
|
ret += "<adept:deviceType>%s</adept:deviceType>" % (proxyData.find("./%s" % (adNS("deviceType"))).text)
|
||||||
|
ret += "<adept:clientOS>%s</adept:clientOS>" % (clientOS)
|
||||||
|
ret += "<adept:clientLocale>%s</adept:clientLocale>" % (clientLocale)
|
||||||
|
ret += "<adept:clientVersion>%s</adept:clientVersion>" % (VAR_VER_SUPP_VERSIONS[useVersionIndex])
|
||||||
|
|
||||||
|
ret += "<adept:proxyDevice>"
|
||||||
|
ret += "<adept:softwareVersion>%s</adept:softwareVersion>" % (version)
|
||||||
|
ret += "<adept:clientOS>%s</adept:clientOS>" % (clientOS)
|
||||||
|
ret += "<adept:clientLocale>%s</adept:clientLocale>" % (clientLocale)
|
||||||
|
ret += "<adept:clientVersion>%s</adept:clientVersion>" % (VAR_VER_SUPP_VERSIONS[useVersionIndex])
|
||||||
|
ret += "<adept:deviceType>%s</adept:deviceType>" % (local_device_xml.find("./%s" % (adNS("deviceType"))).text)
|
||||||
|
ret += "<adept:productName>%s</adept:productName>" % ("ADOBE Digitial Editions")
|
||||||
|
# YES, this typo ("Digitial" instead of "Digital") IS present in ADE!!
|
||||||
|
|
||||||
|
ret += "<adept:fingerprint>%s</adept:fingerprint>" % (local_device_xml.find("./%s" % (adNS("fingerprint"))).text)
|
||||||
|
|
||||||
|
ret += "<adept:activationToken>"
|
||||||
|
ret += "<adept:user>%s</adept:user>" % (local_activation_xml.find("./%s/%s" % (adNS("activationToken"), adNS("user"))).text)
|
||||||
|
ret += "<adept:device>%s</adept:device>" % (local_activation_xml.find("./%s/%s" % (adNS("activationToken"), adNS("device"))).text)
|
||||||
|
ret += "</adept:activationToken>"
|
||||||
|
ret += "</adept:proxyDevice>"
|
||||||
|
|
||||||
|
ret += "<adept:targetDevice>"
|
||||||
|
|
||||||
|
target_hobbes_vers = proxyData.findall("./%s" % (adNS("version")))
|
||||||
|
hobbes_version = None
|
||||||
|
for f in target_hobbes_vers:
|
||||||
|
if f.get("name") == "hobbes":
|
||||||
|
hobbes_version = f.get("value")
|
||||||
|
break
|
||||||
|
|
||||||
|
if hobbes_version is not None:
|
||||||
|
ret += "<adept:softwareVersion>%s</adept:softwareVersion>" % (hobbes_version)
|
||||||
|
|
||||||
|
ret += "<adept:clientVersion>%s</adept:clientVersion>" % (proxyData.find("./%s" % (adNS("deviceClass"))).text)
|
||||||
|
ret += "<adept:deviceType>%s</adept:deviceType>" % (proxyData.find("./%s" % (adNS("deviceType"))).text)
|
||||||
|
ret += "<adept:productName>%s</adept:productName>" % ("ADOBE Digitial Editions")
|
||||||
|
ret += "<adept:fingerprint>%s</adept:fingerprint>" % (proxyData.find("./%s" % (adNS("fingerprint"))).text)
|
||||||
|
|
||||||
|
|
||||||
|
ret += "</adept:targetDevice>"
|
||||||
|
|
||||||
|
ret += addNonce()
|
||||||
|
|
||||||
|
ret += "<adept:user>%s</adept:user>" % (local_activation_xml.find("./%s/%s" % (adNS("activationToken"), adNS("user"))).text)
|
||||||
|
|
||||||
|
ret += "</adept:activate>"
|
||||||
|
|
||||||
|
return True, ret
|
||||||
|
|
||||||
|
|
||||||
|
def buildActivateReq(useVersionIndex = 0):
|
||||||
|
|
||||||
|
if useVersionIndex >= len(VAR_VER_SUPP_CONFIG_NAMES):
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
build_id = VAR_VER_BUILD_IDS[useVersionIndex]
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if build_id not in VAR_VER_ALLOWED_BUILD_IDS_AUTHORIZE:
|
||||||
|
# ADE 1.7.2 or another version that authorization is disabled for
|
||||||
|
return False
|
||||||
|
|
||||||
|
devicexml = etree.parse(get_device_path())
|
||||||
|
activationxml = etree.parse(get_activation_xml_path())
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
|
||||||
|
|
||||||
|
version = None
|
||||||
|
clientOS = None
|
||||||
|
clientLocale = None
|
||||||
|
|
||||||
|
ver = devicexml.findall("./%s" % (adNS("version")))
|
||||||
|
|
||||||
|
|
||||||
|
for f in ver:
|
||||||
|
if f.get("name") == "hobbes":
|
||||||
|
version = f.get("value")
|
||||||
|
elif f.get("name") == "clientOS":
|
||||||
|
clientOS = f.get("value")
|
||||||
|
elif f.get("name") == "clientLocale":
|
||||||
|
clientLocale = f.get("value")
|
||||||
|
|
||||||
|
if (version is None or clientOS is None or clientLocale is None):
|
||||||
|
return False, "Required version information missing"
|
||||||
|
|
||||||
|
ret = ""
|
||||||
|
|
||||||
|
ret += "<?xml version=\"1.0\"?>"
|
||||||
|
ret += "<adept:activate xmlns:adept=\"http://ns.adobe.com/adept\" requestType=\"initial\">"
|
||||||
|
ret += "<adept:fingerprint>%s</adept:fingerprint>" % (devicexml.find("./%s" % (adNS("fingerprint"))).text)
|
||||||
|
ret += "<adept:deviceType>%s</adept:deviceType>" % (devicexml.find("./%s" % (adNS("deviceType"))).text)
|
||||||
|
ret += "<adept:clientOS>%s</adept:clientOS>" % (clientOS)
|
||||||
|
ret += "<adept:clientLocale>%s</adept:clientLocale>" % (clientLocale)
|
||||||
|
ret += "<adept:clientVersion>%s</adept:clientVersion>" % (VAR_VER_SUPP_VERSIONS[useVersionIndex])
|
||||||
|
ret += "<adept:targetDevice>"
|
||||||
|
|
||||||
|
|
||||||
|
ret += "<adept:softwareVersion>%s</adept:softwareVersion>" % (version)
|
||||||
|
ret += "<adept:clientOS>%s</adept:clientOS>" % (clientOS)
|
||||||
|
ret += "<adept:clientLocale>%s</adept:clientLocale>" % (clientLocale)
|
||||||
|
ret += "<adept:clientVersion>%s</adept:clientVersion>" % (VAR_VER_SUPP_VERSIONS[useVersionIndex])
|
||||||
|
ret += "<adept:deviceType>%s</adept:deviceType>" % (devicexml.find("./%s" % (adNS("deviceType"))).text)
|
||||||
|
ret += "<adept:productName>%s</adept:productName>" % ("ADOBE Digitial Editions")
|
||||||
|
# YES, this typo ("Digitial" instead of "Digital") IS present in ADE!!
|
||||||
|
|
||||||
|
ret += "<adept:fingerprint>%s</adept:fingerprint>" % (devicexml.find("./%s" % (adNS("fingerprint"))).text)
|
||||||
|
|
||||||
|
# TODO: Here's where multiple <adept:activationToken>s, each with a user and a device,
|
||||||
|
# TODO: would show up if the client was already activated and just adds an additional activation.
|
||||||
|
# TODO: Not sure if I want to replicate this, or if I'd rather replicate independant installations ...
|
||||||
|
|
||||||
|
ret += "</adept:targetDevice>"
|
||||||
|
|
||||||
|
ret += addNonce()
|
||||||
|
|
||||||
|
ret += "<adept:user>%s</adept:user>" % (activationxml.find("./%s/%s" % (adNS("credentials"), adNS("user"))).text)
|
||||||
|
|
||||||
|
ret += "</adept:activate>"
|
||||||
|
|
||||||
|
return True, ret
|
||||||
|
|
||||||
|
|
||||||
|
# Call this function to change from ADE2 to ADE3 and vice versa.
|
||||||
|
def changeDeviceVersion(useVersionIndex = 0):
|
||||||
|
if useVersionIndex >= len(VAR_VER_SUPP_CONFIG_NAMES):
|
||||||
|
return False, "Invalid Version index"
|
||||||
|
|
||||||
|
try:
|
||||||
|
build_id = VAR_VER_BUILD_IDS[useVersionIndex]
|
||||||
|
except:
|
||||||
|
return False, "Unknown build ID"
|
||||||
|
|
||||||
|
if build_id not in VAR_VER_ALLOWED_BUILD_IDS_SWITCH_TO:
|
||||||
|
# A version that we no longer want to allow switching to
|
||||||
|
return False, "BuildID not supported"
|
||||||
|
|
||||||
|
try:
|
||||||
|
devicexml = etree.parse(get_device_path())
|
||||||
|
new_hobbes = VAR_VER_HOBBES_VERSIONS[useVersionIndex]
|
||||||
|
new_os = VAR_VER_OS_IDENTIFIERS[useVersionIndex]
|
||||||
|
except:
|
||||||
|
return False, "Error preparing version change"
|
||||||
|
|
||||||
|
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
ver = devicexml.findall("./%s" % (adNS("version")))
|
||||||
|
|
||||||
|
for f in ver:
|
||||||
|
if f.get("name") == "hobbes":
|
||||||
|
#print("Changing hobbes from {0} to {1}".format(f.attrib["value"], new_hobbes))
|
||||||
|
f.attrib["value"] = new_hobbes
|
||||||
|
if f.get("name") == "clientOS":
|
||||||
|
#print("Changing OS from {0} to {1}".format(f.attrib["value"], new_os))
|
||||||
|
f.attrib["value"] = new_os
|
||||||
|
|
||||||
|
try:
|
||||||
|
f = open(get_device_path(), "w")
|
||||||
|
f.write("<?xml version=\"1.0\"?>\n")
|
||||||
|
f.write(etree.tostring(devicexml, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("latin-1"))
|
||||||
|
f.close()
|
||||||
|
except:
|
||||||
|
return False, "Failed to update device file."
|
||||||
|
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def activateDevice(useVersionIndex = 0, proxyData = None):
|
||||||
|
|
||||||
|
if useVersionIndex >= len(VAR_VER_SUPP_CONFIG_NAMES):
|
||||||
|
return False, "Invalid Version index"
|
||||||
|
|
||||||
|
try:
|
||||||
|
build_id = VAR_VER_BUILD_IDS[useVersionIndex]
|
||||||
|
except:
|
||||||
|
return False, "error checking build ID"
|
||||||
|
|
||||||
|
if build_id not in VAR_VER_ALLOWED_BUILD_IDS_AUTHORIZE:
|
||||||
|
# ADE 1.7.2 or another version that authorization is disabled for
|
||||||
|
return False, "Authorization not supported for this build ID"
|
||||||
|
|
||||||
|
verbose_logging = False
|
||||||
|
try:
|
||||||
|
import calibre_plugins.deacsm.prefs as prefs
|
||||||
|
deacsmprefs = prefs.ACSMInput_Prefs()
|
||||||
|
verbose_logging = deacsmprefs["detailed_logging"]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if proxyData is not None:
|
||||||
|
result, activate_req = buildActivateReqProxy(useVersionIndex, proxyData)
|
||||||
|
else:
|
||||||
|
result, activate_req = buildActivateReq(useVersionIndex)
|
||||||
|
if (result is False):
|
||||||
|
return False, "Building activation request failed: " + activate_req
|
||||||
|
|
||||||
|
|
||||||
|
NSMAP = { "adept" : "http://ns.adobe.com/adept" }
|
||||||
|
etree.register_namespace("adept", NSMAP["adept"])
|
||||||
|
|
||||||
|
req_xml = etree.fromstring(activate_req)
|
||||||
|
|
||||||
|
signature = sign_node(req_xml)
|
||||||
|
|
||||||
|
etree.SubElement(req_xml, etree.QName(NSMAP["adept"], "signature")).text = signature
|
||||||
|
|
||||||
|
if verbose_logging:
|
||||||
|
print ("Activation request:")
|
||||||
|
print(etree.tostring(req_xml, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("latin-1"))
|
||||||
|
|
||||||
|
data = "<?xml version=\"1.0\"?>\n" + etree.tostring(req_xml, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("latin-1")
|
||||||
|
|
||||||
|
useHTTPS = False
|
||||||
|
if VAR_VER_BUILD_IDS[useVersionIndex] >= VAR_VER_NEED_HTTPS_BUILD_ID_LIMIT:
|
||||||
|
useHTTPS = True
|
||||||
|
|
||||||
|
if useHTTPS:
|
||||||
|
# ADE 4.X uses HTTPS
|
||||||
|
ret = sendRequestDocu(data, VAR_ACS_SERVER_HTTPS + "/Activate")
|
||||||
|
else:
|
||||||
|
ret = sendRequestDocu(data, VAR_ACS_SERVER_HTTP + "/Activate")
|
||||||
|
|
||||||
|
try:
|
||||||
|
credentialsXML = etree.fromstring(ret)
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
|
||||||
|
|
||||||
|
if (credentialsXML.tag == adNS("error")):
|
||||||
|
err = credentialsXML.get("data")
|
||||||
|
return False, "Adobe error: " + err.split(' ')[0] + "\n" + err
|
||||||
|
|
||||||
|
elif (credentialsXML.tag == adNS("activationToken")):
|
||||||
|
pass
|
||||||
|
#print("Login successful")
|
||||||
|
else:
|
||||||
|
return False, "Invalid main tag " + credentialsXML.tag
|
||||||
|
except:
|
||||||
|
return False, "Error parsing Adobe /Activate response"
|
||||||
|
|
||||||
|
if verbose_logging:
|
||||||
|
print("Response from server: ")
|
||||||
|
print(ret)
|
||||||
|
|
||||||
|
if proxyData is not None:
|
||||||
|
# If we have a proxy device, this function doesn't know where to store the activation.
|
||||||
|
# Just return the data and have the caller figure that out.
|
||||||
|
return True, ret
|
||||||
|
|
||||||
|
# Soooo, lets go and append that to the XML:
|
||||||
|
|
||||||
|
f = open(get_activation_xml_path(), "r")
|
||||||
|
old_xml = f.read().replace("</activationInfo>", "")
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
f = open(get_activation_xml_path(), "w")
|
||||||
|
|
||||||
|
f.write(old_xml)
|
||||||
|
f.write(ret.decode("latin-1"))
|
||||||
|
f.write("</activationInfo>\n")
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
return True, ret
|
||||||
|
|
||||||
|
def getAccountUUID():
|
||||||
|
try:
|
||||||
|
activationxml = etree.parse(get_activation_xml_path())
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
user_uuid = activationxml.find("./%s/%s" % (adNS("credentials"), adNS("user"))).text
|
||||||
|
|
||||||
|
if not user_uuid.startswith("urn:uuid:"):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return user_uuid[9:]
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def exportAccountEncryptionKeyDER(output_file):
|
||||||
|
# type: (str) -> bool
|
||||||
|
try:
|
||||||
|
activationxml = etree.parse(get_activation_xml_path())
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
privatekey = activationxml.find("./%s/%s" % (adNS("credentials"), adNS("privateLicenseKey"))).text
|
||||||
|
privatekey = base64.b64decode(privatekey)
|
||||||
|
privatekey = privatekey[26:]
|
||||||
|
|
||||||
|
f = open(output_file, "wb")
|
||||||
|
f.write(privatekey)
|
||||||
|
f.close()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def exportAccountEncryptionKeyBytes():
|
||||||
|
try:
|
||||||
|
activationxml = etree.parse(get_activation_xml_path())
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
privatekey = activationxml.find("./%s/%s" % (adNS("credentials"), adNS("privateLicenseKey"))).text
|
||||||
|
privatekey = base64.b64decode(privatekey)
|
||||||
|
privatekey = privatekey[26:]
|
||||||
|
return privatekey
|
||||||
|
except:
|
||||||
|
return None
|
961
setup/libadobeFulfill.py
Normal file
961
setup/libadobeFulfill.py
Normal file
|
@ -0,0 +1,961 @@
|
||||||
|
from lxml import etree
|
||||||
|
import base64
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
|
||||||
|
#@@CALIBRE_COMPAT_CODE@@
|
||||||
|
|
||||||
|
from setup.libadobe import addNonce, sign_node, get_cert_from_pkcs12, sendRequestDocu, sendRequestDocuRC, sendHTTPRequest
|
||||||
|
from setup.libadobe import get_devkey_path, get_device_path, get_activation_xml_path
|
||||||
|
from setup.libadobe import VAR_VER_SUPP_VERSIONS, VAR_VER_HOBBES_VERSIONS
|
||||||
|
from setup.libadobe import VAR_VER_BUILD_IDS, VAR_VER_USE_DIFFERENT_NOTIFICATION_XML_ORDER
|
||||||
|
|
||||||
|
|
||||||
|
def buildFulfillRequest(acsm):
|
||||||
|
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
|
||||||
|
|
||||||
|
activationxml = etree.parse(get_activation_xml_path())
|
||||||
|
devicexml = etree.parse(get_device_path())
|
||||||
|
|
||||||
|
|
||||||
|
user_uuid = activationxml.find("./%s/%s" % (adNS("credentials"), adNS("user"))).text
|
||||||
|
device_uuid = activationxml.find("./%s/%s" % (adNS("activationToken"), adNS("device"))).text
|
||||||
|
try:
|
||||||
|
fingerprint = None
|
||||||
|
device_type = None
|
||||||
|
fingerprint = activationxml.find("./%s/%s" % (adNS("activationToken"), adNS("fingerprint"))).text
|
||||||
|
device_type = activationxml.find("./%s/%s" % (adNS("activationToken"), adNS("deviceType"))).text
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if (fingerprint is None or fingerprint == "" or device_type is None or device_type == ""):
|
||||||
|
# This should usually never happen with a proper activation, but just in case it does,
|
||||||
|
# I'll leave this code in - it loads the fingerprint from the device data instead.
|
||||||
|
fingerprint = devicexml.find("./%s" % (adNS("fingerprint"))).text
|
||||||
|
device_type = devicexml.find("./%s" % (adNS("deviceType"))).text
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
version = None
|
||||||
|
clientOS = None
|
||||||
|
clientLocale = None
|
||||||
|
|
||||||
|
ver = devicexml.findall("./%s" % (adNS("version")))
|
||||||
|
|
||||||
|
|
||||||
|
for f in ver:
|
||||||
|
if f.get("name") == "hobbes":
|
||||||
|
version = f.get("value")
|
||||||
|
elif f.get("name") == "clientOS":
|
||||||
|
clientOS = f.get("value")
|
||||||
|
elif f.get("name") == "clientLocale":
|
||||||
|
clientLocale = f.get("value")
|
||||||
|
|
||||||
|
# Find matching client version depending on the Hobbes version.
|
||||||
|
# This way we don't need to store and re-load it for each fulfillment.
|
||||||
|
|
||||||
|
try:
|
||||||
|
v_idx = VAR_VER_HOBBES_VERSIONS.index(version)
|
||||||
|
clientVersion = VAR_VER_SUPP_VERSIONS[v_idx]
|
||||||
|
|
||||||
|
except:
|
||||||
|
# Version not present, probably the "old" 10.0.4 entry.
|
||||||
|
# As 10.X is in the 3.0 range, assume we're on ADE 3.0
|
||||||
|
clientVersion = "3.0.1.91394"
|
||||||
|
|
||||||
|
if clientVersion == "ADE WIN 9,0,1131,27":
|
||||||
|
# Ancient ADE 1.7.2 does this request differently
|
||||||
|
request = "<fulfill xmlns=\"http://ns.adobe.com/adept\">\n"
|
||||||
|
request += "<user>%s</user>\n" % (user_uuid)
|
||||||
|
request += "<device>%s</device>\n" % (device_uuid)
|
||||||
|
request += "<deviceType>%s</deviceType>\n" % (device_type)
|
||||||
|
request += etree.tostring(acsm, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("utf-8")
|
||||||
|
request += "</fulfill>"
|
||||||
|
return request, False
|
||||||
|
|
||||||
|
else:
|
||||||
|
request = ""
|
||||||
|
request += "<?xml version=\"1.0\"?>"
|
||||||
|
request += "<adept:fulfill xmlns:adept=\"http://ns.adobe.com/adept\">"
|
||||||
|
request += "<adept:user>%s</adept:user>" % (user_uuid)
|
||||||
|
request += "<adept:device>%s</adept:device>" % (device_uuid)
|
||||||
|
request += "<adept:deviceType>%s</adept:deviceType>" % (device_type)
|
||||||
|
request += etree.tostring(acsm, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("utf-8")
|
||||||
|
request += "<adept:targetDevice>"
|
||||||
|
|
||||||
|
request += "<adept:softwareVersion>%s</adept:softwareVersion>" % (version)
|
||||||
|
request += "<adept:clientOS>%s</adept:clientOS>" % (clientOS)
|
||||||
|
request += "<adept:clientLocale>%s</adept:clientLocale>" % (clientLocale)
|
||||||
|
request += "<adept:clientVersion>%s</adept:clientVersion>" % (clientVersion)
|
||||||
|
request += "<adept:deviceType>%s</adept:deviceType>" % (device_type)
|
||||||
|
request += "<adept:productName>%s</adept:productName>" % ("ADOBE Digitial Editions")
|
||||||
|
# YES, this typo ("Digitial" instead of "Digital") IS present in ADE!!
|
||||||
|
request += "<adept:fingerprint>%s</adept:fingerprint>" % (fingerprint)
|
||||||
|
|
||||||
|
request += "<adept:activationToken>"
|
||||||
|
request += "<adept:user>%s</adept:user>" % (user_uuid)
|
||||||
|
request += "<adept:device>%s</adept:device>" % (device_uuid)
|
||||||
|
request += "</adept:activationToken>"
|
||||||
|
request += "</adept:targetDevice>"
|
||||||
|
request += "</adept:fulfill>"
|
||||||
|
return request, True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def buildInitLicenseServiceRequest(authURL):
|
||||||
|
# type: (str) -> str
|
||||||
|
|
||||||
|
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
NSMAP = { "adept" : "http://ns.adobe.com/adept" }
|
||||||
|
etree.register_namespace("adept", NSMAP["adept"])
|
||||||
|
|
||||||
|
activationxml = etree.parse(get_activation_xml_path())
|
||||||
|
user_uuid = activationxml.find("./%s/%s" % (adNS("credentials"), adNS("user"))).text
|
||||||
|
|
||||||
|
ret = ""
|
||||||
|
ret += "<?xml version=\"1.0\"?>"
|
||||||
|
ret += "<adept:licenseServiceRequest xmlns:adept=\"http://ns.adobe.com/adept\" identity=\"user\">"
|
||||||
|
ret += "<adept:operatorURL>%s</adept:operatorURL>" % (authURL)
|
||||||
|
ret += addNonce()
|
||||||
|
ret += "<adept:user>%s</adept:user>" % (user_uuid)
|
||||||
|
ret += "</adept:licenseServiceRequest>"
|
||||||
|
|
||||||
|
NSMAP = { "adept" : "http://ns.adobe.com/adept" }
|
||||||
|
etree.register_namespace("adept", NSMAP["adept"])
|
||||||
|
|
||||||
|
req_xml = etree.fromstring(ret)
|
||||||
|
|
||||||
|
signature = sign_node(req_xml)
|
||||||
|
if (signature is None):
|
||||||
|
return None
|
||||||
|
|
||||||
|
etree.SubElement(req_xml, etree.QName(NSMAP["adept"], "signature")).text = signature
|
||||||
|
|
||||||
|
return "<?xml version=\"1.0\"?>\n" + etree.tostring(req_xml, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def getDecryptedCert(pkcs12_b64_string = None):
|
||||||
|
|
||||||
|
if pkcs12_b64_string is None:
|
||||||
|
activationxml = etree.parse(get_activation_xml_path())
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
|
||||||
|
pkcs12_b64_string = activationxml.find("./%s/%s" % (adNS("credentials"), adNS("pkcs12"))).text
|
||||||
|
|
||||||
|
pkcs12_data = base64.b64decode(pkcs12_b64_string)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from setup.libadobe import devkey_bytes as devkey_adobe
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if devkey_adobe is not None:
|
||||||
|
devkey_bytes = devkey_adobe
|
||||||
|
else:
|
||||||
|
f = open(get_devkey_path(), "rb")
|
||||||
|
devkey_bytes = f.read()
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
try:
|
||||||
|
return get_cert_from_pkcs12(pkcs12_data, base64.b64encode(devkey_bytes))
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def buildAuthRequest():
|
||||||
|
|
||||||
|
activationxml = etree.parse(get_activation_xml_path())
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
|
||||||
|
my_cert = getDecryptedCert()
|
||||||
|
if my_cert is None:
|
||||||
|
print("Can't decrypt pkcs12 with devkey!")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
ret = "<?xml version=\"1.0\"?>\n"
|
||||||
|
ret += "<adept:credentials xmlns:adept=\"http://ns.adobe.com/adept\">\n"
|
||||||
|
ret += "<adept:user>%s</adept:user>\n" % (activationxml.find("./%s/%s" % (adNS("credentials"), adNS("user"))).text)
|
||||||
|
ret += "<adept:certificate>%s</adept:certificate>\n" % (base64.b64encode(my_cert).decode("utf-8"))
|
||||||
|
ret += "<adept:licenseCertificate>%s</adept:licenseCertificate>\n" % (activationxml.find("./%s/%s" % (adNS("credentials"), adNS("licenseCertificate"))).text)
|
||||||
|
ret += "<adept:authenticationCertificate>%s</adept:authenticationCertificate>\n" % (activationxml.find("./%s/%s" % (adNS("credentials"), adNS("authenticationCertificate"))).text)
|
||||||
|
ret += "</adept:credentials>"
|
||||||
|
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def doOperatorAuth(operatorURL):
|
||||||
|
# type: (str) -> str
|
||||||
|
|
||||||
|
auth_req = buildAuthRequest()
|
||||||
|
|
||||||
|
if auth_req is None:
|
||||||
|
return "Failed to create auth request"
|
||||||
|
|
||||||
|
|
||||||
|
authURL = operatorURL
|
||||||
|
if authURL.endswith("Fulfill"):
|
||||||
|
authURL = authURL.replace("/Fulfill", "")
|
||||||
|
|
||||||
|
|
||||||
|
replyData = sendRequestDocu(auth_req, authURL + "/Auth").decode("utf-8")
|
||||||
|
|
||||||
|
if not "<success" in replyData:
|
||||||
|
return "ERROR: Operator responded with %s\n" % replyData
|
||||||
|
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
NSMAP = { "adept" : "http://ns.adobe.com/adept" }
|
||||||
|
etree.register_namespace("adept", NSMAP["adept"])
|
||||||
|
|
||||||
|
activationxml = etree.parse(get_activation_xml_path())
|
||||||
|
|
||||||
|
activationURL = activationxml.find("./%s/%s" % (adNS("activationToken"), adNS("activationURL"))).text
|
||||||
|
|
||||||
|
init_license_service_request = buildInitLicenseServiceRequest(authURL)
|
||||||
|
|
||||||
|
if (init_license_service_request is None):
|
||||||
|
return "Creating license request failed!"
|
||||||
|
|
||||||
|
|
||||||
|
resp = sendRequestDocu(init_license_service_request, activationURL + "/InitLicenseService").decode("utf-8")
|
||||||
|
if "<error" in resp:
|
||||||
|
return "Looks like that failed: %s" % resp
|
||||||
|
elif "<success" in resp:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return "Useless response: %s" % resp
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def operatorAuth(operatorURL):
|
||||||
|
# type: (str) -> str
|
||||||
|
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
NSMAP = { "adept" : "http://ns.adobe.com/adept" }
|
||||||
|
etree.register_namespace("adept", NSMAP["adept"])
|
||||||
|
|
||||||
|
activationxml = etree.parse(get_activation_xml_path())
|
||||||
|
try:
|
||||||
|
operator_url_list = activationxml.findall("./%s/%s" % (adNS("operatorURLList"), adNS("operatorURL")))
|
||||||
|
|
||||||
|
for member in operator_url_list:
|
||||||
|
if member.text.strip() == operatorURL:
|
||||||
|
#print("Already authenticated to operator")
|
||||||
|
return None
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
ret = doOperatorAuth(operatorURL)
|
||||||
|
if (ret is not None):
|
||||||
|
return "doOperatorAuth error: %s" % ret
|
||||||
|
|
||||||
|
# Check if list exists:
|
||||||
|
list = activationxml.find("./%s" % (adNS("operatorURLList")))
|
||||||
|
user_uuid = activationxml.find("./%s/%s" % (adNS("credentials"), adNS("user"))).text
|
||||||
|
|
||||||
|
if list is None:
|
||||||
|
x = etree.SubElement(activationxml.getroot(), etree.QName(NSMAP["adept"], "operatorURLList"), nsmap=NSMAP)
|
||||||
|
etree.SubElement(x, etree.QName(NSMAP["adept"], "user")).text = user_uuid
|
||||||
|
list = activationxml.find("./%s" % (adNS("operatorURLList")))
|
||||||
|
if list is None:
|
||||||
|
return "Err, this list should not be none right now ..."
|
||||||
|
|
||||||
|
etree.SubElement(list, etree.QName(NSMAP["adept"], "operatorURL")).text = operatorURL
|
||||||
|
|
||||||
|
f = open(get_activation_xml_path(), "w")
|
||||||
|
f.write("<?xml version=\"1.0\"?>\n")
|
||||||
|
f.write(etree.tostring(activationxml, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("utf-8"))
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def buildRights(license_token_node):
|
||||||
|
ret = "<?xml version=\"1.0\"?>\n"
|
||||||
|
ret += "<adept:rights xmlns:adept=\"http://ns.adobe.com/adept\">\n"
|
||||||
|
|
||||||
|
# Add license token
|
||||||
|
ret += etree.tostring(license_token_node, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("utf-8")
|
||||||
|
|
||||||
|
ret += "<adept:licenseServiceInfo>\n"
|
||||||
|
|
||||||
|
NSMAP = { "adept" : "http://ns.adobe.com/adept" }
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
lic_token_url = license_token_node.find("./%s" % (adNS("licenseURL"))).text
|
||||||
|
|
||||||
|
ret += "<adept:licenseURL>%s</adept:licenseURL>\n" % lic_token_url
|
||||||
|
|
||||||
|
# Get cert for this license URL:
|
||||||
|
activationxml = etree.parse(get_activation_xml_path())
|
||||||
|
|
||||||
|
try:
|
||||||
|
licInfo = activationxml.findall("./%s/%s" % (adNS("licenseServices"), adNS("licenseServiceInfo")))
|
||||||
|
|
||||||
|
found = False
|
||||||
|
|
||||||
|
for member in licInfo:
|
||||||
|
if member.find("./%s" % (adNS("licenseURL"))).text == lic_token_url:
|
||||||
|
ret += "<adept:certificate>%s</adept:certificate>\n" % (member.find("./%s" % (adNS("certificate"))).text)
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
print("Did not find the licenseService certificate in the activation data.")
|
||||||
|
print("This usually means it failed to download from the distributor's servers.")
|
||||||
|
print("Please try to download an ACSM book from the Adobe Sample Library, then if that was successful, ")
|
||||||
|
print("try your ACSM book file again.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
ret += "</adept:licenseServiceInfo>\n"
|
||||||
|
ret += "</adept:rights>\n"
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def fulfill(acsm_file, do_notify = False):
|
||||||
|
|
||||||
|
verbose_logging = False
|
||||||
|
try:
|
||||||
|
import prefs
|
||||||
|
deacsmprefs = prefs.ACSMInput_Prefs()
|
||||||
|
verbose_logging = deacsmprefs["detailed_logging"]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Get pkcs12:
|
||||||
|
|
||||||
|
pkcs12 = None
|
||||||
|
acsmxml = None
|
||||||
|
try:
|
||||||
|
activationxml = etree.parse(get_activation_xml_path())
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
pkcs12 = activationxml.find("./%s/%s" % (adNS("credentials"), adNS("pkcs12"))).text
|
||||||
|
except:
|
||||||
|
return False, "Activation not found or invalid"
|
||||||
|
|
||||||
|
if pkcs12 is None or len(pkcs12) == 0:
|
||||||
|
return False, "Activation missing"
|
||||||
|
|
||||||
|
try:
|
||||||
|
acsmxml = etree.parse(acsm_file)
|
||||||
|
except:
|
||||||
|
return False, "ACSM not found or invalid"
|
||||||
|
|
||||||
|
#print(etree.tostring(acsmxml, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("utf-8"))
|
||||||
|
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
dcNS = lambda tag: '{%s}%s' % ('http://purl.org/dc/elements/1.1/', tag)
|
||||||
|
|
||||||
|
try:
|
||||||
|
mimetype = acsmxml.find("./%s/%s/%s" % (adNS("resourceItemInfo"), adNS("metadata"), dcNS("format"))).text
|
||||||
|
|
||||||
|
if (mimetype == "application/pdf"):
|
||||||
|
#print("You're trying to fulfill a PDF file.")
|
||||||
|
pass
|
||||||
|
elif (mimetype == "application/epub+zip"):
|
||||||
|
#print("Trying to fulfill an EPUB file ...")
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
print("Weird mimetype: %s" % (mimetype))
|
||||||
|
print("Continuing anyways ...")
|
||||||
|
|
||||||
|
except:
|
||||||
|
# Some books, like from Google Play books, use a different format and don't have that metadata tag.
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
fulfill_request, adept_ns = buildFulfillRequest(acsmxml)
|
||||||
|
|
||||||
|
if verbose_logging:
|
||||||
|
print("Fulfill request:")
|
||||||
|
print(fulfill_request)
|
||||||
|
|
||||||
|
fulfill_request_xml = etree.fromstring(fulfill_request)
|
||||||
|
# Sign the request:
|
||||||
|
signature = sign_node(fulfill_request_xml)
|
||||||
|
if (signature is None):
|
||||||
|
return False, "Signing failed!"
|
||||||
|
|
||||||
|
NSMAP = { "adept" : "http://ns.adobe.com/adept" }
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
|
||||||
|
if adept_ns:
|
||||||
|
# "new" ADE
|
||||||
|
etree.SubElement(fulfill_request_xml, etree.QName(NSMAP["adept"], "signature")).text = signature
|
||||||
|
else:
|
||||||
|
# ADE 1.7.2
|
||||||
|
etree.SubElement(fulfill_request_xml, etree.QName("signature")).text = signature
|
||||||
|
|
||||||
|
# Get operator URL:
|
||||||
|
operatorURL = None
|
||||||
|
try:
|
||||||
|
operatorURL = acsmxml.find("./%s" % (adNS("operatorURL"))).text.strip()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if (operatorURL is None or len(operatorURL) == 0):
|
||||||
|
return False, "OperatorURL missing in ACSM"
|
||||||
|
|
||||||
|
fulfillURL = operatorURL + "/Fulfill"
|
||||||
|
|
||||||
|
ret = operatorAuth(fulfillURL)
|
||||||
|
if (ret is not None):
|
||||||
|
return False, "operatorAuth error: %s" % ret
|
||||||
|
|
||||||
|
if adept_ns:
|
||||||
|
# "new" ADE
|
||||||
|
fulfill_req_signed = "<?xml version=\"1.0\"?>\n" + etree.tostring(fulfill_request_xml, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("utf-8")
|
||||||
|
else:
|
||||||
|
# ADE 1.7.2
|
||||||
|
fulfill_req_signed = etree.tostring(fulfill_request_xml, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("utf-8")
|
||||||
|
|
||||||
|
#print("will send:\n %s" % fulfill_req_signed)
|
||||||
|
#print("Sending fulfill request to %s" % fulfillURL)
|
||||||
|
|
||||||
|
# For debugging only
|
||||||
|
# fulfillURL = fulfillURL.replace("https:", "http:")
|
||||||
|
|
||||||
|
replyData = sendRequestDocu(fulfill_req_signed, fulfillURL).decode("utf-8")
|
||||||
|
|
||||||
|
if "<error" in replyData:
|
||||||
|
if "E_ADEPT_DISTRIBUTOR_AUTH" in replyData:
|
||||||
|
# This distributor *always* wants authentication, so force that again
|
||||||
|
ret = doOperatorAuth(fulfillURL)
|
||||||
|
|
||||||
|
if (ret is not None):
|
||||||
|
return False, "doOperatorAuth error: %s" % ret
|
||||||
|
|
||||||
|
replyData = sendRequestDocu(fulfill_req_signed, fulfillURL).decode("utf-8")
|
||||||
|
if "<error" in replyData:
|
||||||
|
return False, "Looks like there's been an error during Fulfillment even after auth: %s" % replyData
|
||||||
|
else:
|
||||||
|
return False, "Looks like there's been an error during Fulfillment: %s" % replyData
|
||||||
|
|
||||||
|
if verbose_logging:
|
||||||
|
print("fulfillmentResult:")
|
||||||
|
print(replyData)
|
||||||
|
|
||||||
|
adobe_fulfill_response = etree.fromstring(replyData)
|
||||||
|
NSMAP = { "adept" : "http://ns.adobe.com/adept" }
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
|
||||||
|
licenseURL = adobe_fulfill_response.find("./%s/%s/%s/%s" % (adNS("fulfillmentResult"), adNS("resourceItemInfo"), adNS("licenseToken"), adNS("licenseURL"))).text
|
||||||
|
|
||||||
|
if adept_ns:
|
||||||
|
if do_notify:
|
||||||
|
print("Notifying server ...")
|
||||||
|
success, response = performFulfillmentNotification(adobe_fulfill_response)
|
||||||
|
if not success:
|
||||||
|
print("Some errors occurred during notify: ")
|
||||||
|
print(response)
|
||||||
|
print("The book was probably still downloaded correctly.")
|
||||||
|
else:
|
||||||
|
print("Not notifying any server since that was disabled.")
|
||||||
|
else:
|
||||||
|
print("Skipping notify, not supported properly with ADE 1.7.2")
|
||||||
|
|
||||||
|
|
||||||
|
is_returnable = False
|
||||||
|
try:
|
||||||
|
is_returnable_tx = adobe_fulfill_response.find("./%s/%s" % (adNS("fulfillmentResult"), adNS("returnable"))).text
|
||||||
|
if is_returnable_tx.lower() == "true":
|
||||||
|
is_returnable = True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if (is_returnable and do_notify and adept_ns):
|
||||||
|
# Only support loan returning if we also notified ACS.
|
||||||
|
# Otherwise the server gets confused and we don't want that.
|
||||||
|
# Also, only do that for new-ish ADE and not for ADE 1.7.2
|
||||||
|
updateLoanReturnData(adobe_fulfill_response)
|
||||||
|
|
||||||
|
success, response = fetchLicenseServiceCertificate(licenseURL, operatorURL)
|
||||||
|
|
||||||
|
if success is False:
|
||||||
|
print("Why did the license download fail?")
|
||||||
|
print("This is probably a temporary error on the distributor's server")
|
||||||
|
return False, response
|
||||||
|
|
||||||
|
return True, replyData
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def updateLoanReturnData(fulfillmentResultToken, forceTestBehaviour=False):
|
||||||
|
|
||||||
|
NSMAP = { "adept" : "http://ns.adobe.com/adept" }
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
dcNS = lambda tag: '{%s}%s' % ('http://purl.org/dc/elements/1.1/', tag)
|
||||||
|
|
||||||
|
try:
|
||||||
|
fulfillment_id = fulfillmentResultToken.find("./%s/%s/%s/%s" % (adNS("fulfillmentResult"), adNS("resourceItemInfo"), adNS("licenseToken"), adNS("fulfillment"))).text
|
||||||
|
if (fulfillment_id is None):
|
||||||
|
print("Fulfillment ID not found, can't generate loan token")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except:
|
||||||
|
print("Loan token error")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
operatorURL = fulfillmentResultToken.find("./%s/%s/%s/%s" % (adNS("fulfillmentResult"), adNS("resourceItemInfo"), adNS("licenseToken"), adNS("operatorURL"))).text
|
||||||
|
except:
|
||||||
|
print("OperatorURL missing")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
book_name = fulfillmentResultToken.find("./%s/%s/%s/%s" % (adNS("fulfillmentResult"), adNS("resourceItemInfo"), adNS("metadata"), dcNS("title"))).text
|
||||||
|
|
||||||
|
userUUID = fulfillmentResultToken.find("./%s/%s/%s/%s" % (adNS("fulfillmentResult"), adNS("resourceItemInfo"), adNS("licenseToken"), adNS("user"))).text
|
||||||
|
try:
|
||||||
|
deviceUUID = fulfillmentResultToken.find("./%s/%s/%s/%s" % (adNS("fulfillmentResult"), adNS("resourceItemInfo"), adNS("licenseToken"), adNS("device"))).text
|
||||||
|
except:
|
||||||
|
activationxml = etree.parse(get_activation_xml_path())
|
||||||
|
deviceUUID = activationxml.find("./%s/%s" % (adNS("activationToken"), adNS("device"))).text
|
||||||
|
|
||||||
|
|
||||||
|
permissions = fulfillmentResultToken.find("./%s/%s/%s/%s" % (adNS("fulfillmentResult"), adNS("resourceItemInfo"), adNS("licenseToken"), adNS("permissions")))
|
||||||
|
|
||||||
|
display = permissions.findall("./%s" % (adNS("display")))[0]
|
||||||
|
|
||||||
|
try:
|
||||||
|
dsp_until = display.find("./%s" % (adNS("until"))).text
|
||||||
|
except:
|
||||||
|
print("error with DSP")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if (dsp_until is None):
|
||||||
|
print("No validUntil thing")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# "userUUID" is the user UUID
|
||||||
|
# "deviceUUID" is the device UUID
|
||||||
|
# "loanID" is the loan ID
|
||||||
|
# "validUntil" is how long it's valid
|
||||||
|
|
||||||
|
new_loan_record = {
|
||||||
|
"book_name": book_name,
|
||||||
|
"user": userUUID,
|
||||||
|
"device": deviceUUID,
|
||||||
|
"loanID": fulfillment_id,
|
||||||
|
"operatorURL": operatorURL,
|
||||||
|
"validUntil": dsp_until
|
||||||
|
}
|
||||||
|
|
||||||
|
if forceTestBehaviour:
|
||||||
|
return new_loan_record
|
||||||
|
|
||||||
|
addLoanRecordToConfigFile(new_loan_record)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def addLoanRecordToConfigFile(new_loan_record):
|
||||||
|
|
||||||
|
try:
|
||||||
|
import calibre_plugins.deacsm.prefs as prefs # type: ignore
|
||||||
|
deacsmprefs = prefs.ACSMInput_Prefs()
|
||||||
|
except:
|
||||||
|
print("Exception while reading config file")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
error_counter = 0
|
||||||
|
last_token = None
|
||||||
|
random_identifier = None
|
||||||
|
|
||||||
|
while True:
|
||||||
|
|
||||||
|
if error_counter >= 10:
|
||||||
|
print("Took us 10 attempts to acquire loan token lock, still didn't work.")
|
||||||
|
print("(still the same token %s)" % (deacsmprefs["loan_identifier_token"]))
|
||||||
|
print("If you see this error message please open a bug report.")
|
||||||
|
|
||||||
|
|
||||||
|
# "Mark" the current access with a random token, to prevent multiple instances
|
||||||
|
# of the plugin overwriting eachother's data.
|
||||||
|
deacsmprefs.refresh()
|
||||||
|
if deacsmprefs["loan_identifier_token"] == 0:
|
||||||
|
random_identifier = random.getrandbits(64)
|
||||||
|
deacsmprefs.set("loan_identifier_token", random_identifier)
|
||||||
|
deacsmprefs.commit()
|
||||||
|
deacsmprefs.refresh()
|
||||||
|
if random_identifier != deacsmprefs["loan_identifier_token"]:
|
||||||
|
#print("we broke another thread's token, try again")
|
||||||
|
last_token = deacsmprefs["loan_identifier_token"]
|
||||||
|
error_counter = error_counter + 1
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if last_token != deacsmprefs["loan_identifier_token"]:
|
||||||
|
#print("Token changed in the meantime ...")
|
||||||
|
# Give it another 5 tries
|
||||||
|
error_counter = max(0, error_counter - 5)
|
||||||
|
pass
|
||||||
|
|
||||||
|
last_token = deacsmprefs["loan_identifier_token"]
|
||||||
|
#print("waiting on another thread ...")
|
||||||
|
sleeptime = random.randrange(2, 10) / 1000
|
||||||
|
print(str(sleeptime))
|
||||||
|
time.sleep(sleeptime)
|
||||||
|
error_counter = error_counter + 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Okay, now this thread can "use" the config list, and no other thread should overwrite it ...
|
||||||
|
# Check if that exact loan is already in the list, and if so, delete it:
|
||||||
|
done = False
|
||||||
|
while not done:
|
||||||
|
done = True
|
||||||
|
for book in deacsmprefs["list_of_rented_books"]:
|
||||||
|
if book["loanID"] == new_loan_record["loanID"]:
|
||||||
|
done = False
|
||||||
|
deacsmprefs["list_of_rented_books"].remove(book)
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
# Add all necessary information for a book return to the JSON array.
|
||||||
|
# The config widget can then read this and present a list of not-yet-returned
|
||||||
|
# books, and can then return them.
|
||||||
|
# Also, the config widget is responsible for cleaning up that list once a book's validity period is up.
|
||||||
|
deacsmprefs["list_of_rented_books"].append(new_loan_record)
|
||||||
|
|
||||||
|
# Okay, now we added our loan record.
|
||||||
|
# Remove the identifier token so other threads can use the config again:
|
||||||
|
deacsmprefs.commit()
|
||||||
|
deacsmprefs.refresh()
|
||||||
|
if deacsmprefs["loan_identifier_token"] != random_identifier:
|
||||||
|
print("Another thread stole the loan token while we were working with it - that's not supposed to happen ...")
|
||||||
|
print("If you see this message, please open a bug report.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
deacsmprefs.set("loan_identifier_token", 0)
|
||||||
|
deacsmprefs.commit()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def tryReturnBook(bookData):
|
||||||
|
|
||||||
|
|
||||||
|
verbose_logging = False
|
||||||
|
try:
|
||||||
|
import calibre_plugins.deacsm.prefs as prefs
|
||||||
|
deacsmprefs = prefs.ACSMInput_Prefs()
|
||||||
|
verbose_logging = deacsmprefs["detailed_logging"]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = bookData["user"]
|
||||||
|
loanID = bookData["loanID"]
|
||||||
|
device = bookData["device"]
|
||||||
|
operatorURL = bookData["operatorURL"]
|
||||||
|
except:
|
||||||
|
print("Invalid book data!")
|
||||||
|
return False, "Invalid book data"
|
||||||
|
|
||||||
|
|
||||||
|
req_data = "<?xml version=\"1.0\"?>"
|
||||||
|
req_data += "<adept:loanReturn xmlns:adept=\"http://ns.adobe.com/adept\">"
|
||||||
|
req_data += "<adept:user>%s</adept:user>" % (user)
|
||||||
|
if device is not None:
|
||||||
|
req_data += "<adept:device>%s</adept:device>" % (device)
|
||||||
|
req_data += "<adept:loan>%s</adept:loan>" % (loanID)
|
||||||
|
req_data += addNonce()
|
||||||
|
req_data += "</adept:loanReturn>"
|
||||||
|
|
||||||
|
NSMAP = { "adept" : "http://ns.adobe.com/adept" }
|
||||||
|
etree.register_namespace("adept", NSMAP["adept"])
|
||||||
|
|
||||||
|
full_text_xml = etree.fromstring(req_data)
|
||||||
|
|
||||||
|
signature = sign_node(full_text_xml)
|
||||||
|
if (signature is None):
|
||||||
|
print("SIGN ERROR!")
|
||||||
|
return False, "Sign error"
|
||||||
|
|
||||||
|
etree.SubElement(full_text_xml, etree.QName(NSMAP["adept"], "signature")).text = signature
|
||||||
|
|
||||||
|
print("Notifying loan return server %s" % (operatorURL + "/LoanReturn"))
|
||||||
|
doc_send = "<?xml version=\"1.0\"?>\n" + etree.tostring(full_text_xml, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("utf-8")
|
||||||
|
if verbose_logging:
|
||||||
|
print(doc_send)
|
||||||
|
|
||||||
|
|
||||||
|
retval = sendRequestDocu(doc_send, operatorURL + "/LoanReturn").decode("utf-8")
|
||||||
|
|
||||||
|
if "<error" in retval:
|
||||||
|
print("Loan return failed: %s" % (retval))
|
||||||
|
return False, retval
|
||||||
|
elif "<envelope" in retval:
|
||||||
|
print("Loan return successful")
|
||||||
|
if verbose_logging:
|
||||||
|
print(retval)
|
||||||
|
bl, txt = performFulfillmentNotification(etree.fromstring(retval), True, user=user, device=device)
|
||||||
|
if not bl:
|
||||||
|
print("Error while notifying of book return. Book's probably still been returned properly.")
|
||||||
|
return True, retval
|
||||||
|
else:
|
||||||
|
print("Invalid loan return response: %s" % (retval))
|
||||||
|
return False, retval
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def performFulfillmentNotification(fulfillmentResultToken, forceOptional = False, user = None, device = None):
|
||||||
|
|
||||||
|
verbose_logging = False
|
||||||
|
try:
|
||||||
|
import calibre_plugins.deacsm.prefs as prefs
|
||||||
|
deacsmprefs = prefs.ACSMInput_Prefs()
|
||||||
|
verbose_logging = deacsmprefs["detailed_logging"]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
NSMAP = { "adept" : "http://ns.adobe.com/adept" }
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
|
||||||
|
# Debug output for PassHash testing:
|
||||||
|
# print(etree.tostring(fulfillmentResultToken, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("utf-8"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
notifiers = fulfillmentResultToken.findall("./%s/%s" % (adNS("fulfillmentResult"), adNS("notify")))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if len(notifiers) == 0:
|
||||||
|
try:
|
||||||
|
notifiers = fulfillmentResultToken.findall("./%s" % (adNS("notify")))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if len(notifiers) == 0:
|
||||||
|
try:
|
||||||
|
notifiers = fulfillmentResultToken.findall("./%s/%s" % (adNS("envelope"), adNS("notify")))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if len(notifiers) == 0:
|
||||||
|
print("<notify> tag not found. Guess nobody wants to be notified.")
|
||||||
|
#print(etree.tostring(fulfillmentResultToken, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("utf-8"))
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
|
||||||
|
errmsg = ""
|
||||||
|
errmsg_crit = ""
|
||||||
|
|
||||||
|
for element in notifiers:
|
||||||
|
|
||||||
|
url = element.find("./%s" % (adNS("notifyURL"))).text
|
||||||
|
body = element.find("./%s" % (adNS("body")))
|
||||||
|
|
||||||
|
critical = True
|
||||||
|
|
||||||
|
if element.get("critical", "yes") == "no":
|
||||||
|
critical = False
|
||||||
|
print("Notifying optional server %s" % (url))
|
||||||
|
else:
|
||||||
|
print("Notifying server %s" % (url))
|
||||||
|
|
||||||
|
|
||||||
|
if (user is None):
|
||||||
|
try:
|
||||||
|
# "Normal" Adobe fulfillment
|
||||||
|
user = fulfillmentResultToken.find("./%s/%s/%s/%s" % (adNS("fulfillmentResult"), adNS("resourceItemInfo"), adNS("licenseToken"), adNS("user"))).text
|
||||||
|
except AttributeError:
|
||||||
|
# B&N Adobe PassHash fulfillment. Doesn't use notifications usually ...
|
||||||
|
#user = body.find("./%s" % (adNS("user"))).text
|
||||||
|
print("Skipping notify due to passHash?")
|
||||||
|
print("If this is not a passHash book pls open a bug report.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (device is None):
|
||||||
|
try:
|
||||||
|
# "Normal" Adobe fulfillment
|
||||||
|
device = fulfillmentResultToken.find("./%s/%s/%s/%s" % (adNS("fulfillmentResult"), adNS("resourceItemInfo"), adNS("licenseToken"), adNS("device"))).text
|
||||||
|
except:
|
||||||
|
print("Missing deviceID for loan metadata ... why?")
|
||||||
|
print("Reading from device.xml instead.")
|
||||||
|
# Lets try to read this from the activation ...
|
||||||
|
activationxml = etree.parse(get_activation_xml_path())
|
||||||
|
device = activationxml.find("./%s/%s" % (adNS("activationToken"), adNS("device"))).text
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
full_text = "<adept:notification xmlns:adept=\"http://ns.adobe.com/adept\">"
|
||||||
|
full_text += "<adept:user>%s</adept:user>" % user
|
||||||
|
full_text += "<adept:device>%s</adept:device>" % device
|
||||||
|
|
||||||
|
|
||||||
|
# ADE 4.0 apparently changed the order of these two elements.
|
||||||
|
# I still don't know exactly how this order is determined, but in most cases
|
||||||
|
# ADE 4+ has the body first, then the nonce, while ADE 3 and lower usually has nonce first, then body.
|
||||||
|
# It probably doesn't matter, but still, we want to behave exactly like ADE, so check the version number:
|
||||||
|
|
||||||
|
devicexml = etree.parse(get_device_path())
|
||||||
|
for f in devicexml.findall("./%s" % (adNS("version"))):
|
||||||
|
if f.get("name") == "hobbes":
|
||||||
|
version = f.get("value")
|
||||||
|
|
||||||
|
try:
|
||||||
|
v_idx = VAR_VER_HOBBES_VERSIONS.index(version)
|
||||||
|
clientVersion = VAR_VER_BUILD_IDS[v_idx]
|
||||||
|
except:
|
||||||
|
clientVersion = 0
|
||||||
|
|
||||||
|
if (clientVersion >= VAR_VER_USE_DIFFERENT_NOTIFICATION_XML_ORDER):
|
||||||
|
full_text += etree.tostring(body, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("utf-8")
|
||||||
|
full_text += addNonce()
|
||||||
|
else:
|
||||||
|
full_text += addNonce()
|
||||||
|
full_text += etree.tostring(body, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
full_text += "</adept:notification>"
|
||||||
|
|
||||||
|
|
||||||
|
NSMAP = { "adept" : "http://ns.adobe.com/adept" }
|
||||||
|
etree.register_namespace("adept", NSMAP["adept"])
|
||||||
|
|
||||||
|
full_text_xml = etree.fromstring(full_text)
|
||||||
|
|
||||||
|
signature = sign_node(full_text_xml)
|
||||||
|
if (signature is None):
|
||||||
|
print("SIGN ERROR!")
|
||||||
|
continue
|
||||||
|
|
||||||
|
etree.SubElement(full_text_xml, etree.QName(NSMAP["adept"], "signature")).text = signature
|
||||||
|
|
||||||
|
doc_send = "<?xml version=\"1.0\"?>\n" + etree.tostring(full_text_xml, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("utf-8")
|
||||||
|
|
||||||
|
# Debug: Print notify request
|
||||||
|
if (verbose_logging):
|
||||||
|
print("Notify payload XML:")
|
||||||
|
print(doc_send)
|
||||||
|
|
||||||
|
try:
|
||||||
|
code, msg = sendRequestDocuRC(doc_send, url)
|
||||||
|
except:
|
||||||
|
if not critical:
|
||||||
|
print("There was an error during an optional fulfillment notification:")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
print("Continuing execution ...")
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
print("Error during critical notification:")
|
||||||
|
raise
|
||||||
|
|
||||||
|
try:
|
||||||
|
msg = msg.decode("utf-8")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if verbose_logging:
|
||||||
|
print("MSG:")
|
||||||
|
print(msg)
|
||||||
|
|
||||||
|
if "<error" in msg:
|
||||||
|
print("Fulfillment notification error: %s" % (msg))
|
||||||
|
errmsg += "ERROR\n" + url + "\n" + msg + "\n\n"
|
||||||
|
if critical:
|
||||||
|
errmsg_crit += "ERROR\n" + url + "\n" + msg + "\n\n"
|
||||||
|
|
||||||
|
elif "<success" in msg:
|
||||||
|
print("Fulfillment notification successful.")
|
||||||
|
elif code == 204:
|
||||||
|
print("Fulfillment notification successful (204).")
|
||||||
|
else:
|
||||||
|
print("Weird Fulfillment Notification response: %s" % (msg))
|
||||||
|
errmsg += "ERROR\n" + url + "\n" + msg + "\n\n"
|
||||||
|
if critical:
|
||||||
|
errmsg_crit += "ERROR\n" + url + "\n" + msg + "\n\n"
|
||||||
|
|
||||||
|
|
||||||
|
if device is None and errmsg_crit != "":
|
||||||
|
errmsg_crit = ""
|
||||||
|
print("Skipping critical notification failure due to weird book.")
|
||||||
|
|
||||||
|
|
||||||
|
if errmsg_crit == "":
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
return False, errmsg
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def fetchLicenseServiceCertificate(licenseURL, operatorURL):
|
||||||
|
|
||||||
|
# Check if we already have a cert for this URL:
|
||||||
|
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
|
||||||
|
NSMAP = { "adept" : "http://ns.adobe.com/adept" }
|
||||||
|
etree.register_namespace("adept", NSMAP["adept"])
|
||||||
|
|
||||||
|
activationxml = etree.parse(get_activation_xml_path())
|
||||||
|
|
||||||
|
try:
|
||||||
|
licInfo = activationxml.findall("./%s/%s" % (adNS("licenseServices"), adNS("licenseServiceInfo")))
|
||||||
|
|
||||||
|
for member in licInfo:
|
||||||
|
if member.find("./%s" % (adNS("licenseURL"))).text == licenseURL:
|
||||||
|
return True, "Done"
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Check if list exists:
|
||||||
|
list = activationxml.find("./%s" % (adNS("licenseServices")))
|
||||||
|
|
||||||
|
if list is None:
|
||||||
|
x = etree.SubElement(activationxml.getroot(), etree.QName(NSMAP["adept"], "licenseServices"), nsmap=NSMAP)
|
||||||
|
list = activationxml.find("./%s" % (adNS("licenseServices")))
|
||||||
|
if list is None:
|
||||||
|
return False, "Err, this list should not be none right now ..."
|
||||||
|
|
||||||
|
info_entry = etree.SubElement(list, etree.QName(NSMAP["adept"], "licenseServiceInfo"))
|
||||||
|
|
||||||
|
licenseServiceInfoReq = operatorURL + "/LicenseServiceInfo?licenseURL=" + licenseURL
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = sendHTTPRequest(licenseServiceInfoReq).decode("utf-8")
|
||||||
|
except:
|
||||||
|
return False, "HTTP download for the licenseServiceInfo failed ... why?"
|
||||||
|
|
||||||
|
if "<error" in response:
|
||||||
|
return False, "Looks like that failed: %s" % response
|
||||||
|
elif "<licenseServiceInfo" in response:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return False, "Looks like that failed: %s" % response
|
||||||
|
|
||||||
|
|
||||||
|
#print(response)
|
||||||
|
|
||||||
|
responseXML = etree.fromstring(response)
|
||||||
|
|
||||||
|
server_cert = responseXML.find("./%s" % (adNS("certificate"))).text
|
||||||
|
server_lic_url = responseXML.find("./%s" % (adNS("licenseURL"))).text
|
||||||
|
|
||||||
|
etree.SubElement(info_entry, etree.QName(NSMAP["adept"], "licenseURL")).text = server_lic_url
|
||||||
|
etree.SubElement(info_entry, etree.QName(NSMAP["adept"], "certificate")).text = server_cert
|
||||||
|
|
||||||
|
f = open(get_activation_xml_path(), "w")
|
||||||
|
f.write("<?xml version=\"1.0\"?>\n")
|
||||||
|
f.write(etree.tostring(activationxml, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("utf-8"))
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
return True, "Done"
|
||||||
|
|
||||||
|
|
||||||
|
|
286
setup/libpdf.py
Normal file
286
setup/libpdf.py
Normal file
|
@ -0,0 +1,286 @@
|
||||||
|
import sys, os, zlib, base64, time
|
||||||
|
|
||||||
|
class BackwardReader:
|
||||||
|
|
||||||
|
def __init__(self, file):
|
||||||
|
self.file = file
|
||||||
|
|
||||||
|
|
||||||
|
def readlines(self):
|
||||||
|
BLKSIZE = 4096
|
||||||
|
# Move reader to the end of file
|
||||||
|
self.file.seek(0, os.SEEK_END)
|
||||||
|
if sys.version_info[0] >= 3:
|
||||||
|
buffer = bytearray()
|
||||||
|
else:
|
||||||
|
buffer = ""
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if sys.version_info[0] >= 3:
|
||||||
|
pos_newline = buffer.rfind(bytes([0x0a]))
|
||||||
|
else:
|
||||||
|
pos_newline = buffer.rfind("\n")
|
||||||
|
|
||||||
|
# Get the current position of the reader
|
||||||
|
current_pos = self.file.tell()
|
||||||
|
if pos_newline != -1:
|
||||||
|
# Newline is found
|
||||||
|
line = buffer[pos_newline+1:]
|
||||||
|
buffer = buffer[:pos_newline]
|
||||||
|
if sys.version_info[0] >= 3:
|
||||||
|
yield line.decode("latin-1")
|
||||||
|
else:
|
||||||
|
yield line
|
||||||
|
|
||||||
|
elif current_pos:
|
||||||
|
# Need to fill the buffer
|
||||||
|
to_read = min(BLKSIZE, current_pos)
|
||||||
|
self.file.seek(current_pos-to_read, 0)
|
||||||
|
buffer = self.file.read(to_read) + buffer
|
||||||
|
self.file.seek(current_pos-to_read, 0)
|
||||||
|
if current_pos is to_read:
|
||||||
|
if sys.version_info[0] >= 3:
|
||||||
|
buffer = bytes([0x0a]) + buffer
|
||||||
|
else:
|
||||||
|
buffer = "\n" + buffer
|
||||||
|
else:
|
||||||
|
# Start of file
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def trim_encrypt_string(encrypt):
|
||||||
|
|
||||||
|
string_list = list(encrypt)
|
||||||
|
strlen = len(encrypt)
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
bracket_count = 0
|
||||||
|
while (i < strlen):
|
||||||
|
if string_list[i] == "<" and string_list[i+1] == "<":
|
||||||
|
bracket_count += 1
|
||||||
|
|
||||||
|
if string_list[i] == ">" and string_list[i+1] == ">":
|
||||||
|
bracket_count -= 1
|
||||||
|
|
||||||
|
if bracket_count == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
i = i + 1
|
||||||
|
|
||||||
|
len_to_use = i+2
|
||||||
|
|
||||||
|
return encrypt[0:len_to_use]
|
||||||
|
|
||||||
|
def cleanup_encrypt_element(element):
|
||||||
|
|
||||||
|
if element.startswith("ID[<"):
|
||||||
|
element = element.replace("><", "> <")
|
||||||
|
|
||||||
|
element = ' '.join(element.split())
|
||||||
|
element = element.replace("[ ", "[").replace("] ", "]")
|
||||||
|
|
||||||
|
return element
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def deflate_and_base64_encode( string_val ):
|
||||||
|
zlibbed_str = zlib.compress( string_val )
|
||||||
|
compressed_string = zlibbed_str[2:-4]
|
||||||
|
return base64.b64encode( compressed_string )
|
||||||
|
|
||||||
|
def update_ebx_with_keys(ebx_data, adept_license, ebx_bookid):
|
||||||
|
|
||||||
|
b64data = deflate_and_base64_encode(adept_license.encode("utf-8")).decode("utf-8")
|
||||||
|
|
||||||
|
ebx_new = ebx_data[:-2]
|
||||||
|
ebx_new += "/EBX_BOOKID(%s)/ADEPT_LICENSE(%s)>>" % (ebx_bookid, b64data)
|
||||||
|
|
||||||
|
return ebx_new
|
||||||
|
|
||||||
|
|
||||||
|
def find_ebx(filename_in):
|
||||||
|
find_ebx_start = int(time.time() * 1000)
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
fl = open(filename_in, "rb")
|
||||||
|
br = BackwardReader(fl)
|
||||||
|
|
||||||
|
for line in br.readlines():
|
||||||
|
i = i + 1
|
||||||
|
if "/EBX_HANDLER/" in line:
|
||||||
|
find_ebx_end = int(time.time() * 1000)
|
||||||
|
print("Found EBX after %d attempts - took %d ms" % (i, find_ebx_end - find_ebx_start))
|
||||||
|
print()
|
||||||
|
return line
|
||||||
|
|
||||||
|
find_ebx_end = int(time.time() * 1000)
|
||||||
|
print("Error: Did not find EBX_HANDLER - took %d ms" % (find_ebx_end - find_ebx_start))
|
||||||
|
return None
|
||||||
|
|
||||||
|
def find_enc(filename_in):
|
||||||
|
find_enc_start = int(time.time() * 1000)
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
fl = open(filename_in, "rb")
|
||||||
|
br = BackwardReader(fl)
|
||||||
|
|
||||||
|
for line in br.readlines():
|
||||||
|
i = i + 1
|
||||||
|
is_encrypt_normal = "R/Encrypt" in line and "R/ID" in line
|
||||||
|
is_encrypt_odd = "R" in line and "/Encrypt" in line and "/ID" in line
|
||||||
|
if is_encrypt_normal or is_encrypt_odd:
|
||||||
|
|
||||||
|
find_enc_end = int(time.time() * 1000)
|
||||||
|
print("Found ENC after %d attempts - took %d ms" % (i, find_enc_end - find_enc_start))
|
||||||
|
if is_encrypt_odd:
|
||||||
|
print("Odd formatting of encryption blob?")
|
||||||
|
print("If this doesn't work correctly please open a bug report.")
|
||||||
|
|
||||||
|
return line
|
||||||
|
|
||||||
|
find_enc_end = int(time.time() * 1000)
|
||||||
|
print("Error: Did not find ENC - took %d ms" % (find_enc_end - find_enc_start))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def patch_drm_into_pdf(filename_in, adept_license_string, filename_out, ebx_bookid):
|
||||||
|
|
||||||
|
drm_start_time = int(time.time() * 1000)
|
||||||
|
|
||||||
|
trailer = ""
|
||||||
|
trailer_idx = 0
|
||||||
|
|
||||||
|
startxref_offset = 0
|
||||||
|
prevline = ""
|
||||||
|
|
||||||
|
|
||||||
|
fl = open(filename_in, "rb")
|
||||||
|
br = BackwardReader(fl)
|
||||||
|
|
||||||
|
print("Searching for startxref ...")
|
||||||
|
for line in br.readlines():
|
||||||
|
trailer_idx += 1
|
||||||
|
trailer = line + "\n" + trailer
|
||||||
|
|
||||||
|
#print ("LINE: " + line)
|
||||||
|
|
||||||
|
if (trailer_idx > 10):
|
||||||
|
print("Took more than 10 attempts to find startxref ...")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if (line == "startxref"):
|
||||||
|
startxref_offset = int(prevline)
|
||||||
|
print("Got startxref: %d" % (startxref_offset))
|
||||||
|
break
|
||||||
|
prevline = line
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
r_encrypt_offs1 = 0
|
||||||
|
r_encrypt_offs2 = 0
|
||||||
|
|
||||||
|
encrypt = None
|
||||||
|
|
||||||
|
|
||||||
|
encrypt = find_enc(filename_in)
|
||||||
|
if encrypt is None:
|
||||||
|
print("Error, enc not found")
|
||||||
|
return False
|
||||||
|
|
||||||
|
line_split = encrypt.split(' ')
|
||||||
|
next = 0
|
||||||
|
for element in line_split:
|
||||||
|
if element == "R/Encrypt" or element == "/Encrypt":
|
||||||
|
next = 2
|
||||||
|
continue
|
||||||
|
if next == 2:
|
||||||
|
r_encrypt_offs1 = element
|
||||||
|
next = 1
|
||||||
|
continue
|
||||||
|
if next == 1:
|
||||||
|
r_encrypt_offs2 = element
|
||||||
|
next = 0
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
# read EBX element:
|
||||||
|
ebx_elem = find_ebx(filename_in)
|
||||||
|
|
||||||
|
if (ebx_elem is None):
|
||||||
|
print("Err: EBX is None")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
print("Encryption handler:")
|
||||||
|
print(encrypt)
|
||||||
|
print()
|
||||||
|
print("EBX handler:")
|
||||||
|
print(ebx_elem)
|
||||||
|
print()
|
||||||
|
|
||||||
|
encrypt = trim_encrypt_string(encrypt)
|
||||||
|
print("Trimmed encryption handler:")
|
||||||
|
print(encrypt)
|
||||||
|
print()
|
||||||
|
|
||||||
|
ebx_elem = update_ebx_with_keys(ebx_elem, adept_license_string, ebx_bookid)
|
||||||
|
print("Updated EBX handler not logged due to sensitive data")
|
||||||
|
#print(ebx_elem)
|
||||||
|
|
||||||
|
filesize_str = str(os.path.getsize(filename_in))
|
||||||
|
filesize_pad = filesize_str.zfill(10)
|
||||||
|
|
||||||
|
additional_data = "\r"
|
||||||
|
additional_data += r_encrypt_offs1 + " " + r_encrypt_offs2 + " " + "obj" + "\r"
|
||||||
|
additional_data += ebx_elem
|
||||||
|
additional_data += "\r"
|
||||||
|
additional_data += "endobj"
|
||||||
|
|
||||||
|
ptr = int(filesize_str) + len(additional_data)
|
||||||
|
|
||||||
|
additional_data += "\rxref\r" + r_encrypt_offs1 + " " + str((int(r_encrypt_offs2) + 1)) + "\r"
|
||||||
|
additional_data += filesize_pad + " 00000 n" + "\r\n"
|
||||||
|
additional_data += "trailer"
|
||||||
|
additional_data += "\r"
|
||||||
|
|
||||||
|
arr_root_str = encrypt.split('/')
|
||||||
|
did_prev = False
|
||||||
|
for elem in arr_root_str:
|
||||||
|
if elem.startswith("Prev"):
|
||||||
|
did_prev = True
|
||||||
|
additional_data += "Prev " + str(startxref_offset)
|
||||||
|
#print("Replacing prev from '%s' to '%s'" % (elem, "Prev " + startxref))
|
||||||
|
else:
|
||||||
|
additional_data += cleanup_encrypt_element(elem)
|
||||||
|
additional_data += "/"
|
||||||
|
|
||||||
|
if not did_prev:
|
||||||
|
# remove two >> at end
|
||||||
|
additional_data = additional_data[:-3]
|
||||||
|
additional_data += "/Prev " + str(startxref_offset) + ">>" + "/"
|
||||||
|
#print("Faking Prev %s" % startxref)
|
||||||
|
|
||||||
|
additional_data = additional_data[:-1]
|
||||||
|
|
||||||
|
additional_data += "\r" + "startxref\r" + str(ptr) + "\r" + "%%EOF"
|
||||||
|
|
||||||
|
#print("Appending DRM data: %s" % (additional_data))
|
||||||
|
|
||||||
|
|
||||||
|
inp = open(filename_in, "rb")
|
||||||
|
|
||||||
|
out = open(filename_out, "wb")
|
||||||
|
out.write(inp.read())
|
||||||
|
out.write(additional_data.encode("latin-1"))
|
||||||
|
inp.close()
|
||||||
|
out.close()
|
||||||
|
|
||||||
|
drm_end_time = int(time.time() * 1000)
|
||||||
|
|
||||||
|
print("Whole DRM patching took %d milliseconds." % (drm_end_time - drm_start_time))
|
||||||
|
print()
|
||||||
|
return True
|
95
setup/login_account.py
Normal file
95
setup/login_account.py
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
'''
|
||||||
|
This is an experimental Python version of libgourou.
|
||||||
|
'''
|
||||||
|
|
||||||
|
from setup.libadobe import createDeviceKeyFile, FILE_DEVICEKEY, FILE_DEVICEXML, FILE_ACTIVATIONXML
|
||||||
|
from setup.libadobeAccount import createDeviceFile, createUser, signIn, activateDevice, exportAccountEncryptionKeyDER, getAccountUUID
|
||||||
|
from os.path import exists
|
||||||
|
|
||||||
|
VAR_MAIL = ""
|
||||||
|
VAR_PASS = ""
|
||||||
|
VAR_VER = 1 # None # 1 for ADE2.0.1, 2 for ADE3.0.1
|
||||||
|
KEYPATH = "adobekey.der"
|
||||||
|
|
||||||
|
#################################################################
|
||||||
|
|
||||||
|
def takeInput():
|
||||||
|
|
||||||
|
global VAR_MAIL
|
||||||
|
global VAR_PASS
|
||||||
|
|
||||||
|
VAR_MAIL = input("Enter Mail: ")
|
||||||
|
VAR_PASS = input("Enter Password: ")
|
||||||
|
|
||||||
|
if VAR_MAIL == "" or VAR_MAIL == "":
|
||||||
|
print("It cannot be empty")
|
||||||
|
print()
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def loginAndGetKey():
|
||||||
|
|
||||||
|
global VAR_MAIL
|
||||||
|
global VAR_PASS
|
||||||
|
global VAR_VER
|
||||||
|
global KEYPATH
|
||||||
|
|
||||||
|
# acc files
|
||||||
|
if (not exists(FILE_ACTIVATIONXML)) or (not exists(FILE_DEVICEXML)) or (not exists(FILE_DEVICEKEY)):
|
||||||
|
|
||||||
|
takeInput()
|
||||||
|
print("Logging in")
|
||||||
|
print()
|
||||||
|
|
||||||
|
createDeviceKeyFile()
|
||||||
|
|
||||||
|
success = createDeviceFile(True, VAR_VER)
|
||||||
|
if (success is False):
|
||||||
|
print("Error, couldn't create device file.")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
success, resp = createUser(VAR_VER, None)
|
||||||
|
if (success is False):
|
||||||
|
print("Error, couldn't create user: %s" % resp)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
success, resp = signIn("AdobeID", VAR_MAIL, VAR_PASS)
|
||||||
|
|
||||||
|
if (success is False):
|
||||||
|
print("Login unsuccessful: " + resp)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
success, resp = activateDevice(VAR_VER, None)
|
||||||
|
if (success is False):
|
||||||
|
print("Couldn't activate device: " + resp)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
print("Authorized to account " + VAR_MAIL)
|
||||||
|
|
||||||
|
|
||||||
|
# KEY
|
||||||
|
if not exists(KEYPATH):
|
||||||
|
print("Exporting keys ...")
|
||||||
|
try:
|
||||||
|
account_uuid = getAccountUUID()
|
||||||
|
if (account_uuid is not None):
|
||||||
|
filename = KEYPATH
|
||||||
|
success = exportAccountEncryptionKeyDER(filename)
|
||||||
|
if (success is False):
|
||||||
|
print("Couldn't export key.")
|
||||||
|
exit(1)
|
||||||
|
print("Successfully exported key for account " + VAR_MAIL + " to file " + filename)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("failed")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
print('All Set')
|
||||||
|
print()
|
Loading…
Reference in a new issue