From 01d34288c4033ea94e431ec2a0f231874864162e Mon Sep 17 00:00:00 2001 From: Florian Bach Date: Sat, 18 Dec 2021 11:50:30 +0100 Subject: [PATCH] Experimental eReader authorization support --- README.md | 9 +- calibre-plugin/__init__.py | 3 +- calibre-plugin/config.py | 147 ++++++++++++++++++++---- calibre-plugin/libadobeAccount.py | 182 +++++++++++++++++++++++++++++- 4 files changed, 313 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index cf01937..1db9ccd 100644 --- a/README.md +++ b/README.md @@ -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 - ... \ No newline at end of file diff --git a/calibre-plugin/__init__.py b/calibre-plugin/__init__.py index 6d74f93..0b01963 100644 --- a/calibre-plugin/__init__.py +++ b/calibre-plugin/__init__.py @@ -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) diff --git a/calibre-plugin/config.py b/calibre-plugin/config.py index 9d10f78..5531cb9 100644 --- a/calibre-plugin/config.py +++ b/calibre-plugin/config.py @@ -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) diff --git a/calibre-plugin/libadobeAccount.py b/calibre-plugin/libadobeAccount.py index 6adecd8..2c136c9 100644 --- a/calibre-plugin/libadobeAccount.py +++ b/calibre-plugin/libadobeAccount.py @@ -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 = "" + ret += "" + ret += "" + ret += "%s" % (rt_si_authURL) + ret += "%s" % (rt_si_userInfoURL) + ret += "%s" % (rt_si_activationURL) + ret += "%s" % (rt_si_certificate) + ret += "" + + ret += "" + ret += "%s" % (rt_c_user) + ret += "%s" % (rt_c_licenseCertificate) + ret += "%s" % (rt_c_privateLicenseKey) + ret += "%s" % (rt_c_authenticationCertificate) + + if rt_c_username is not None: + ret += "%s" % (rt_c_usernameMethod, rt_c_username) + + ret += "" + + 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 += "" + + # 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 += "" + ret += "" + ret += "%s" % (proxyData.find("./%s" % (adNS("fingerprint"))).text) + ret += "%s" % (proxyData.find("./%s" % (adNS("deviceType"))).text) + ret += "%s" % (clientOS) + ret += "%s" % (clientLocale) + ret += "%s" % (VAR_VER_SUPP_VERSIONS[useVersionIndex]) + + ret += "" + ret += "%s" % (version) + ret += "%s" % (clientOS) + ret += "%s" % (clientLocale) + ret += "%s" % (VAR_VER_SUPP_VERSIONS[useVersionIndex]) + ret += "%s" % (local_device_xml.find("./%s" % (adNS("deviceType"))).text) + ret += "%s" % ("ADOBE Digitial Editions") + # YES, this typo ("Digitial" instead of "Digital") IS present in ADE!! + + ret += "%s" % (local_device_xml.find("./%s" % (adNS("fingerprint"))).text) + + ret += "" + ret += "%s" % (local_activation_xml.find("./%s/%s" % (adNS("activationToken"), adNS("user"))).text) + ret += "%s" % (local_activation_xml.find("./%s/%s" % (adNS("activationToken"), adNS("device"))).text) + ret += "" + ret += "" + + ret += "" + + 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 += "%s" % (hobbes_version) + + ret += "%s" % (proxyData.find("./%s" % (adNS("deviceClass"))).text) + ret += "%s" % (proxyData.find("./%s" % (adNS("deviceType"))).text) + ret += "%s" % ("ADOBE Digitial Editions") + ret += "%s" % (proxyData.find("./%s" % (adNS("fingerprint"))).text) + + + ret += "" + + ret += addNonce() + + ret += "%s" % (local_activation_xml.find("./%s/%s" % (adNS("activationToken"), adNS("user"))).text) + + ret += "" + + 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")