Experimental eReader authorization support

This commit is contained in:
Florian Bach 2021-12-18 11:50:30 +01:00
parent 7373a33709
commit 01d34288c4
4 changed files with 313 additions and 28 deletions

View file

@ -35,6 +35,14 @@ This makes the book available for someone else again, but it does not automatica
Note: You can only return books that you downloaded with version 0.0.9 (or newer) of this plugin. You cannot return books downloaded with ADE or with earlier versions of this plugin.
## Authorizing eReaders
As of v0.0.16, the plugin can also authorize an eReader connected to the Computer through USB. For now, this only works with devices that export their `.adobe-digital-editions` folder through USB. In order to authorize such an eReader, just open the plugin settings and click "Authorize eReader over USB" (only available if the plugin is authorized with an AdobeID). Then select the eReader in the folder selection dialog. This process does not work with eReaders relying on a specific USB driver for the ADE connection such as the Sony PRS-T2 (and probably some other older Sony devices).
Right now, this process is fairly experimental as I do not own a physical eReader that supports this functionality, so I've only been able to test this with a fake, emulated eReader and not with a real device.
Note that this process will use up one of your six mobile/tethered eReader authorizations on your AdobeID. While it is possible to clone a computer activation by exporting it on one computer and importing it on another, this is not possible with eReader authorizations.
## Standalone version
In the folder "calibre-plugin" in this repo (or inside the Calibre plugin ZIP file) there's some scripts that can also be used standalone without Calibre. If you want to use these, you need to extract the whole ZIP file.
@ -49,5 +57,4 @@ Though, generally it's recommended to use the Calibre plugin instead of these st
- Support to copy an authorization from the plugin to an ADE install
- Support for Adobe's "auth" download method instead of the "simple" method.
- Support to authorize an eReader that's connected over USB
- ...

View file

@ -34,7 +34,8 @@
# allow converting an anonymous auth to an AdobeID auth,
# update python-cryptography from 3.4.8 to 36.0.1, update python-rsa from 4.7.2 to 4.8.
# Currently in development:
# Ignore fatal HTTP errors during optional fulfillment notifications.
# Ignore fatal HTTP errors during optional fulfillment notifications,
# allow authorizing an eReader through USB.
PLUGIN_NAME = "DeACSM"
PLUGIN_VERSION_TUPLE = (0, 0, 15)

View file

@ -111,13 +111,13 @@ class ConfigWidget(QWidget):
self.button_convert_anon_to_account.clicked.connect(self.convert_anon_to_account)
ua_group_box_layout.addWidget(self.button_convert_anon_to_account)
#if mail is not None:
if mail is not None:
# We do have an email. Offer to manage devices / eReaders
# Button commented out as this isn't fully implemented yet.
#self.button_manage_ext_device = QtGui.QPushButton(self)
#self.button_manage_ext_device.setText(_("Manage connected eReaders"))
#self.button_manage_ext_device.clicked.connect(self.manage_ext_device)
#ua_group_box_layout.addWidget(self.button_manage_ext_device)
# This isn't really tested a lot ...
self.button_manage_ext_device = QtGui.QPushButton(self)
self.button_manage_ext_device.setText(_("Authorize eReader over USB"))
self.button_manage_ext_device.clicked.connect(self.manage_ext_device)
ua_group_box_layout.addWidget(self.button_manage_ext_device)
self.button_switch_ade_version = QtGui.QPushButton(self)
self.button_switch_ade_version.setText(_("Change ADE version"))
@ -241,6 +241,20 @@ class ConfigWidget(QWidget):
# Unfortunately, I didn't find a nice cross-platform API to query for USB mass storage devices.
# So just open up a folder picker dialog and have the user select the eReader's root folder.
try:
from calibre_plugins.deacsm.libadobe import update_account_path, VAR_VER_HOBBES_VERSIONS
from calibre_plugins.deacsm.libadobeAccount import activateDevice, exportProxyAuth
except:
try:
from libadobe import update_account_path, VAR_VER_HOBBES_VERSIONS
from libadobeAccount import activateDevice, exportProxyAuth
except:
print("{0} v{1}: Error while importing Account stuff".format(PLUGIN_NAME, PLUGIN_VERSION))
traceback.print_exc()
update_account_path(self.deacsmprefs["path_to_account_data"])
info_string, activated, mail = self.get_account_info()
if not activated:
@ -249,7 +263,12 @@ class ConfigWidget(QWidget):
if mail is None:
return
info_dialog(None, "Manage eReader", "Please select the eBook reader you want to manage", show=True, show_copy_button=False)
msg = "Please select the eBook reader you want to link to your account.\n"
msg += "Either select the root drive / mountpoint, or any folder on the eReader.\n"
msg += "Note that this feature is experimental and only works with eReaders that "
msg += "export their .adobe-digital-editions folder."
info_dialog(None, "Authorize eReader", msg, show=True, show_copy_button=False)
dialog = QFileDialog()
dialog.setFileMode(QFileDialog.Directory)
@ -261,7 +280,7 @@ class ConfigWidget(QWidget):
x = dialog.selectedFiles()[0]
if not os.path.isdir(x):
# This is not supposed to happen.
error_dialog(None, "Manage eReader", "Device not found", show=True, show_copy_button=False)
error_dialog(None, "Authorize eReader", "Device not found", show=True, show_copy_button=False)
return
idx = 0
@ -269,7 +288,9 @@ class ConfigWidget(QWidget):
idx = idx + 1
if idx > 15:
# Failsafe, max. 15 folder levels.
break
error_dialog(None, "Authorize eReader", "Didn't find an ADE-compatible eReader in that location. (Too many levels)", show=True, show_copy_button=False)
return
adobe_path = os.path.join(x, ".adobe-digital-editions")
print("Checking " + adobe_path)
if os.path.isdir(adobe_path):
@ -281,11 +302,11 @@ class ConfigWidget(QWidget):
if x_old == x:
# We're at the drive root and still didn't find an activation
error_dialog(None, "Manage eReader", "Didn't find an ADE-compatible eReader in that location. (No Folder)", show=True, show_copy_button=False)
error_dialog(None, "Authorize eReader", "Didn't find an ADE-compatible eReader in that location. (No Folder)", show=True, show_copy_button=False)
return
if not os.path.isfile(os.path.join(adobe_path, "device.xml")):
error_dialog(None, "Manage eReader", "Didn't find an ADE-compatible eReader in that location. (No File)", show=True, show_copy_button=False)
error_dialog(None, "Authorize eReader", "Didn't find an ADE-compatible eReader in that location. (No File)", show=True, show_copy_button=False)
return
dev_xml_path = os.path.join(adobe_path, "device.xml")
@ -297,7 +318,7 @@ class ConfigWidget(QWidget):
devClass = dev_xml_tree.find("./%s" % (adNS("deviceClass"))).text
devName = dev_xml_tree.find("./%s" % (adNS("deviceName"))).text
except:
error_dialog(None, "Manage eReader", "Reader data is invalid.", show=True, show_copy_button=False)
error_dialog(None, "Authorize eReader", "Reader data is invalid.", show=True, show_copy_button=False)
return
try:
@ -310,14 +331,57 @@ class ConfigWidget(QWidget):
print("Found Reader with Class " + devClass + " and Name " + devName)
if os.path.isfile(act_xml_path):
print("Already activated.")
msg = "The given device (Type \""+devClass+"\", Name \""+devName+"\") is already connected to an AdobeID.\n"
msg += "Currently, this plugin does not support un-authorizing an eReader."
error_dialog(None, "Manage eReader", msg, show=True, show_copy_button=False)
return
active_device_act = etree.parse(act_xml_path)
adNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
act_uuid = active_device_act.find("./%s/%s" % (adNS("credentials"), adNS("user"))).text
act_username_name = None
try:
act_username = active_device_act.find("./%s/%s" % (adNS("credentials"), adNS("username")))
act_username_name = act_username.text
act_username_method = act_username.get("method", "AdobeID")
except:
pass
try:
act_dev_uuid = None
act_dev_uuid = active_device_act.find("./%s/%s" % (adNS("activationToken"), adNS("device"))).text
except:
pass
msg = "The following device:\n\n"
msg += "Path: " + os.path.dirname(adobe_path) + "\n"
msg += "Type: " + devClass + "\n"
msg += "Name: " + devName + "\n"
if devSerial is not None:
msg += "Serial: " + devSerial + "\n"
if act_dev_uuid is not None:
msg += "UUID: " + act_dev_uuid + "\n"
msg += "\nis already connected to the following AdobeID:\n\n"
msg += "UUID: " + act_uuid + "\n"
if (act_username_name is not None):
msg += "Username: " + act_username_name + "\n"
msg += "Type: " + act_username_method + "\n"
else:
msg += "Type: anonymous authorization\n"
msg += "\nDo you want to remove this authorization and link this device to your AdobeID?\n\n"
msg += "Click \"No\" to cancel, or \"Yes\" to remove the existing authorization from the device and authorize it with your AdobeID."
ok = question_dialog(None, "Authorize eReader", msg)
if (not ok):
return
try:
os.remove(act_xml_path)
except:
error_dialog(None, "Authorize eReader", "Failed to remove existing authorization.", show=True, show_copy_button=False)
return
msg = "Found an unactivated eReader:\n\n"
msg += "Path: " + os.path.dirname(adobe_path) + "\n"
msg += "Type: " + devClass + "\n"
@ -326,18 +390,55 @@ class ConfigWidget(QWidget):
msg += "Serial: " + devSerial + "\n"
msg += "\nDo you want to authorize this device with your AdobeID?"
ok = question_dialog(None, "Manage eReader", msg)
ok = question_dialog(None, "Authorize eReader", msg)
if not ok:
return
# Okay, if we end up here, the user wants to authorize his eReader to his current AdobeID.
# Rest still needs to be implemented.
#
# Figure out what ADE version we're currently emulating:
error_dialog(None, "Manage eReader", "Not yet implemented.", show=True, show_copy_button=False)
device_xml_path = os.path.join(self.deacsmprefs["path_to_account_data"], "device.xml")
try:
containerdev = etree.parse(device_xml_path)
except (FileNotFoundError, OSError) as e:
return error_dialog(None, "Failed", "Error while reading device.xml", show=True, show_copy_button=False)
try:
adeptNS = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
# Determine the ADE version we're emulating:
ver = containerdev.findall("./%s" % (adeptNS("version")))
# "Default" entry would be for the old 10.0.4 entry.
# As 10.X is in the 3.0 range, assume we're on ADE 3.0.1 with hobbes version 10.0.85385
v_idx = VAR_VER_HOBBES_VERSIONS.index("10.0.85385")
for f in ver:
if f.get("name") == "hobbes":
hobbes_version = f.get("value")
if hobbes_version is not None:
v_idx = VAR_VER_HOBBES_VERSIONS.index(hobbes_version)
except:
return error_dialog(None, "Authorize eReader", "Error while determining ADE version", show=True, show_copy_button=False)
# Activate the target device with that version.
ret, data = activateDevice(v_idx, dev_xml_tree)
if not ret:
return error_dialog(None, "Authorize eReader", "Couldn't activate device, server returned error.", show=True, det_msg=data, show_copy_button=False)
ret, data = exportProxyAuth(act_xml_path, data)
if not ret:
return error_dialog(None, "Authorize eReader", "Error while writing activation to device.", show=True, det_msg=data, show_copy_button=False)
return info_dialog(None, "Authorize eReader", "Reader authorized successfully.", show=True, show_copy_button=False)
def delete_ade_auth(self):
@ -921,7 +1022,7 @@ class ConfigWidget(QWidget):
if (success is False):
return error_dialog(None, "ADE activation failed", "Login unsuccessful", det_msg=str(resp), show=True, show_copy_button=True)
success, resp = activateDevice(idx)
success, resp = activateDevice(idx, None)
if (success is False):
return error_dialog(None, "ADE activation failed", "Couldn't activate device", det_msg=str(resp), show=True, show_copy_button=True)
@ -1106,7 +1207,7 @@ class ConfigWidget(QWidget):
if (success is False):
return error_dialog(None, "ADE activation failed", "Login unsuccessful", det_msg=str(resp), show=True, show_copy_button=True)
success, resp = activateDevice(vers_idx)
success, resp = activateDevice(vers_idx, None)
if (success is False):
return error_dialog(None, "ADE activation failed", "Couldn't activate device", det_msg=str(resp), show=True, show_copy_button=True)

View file

@ -472,6 +472,175 @@ def signIn(account_type: str, username: str, passwd: str):
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: int = 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: int = 0):
@ -589,7 +758,7 @@ def changeDeviceVersion(useVersionIndex: int = 0):
def activateDevice(useVersionIndex: int = 0):
def activateDevice(useVersionIndex: int = 0, proxyData = None):
if useVersionIndex >= len(VAR_VER_SUPP_CONFIG_NAMES):
return False, "Invalid Version index"
@ -611,8 +780,10 @@ def activateDevice(useVersionIndex: int = 0):
except:
pass
result, activate_req = buildActivateReq(useVersionIndex)
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
@ -663,6 +834,11 @@ def activateDevice(useVersionIndex: int = 0):
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")